diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 667b43e3a73da..7d57bd2924ba0 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -88,7 +88,9 @@ src/plugins/console @elastic/platform-deployment-management packages/content-management/content_editor @elastic/appex-sharedux examples/content_management_examples @elastic/appex-sharedux src/plugins/content_management @elastic/appex-sharedux -packages/content-management/table_list @elastic/appex-sharedux +packages/content-management/tabbed_table_list_view @elastic/appex-sharedux +packages/content-management/table_list_view @elastic/appex-sharedux +packages/content-management/table_list_view_table @elastic/appex-sharedux packages/kbn-content-management-utils @elastic/kibana-data-discovery examples/controls_example @elastic/kibana-presentation src/plugins/controls @elastic/kibana-presentation diff --git a/examples/content_management_examples/public/examples/msearch/msearch_app.tsx b/examples/content_management_examples/public/examples/msearch/msearch_app.tsx index aa2bc6c1a8b7b..ae05e15db91d0 100644 --- a/examples/content_management_examples/public/examples/msearch/msearch_app.tsx +++ b/examples/content_management_examples/public/examples/msearch/msearch_app.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { ContentClientProvider, type ContentClient } from '@kbn/content-management-plugin/public'; -import { TableListViewKibanaProvider } from '@kbn/content-management-table-list'; +import { TableListViewKibanaProvider } from '@kbn/content-management-table-list-view-table'; import type { CoreStart } from '@kbn/core/public'; import { toMountPoint } from '@kbn/kibana-react-plugin/public'; import { FormattedRelative, I18nProvider } from '@kbn/i18n-react'; diff --git a/examples/content_management_examples/public/examples/msearch/msearch_table.tsx b/examples/content_management_examples/public/examples/msearch/msearch_table.tsx index 47eaab4ba602b..a30c652cf0f79 100644 --- a/examples/content_management_examples/public/examples/msearch/msearch_table.tsx +++ b/examples/content_management_examples/public/examples/msearch/msearch_table.tsx @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { TableListView, UserContentCommonSchema } from '@kbn/content-management-table-list'; +import { TableListView, UserContentCommonSchema } from '@kbn/content-management-table-list-view'; import { useContentClient } from '@kbn/content-management-plugin/public'; import React from 'react'; import { SavedObjectsFindOptionsReference } from '@kbn/core-saved-objects-api-browser'; @@ -51,7 +51,7 @@ export const MSearchTable = () => { initialPageSize={50} entityName={`ContentItem`} entityNamePlural={`ContentItems`} - tableListTitle={`MSearch Demo`} + title={`MSearch Demo`} urlStateEnabled={false} emptyPrompt={<>No data found. Try to install some sample data first.} onClickTitle={(item) => { diff --git a/examples/content_management_examples/tsconfig.json b/examples/content_management_examples/tsconfig.json index 7f07213ce82b6..ba1c3f19850c6 100644 --- a/examples/content_management_examples/tsconfig.json +++ b/examples/content_management_examples/tsconfig.json @@ -20,10 +20,13 @@ "@kbn/content-management-plugin", "@kbn/core-application-browser", "@kbn/shared-ux-link-redirect-app", - "@kbn/content-management-table-list", + "@kbn/content-management-table-list-view", + "@kbn/content-management-table-list-view-table", "@kbn/kibana-react-plugin", "@kbn/i18n-react", "@kbn/saved-objects-tagging-oss-plugin", "@kbn/core-saved-objects-api-browser", + "@kbn/content-management-table-list-view-table", + "@kbn/content-management-table-list-view", ] } diff --git a/package.json b/package.json index c00cffaf22cb4..f65e02b6dabec 100644 --- a/package.json +++ b/package.json @@ -189,7 +189,9 @@ "@kbn/content-management-content-editor": "link:packages/content-management/content_editor", "@kbn/content-management-examples-plugin": "link:examples/content_management_examples", "@kbn/content-management-plugin": "link:src/plugins/content_management", - "@kbn/content-management-table-list": "link:packages/content-management/table_list", + "@kbn/content-management-tabbed-table-list-view": "link:packages/content-management/tabbed_table_list_view", + "@kbn/content-management-table-list-view": "link:packages/content-management/table_list_view", + "@kbn/content-management-table-list-view-table": "link:packages/content-management/table_list_view_table", "@kbn/content-management-utils": "link:packages/kbn-content-management-utils", "@kbn/controls-example-plugin": "link:examples/controls_example", "@kbn/controls-plugin": "link:src/plugins/controls", diff --git a/packages/content-management/tabbed_table_list_view/README.mdx b/packages/content-management/tabbed_table_list_view/README.mdx new file mode 100644 index 0000000000000..357a07f8fc1c3 --- /dev/null +++ b/packages/content-management/tabbed_table_list_view/README.mdx @@ -0,0 +1,20 @@ +--- +id: sharedUX/contentManagement/TabbedTableListView +slug: /shared-ux/content-management/tabbed-table-list-view +title: Tabbed table list view +summary: A table to render user generated saved objects. +tags: ['shared-ux', 'content-management'] +date: 2022-08-09 +--- + +The `` renders an eui page to display a list of user content saved object. + +**Uncomplete documentation**. Will be updated. + +## API + +TODO: https://github.com/elastic/kibana/issues/144402 + +## EUI Promotion Status + +This component is not currently considered for promotion to EUI. diff --git a/packages/content-management/tabbed_table_list_view/index.ts b/packages/content-management/tabbed_table_list_view/index.ts new file mode 100644 index 0000000000000..cf228d45a10e3 --- /dev/null +++ b/packages/content-management/tabbed_table_list_view/index.ts @@ -0,0 +1,11 @@ +/* + * 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 { TabbedTableListView, type TableListTab, type TableListTabParentProps } from './src'; + +export type { UserContentCommonSchema } from '@kbn/content-management-table-list-view-table'; diff --git a/packages/content-management/tabbed_table_list_view/jest.config.js b/packages/content-management/tabbed_table_list_view/jest.config.js new file mode 100644 index 0000000000000..dcb469837dcbc --- /dev/null +++ b/packages/content-management/tabbed_table_list_view/jest.config.js @@ -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 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. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['/packages/content-management/tabbed_table_list_view'], +}; diff --git a/packages/content-management/tabbed_table_list_view/kibana.jsonc b/packages/content-management/tabbed_table_list_view/kibana.jsonc new file mode 100644 index 0000000000000..335fdd602037a --- /dev/null +++ b/packages/content-management/tabbed_table_list_view/kibana.jsonc @@ -0,0 +1,5 @@ +{ + "type": "shared-common", + "id": "@kbn/content-management-tabbed-table-list-view", + "owner": "@elastic/appex-sharedux" +} diff --git a/packages/content-management/tabbed_table_list_view/package.json b/packages/content-management/tabbed_table_list_view/package.json new file mode 100644 index 0000000000000..42c4ca6b19c0d --- /dev/null +++ b/packages/content-management/tabbed_table_list_view/package.json @@ -0,0 +1,6 @@ +{ + "name": "@kbn/content-management-tabbed-table-list-view", + "private": true, + "version": "1.0.0", + "license": "SSPL-1.0 OR Elastic License 2.0" +} diff --git a/packages/content-management/tabbed_table_list_view/src/index.ts b/packages/content-management/tabbed_table_list_view/src/index.ts new file mode 100644 index 0000000000000..515598b765bcd --- /dev/null +++ b/packages/content-management/tabbed_table_list_view/src/index.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 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 { + TabbedTableListView, + type TableListTab, + type TableListTabParentProps, +} from './tabbed_table_list_view'; diff --git a/packages/content-management/tabbed_table_list_view/src/tabbed_table_list_view.test.tsx b/packages/content-management/tabbed_table_list_view/src/tabbed_table_list_view.test.tsx new file mode 100644 index 0000000000000..cc7ced86951a4 --- /dev/null +++ b/packages/content-management/tabbed_table_list_view/src/tabbed_table_list_view.test.tsx @@ -0,0 +1,128 @@ +/* + * 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 { ReactWrapper, mount, shallow } from 'enzyme'; +import { + TabbedTableListView, + TableListTabParentProps, + TableListTab, +} from './tabbed_table_list_view'; +import { KibanaPageTemplate } from '@kbn/shared-ux-page-kibana-template'; +import { EuiPageTemplate } from '@elastic/eui'; +import { act } from 'react-dom/test-utils'; + +// Mock the necessary props for the component +const title = 'Test Title'; +const description = 'Test Description'; +const headingId = 'test-heading-id'; +const children =
Test Children
; + +const tableList1 = 'Test Table List 1'; +const tableList2 = 'Test Table List 2'; + +const tabs: TableListTab[] = [ + { + title: 'Tab 1', + id: 'tab-1', + getTableList: async (props: TableListTabParentProps) =>
{tableList1}
, + }, + { + title: 'Tab 2', + id: 'tab-2', + getTableList: async (props: TableListTabParentProps) =>
{tableList2}
, + }, +]; + +describe('TabbedTableListView', () => { + it('should render without errors', () => { + const wrapper = shallow( + {}} + /> + ); + expect(wrapper.exists()).toBe(true); + }); + + it('should render the correct title and description', () => { + const wrapper = shallow( + {}} + /> + ); + expect(wrapper.find(KibanaPageTemplate.Header).prop('pageTitle')).toMatchInlineSnapshot(` + + Test Title + + `); + expect(wrapper.find(KibanaPageTemplate.Header).prop('description')).toContain(description); + }); + + it('should render the correct number of tabs', () => { + const wrapper = shallow( + {}} + /> + ); + + expect(wrapper.find(EuiPageTemplate.Header).prop('tabs')).toHaveLength(2); + }); + + it('should switch tabs when props change', async () => { + const changeActiveTab = jest.fn(); + + let wrapper: ReactWrapper | undefined; + await act(async () => { + wrapper = mount( + + ); + }); + + if (!wrapper) { + throw new Error("enzyme wrapper didn't initialize"); + } + + expect(wrapper.find(EuiPageTemplate.Section).text()).toContain(tableList1); + + await act(async () => { + wrapper?.setProps({ + activeTabId: 'tab-2', + }); + }); + + expect(wrapper.find(EuiPageTemplate.Section).text()).toContain(tableList2); + }); +}); diff --git a/packages/content-management/tabbed_table_list_view/src/tabbed_table_list_view.tsx b/packages/content-management/tabbed_table_list_view/src/tabbed_table_list_view.tsx new file mode 100644 index 0000000000000..9872e35d90c88 --- /dev/null +++ b/packages/content-management/tabbed_table_list_view/src/tabbed_table_list_view.tsx @@ -0,0 +1,88 @@ +/* + * 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 { KibanaPageTemplate } from '@kbn/shared-ux-page-kibana-template'; +import React, { useCallback, useEffect, useState } from 'react'; +import type { + TableListViewTableProps, + UserContentCommonSchema, +} from '@kbn/content-management-table-list-view-table'; +import type { TableListViewProps } from '@kbn/content-management-table-list-view'; + +export type TableListTabParentProps = + Pick, 'onFetchSuccess' | 'setPageDataTestSubject'>; + +export interface TableListTab { + title: string; + id: string; + getTableList: ( + propsFromParent: TableListTabParentProps + ) => Promise | React.ReactNode; +} + +type TabbedTableListViewProps = Pick< + TableListViewProps, + 'title' | 'description' | 'headingId' | 'children' +> & { tabs: TableListTab[]; activeTabId: string; changeActiveTab: (id: string) => void }; + +export const TabbedTableListView = ({ + title, + description, + headingId, + children, + tabs, + activeTabId, + changeActiveTab, +}: TabbedTableListViewProps) => { + const [hasInitialFetchReturned, setHasInitialFetchReturned] = useState(false); + const [pageDataTestSubject, setPageDataTestSubject] = useState(); + + const getActiveTab = useCallback( + () => tabs.find((tab) => tab.id === activeTabId) ?? tabs[0], + [activeTabId, tabs] + ); + + const [tableList, setTableList] = useState(null); + + useEffect(() => { + async function loadTableList() { + const newTableList = await getActiveTab().getTableList({ + onFetchSuccess: () => { + if (!hasInitialFetchReturned) { + setHasInitialFetchReturned(true); + } + }, + setPageDataTestSubject, + }); + setTableList(newTableList); + } + + loadTableList(); + }, [hasInitialFetchReturned, activeTabId, tabs, getActiveTab]); + + return ( + + {title}} + description={description} + data-test-subj="top-nav" + tabs={tabs.map((tab) => ({ + onClick: () => changeActiveTab(tab.id), + isSelected: tab.id === getActiveTab().id, + label: tab.title, + }))} + /> + + {/* Any children passed to the component */} + {children} + + {tableList} + + + ); +}; diff --git a/packages/content-management/tabbed_table_list_view/tsconfig.json b/packages/content-management/tabbed_table_list_view/tsconfig.json new file mode 100644 index 0000000000000..7d0e24d7aff69 --- /dev/null +++ b/packages/content-management/tabbed_table_list_view/tsconfig.json @@ -0,0 +1,27 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "target/types", + "types": [ + "jest", + "node", + "react", + "@kbn/ambient-ui-types", + "@kbn/ambient-storybook-types", + "@emotion/react/types/css-prop" + ] + }, + "include": [ + "**/*.ts", + "**/*.tsx", + ], + "kbn_references": [ + "@kbn/content-management-table-list-view", + "@kbn/shared-ux-page-kibana-template", + "@kbn/content-management-table-list-view-table", + "@kbn/content-management-table-list-view", + ], + "exclude": [ + "target/**/*", + ] +} diff --git a/packages/content-management/table_list/README.mdx b/packages/content-management/table_list_view/README.mdx similarity index 100% rename from packages/content-management/table_list/README.mdx rename to packages/content-management/table_list_view/README.mdx diff --git a/packages/content-management/table_list_view/index.ts b/packages/content-management/table_list_view/index.ts new file mode 100644 index 0000000000000..cb075152947ed --- /dev/null +++ b/packages/content-management/table_list_view/index.ts @@ -0,0 +1,12 @@ +/* + * 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 { TableListView } from './src/table_list_view'; +export type { TableListViewProps } from './src/table_list_view'; + +export type { UserContentCommonSchema } from '@kbn/content-management-table-list-view-table'; diff --git a/packages/content-management/table_list/jest.config.js b/packages/content-management/table_list_view/jest.config.js similarity index 86% rename from packages/content-management/table_list/jest.config.js rename to packages/content-management/table_list_view/jest.config.js index 546d16dd86cf0..aab014b44dcb8 100644 --- a/packages/content-management/table_list/jest.config.js +++ b/packages/content-management/table_list_view/jest.config.js @@ -9,5 +9,5 @@ module.exports = { preset: '@kbn/test', rootDir: '../../..', - roots: ['/packages/content-management/table_list'], + roots: ['/packages/content-management/table_list_view'], }; diff --git a/packages/content-management/table_list/kibana.jsonc b/packages/content-management/table_list_view/kibana.jsonc similarity index 57% rename from packages/content-management/table_list/kibana.jsonc rename to packages/content-management/table_list_view/kibana.jsonc index 2e4dd9548f604..a9c3a12553ff4 100644 --- a/packages/content-management/table_list/kibana.jsonc +++ b/packages/content-management/table_list_view/kibana.jsonc @@ -1,5 +1,5 @@ { "type": "shared-common", - "id": "@kbn/content-management-table-list", + "id": "@kbn/content-management-table-list-view", "owner": "@elastic/appex-sharedux" } diff --git a/packages/content-management/table_list/package.json b/packages/content-management/table_list_view/package.json similarity index 62% rename from packages/content-management/table_list/package.json rename to packages/content-management/table_list_view/package.json index b387c8a466b5e..1a3dbce9e195c 100644 --- a/packages/content-management/table_list/package.json +++ b/packages/content-management/table_list_view/package.json @@ -1,6 +1,6 @@ { - "name": "@kbn/content-management-table-list", + "name": "@kbn/content-management-table-list-view", "private": true, "version": "1.0.0", "license": "SSPL-1.0 OR Elastic License 2.0" -} \ No newline at end of file +} diff --git a/packages/content-management/table_list/src/table_list_view.stories.tsx b/packages/content-management/table_list_view/src/table_list_view.stories.tsx similarity index 90% rename from packages/content-management/table_list/src/table_list_view.stories.tsx rename to packages/content-management/table_list_view/src/table_list_view.stories.tsx index 4943c9d0be657..878e5413b6774 100644 --- a/packages/content-management/table_list/src/table_list_view.stories.tsx +++ b/packages/content-management/table_list_view/src/table_list_view.stories.tsx @@ -11,10 +11,16 @@ import Chance from 'chance'; import moment from 'moment'; import { action } from '@storybook/addon-actions'; -import { Params, getStoryArgTypes, getStoryServices } from './mocks'; - -import { TableListView as Component, UserContentCommonSchema } from './table_list_view'; -import { TableListViewProvider } from './services'; +import { + TableListViewProvider, + UserContentCommonSchema, +} from '@kbn/content-management-table-list-view-table'; +import { + Params, + getStoryArgTypes, + getStoryServices, +} from '@kbn/content-management-table-list-view-table/src/mocks'; +import { TableListView as Component } from './table_list_view'; import mdx from '../README.mdx'; diff --git a/packages/content-management/table_list_view/src/table_list_view.tsx b/packages/content-management/table_list_view/src/table_list_view.tsx new file mode 100644 index 0000000000000..59fbe67a4a4c5 --- /dev/null +++ b/packages/content-management/table_list_view/src/table_list_view.tsx @@ -0,0 +1,131 @@ +/* + * 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 { KibanaPageTemplate } from '@kbn/shared-ux-page-kibana-template'; +import React, { ReactNode, useState } from 'react'; +import { + TableListViewTable, + type TableListViewTableProps, + type UserContentCommonSchema, +} from '@kbn/content-management-table-list-view-table'; + +export type TableListViewProps = Pick< + TableListViewTableProps, + | 'entityName' + | 'entityNamePlural' + | 'initialFilter' + | 'headingId' + | 'initialPageSize' + | 'listingLimit' + | 'urlStateEnabled' + | 'customTableColumn' + | 'emptyPrompt' + | 'findItems' + | 'createItem' + | 'editItem' + | 'deleteItems' + | 'getDetailViewLink' + | 'onClickTitle' + | 'id' + | 'rowItemActions' + | 'contentEditor' + | 'titleColumnName' + | 'withoutPageTemplateWrapper' + | 'showEditActionForItem' +> & { + title: string; + description?: string; + /** + * Additional actions (buttons) to be placed in the page header. + * @note only the first two values will be used. + */ + additionalRightSideActions?: ReactNode[]; + children?: ReactNode | undefined; +}; + +export const TableListView = ({ + title, + description, + entityName, + entityNamePlural, + initialFilter, + headingId, + initialPageSize, + listingLimit, + urlStateEnabled = true, + customTableColumn, + emptyPrompt, + findItems, + createItem, + editItem, + deleteItems, + getDetailViewLink, + onClickTitle, + rowItemActions, + id: listingId, + contentEditor, + children, + titleColumnName, + additionalRightSideActions, + withoutPageTemplateWrapper, +}: TableListViewProps) => { + const PageTemplate = withoutPageTemplateWrapper + ? (React.Fragment as unknown as typeof KibanaPageTemplate) + : KibanaPageTemplate; + + const [hasInitialFetchReturned, setHasInitialFetchReturned] = useState(false); + const [pageDataTestSubject, setPageDataTestSubject] = useState(); + + return ( + + {title}} + description={description} + rightSideItems={additionalRightSideActions?.slice(0, 2)} + data-test-subj="top-nav" + /> + + {/* Any children passed to the component */} + {children} + + { + if (!hasInitialFetchReturned) { + setHasInitialFetchReturned(true); + } + }} + setPageDataTestSubject={setPageDataTestSubject} + /> + + + ); +}; + +// eslint-disable-next-line import/no-default-export +export default TableListView; diff --git a/packages/content-management/table_list_view/tsconfig.json b/packages/content-management/table_list_view/tsconfig.json new file mode 100644 index 0000000000000..8b09db8f78c5a --- /dev/null +++ b/packages/content-management/table_list_view/tsconfig.json @@ -0,0 +1,25 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "target/types", + "types": [ + "jest", + "node", + "react", + "@kbn/ambient-ui-types", + "@kbn/ambient-storybook-types", + "@emotion/react/types/css-prop" + ] + }, + "include": [ + "**/*.ts", + "**/*.tsx", + ], + "kbn_references": [ + "@kbn/shared-ux-page-kibana-template", + "@kbn/content-management-table-list-view-table" + ], + "exclude": [ + "target/**/*", + ] +} diff --git a/packages/content-management/table_list_view_table/README.mdx b/packages/content-management/table_list_view_table/README.mdx new file mode 100644 index 0000000000000..df3fdc9db53e9 --- /dev/null +++ b/packages/content-management/table_list_view_table/README.mdx @@ -0,0 +1,20 @@ +--- +id: sharedUX/contentManagement/TableListViewTable +slug: /shared-ux/content-management/table-list-view-table +title: Table list view +summary: A table to render user generated saved objects. +tags: ['shared-ux', 'content-management'] +date: 2022-08-09 +--- + +The `` renders a list of user content saved object. + +**Uncomplete documentation**. Will be updated. + +## API + +TODO: https://github.com/elastic/kibana/issues/144402 + +## EUI Promotion Status + +This component is not currently considered for promotion to EUI. diff --git a/packages/content-management/table_list/index.ts b/packages/content-management/table_list_view_table/index.ts similarity index 69% rename from packages/content-management/table_list/index.ts rename to packages/content-management/table_list_view_table/index.ts index 9a608b2d6dda3..28acf44221f89 100644 --- a/packages/content-management/table_list/index.ts +++ b/packages/content-management/table_list_view_table/index.ts @@ -6,7 +6,8 @@ * Side Public License, v 1. */ -export { TableListView, TableListViewProvider, TableListViewKibanaProvider } from './src'; +export { TableListViewTable, TableListViewProvider, TableListViewKibanaProvider } from './src'; + +export type { UserContentCommonSchema, TableListViewTableProps, RowActions } from './src'; -export type { UserContentCommonSchema, RowActions } from './src'; export type { TableListViewKibanaDependencies } from './src/services'; diff --git a/packages/content-management/table_list_view_table/jest.config.js b/packages/content-management/table_list_view_table/jest.config.js new file mode 100644 index 0000000000000..6f81c7abbaf3c --- /dev/null +++ b/packages/content-management/table_list_view_table/jest.config.js @@ -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 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. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['/packages/content-management/table_list_view_table'], +}; diff --git a/packages/content-management/table_list_view_table/kibana.jsonc b/packages/content-management/table_list_view_table/kibana.jsonc new file mode 100644 index 0000000000000..85861c453bffb --- /dev/null +++ b/packages/content-management/table_list_view_table/kibana.jsonc @@ -0,0 +1,5 @@ +{ + "type": "shared-common", + "id": "@kbn/content-management-table-list-view-table", + "owner": "@elastic/appex-sharedux" +} diff --git a/packages/content-management/table_list_view_table/package.json b/packages/content-management/table_list_view_table/package.json new file mode 100644 index 0000000000000..b9d5dcf7a03f2 --- /dev/null +++ b/packages/content-management/table_list_view_table/package.json @@ -0,0 +1,6 @@ +{ + "name": "@kbn/content-management-table-list-view-table", + "private": true, + "version": "1.0.0", + "license": "SSPL-1.0 OR Elastic License 2.0" +} diff --git a/packages/content-management/table_list/src/__jest__/index.ts b/packages/content-management/table_list_view_table/src/__jest__/index.ts similarity index 100% rename from packages/content-management/table_list/src/__jest__/index.ts rename to packages/content-management/table_list_view_table/src/__jest__/index.ts diff --git a/packages/content-management/table_list/src/__jest__/tests.helpers.tsx b/packages/content-management/table_list_view_table/src/__jest__/tests.helpers.tsx similarity index 100% rename from packages/content-management/table_list/src/__jest__/tests.helpers.tsx rename to packages/content-management/table_list_view_table/src/__jest__/tests.helpers.tsx diff --git a/packages/content-management/table_list/src/actions.ts b/packages/content-management/table_list_view_table/src/actions.ts similarity index 99% rename from packages/content-management/table_list/src/actions.ts rename to packages/content-management/table_list_view_table/src/actions.ts index 6ae2740381fcc..0a18c74a571aa 100644 --- a/packages/content-management/table_list/src/actions.ts +++ b/packages/content-management/table_list_view_table/src/actions.ts @@ -8,7 +8,7 @@ import type { IHttpFetchError } from '@kbn/core-http-browser'; import type { Query } from '@elastic/eui'; -import type { State, UserContentCommonSchema } from './table_list_view'; +import type { State, UserContentCommonSchema } from './table_list_view_table'; /** Action to trigger a fetch of the table items */ export interface OnFetchItemsAction { diff --git a/packages/content-management/table_list/src/components/confirm_delete_modal.tsx b/packages/content-management/table_list_view_table/src/components/confirm_delete_modal.tsx similarity index 100% rename from packages/content-management/table_list/src/components/confirm_delete_modal.tsx rename to packages/content-management/table_list_view_table/src/components/confirm_delete_modal.tsx diff --git a/packages/content-management/table_list/src/components/index.ts b/packages/content-management/table_list_view_table/src/components/index.ts similarity index 100% rename from packages/content-management/table_list/src/components/index.ts rename to packages/content-management/table_list_view_table/src/components/index.ts diff --git a/packages/content-management/table_list/src/components/item_details.tsx b/packages/content-management/table_list_view_table/src/components/item_details.tsx similarity index 96% rename from packages/content-management/table_list/src/components/item_details.tsx rename to packages/content-management/table_list_view_table/src/components/item_details.tsx index b7f4186438b66..0b2f52b216905 100644 --- a/packages/content-management/table_list/src/components/item_details.tsx +++ b/packages/content-management/table_list_view_table/src/components/item_details.tsx @@ -12,11 +12,11 @@ import { RedirectAppLinks } from '@kbn/shared-ux-link-redirect-app'; import type { Tag } from '../types'; import { useServices } from '../services'; -import type { UserContentCommonSchema, Props as TableListViewProps } from '../table_list_view'; +import type { UserContentCommonSchema, TableListViewTableProps } from '../table_list_view_table'; import { TagBadge } from './tag_badge'; type InheritedProps = Pick< - TableListViewProps, + TableListViewTableProps, 'onClickTitle' | 'getDetailViewLink' | 'id' >; interface Props extends InheritedProps { diff --git a/packages/content-management/table_list/src/components/listing_limit_warning.tsx b/packages/content-management/table_list_view_table/src/components/listing_limit_warning.tsx similarity index 100% rename from packages/content-management/table_list/src/components/listing_limit_warning.tsx rename to packages/content-management/table_list_view_table/src/components/listing_limit_warning.tsx diff --git a/packages/content-management/table_list/src/components/table.tsx b/packages/content-management/table_list_view_table/src/components/table.tsx similarity index 94% rename from packages/content-management/table_list/src/components/table.tsx rename to packages/content-management/table_list_view_table/src/components/table.tsx index 3214e7bf00a72..875d1ddedfa41 100644 --- a/packages/content-management/table_list/src/components/table.tsx +++ b/packages/content-management/table_list_view_table/src/components/table.tsx @@ -17,6 +17,7 @@ import { SearchFilterConfig, Direction, Query, + Search, type EuiTableSelectionType, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; @@ -25,9 +26,9 @@ import { useServices } from '../services'; import type { Action } from '../actions'; import type { State as TableListViewState, - Props as TableListViewProps, + TableListViewTableProps, UserContentCommonSchema, -} from '../table_list_view'; +} from '../table_list_view_table'; import type { TableItemsRowActions } from '../types'; import { TableSortSelect } from './table_sort_select'; import { TagFilterPanel } from './tag_filter_panel'; @@ -53,8 +54,9 @@ interface Props extends State, TagManageme tableCaption: string; tableColumns: Array>; hasUpdatedAtMetadata: boolean; - deleteItems: TableListViewProps['deleteItems']; + deleteItems: TableListViewTableProps['deleteItems']; tableItemsRowActions: TableItemsRowActions; + renderCreateButton: () => React.ReactElement | undefined; onSortChange: (column: SortColumnField, direction: Direction) => void; onTableChange: (criteria: CriteriaWithPagination) => void; onTableSearchChange: (arg: { query: Query | null; queryText: string }) => void; @@ -76,6 +78,7 @@ export function Table({ tagsToTableItemMap, tableItemsRowActions, deleteItems, + renderCreateButton, tableCaption, onTableChange, onTableSearchChange, @@ -201,10 +204,11 @@ export function Table({ return [tableSortSelectFilter, tagFilterPanel]; }, [tableSortSelectFilter, tagFilterPanel]); - const search = useMemo(() => { + const search = useMemo((): Search => { return { onChange: onTableSearchChange, toolsLeft: renderToolsLeft(), + toolsRight: renderCreateButton(), query: searchQuery.query ?? undefined, box: { incremental: true, @@ -212,7 +216,7 @@ export function Table({ }, filters: searchFilters, }; - }, [onTableSearchChange, renderToolsLeft, searchFilters, searchQuery.query]); + }, [onTableSearchChange, renderCreateButton, renderToolsLeft, searchFilters, searchQuery.query]); const noItemsMessage = ( {}) => * consuming component stories. */ export const getStoryArgTypes = () => ({ - tableListTitle: { + title: { control: { type: 'text', }, diff --git a/packages/content-management/table_list/src/reducer.tsx b/packages/content-management/table_list_view_table/src/reducer.tsx similarity index 99% rename from packages/content-management/table_list/src/reducer.tsx rename to packages/content-management/table_list_view_table/src/reducer.tsx index 84ff8c0308f96..c8486d92caced 100644 --- a/packages/content-management/table_list/src/reducer.tsx +++ b/packages/content-management/table_list_view_table/src/reducer.tsx @@ -5,7 +5,7 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ -import type { State, UserContentCommonSchema } from './table_list_view'; +import type { State, UserContentCommonSchema } from './table_list_view_table'; import type { Action } from './actions'; export function getReducer() { diff --git a/packages/content-management/table_list/src/services.tsx b/packages/content-management/table_list_view_table/src/services.tsx similarity index 100% rename from packages/content-management/table_list/src/services.tsx rename to packages/content-management/table_list_view_table/src/services.tsx diff --git a/packages/content-management/table_list/src/table_list_view.test.tsx b/packages/content-management/table_list_view_table/src/table_list_view.test.tsx similarity index 91% rename from packages/content-management/table_list/src/table_list_view.test.tsx rename to packages/content-management/table_list_view_table/src/table_list_view.test.tsx index 6b0b850cddcd4..f03ab50c5234b 100644 --- a/packages/content-management/table_list/src/table_list_view.test.tsx +++ b/packages/content-management/table_list_view_table/src/table_list_view.test.tsx @@ -18,10 +18,10 @@ import type { LocationDescriptor, History } from 'history'; import { WithServices } from './__jest__'; import { getTagList } from './mocks'; import { - TableListView, - Props as TableListViewProps, - UserContentCommonSchema, -} from './table_list_view'; + TableListViewTable, + type TableListViewTableProps, + type UserContentCommonSchema, +} from './table_list_view_table'; const mockUseEffect = useEffect; @@ -49,18 +49,6 @@ interface Router { }; } -const requiredProps: TableListViewProps = { - entityName: 'test', - entityNamePlural: 'tests', - listingLimit: 500, - initialFilter: '', - initialPageSize: 20, - tableListTitle: 'test title', - findItems: jest.fn().mockResolvedValue({ total: 0, hits: [] }), - getDetailViewLink: () => 'http://elastic.co', - urlStateEnabled: false, -}; - const twoDaysAgo = new Date(new Date().setDate(new Date().getDate() - 2)); const twoDaysAgoToString = new Date(twoDaysAgo.getTime()).toDateString(); const yesterday = new Date(new Date().setDate(new Date().getDate() - 1)); @@ -73,6 +61,20 @@ const getActions = (testBed: TestBed) => ({ }); describe('TableListView', () => { + const requiredProps: TableListViewTableProps = { + entityName: 'test', + entityNamePlural: 'tests', + listingLimit: 500, + initialFilter: '', + initialPageSize: 20, + findItems: jest.fn().mockResolvedValue({ total: 0, hits: [] }), + getDetailViewLink: () => 'http://elastic.co', + urlStateEnabled: false, + onFetchSuccess: () => {}, + tableCaption: 'my caption', + setPageDataTestSubject: () => {}, + }; + beforeAll(() => { jest.useFakeTimers({ legacyFakeTimers: true }); }); @@ -81,8 +83,8 @@ describe('TableListView', () => { jest.useRealTimers(); }); - const setup = registerTestBed( - WithServices(TableListView), + const setup = registerTestBed( + WithServices(TableListViewTable), { defaultProps: { ...requiredProps }, memoryRouter: { wrapComponent: true }, @@ -376,8 +378,8 @@ describe('TableListView', () => { }); describe('column sorting', () => { - const setupColumnSorting = registerTestBed( - WithServices(TableListView, { + const setupColumnSorting = registerTestBed( + WithServices(TableListViewTable, { TagList: getTagList({ references: [] }), }), { @@ -579,8 +581,8 @@ describe('TableListView', () => { }); describe('content editor', () => { - const setupInspector = registerTestBed( - WithServices(TableListView), + const setupInspector = registerTestBed( + WithServices(TableListViewTable), { defaultProps: { ...requiredProps }, memoryRouter: { wrapComponent: true }, @@ -630,8 +632,8 @@ describe('TableListView', () => { }); describe('tag filtering', () => { - const setupTagFiltering = registerTestBed( - WithServices(TableListView, { + const setupTagFiltering = registerTestBed( + WithServices(TableListViewTable, { getTagList: () => [ { id: 'id-tag-1', name: 'tag-1', type: 'tag', description: '', color: '' }, { id: 'id-tag-2', name: 'tag-2', type: 'tag', description: '', color: '' }, @@ -782,8 +784,8 @@ describe('TableListView', () => { describe('url state', () => { let router: Router | undefined; - const setupTagFiltering = registerTestBed( - WithServices(TableListView, { + const setupTagFiltering = registerTestBed( + WithServices(TableListViewTable, { getTagList: () => [ { id: 'id-tag-1', name: 'tag-1', type: 'tag', description: '', color: '' }, { id: 'id-tag-2', name: 'tag-2', type: 'tag', description: '', color: '' }, @@ -1092,7 +1094,7 @@ describe('TableListView', () => { }, ]; - const setupTest = async (props?: Partial) => { + const setupTest = async (props?: Partial) => { let testBed: TestBed | undefined; const deleteItems = jest.fn(); await act(async () => { @@ -1173,3 +1175,87 @@ describe('TableListView', () => { }); }); }); + +describe('TableList', () => { + const requiredProps: TableListViewTableProps = { + entityName: 'test', + entityNamePlural: 'tests', + initialPageSize: 20, + listingLimit: 500, + findItems: jest.fn().mockResolvedValue({ total: 0, hits: [] }), + onFetchSuccess: jest.fn(), + tableCaption: 'test title', + getDetailViewLink: () => '', + setPageDataTestSubject: () => {}, + }; + + const setup = registerTestBed( + WithServices(TableListViewTable), + { + defaultProps: { ...requiredProps, refreshListBouncer: false }, + memoryRouter: { wrapComponent: true }, + } + ); + + it('refreshes the list when the bouncer changes', async () => { + let testBed: TestBed; + + const findItems = jest.fn().mockResolvedValue({ total: 0, hits: [] }); + + await act(async () => { + testBed = setup({ findItems }); + }); + + const { component, table } = testBed!; + + findItems.mockClear(); + expect(findItems).not.toHaveBeenCalled(); + + const hits: UserContentCommonSchema[] = [ + { + id: `item`, + type: 'dashboard', + updatedAt: 'some date', + attributes: { + title: `Updated title`, + }, + references: [], + }, + ]; + findItems.mockResolvedValue({ total: hits.length, hits }); + + await act(async () => { + component.setProps({ + refreshListBouncer: true, + }); + }); + + component.update(); + + expect(findItems).toHaveBeenCalledTimes(1); + + const metadata = table.getMetaData('itemsInMemTable'); + + expect(metadata.tableCellsValues[0][0]).toBe('Updated title'); + }); + + it('reports successful fetches', async () => { + const onFetchSuccess = jest.fn(); + + await act(async () => { + setup({ onFetchSuccess }); + }); + + expect(onFetchSuccess).toHaveBeenCalled(); + }); + + it('reports the page data test subject', async () => { + const setPageDataTestSubject = jest.fn(); + + act(() => { + setup({ setPageDataTestSubject }); + }); + + expect(setPageDataTestSubject).toHaveBeenCalledWith('testLandingPage'); + }); +}); diff --git a/packages/content-management/table_list/src/table_list_view.tsx b/packages/content-management/table_list_view_table/src/table_list_view_table.tsx similarity index 88% rename from packages/content-management/table_list/src/table_list_view.tsx rename to packages/content-management/table_list_view_table/src/table_list_view_table.tsx index 030aaad0527a9..a0d544de5687e 100644 --- a/packages/content-management/table_list/src/table_list_view.tsx +++ b/packages/content-management/table_list_view_table/src/table_list_view_table.tsx @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import React, { useReducer, useCallback, useEffect, useRef, useMemo, ReactNode } from 'react'; +import React, { useReducer, useCallback, useEffect, useRef, useMemo } from 'react'; import useDebounce from 'react-use/lib/useDebounce'; import { EuiBasicTableColumn, @@ -49,11 +49,11 @@ interface ContentEditorConfig enabled?: boolean; } -export interface Props { +export interface TableListViewTableProps< + T extends UserContentCommonSchema = UserContentCommonSchema +> { entityName: string; entityNamePlural: string; - tableListTitle: string; - tableListDescription?: string; listingLimit: number; initialFilter?: string; initialPageSize: number; @@ -73,7 +73,6 @@ export interface Props RowActions | undefined; - children?: ReactNode | undefined; findItems( searchQuery: string, refs?: { @@ -99,11 +98,7 @@ export interface Props void; + setPageDataTestSubject: (subject: string) => void; } export interface State { @@ -242,9 +242,8 @@ const tableColumnMetadata = { }, } as const; -function TableListViewComp({ - tableListTitle, - tableListDescription, +function TableListViewTableComp({ + tableCaption, entityName, entityNamePlural, initialFilter: initialQuery, @@ -264,11 +263,16 @@ function TableListViewComp({ onClickTitle, id: listingId = 'userContent', contentEditor = { enabled: false }, - children, titleColumnName, - additionalRightSideActions = [], withoutPageTemplateWrapper, -}: Props) { + onFetchSuccess, + refreshListBouncer, + setPageDataTestSubject, +}: TableListViewTableProps) { + useEffect(() => { + setPageDataTestSubject(`${entityName}LandingPage`); + }, [entityName, setPageDataTestSubject]); + if (!getDetailViewLink && !onClickTitle) { throw new Error( `[TableListView] One o["getDetailViewLink" or "onClickTitle"] prop must be provided.` @@ -366,7 +370,6 @@ function TableListViewComp({ const hasQuery = searchQuery.text !== ''; const hasNoItems = !isFetchingItems && items.length === 0 && !hasQuery; - const pageDataTestSubject = `${entityName}LandingPage`; const showFetchError = Boolean(fetchError); const showLimitError = !showFetchError && totalItems > listingLimit; @@ -397,6 +400,8 @@ function TableListViewComp({ response, }, }); + + onFetchSuccess(); } } catch (err) { dispatch({ @@ -404,7 +409,11 @@ function TableListViewComp({ data: err, }); } - }, [searchQueryParser, findItems, searchQuery.text]); + }, [searchQueryParser, searchQuery.text, findItems, onFetchSuccess]); + + useEffect(() => { + fetchItems(); + }, [fetchItems, refreshListBouncer]); const updateQuery = useCallback( (query: Query) => { @@ -903,7 +912,7 @@ function TableListViewComp({ if (!showFetchError && hasNoItems) { return ( - + @@ -920,80 +929,64 @@ function TableListViewComp({ : 'table-is-loading'; return ( - - {tableListTitle}} - description={tableListDescription} - rightSideItems={[ - renderCreateButton() ?? , - ...additionalRightSideActions?.slice(0, 2), - ]} - data-test-subj="top-nav" - /> - - {/* Any children passed to the component */} - {children} - - {/* Too many items error */} - {showLimitError && ( - - )} + <> + {/* Too many items error */} + {showLimitError && ( + + )} + + {/* Error while fetching items */} + {showFetchError && renderFetchError()} + + {/* Table of items */} +
+ + dispatch={dispatch} + items={items} + renderCreateButton={renderCreateButton} + isFetchingItems={isFetchingItems} + searchQuery={searchQuery} + tableColumns={tableColumns} + hasUpdatedAtMetadata={hasUpdatedAtMetadata} + tableSort={tableSort} + tableItemsRowActions={tableItemsRowActions} + pagination={pagination} + selectedIds={selectedIds} + entityName={entityName} + entityNamePlural={entityNamePlural} + tagsToTableItemMap={tagsToTableItemMap} + deleteItems={deleteItems} + tableCaption={tableCaption} + onTableChange={onTableChange} + onTableSearchChange={onTableSearchChange} + onSortChange={onSortChange} + addOrRemoveIncludeTagFilter={addOrRemoveIncludeTagFilter} + addOrRemoveExcludeTagFilter={addOrRemoveExcludeTagFilter} + clearTagSelection={clearTagSelection} + /> - {/* Error while fetching items */} - {showFetchError && renderFetchError()} - - {/* Table of items */} -
- - dispatch={dispatch} - items={items} - isFetchingItems={isFetchingItems} - searchQuery={searchQuery} - tableColumns={tableColumns} - hasUpdatedAtMetadata={hasUpdatedAtMetadata} - tableSort={tableSort} - pagination={pagination} - selectedIds={selectedIds} + {/* Delete modal */} + {showDeleteModal && ( + + isDeletingItems={isDeletingItems} entityName={entityName} entityNamePlural={entityNamePlural} - tagsToTableItemMap={tagsToTableItemMap} - deleteItems={deleteItems} - tableCaption={tableListTitle} - tableItemsRowActions={tableItemsRowActions} - onTableChange={onTableChange} - onTableSearchChange={onTableSearchChange} - onSortChange={onSortChange} - addOrRemoveIncludeTagFilter={addOrRemoveIncludeTagFilter} - addOrRemoveExcludeTagFilter={addOrRemoveExcludeTagFilter} - clearTagSelection={clearTagSelection} + items={selectedItems} + onConfirm={deleteSelectedItems} + onCancel={() => dispatch({ type: 'onCancelDeleteItems' })} /> - - {/* Delete modal */} - {showDeleteModal && ( - - isDeletingItems={isDeletingItems} - entityName={entityName} - entityNamePlural={entityNamePlural} - items={selectedItems} - onConfirm={deleteSelectedItems} - onCancel={() => dispatch({ type: 'onCancelDeleteItems' })} - /> - )} -
- - + )} +
+ ); } -const TableListView = React.memo(TableListViewComp) as typeof TableListViewComp; - -export { TableListView }; - -// eslint-disable-next-line import/no-default-export -export default TableListView; +export const TableListViewTable = React.memo( + TableListViewTableComp +) as typeof TableListViewTableComp; diff --git a/packages/content-management/table_list/src/types.ts b/packages/content-management/table_list_view_table/src/types.ts similarity index 100% rename from packages/content-management/table_list/src/types.ts rename to packages/content-management/table_list_view_table/src/types.ts diff --git a/packages/content-management/table_list/src/use_tags.ts b/packages/content-management/table_list_view_table/src/use_tags.ts similarity index 98% rename from packages/content-management/table_list/src/use_tags.ts rename to packages/content-management/table_list_view_table/src/use_tags.ts index 345a3484306ff..207304564a829 100644 --- a/packages/content-management/table_list/src/use_tags.ts +++ b/packages/content-management/table_list_view_table/src/use_tags.ts @@ -9,7 +9,7 @@ import { useCallback, useMemo } from 'react'; import { Query } from '@elastic/eui'; import type { Tag } from './types'; -import type { UserContentCommonSchema } from './table_list_view'; +import type { UserContentCommonSchema } from './table_list_view_table'; type QueryUpdater = (query: Query, tag: Tag) => Query; diff --git a/packages/content-management/table_list/src/use_url_state.ts b/packages/content-management/table_list_view_table/src/use_url_state.ts similarity index 100% rename from packages/content-management/table_list/src/use_url_state.ts rename to packages/content-management/table_list_view_table/src/use_url_state.ts diff --git a/packages/content-management/table_list/tsconfig.json b/packages/content-management/table_list_view_table/tsconfig.json similarity index 100% rename from packages/content-management/table_list/tsconfig.json rename to packages/content-management/table_list_view_table/tsconfig.json diff --git a/packages/kbn-dom-drag-drop/README.md b/packages/kbn-dom-drag-drop/README.md index c0145516a811e..892bb60675d92 100644 --- a/packages/kbn-dom-drag-drop/README.md +++ b/packages/kbn-dom-drag-drop/README.md @@ -23,9 +23,7 @@ const context = useContext(DragContext); In your child application, place a `ChildDragDropProvider` at the root of that, and spread the context into it: ```js - - ... your child app here ... - +... your child app here ... ``` This enables your child application to share the same drag / drop context as the root application. @@ -71,9 +69,7 @@ return ( To create a reordering group, surround the elements from the same group with a `ReorderProvider`: ```js - - ... elements from one group here ... - +... elements from one group here ... ``` The children `DragDrop` components must have props defined as in the example: @@ -85,8 +81,8 @@ The children `DragDrop` components must have props defined as in the example: ``` - diff --git a/packages/kbn-optimizer/limits.yml b/packages/kbn-optimizer/limits.yml index 0ea5cfba97e61..db5f45fb4447f 100644 --- a/packages/kbn-optimizer/limits.yml +++ b/packages/kbn-optimizer/limits.yml @@ -41,7 +41,7 @@ pageLoadAssetSize: enterpriseSearch: 35741 essSecurity: 16573 esUiShared: 326654 - eventAnnotation: 22000 + eventAnnotation: 48565 exploratoryView: 74673 expressionError: 22127 expressionGauge: 25000 diff --git a/src/plugins/dashboard/public/dashboard_listing/dashboard_listing.test.tsx b/src/plugins/dashboard/public/dashboard_listing/dashboard_listing.test.tsx index 383b51fbd65fa..99b164433fe41 100644 --- a/src/plugins/dashboard/public/dashboard_listing/dashboard_listing.test.tsx +++ b/src/plugins/dashboard/public/dashboard_listing/dashboard_listing.test.tsx @@ -19,16 +19,23 @@ import { DashboardListing, DashboardListingProps } from './dashboard_listing'; * need to ensure we're passing down the correct props, but the table list view itself doesn't need to be rendered * in our tests because it is covered in its package. */ -import { TableListView } from '@kbn/content-management-table-list'; -// import { TableListViewKibanaProvider } from '@kbn/content-management-table-list'; -jest.mock('@kbn/content-management-table-list', () => { - const originalModule = jest.requireActual('@kbn/content-management-table-list'); +import { TableListView } from '@kbn/content-management-table-list-view'; +// import { TableListViewKibanaProvider } from '@kbn/content-management-table-list-view'; +jest.mock('@kbn/content-management-table-list-view-table', () => { + const originalModule = jest.requireActual('@kbn/content-management-table-list-view-table'); return { __esModule: true, ...originalModule, TableListViewKibanaProvider: jest.fn().mockImplementation(({ children }) => { return <>{children}; }), + }; +}); +jest.mock('@kbn/content-management-table-list-view', () => { + const originalModule = jest.requireActual('@kbn/content-management-table-list-view-table'); + return { + __esModule: true, + ...originalModule, TableListView: jest.fn().mockReturnValue(null), }; }); diff --git a/src/plugins/dashboard/public/dashboard_listing/dashboard_listing.tsx b/src/plugins/dashboard/public/dashboard_listing/dashboard_listing.tsx index 4d867be5c1fd1..57716ee8b525e 100644 --- a/src/plugins/dashboard/public/dashboard_listing/dashboard_listing.tsx +++ b/src/plugins/dashboard/public/dashboard_listing/dashboard_listing.tsx @@ -10,11 +10,11 @@ import { FormattedRelative, I18nProvider } from '@kbn/i18n-react'; import React, { PropsWithChildren, useCallback, useState } from 'react'; import { - TableListView, - TableListViewKibanaDependencies, + type TableListViewKibanaDependencies, TableListViewKibanaProvider, type UserContentCommonSchema, -} from '@kbn/content-management-table-list'; +} from '@kbn/content-management-table-list-view-table'; +import { TableListView } from '@kbn/content-management-table-list-view'; import { ViewMode } from '@kbn/embeddable-plugin/public'; import { reportPerformanceMetricEvent } from '@kbn/ebt-tools'; import type { SavedObjectsFindOptionsReference } from '@kbn/core/public'; @@ -233,7 +233,7 @@ export const DashboardListing = ({ createItem={!showWriteControls ? undefined : createItem} editItem={!showWriteControls ? undefined : editItem} entityNamePlural={getEntityNamePlural()} - tableListTitle={getTableListTitle()} + title={getTableListTitle()} headingId="dashboardListingHeading" initialPageSize={initialPageSize} initialFilter={initialFilter} diff --git a/src/plugins/dashboard/tsconfig.json b/src/plugins/dashboard/tsconfig.json index 78c7ba0dccd52..a4ca23893c48c 100644 --- a/src/plugins/dashboard/tsconfig.json +++ b/src/plugins/dashboard/tsconfig.json @@ -32,7 +32,6 @@ "@kbn/data-view-editor-plugin", "@kbn/unified-search-plugin", "@kbn/shared-ux-page-analytics-no-data", - "@kbn/content-management-table-list", "@kbn/content-management-plugin", "@kbn/content-management-utils", "@kbn/i18n-react", @@ -60,7 +59,9 @@ "@kbn/core-saved-objects-server", "@kbn/core-saved-objects-utils-server", "@kbn/object-versioning", - "@kbn/core-saved-objects-api-server" + "@kbn/core-saved-objects-api-server", + "@kbn/content-management-table-list-view", + "@kbn/content-management-table-list-view-table" ], "exclude": ["target/**/*"] } diff --git a/src/plugins/event_annotation/.i18nrc.json b/src/plugins/event_annotation/.i18nrc.json new file mode 100755 index 0000000000000..0be009b9d4364 --- /dev/null +++ b/src/plugins/event_annotation/.i18nrc.json @@ -0,0 +1,6 @@ +{ + "prefix": "eventAnnotations", + "paths": { + "eventAnnotations": "." + } +} diff --git a/src/plugins/event_annotation/common/constants.ts b/src/plugins/event_annotation/common/constants.ts index 04255cee00c22..08398d53119cc 100644 --- a/src/plugins/event_annotation/common/constants.ts +++ b/src/plugins/event_annotation/common/constants.ts @@ -25,3 +25,7 @@ export const AvailableAnnotationIcons = { } as const; export const EVENT_ANNOTATION_GROUP_TYPE = 'event-annotation-group'; + +export const ANNOTATIONS_LISTING_VIEW_ID = 'annotations'; + +export const EVENT_ANNOTATION_APP_NAME = 'event-annotations'; diff --git a/src/plugins/event_annotation/common/create_copied_annotation.ts b/src/plugins/event_annotation/common/create_copied_annotation.ts new file mode 100644 index 0000000000000..caab8923c855e --- /dev/null +++ b/src/plugins/event_annotation/common/create_copied_annotation.ts @@ -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 { getDefaultManualAnnotation } from './manual_event_annotation'; +import { EventAnnotationConfig } from './types'; + +export const createCopiedAnnotation = ( + newId: string, + timestamp: string, + source?: EventAnnotationConfig +): EventAnnotationConfig => { + if (!source) { + return getDefaultManualAnnotation(newId, timestamp); + } + return { + ...source, + id: newId, + }; +}; diff --git a/src/plugins/event_annotation/common/fetch_event_annotations/utils.ts b/src/plugins/event_annotation/common/fetch_event_annotations/utils.ts index b8658b8b81565..88979fce32002 100644 --- a/src/plugins/event_annotation/common/fetch_event_annotations/utils.ts +++ b/src/plugins/event_annotation/common/fetch_event_annotations/utils.ts @@ -12,6 +12,7 @@ import { omit, pick } from 'lodash'; import dateMath from '@kbn/datemath'; import moment from 'moment'; import { IUiSettingsClient } from '@kbn/core-ui-settings-browser'; +import { LineStyle } from '@kbn/visualization-ui-components/common/types'; import { ManualEventAnnotationOutput, ManualPointEventAnnotationOutput, @@ -22,7 +23,6 @@ import { annotationColumns, AvailableAnnotationIcon, EventAnnotationOutput, - LineStyle, PointStyleProps, } from '../types'; diff --git a/src/plugins/event_annotation/common/index.ts b/src/plugins/event_annotation/common/index.ts index f7a62d4f3918a..0341a9e5ed4a2 100644 --- a/src/plugins/event_annotation/common/index.ts +++ b/src/plugins/event_annotation/common/index.ts @@ -18,8 +18,16 @@ export type { QueryPointEventAnnotationArgs, QueryPointEventAnnotationOutput, } from './query_point_event_annotation/types'; -export { manualPointEventAnnotation, manualRangeEventAnnotation } from './manual_event_annotation'; -export { queryPointEventAnnotation } from './query_point_event_annotation'; +export { + manualPointEventAnnotation, + manualRangeEventAnnotation, + getDefaultManualAnnotation, +} from './manual_event_annotation'; +export { + queryPointEventAnnotation, + getDefaultQueryAnnotation, +} from './query_point_event_annotation'; +export { createCopiedAnnotation } from './create_copied_annotation'; export { eventAnnotationGroup } from './event_annotation_group'; export type { EventAnnotationGroupArgs } from './event_annotation_group'; @@ -36,4 +44,4 @@ export type { EventAnnotationGroupAttributes, } from './types'; -export { EVENT_ANNOTATION_GROUP_TYPE } from './constants'; +export { EVENT_ANNOTATION_GROUP_TYPE, ANNOTATIONS_LISTING_VIEW_ID } from './constants'; diff --git a/src/plugins/event_annotation/common/manual_event_annotation/index.ts b/src/plugins/event_annotation/common/manual_event_annotation/index.ts index a178211164822..12d00cefc75b4 100644 --- a/src/plugins/event_annotation/common/manual_event_annotation/index.ts +++ b/src/plugins/event_annotation/common/manual_event_annotation/index.ts @@ -9,6 +9,7 @@ import type { ExpressionFunctionDefinition } from '@kbn/expressions-plugin/common'; import { i18n } from '@kbn/i18n'; import { AvailableAnnotationIcons } from '../constants'; +import { EventAnnotationConfig } from '../types'; import type { ManualRangeEventAnnotationArgs, @@ -163,3 +164,24 @@ export const manualRangeEventAnnotation: ExpressionFunctionDefinition< }; }, }; + +export const defaultAnnotationLabel = i18n.translate( + 'eventAnnotation.manualAnnotation.defaultAnnotationLabel', + { + defaultMessage: 'Event', + } +); + +export const getDefaultManualAnnotation = ( + id: string, + timestamp: string +): EventAnnotationConfig => ({ + label: defaultAnnotationLabel, + type: 'manual', + key: { + type: 'point_in_time', + timestamp, + }, + icon: 'triangle', + id, +}); diff --git a/src/plugins/event_annotation/common/query_point_event_annotation/index.ts b/src/plugins/event_annotation/common/query_point_event_annotation/index.ts index cb9ba882a9f89..6c0ee0bd62eb7 100644 --- a/src/plugins/event_annotation/common/query_point_event_annotation/index.ts +++ b/src/plugins/event_annotation/common/query_point_event_annotation/index.ts @@ -9,6 +9,7 @@ import type { ExpressionFunctionDefinition } from '@kbn/expressions-plugin/common'; import { i18n } from '@kbn/i18n'; import { AvailableAnnotationIcons } from '../constants'; +import { EventAnnotationConfig } from '../types'; import type { QueryPointEventAnnotationArgs, QueryPointEventAnnotationOutput } from './types'; @@ -111,3 +112,22 @@ export const queryPointEventAnnotation: ExpressionFunctionDefinition< }; }, }; + +export const getDefaultQueryAnnotation = ( + id: string, + fieldName: string, + timeField: string +): EventAnnotationConfig => ({ + filter: { + type: 'kibana_query', + query: `${fieldName}: *`, + language: 'kuery', + }, + timeField, + type: 'query', + key: { + type: 'point_in_time', + }, + id, + label: `${fieldName}: *`, +}); diff --git a/src/plugins/event_annotation/common/types.ts b/src/plugins/event_annotation/common/types.ts index b3e704ef647e5..9bdd4cd283523 100644 --- a/src/plugins/event_annotation/common/types.ts +++ b/src/plugins/event_annotation/common/types.ts @@ -6,9 +6,11 @@ * Side Public License, v 1. */ +import type { UserContentCommonSchema } from '@kbn/content-management-table-list-view'; import { DataViewSpec, KibanaQueryOutput } from '@kbn/data-plugin/common'; import { DatatableColumn } from '@kbn/expressions-plugin/common'; import { $Values } from '@kbn/utility-types'; +import { LineStyle } from '@kbn/visualization-ui-components/common/types'; import { AvailableAnnotationIcons } from './constants'; import { ManualEventAnnotationOutput, @@ -20,7 +22,6 @@ import { QueryPointEventAnnotationOutput, } from './query_point_event_annotation/types'; -export type LineStyle = 'solid' | 'dashed' | 'dotted'; export type Fill = 'inside' | 'outside' | 'none'; export type ManualAnnotationType = 'manual'; export type QueryAnnotationType = 'query'; @@ -85,10 +86,9 @@ export type EventAnnotationConfig = export interface EventAnnotationGroupAttributes { title: string; description: string; - tags: string[]; ignoreGlobalFilters: boolean; annotations: EventAnnotationConfig[]; - dataViewSpec?: DataViewSpec; + dataViewSpec?: DataViewSpec | null; } export interface EventAnnotationGroupConfig { @@ -101,6 +101,10 @@ export interface EventAnnotationGroupConfig { dataViewSpec?: DataViewSpec; } +export type EventAnnotationGroupContent = UserContentCommonSchema & { + attributes: { indexPatternId: string; dataViewSpec?: DataViewSpec }; +}; + export type EventAnnotationArgs = | ManualPointEventAnnotationArgs | ManualRangeEventAnnotationArgs diff --git a/src/plugins/event_annotation/jest.config.js b/src/plugins/event_annotation/jest.config.js index 61269c91a8bfd..e7e58e57eb37d 100644 --- a/src/plugins/event_annotation/jest.config.js +++ b/src/plugins/event_annotation/jest.config.js @@ -10,7 +10,10 @@ module.exports = { preset: '@kbn/test', rootDir: '../../..', roots: ['/src/plugins/event_annotation'], - coverageDirectory: '/target/kibana-coverage/jest/src/plugins/event_ann', + coverageDirectory: '/target/kibana-coverage/jest/src/plugins/event_annotation', coverageReporters: ['text', 'html'], - collectCoverageFrom: ['/src/plugins/event_ann/{common,public,server}/**/*.{ts,tsx}'], + collectCoverageFrom: [ + '/src/plugins/event_annotation/{common,public,server}/**/*.{ts,tsx}', + ], + setupFiles: ['jest-canvas-mock'], }; diff --git a/src/plugins/event_annotation/kibana.jsonc b/src/plugins/event_annotation/kibana.jsonc index b4df5edf135af..1099c467d502f 100644 --- a/src/plugins/event_annotation/kibana.jsonc +++ b/src/plugins/event_annotation/kibana.jsonc @@ -11,10 +11,23 @@ "expressions", "savedObjectsManagement", "data", + "presentationUtil", + "visualizations", + "dataViews", + "unifiedSearch", + "kibanaUtils", + "visualizationUiComponents" + ], + "optionalPlugins": [ + "savedObjectsTagging", ], "requiredBundles": [ + "data", "savedObjectsFinder", - "dataViews" + "dataViews", + "kibanaReact", + "visualizationUiComponents", + "unifiedFieldList" ], "extraPublicDirs": [ "common" diff --git a/src/plugins/event_annotation/public/components/__snapshots__/group_editor_flyout.test.tsx.snap b/src/plugins/event_annotation/public/components/__snapshots__/group_editor_flyout.test.tsx.snap new file mode 100644 index 0000000000000..49def88aecb70 --- /dev/null +++ b/src/plugins/event_annotation/public/components/__snapshots__/group_editor_flyout.test.tsx.snap @@ -0,0 +1,38 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`group editor flyout renders controls 1`] = ` +Object { + "TagSelector": [MockFunction], + "createDataView": [MockFunction], + "dataViews": Array [ + Object { + "id": "some-id", + "title": "My Data View", + }, + ], + "group": Object { + "annotations": Array [ + Object { + "icon": "triangle", + "id": "my-id", + "key": Object { + "timestamp": "some-timestamp", + "type": "point_in_time", + }, + "label": "Event", + "type": "manual", + }, + ], + "description": "", + "ignoreGlobalFilters": false, + "indexPatternId": "some-id", + "tags": Array [], + "title": "My group", + }, + "queryInputServices": Object {}, + "selectedAnnotation": undefined, + "setSelectedAnnotation": [Function], + "showValidation": false, + "update": [MockFunction], +} +`; diff --git a/src/plugins/event_annotation/public/components/annotation_editor_controls/annotation_editor_controls.tsx b/src/plugins/event_annotation/public/components/annotation_editor_controls/annotation_editor_controls.tsx new file mode 100644 index 0000000000000..e88aa0d72558a --- /dev/null +++ b/src/plugins/event_annotation/public/components/annotation_editor_controls/annotation_editor_controls.tsx @@ -0,0 +1,390 @@ +/* + * 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 './index.scss'; +import { isFieldLensCompatible } from '@kbn/visualization-ui-components/public'; +import React, { useCallback, useEffect } from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiFormRow, EuiSwitch, EuiSwitchEvent, EuiButtonGroup, EuiSpacer } from '@elastic/eui'; +import { + IconSelectSetting, + DimensionEditorSection, + NameInput, + ColorPicker, + LineStyleSettings, + TextDecorationSetting, + FieldPicker, + FieldOption, + type QueryInputServices, +} from '@kbn/visualization-ui-components/public'; +import type { FieldOptionValue } from '@kbn/visualization-ui-components/public'; +import { DataView } from '@kbn/data-views-plugin/common'; +import { useExistingFieldsReader } from '@kbn/unified-field-list-plugin/public'; +import moment from 'moment'; +import { htmlIdGenerator } from '@elastic/eui'; +import { isQueryAnnotationConfig, isRangeAnnotationConfig } from '../..'; +import { + AvailableAnnotationIcon, + EventAnnotationConfig, + PointInTimeEventAnnotationConfig, + QueryPointEventAnnotationConfig, +} from '../../../common'; +import { + defaultAnnotationColor, + defaultAnnotationLabel, + defaultAnnotationRangeColor, + defaultRangeAnnotationLabel, + toLineAnnotationColor, +} from './helpers'; +import { annotationsIconSet } from './icon_set'; +import { sanitizeProperties } from './helpers'; +import { TooltipSection } from './tooltip_annotation_panel'; +import { ConfigPanelManualAnnotation } from './manual_annotation_panel'; +import { ConfigPanelQueryAnnotation } from './query_annotation_panel'; + +export interface Props { + annotation: EventAnnotationConfig; + onAnnotationChange: (annotation: EventAnnotationConfig) => void; + dataView: DataView; + getDefaultRangeEnd: (rangeStart: string) => string; + calendarClassName?: string; + queryInputServices: QueryInputServices; + appName: string; +} + +export const idPrefix = htmlIdGenerator()(); + +const AnnotationEditorControls = ({ + annotation: currentAnnotation, + onAnnotationChange, + dataView, + getDefaultRangeEnd, + calendarClassName, + queryInputServices, + appName, +}: Props) => { + const { hasFieldData } = useExistingFieldsReader(); + + const isQueryBased = isQueryAnnotationConfig(currentAnnotation); + const isRange = isRangeAnnotationConfig(currentAnnotation); + + const [queryInputShouldOpen, setQueryInputShouldOpen] = React.useState(false); + useEffect(() => { + setQueryInputShouldOpen(!isQueryBased); + }, [isQueryBased]); + + const update = useCallback( + (newAnnotation: Partial | undefined) => + newAnnotation && + onAnnotationChange(sanitizeProperties({ ...currentAnnotation, ...newAnnotation })), + [currentAnnotation, onAnnotationChange] + ); + + return ( + <> + + + { + const typeFromId = id.replace( + 'lens_xyChart_annotation_', + '' + ) as EventAnnotationConfig['type']; + if (currentAnnotation?.type === typeFromId) { + return; + } + if (typeFromId === 'query') { + // If coming from a range type, it requires some additional resets + const additionalRangeResets = isRangeAnnotationConfig(currentAnnotation) + ? { + label: + currentAnnotation.label === defaultRangeAnnotationLabel + ? defaultAnnotationLabel + : currentAnnotation.label, + color: toLineAnnotationColor(currentAnnotation.color), + } + : {}; + return update({ + type: typeFromId, + timeField: + (dataView.timeFieldName || + // fallback to the first avaiable date field in the dataView + dataView.fields + .filter(isFieldLensCompatible) + .find(({ type: fieldType }) => fieldType === 'date')?.displayName) ?? + '', + key: { type: 'point_in_time' }, + ...additionalRangeResets, + }); + } + // From query to manual annotation + return update({ + type: typeFromId, + key: { type: 'point_in_time', timestamp: moment().toISOString() }, + }); + }} + isFullWidth + /> + + {isQueryBased ? ( + + ) : ( + + )} + + + { + update({ label: value }); + }} + /> + {!isRange && ( + <> + + currentIcon={currentAnnotation.icon} + setIcon={(icon) => update({ icon })} + defaultIcon="triangle" + customIconSet={annotationsIconSet} + /> + + {(textDecorationSelected) => { + if (textDecorationSelected !== 'field') { + return null; + } + const options = dataView.fields + .filter(isFieldLensCompatible) + .filter(({ displayName, type }) => displayName && type !== 'document') + .map( + (field) => + ({ + label: field.displayName, + value: { + type: 'field', + field: field.name, + dataType: field.type, + }, + exists: hasFieldData(dataView.id!, field.name), + compatible: true, + 'data-test-subj': `lnsXY-annotation-fieldOption-${field.name}`, + } as FieldOption) + ); + const selectedField = (currentAnnotation as QueryPointEventAnnotationConfig) + .textField; + + const fieldIsValid = selectedField + ? Boolean(dataView.getFieldByName(selectedField)) + : true; + + return ( + <> + + + + ); + }} + + + + )} + {isRange && ( + + { + update({ + outside: id === `lens_xyChart_fillStyle_outside`, + }); + }} + isFullWidth + /> + + )} + + + update({ isHidden: ev.target.checked })} + /> + + {isQueryBased && currentAnnotation && ( + + + + + + )} + + ); +}; + +const ConfigPanelGenericSwitch = ({ + label, + ['data-test-subj']: dataTestSubj, + value, + onChange, +}: { + label: string; + 'data-test-subj': string; + value: boolean; + onChange: (event: EuiSwitchEvent) => void; +}) => ( + + + +); + +// eslint-disable-next-line import/no-default-export +export default AnnotationEditorControls; diff --git a/src/plugins/event_annotation/public/components/annotation_editor_controls/helpers.ts b/src/plugins/event_annotation/public/components/annotation_editor_controls/helpers.ts new file mode 100644 index 0000000000000..f823dab7e7357 --- /dev/null +++ b/src/plugins/event_annotation/public/components/annotation_editor_controls/helpers.ts @@ -0,0 +1,93 @@ +/* + * 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 { transparentize } from '@elastic/eui'; +import { pick } from 'lodash'; +import { euiLightVars } from '@kbn/ui-theme'; +import { i18n } from '@kbn/i18n'; +import chroma from 'chroma-js'; +import { isQueryAnnotationConfig, isRangeAnnotationConfig } from '../..'; +import type { + EventAnnotationConfig, + RangeEventAnnotationConfig, + PointInTimeEventAnnotationConfig, + QueryPointEventAnnotationConfig, +} from '../../../common'; + +export const defaultAnnotationColor = euiLightVars.euiColorAccent; +// Do not compute it live as dependencies will add tens of Kbs to the plugin +export const defaultAnnotationRangeColor = `#F04E981A`; // defaultAnnotationColor with opacity 0.1 + +export const defaultAnnotationLabel = i18n.translate( + 'eventAnnotation.xyChart.defaultAnnotationLabel', + { + defaultMessage: 'Event', + } +); + +export const defaultRangeAnnotationLabel = i18n.translate( + 'eventAnnotation.xyChart.defaultRangeAnnotationLabel', + { + defaultMessage: 'Event range', + } +); + +export const toRangeAnnotationColor = (color = defaultAnnotationColor) => { + return chroma(transparentize(color, 0.1)).hex().toUpperCase(); +}; + +export const toLineAnnotationColor = (color = defaultAnnotationRangeColor) => { + return chroma(transparentize(color, 1)).hex().toUpperCase(); +}; + +export const sanitizeProperties = (annotation: EventAnnotationConfig) => { + if (isRangeAnnotationConfig(annotation)) { + const rangeAnnotation: RangeEventAnnotationConfig = pick(annotation, [ + 'type', + 'label', + 'key', + 'id', + 'isHidden', + 'color', + 'outside', + ]); + return rangeAnnotation; + } + if (isQueryAnnotationConfig(annotation)) { + const lineAnnotation: QueryPointEventAnnotationConfig = pick(annotation, [ + 'type', + 'id', + 'label', + 'key', + 'timeField', + 'isHidden', + 'lineStyle', + 'lineWidth', + 'color', + 'icon', + 'textVisibility', + 'textField', + 'filter', + 'extraFields', + ]); + return lineAnnotation; + } + const lineAnnotation: PointInTimeEventAnnotationConfig = pick(annotation, [ + 'type', + 'id', + 'label', + 'key', + 'isHidden', + 'lineStyle', + 'lineWidth', + 'color', + 'icon', + 'textVisibility', + ]); + return lineAnnotation; +}; diff --git a/src/plugins/event_annotation/public/components/annotation_editor_controls/icon_set.ts b/src/plugins/event_annotation/public/components/annotation_editor_controls/icon_set.ts new file mode 100644 index 0000000000000..e4ea40acb48ed --- /dev/null +++ b/src/plugins/event_annotation/public/components/annotation_editor_controls/icon_set.ts @@ -0,0 +1,111 @@ +/* + * 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'; +import { IconTriangle, IconCircle } from '@kbn/chart-icons'; +import type { IconSet } from '@kbn/visualization-ui-components/public'; +import { AvailableAnnotationIcon } from '../../../common'; + +export const annotationsIconSet: IconSet = [ + { + value: 'asterisk', + label: i18n.translate('eventAnnotation.xyChart.iconSelect.asteriskIconLabel', { + defaultMessage: 'Asterisk', + }), + }, + { + value: 'alert', + label: i18n.translate('eventAnnotation.xyChart.iconSelect.alertIconLabel', { + defaultMessage: 'Alert', + }), + }, + { + value: 'bell', + label: i18n.translate('eventAnnotation.xyChart.iconSelect.bellIconLabel', { + defaultMessage: 'Bell', + }), + }, + { + value: 'bolt', + label: i18n.translate('eventAnnotation.xyChart.iconSelect.boltIconLabel', { + defaultMessage: 'Bolt', + }), + }, + { + value: 'bug', + label: i18n.translate('eventAnnotation.xyChart.iconSelect.bugIconLabel', { + defaultMessage: 'Bug', + }), + }, + { + value: 'circle', + label: i18n.translate('eventAnnotation.xyChart.iconSelect.circleIconLabel', { + defaultMessage: 'Circle', + }), + icon: IconCircle, + canFill: true, + }, + + { + value: 'editorComment', + label: i18n.translate('eventAnnotation.xyChart.iconSelect.commentIconLabel', { + defaultMessage: 'Comment', + }), + }, + { + value: 'flag', + label: i18n.translate('eventAnnotation.xyChart.iconSelect.flagIconLabel', { + defaultMessage: 'Flag', + }), + }, + { + value: 'heart', + label: i18n.translate('eventAnnotation.xyChart.iconSelect.heartLabel', { + defaultMessage: 'Heart', + }), + }, + { + value: 'mapMarker', + label: i18n.translate('eventAnnotation.xyChart.iconSelect.mapMarkerLabel', { + defaultMessage: 'Map Marker', + }), + }, + { + value: 'pinFilled', + label: i18n.translate('eventAnnotation.xyChart.iconSelect.mapPinLabel', { + defaultMessage: 'Map Pin', + }), + }, + { + value: 'starEmpty', + label: i18n.translate('eventAnnotation.xyChart.iconSelect.starLabel', { + defaultMessage: 'Star', + }), + }, + { + value: 'starFilled', + label: i18n.translate('eventAnnotation.xyChart.iconSelect.starFilledLabel', { + defaultMessage: 'Star filled', + }), + }, + { + value: 'tag', + label: i18n.translate('eventAnnotation.xyChart.iconSelect.tagIconLabel', { + defaultMessage: 'Tag', + }), + }, + { + value: 'triangle', + label: i18n.translate('eventAnnotation.xyChart.iconSelect.triangleIconLabel', { + defaultMessage: 'Triangle', + }), + icon: IconTriangle, + shouldRotate: true, + canFill: true, + }, +]; diff --git a/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/annotations_config_panel/index.scss b/src/plugins/event_annotation/public/components/annotation_editor_controls/index.scss similarity index 100% rename from x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/annotations_config_panel/index.scss rename to src/plugins/event_annotation/public/components/annotation_editor_controls/index.scss diff --git a/src/plugins/event_annotation/public/components/annotation_editor_controls/index.test.tsx b/src/plugins/event_annotation/public/components/annotation_editor_controls/index.test.tsx new file mode 100644 index 0000000000000..bfbd8a5d65b44 --- /dev/null +++ b/src/plugins/event_annotation/public/components/annotation_editor_controls/index.test.tsx @@ -0,0 +1,435 @@ +/* + * 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 { DataView, DataViewField, IIndexPatternFieldList } from '@kbn/data-views-plugin/common'; +import AnnotationEditorControls from './annotation_editor_controls'; + +import React from 'react'; +import { mount } from 'enzyme'; +import { EventAnnotationConfig, RangeEventAnnotationConfig } from '../../../common'; +import { QueryInputServices } from '@kbn/visualization-ui-components/public'; +import moment from 'moment'; +import { act } from 'react-dom/test-utils'; +import { EuiButtonGroup } from '@elastic/eui'; + +jest.mock('@kbn/unified-search-plugin/public', () => ({ + QueryStringInput: () => { + return 'QueryStringInput'; + }, +})); + +const customLineStaticAnnotation: EventAnnotationConfig = { + id: 'ann1', + type: 'manual', + key: { type: 'point_in_time' as const, timestamp: '2022-03-18T08:25:00.000Z' }, + label: 'Event', + icon: 'triangle' as const, + color: 'red', + lineStyle: 'dashed' as const, + lineWidth: 3, +}; + +describe('AnnotationsPanel', () => { + const mockDataView: DataView = { + fields: [ + new DataViewField({ + type: 'date', + name: 'field1', + searchable: true, + aggregatable: true, + }), + new DataViewField({ + type: 'date', + name: '@timestamp', + searchable: true, + aggregatable: true, + }), + ] as unknown as IIndexPatternFieldList, + getFieldByName: (name) => + new DataViewField({ type: 'some-type', name, searchable: true, aggregatable: true }), + timeFieldName: '@timestamp', + } as Partial as DataView; + + const mockQueryInputServices = { + http: {}, + uiSettings: {}, + storage: {}, + dataViews: {}, + unifiedSearch: {}, + docLinks: {}, + notifications: {}, + data: {}, + } as QueryInputServices; + + describe('Dimension Editor', () => { + test('shows correct options for line annotations', () => { + const component = mount( + {}} + dataView={{} as DataView} + getDefaultRangeEnd={() => ''} + queryInputServices={mockQueryInputServices} + appName="myApp" + /> + ); + + expect( + component.find('EuiDatePicker[data-test-subj="lns-xyAnnotation-time"]').prop('selected') + ).toEqual(moment('2022-03-18T08:25:00.000Z')); + expect( + component.find('EuiDatePicker[data-test-subj="lns-xyAnnotation-fromTime"]').exists() + ).toBeFalsy(); + expect( + component.find('EuiDatePicker[data-test-subj="lns-xyAnnotation-toTime"]').exists() + ).toBeFalsy(); + expect( + component.find('EuiSwitch[data-test-subj="lns-xyAnnotation-rangeSwitch"]').prop('checked') + ).toEqual(false); + expect(component.find('EuiFieldText[data-test-subj="name-input"]').prop('value')).toEqual( + 'Event' + ); + expect( + component.find('EuiComboBox[data-test-subj="lns-icon-select"]').prop('selectedOptions') + ).toEqual([{ label: 'Triangle', value: 'triangle' }]); + expect(component.find('TextDecorationSetting').exists()).toBeTruthy(); + expect(component.find('LineStyleSettings').exists()).toBeTruthy(); + expect( + component.find('EuiButtonGroup[data-test-subj="lns-xyAnnotation-fillStyle"]').exists() + ).toBeFalsy(); + }); + test('shows correct options for range annotations', () => { + const rangeAnnotation: EventAnnotationConfig = { + color: 'red', + icon: 'triangle', + id: 'ann1', + type: 'manual', + isHidden: undefined, + key: { + endTimestamp: '2022-03-21T10:49:00.000Z', + timestamp: '2022-03-18T08:25:00.000Z', + type: 'range', + }, + label: 'Event range', + lineStyle: 'dashed', + lineWidth: 3, + }; + + const component = mount( + {}} + dataView={{} as DataView} + getDefaultRangeEnd={() => ''} + queryInputServices={mockQueryInputServices} + appName="myApp" + /> + ); + + expect( + component.find('EuiDatePicker[data-test-subj="lns-xyAnnotation-fromTime"]').prop('selected') + ).toEqual(moment('2022-03-18T08:25:00.000Z')); + expect( + component.find('EuiDatePicker[data-test-subj="lns-xyAnnotation-toTime"]').prop('selected') + ).toEqual(moment('2022-03-21T10:49:00.000Z')); + expect( + component.find('EuiDatePicker[data-test-subj="lns-xyAnnotation-time"]').exists() + ).toBeFalsy(); + expect( + component.find('EuiSwitch[data-test-subj="lns-xyAnnotation-rangeSwitch"]').prop('checked') + ).toEqual(true); + expect(component.find('EuiFieldText[data-test-subj="name-input"]').prop('value')).toEqual( + 'Event range' + ); + expect(component.find('EuiComboBox[data-test-subj="lns-icon-select"]').exists()).toBeFalsy(); + expect(component.find('TextDecorationSetting').exists()).toBeFalsy(); + expect(component.find('LineStyleSettings').exists()).toBeFalsy(); + expect(component.find('[data-test-subj="lns-xyAnnotation-fillStyle"]').exists()).toBeTruthy(); + }); + + test('calculates correct endTimstamp and transparent color when switching for range annotation and back', async () => { + const onAnnotationChange = jest.fn(); + const rangeEndTimestamp = new Date().toISOString(); + const component = mount( + rangeEndTimestamp} + queryInputServices={mockQueryInputServices} + appName="myApp" + /> + ); + + component.find('button[data-test-subj="lns-xyAnnotation-rangeSwitch"]').simulate('click'); + + const expectedRangeAnnotation: RangeEventAnnotationConfig = { + color: '#FF00001A', + id: 'ann1', + isHidden: undefined, + label: 'Event range', + type: 'manual', + key: { + endTimestamp: rangeEndTimestamp, + timestamp: '2022-03-18T08:25:00.000Z', + type: 'range', + }, + }; + + expect(onAnnotationChange).toBeCalledWith(expectedRangeAnnotation); + + act(() => { + component.setProps({ annotation: expectedRangeAnnotation }); + }); + + expect( + component.find('EuiSwitch[data-test-subj="lns-xyAnnotation-rangeSwitch"]').prop('checked') + ).toEqual(true); + + component.find('button[data-test-subj="lns-xyAnnotation-rangeSwitch"]').simulate('click'); + + expect(onAnnotationChange).toBeCalledWith({ + color: '#FF0000', + id: 'ann1', + isHidden: undefined, + key: { + timestamp: '2022-03-18T08:25:00.000Z', + type: 'point_in_time', + }, + label: 'Event', + type: 'manual', + }); + }); + + test('shows correct options for query based', () => { + const annotation: EventAnnotationConfig = { + color: 'red', + icon: 'triangle', + id: 'ann1', + type: 'query', + isHidden: undefined, + timeField: 'timestamp', + key: { + type: 'point_in_time', + }, + label: 'Query based event', + lineStyle: 'dashed', + lineWidth: 3, + filter: { type: 'kibana_query', query: '', language: 'kuery' }, + }; + + const component = mount( + {}} + dataView={mockDataView} + getDefaultRangeEnd={() => ''} + queryInputServices={mockQueryInputServices} + appName="myApp" + /> + ); + + expect( + component.find('[data-test-subj="lnsXY-annotation-query-based-field-picker"]').exists() + ).toBeTruthy(); + expect( + component.find('[data-test-subj="annotation-query-based-query-input"]').exists() + ).toBeTruthy(); + + // The provided indexPattern has 2 date fields + expect( + component + .find('[data-test-subj="lnsXY-annotation-query-based-field-picker"]') + .at(0) + .prop('options') + ).toHaveLength(2); + // When in query mode a new "field" option is added to the previous 2 ones + expect( + component.find('[data-test-subj="lns-lineMarker-text-visibility"]').at(0).prop('options') + ).toHaveLength(3); + expect( + component.find('[data-test-subj="lnsXY-annotation-tooltip-add_field"]').exists() + ).toBeTruthy(); + }); + + test('should prefill timeField with the default time field when switching to query based annotations', () => { + const onAnnotationChange = jest.fn(); + + const component = mount( + ''} + queryInputServices={mockQueryInputServices} + appName="myApp" + /> + ); + + act(() => { + component + .find(`[data-test-subj="lns-xyAnnotation-placementType"]`) + .find(EuiButtonGroup) + .prop('onChange')!('lens_xyChart_annotation_query'); + }); + component.update(); + + expect(onAnnotationChange).toHaveBeenCalledWith( + expect.objectContaining({ timeField: '@timestamp' }) + ); + }); + + test('should avoid to retain specific manual configurations when switching to query based annotations', () => { + const onAnnotationChange = jest.fn(); + + const component = mount( + ''} + queryInputServices={mockQueryInputServices} + appName="myApp" + /> + ); + + act(() => { + component + .find(`[data-test-subj="lns-xyAnnotation-placementType"]`) + .find(EuiButtonGroup) + .prop('onChange')!('lens_xyChart_annotation_query'); + }); + component.update(); + + expect(onAnnotationChange).toHaveBeenCalledWith( + expect.objectContaining({ + key: expect.not.objectContaining({ timestamp: expect.any('string') }), + }) + ); + }); + + test('should avoid to retain range manual configurations when switching to query based annotations', () => { + const annotation: EventAnnotationConfig = { + color: 'red', + icon: 'triangle', + id: 'ann1', + type: 'manual', + isHidden: undefined, + key: { + endTimestamp: '2022-03-21T10:49:00.000Z', + timestamp: '2022-03-18T08:25:00.000Z', + type: 'range', + }, + label: 'Event range', + lineStyle: 'dashed', + lineWidth: 3, + }; + + const onAnnotationChange = jest.fn(); + + const component = mount( + ''} + queryInputServices={mockQueryInputServices} + appName="myApp" + /> + ); + + act(() => { + component + .find(`[data-test-subj="lns-xyAnnotation-placementType"]`) + .find(EuiButtonGroup) + .prop('onChange')!('lens_xyChart_annotation_query'); + }); + component.update(); + + expect(onAnnotationChange).toHaveBeenCalledWith( + expect.objectContaining({ label: expect.not.stringContaining('Event range') }) + ); + }); + + test('should set a default tiemstamp when switching from query based to manual annotations', () => { + const annotation: EventAnnotationConfig = { + color: 'red', + icon: 'triangle', + id: 'ann1', + type: 'query', + isHidden: undefined, + timeField: 'timestamp', + key: { + type: 'point_in_time', + }, + label: 'Query based event', + lineStyle: 'dashed', + lineWidth: 3, + filter: { type: 'kibana_query', query: '', language: 'kuery' }, + }; + + const onAnnotationChange = jest.fn(); + + const component = mount( + ''} + queryInputServices={mockQueryInputServices} + appName="myApp" + /> + ); + + act(() => { + component + .find(`[data-test-subj="lns-xyAnnotation-placementType"]`) + .find(EuiButtonGroup) + .prop('onChange')!('lens_xyChart_annotation_manual'); + }); + component.update(); + + expect(onAnnotationChange).toHaveBeenCalledWith( + expect.objectContaining({ + key: { type: 'point_in_time', timestamp: expect.any(String) }, + }) + ); + + // also check query specific props are not carried over + expect(onAnnotationChange).toHaveBeenCalledWith( + expect.not.objectContaining({ timeField: 'timestamp' }) + ); + }); + + test('should fallback to the first date field available in the dataView if not time-based', () => { + const onAnnotationChange = jest.fn(); + const component = mount( + ''} + queryInputServices={mockQueryInputServices} + appName="myApp" + /> + ); + + act(() => { + component + .find(`[data-test-subj="lns-xyAnnotation-placementType"]`) + .find(EuiButtonGroup) + .prop('onChange')!('lens_xyChart_annotation_query'); + }); + component.update(); + + expect(onAnnotationChange).toHaveBeenCalledWith( + expect.objectContaining({ timeField: 'field1' }) + ); + }); + }); +}); diff --git a/src/plugins/event_annotation/public/components/annotation_editor_controls/index.tsx b/src/plugins/event_annotation/public/components/annotation_editor_controls/index.tsx new file mode 100644 index 0000000000000..9da375cc584ca --- /dev/null +++ b/src/plugins/event_annotation/public/components/annotation_editor_controls/index.tsx @@ -0,0 +1,20 @@ +/* + * 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, { Suspense, lazy } from 'react'; +import type { Props } from './annotation_editor_controls'; + +const AnnotationEditorControlsLazy = lazy(() => import('./annotation_editor_controls')); + +export const AnnotationEditorControls = (props: Props) => ( + + + +); + +export { annotationsIconSet } from './icon_set'; diff --git a/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/annotations_config_panel/manual_annotation_panel.tsx b/src/plugins/event_annotation/public/components/annotation_editor_controls/manual_annotation_panel.tsx similarity index 80% rename from x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/annotations_config_panel/manual_annotation_panel.tsx rename to src/plugins/event_annotation/public/components/annotation_editor_controls/manual_annotation_panel.tsx index 795d076f22f0f..8ddac6c6173c4 100644 --- a/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/annotations_config_panel/manual_annotation_panel.tsx +++ b/src/plugins/event_annotation/public/components/annotation_editor_controls/manual_annotation_panel.tsx @@ -1,35 +1,31 @@ /* * 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. + * 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 type { DatatableUtilitiesService } from '@kbn/data-plugin/common'; -import { isRangeAnnotationConfig } from '@kbn/event-annotation-plugin/public'; import { i18n } from '@kbn/i18n'; import moment from 'moment'; import React from 'react'; -import type { FramePublicAPI } from '../../../../types'; -import type { XYState } from '../../types'; +import { isRangeAnnotationConfig } from '../..'; import { - ConfigPanelRangeDatePicker, ConfigPanelApplyAsRangeSwitch, + ConfigPanelRangeDatePicker, } from './range_annotation_panel'; import type { ManualEventAnnotationType } from './types'; export const ConfigPanelManualAnnotation = ({ annotation, - frame, - state, onChange, - datatableUtilities, + getDefaultRangeEnd, + calendarClassName, }: { annotation?: ManualEventAnnotationType | undefined; onChange: (annotation: Partial | undefined) => void; - datatableUtilities: DatatableUtilitiesService; - frame: FramePublicAPI; - state: XYState; + getDefaultRangeEnd: (rangeStart: string) => string; + calendarClassName: string | undefined; }) => { const isRange = isRangeAnnotationConfig(annotation); return ( @@ -38,7 +34,8 @@ export const ConfigPanelManualAnnotation = ({ <> ); diff --git a/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/annotations_config_panel/query_annotation_panel.tsx b/src/plugins/event_annotation/public/components/annotation_editor_controls/query_annotation_panel.tsx similarity index 64% rename from x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/annotations_config_panel/query_annotation_panel.tsx rename to src/plugins/event_annotation/public/components/annotation_editor_controls/query_annotation_panel.tsx index e502efe559597..85b9c08d0e138 100644 --- a/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/annotations_config_panel/query_annotation_panel.tsx +++ b/src/plugins/event_annotation/public/components/annotation_editor_controls/query_annotation_panel.tsx @@ -1,27 +1,26 @@ /* * 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. + * 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 { EuiFormRow } from '@elastic/eui'; import type { Query } from '@kbn/data-plugin/common'; -import type { QueryPointEventAnnotationConfig } from '@kbn/event-annotation-plugin/common'; import { i18n } from '@kbn/i18n'; import React from 'react'; import { useExistingFieldsReader } from '@kbn/unified-field-list-plugin/public'; import { FieldOption, - FilterQueryInput, FieldOptionValue, FieldPicker, + FilterQueryInput, + type QueryInputServices, } from '@kbn/visualization-ui-components/public'; -import { useKibana } from '@kbn/kibana-react-plugin/public'; -import { LENS_APP_NAME } from '../../../../../common/constants'; -import type { FramePublicAPI } from '../../../../types'; -import type { XYState, XYAnnotationLayerConfig } from '../../types'; -import { LensAppServices } from '../../../../app_plugin/types'; +import type { DataView } from '@kbn/data-views-plugin/common'; +import { isFieldLensCompatible } from '@kbn/visualization-ui-components/public'; +import type { QueryPointEventAnnotationConfig } from '../../../common'; export const defaultQuery: Query = { query: '', @@ -30,23 +29,23 @@ export const defaultQuery: Query = { export const ConfigPanelQueryAnnotation = ({ annotation, - frame, - state, + dataView, onChange, - layer, queryInputShouldOpen, + queryInputServices, + appName, }: { annotation?: QueryPointEventAnnotationConfig; onChange: (annotations: Partial | undefined) => void; - frame: FramePublicAPI; - state: XYState; - layer: XYAnnotationLayerConfig; + dataView: DataView; queryInputShouldOpen?: boolean; + queryInputServices: QueryInputServices; + appName: string; }) => { - const currentIndexPattern = frame.dataViews.indexPatterns[layer.indexPatternId]; const { hasFieldData } = useExistingFieldsReader(); // list only date fields - const options = currentIndexPattern.fields + const options = dataView.fields + .filter(isFieldLensCompatible) .filter((field) => field.type === 'date' && field.displayName) .map((field) => { return { @@ -56,17 +55,14 @@ export const ConfigPanelQueryAnnotation = ({ field: field.name, dataType: field.type, }, - exists: hasFieldData(currentIndexPattern.id, field.name), + exists: dataView.id ? hasFieldData(dataView.id, field.name) : false, compatible: true, 'data-test-subj': `lns-fieldOption-${field.name}`, } as FieldOption; }); - const selectedField = - annotation?.timeField || currentIndexPattern.timeFieldName || options[0]?.value.field; - const fieldIsValid = selectedField - ? Boolean(currentIndexPattern.getFieldByName(selectedField)) - : true; + const selectedField = annotation?.timeField || dataView.timeFieldName || options[0]?.value.field; + const fieldIsValid = selectedField ? Boolean(dataView.getFieldByName(selectedField)) : true; return ( <> @@ -75,29 +71,28 @@ export const ConfigPanelQueryAnnotation = ({ display="rowCompressed" className="lnsRowCompressedMargin" fullWidth - label={i18n.translate('xpack.lens.xyChart.annotation.queryInput', { + label={i18n.translate('eventAnnotation.xyChart.annotation.queryInput', { defaultMessage: 'Annotation query', })} - data-test-subj="annotation-query-based-query-input" > { onChange({ filter: { type: 'kibana_query', ...query } }); }} - data-test-subj="lnsXY-annotation-query-based-query-input" - dataView={currentIndexPattern} - appName={LENS_APP_NAME} - queryInputServices={useKibana().services} + dataView={dataView} + appName={appName} + queryInputServices={queryInputServices} /> diff --git a/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/annotations_config_panel/range_annotation_panel.tsx b/src/plugins/event_annotation/public/components/annotation_editor_controls/range_annotation_panel.tsx similarity index 72% rename from x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/annotations_config_panel/range_annotation_panel.tsx rename to src/plugins/event_annotation/public/components/annotation_editor_controls/range_annotation_panel.tsx index 1bed2d760514b..0418994677947 100644 --- a/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/annotations_config_panel/range_annotation_panel.tsx +++ b/src/plugins/event_annotation/public/components/annotation_editor_controls/range_annotation_panel.tsx @@ -1,16 +1,11 @@ /* * 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. + * 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 type { DatatableUtilitiesService } from '@kbn/data-plugin/common'; -import type { - PointInTimeEventAnnotationConfig, - RangeEventAnnotationConfig, -} from '@kbn/event-annotation-plugin/common'; -import { isRangeAnnotationConfig } from '@kbn/event-annotation-plugin/public'; import { i18n } from '@kbn/i18n'; import React from 'react'; import { @@ -22,26 +17,20 @@ import { EuiDatePicker, } from '@elastic/eui'; import moment from 'moment'; -import { DONT_CLOSE_DIMENSION_CONTAINER_ON_CLICK_CLASS } from '../../../../utils'; -import type { FramePublicAPI } from '../../../../types'; -import { defaultRangeAnnotationLabel, defaultAnnotationLabel } from '../../annotations/helpers'; -import type { XYState } from '../../types'; -import { getDataLayers } from '../../visualization_helpers'; -import { toLineAnnotationColor, getEndTimestamp, toRangeAnnotationColor } from './helpers'; +import { isRangeAnnotationConfig } from '../..'; +import type { PointInTimeEventAnnotationConfig, RangeEventAnnotationConfig } from '../../../common'; +import { defaultRangeAnnotationLabel, defaultAnnotationLabel } from './helpers'; +import { toLineAnnotationColor, toRangeAnnotationColor } from './helpers'; import type { ManualEventAnnotationType } from './types'; export const ConfigPanelApplyAsRangeSwitch = ({ annotation, - datatableUtilities, onChange, - frame, - state, + getDefaultRangeEnd, }: { annotation?: ManualEventAnnotationType; - datatableUtilities: DatatableUtilitiesService; onChange: (annotations: Partial | undefined) => void; - frame: FramePublicAPI; - state: XYState; + getDefaultRangeEnd: (rangeStart: string) => string; }) => { const isRange = isRangeAnnotationConfig(annotation); return ( @@ -50,7 +39,7 @@ export const ConfigPanelApplyAsRangeSwitch = ({ data-test-subj="lns-xyAnnotation-rangeSwitch" label={ - {i18n.translate('xpack.lens.xyChart.applyAsRange', { + {i18n.translate('eventAnnotation.xyChart.applyAsRange', { defaultMessage: 'Apply as range', })} @@ -74,19 +63,12 @@ export const ConfigPanelApplyAsRangeSwitch = ({ }; onChange(newPointAnnotation); } else if (annotation) { - const fromTimestamp = moment(annotation?.key.timestamp); - const dataLayers = getDataLayers(state.layers); const newRangeAnnotation: RangeEventAnnotationConfig = { type: 'manual', key: { type: 'range', timestamp: annotation.key.timestamp, - endTimestamp: getEndTimestamp( - datatableUtilities, - fromTimestamp.toISOString(), - frame, - dataLayers - ), + endTimestamp: getDefaultRangeEnd(annotation.key.timestamp), }, id: annotation.id, label: @@ -110,12 +92,14 @@ export const ConfigPanelRangeDatePicker = ({ label, prependLabel, onChange, + calendarClassName, dataTestSubj = 'lnsXY_annotation_date_picker', }: { value: moment.Moment; prependLabel?: string; label?: string; onChange: (val: moment.Moment | null) => void; + calendarClassName: string | undefined; dataTestSubj?: string; }) => { return ( @@ -129,7 +113,7 @@ export const ConfigPanelRangeDatePicker = ({ } > ) : ( void; - indexPattern: IndexPattern; + dataView: DataView; invalidFields?: string[]; } @@ -51,7 +51,7 @@ function removeNewEmptyField(v: WrappedValue): v is SafeWrappedValue { export function TooltipSection({ currentConfig, setConfig, - indexPattern, + dataView, invalidFields, }: FieldInputsProps) { const { hasFieldData } = useExistingFieldsReader(); @@ -81,14 +81,14 @@ export function TooltipSection({ (choice, index = 0) => { const fields = [...localValues]; - if (indexPattern.getFieldByName(choice.field)) { + if (dataView.getFieldByName(choice.field)) { fields[index] = { id: generateId(), value: choice.field }; // update the layer state handleInputChange(fields); } }, - [localValues, indexPattern, handleInputChange] + [localValues, dataView, handleInputChange] ); const newBucketButton = ( @@ -98,7 +98,7 @@ export function TooltipSection({ onClick={() => { handleInputChange([...localValues, { id: generateId(), value: undefined, isNew: true }]); }} - label={i18n.translate('xpack.lens.xyChart.annotation.tooltip.addField', { + label={i18n.translate('eventAnnotation.xyChart.annotation.tooltip.addField', { defaultMessage: 'Add field', })} isDisabled={localValues.length > MAX_TOOLTIP_FIELDS_SIZE} @@ -115,7 +115,7 @@ export function TooltipSection({ className="lnsConfigPanelAnnotations__noFieldsPrompt" > - {i18n.translate('xpack.lens.xyChart.annotation.tooltip.noFields', { + {i18n.translate('eventAnnotation.xyChart.annotation.tooltip.noFields', { defaultMessage: 'None selected', })} @@ -126,7 +126,8 @@ export function TooltipSection({ ); } - const options = indexPattern.fields + const options = dataView.fields + .filter(isFieldLensCompatible) .filter( ({ displayName, type }) => displayName && !rawValuesLookup.has(displayName) && supportedTypes.has(type) @@ -140,7 +141,7 @@ export function TooltipSection({ field: field.name, dataType: field.type, }, - exists: hasFieldData(indexPattern.id, field.name), + exists: dataView.id ? hasFieldData(dataView.id, field.name) : false, compatible: true, 'data-test-subj': `lnsXY-annotation-tooltip-fieldOption-${field.name}`, } as FieldOption) @@ -158,7 +159,7 @@ export function TooltipSection({ bgColor="subdued" > {localValues.map(({ id, value, isNew }, index, arrayRef) => { - const fieldIsValid = value ? Boolean(indexPattern.getFieldByName(value)) : true; + const fieldIsValid = value ? Boolean(dataView.getFieldByName(value)) : true; return ( i !== index)); }} removeTitle={i18n.translate( - 'xpack.lens.xyChart.annotation.tooltip.deleteButtonLabel', + 'eventAnnotation.xyChart.annotation.tooltip.deleteButtonLabel', { defaultMessage: 'Delete', } diff --git a/src/plugins/event_annotation/public/components/annotation_editor_controls/types.ts b/src/plugins/event_annotation/public/components/annotation_editor_controls/types.ts new file mode 100644 index 0000000000000..094543e4f71d8 --- /dev/null +++ b/src/plugins/event_annotation/public/components/annotation_editor_controls/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 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 type { PointInTimeEventAnnotationConfig, RangeEventAnnotationConfig } from '../../../common'; + +export type ManualEventAnnotationType = + | PointInTimeEventAnnotationConfig + | RangeEventAnnotationConfig; diff --git a/src/plugins/event_annotation/public/components/get_annotation_accessor.ts b/src/plugins/event_annotation/public/components/get_annotation_accessor.ts new file mode 100644 index 0000000000000..1ece7164f2557 --- /dev/null +++ b/src/plugins/event_annotation/public/components/get_annotation_accessor.ts @@ -0,0 +1,32 @@ +/* + * 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 type { AccessorConfig } from '@kbn/visualization-ui-components/public'; +import { EventAnnotationConfig } from '../../common'; +import { + defaultAnnotationColor, + defaultAnnotationRangeColor, + isRangeAnnotationConfig, +} from '../event_annotation_service/helpers'; +import { annotationsIconSet } from './annotation_editor_controls/icon_set'; + +export const getAnnotationAccessor = (annotation: EventAnnotationConfig): AccessorConfig => { + const annotationIcon = !isRangeAnnotationConfig(annotation) + ? annotationsIconSet.find((option) => option.value === annotation?.icon) || + annotationsIconSet.find((option) => option.value === 'triangle') + : undefined; + const icon = annotationIcon?.icon ?? annotationIcon?.value; + return { + columnId: annotation.id, + triggerIconType: annotation.isHidden ? 'invisible' : icon ? 'custom' : 'color', + customIcon: icon, + color: + annotation?.color || + (isRangeAnnotationConfig(annotation) ? defaultAnnotationRangeColor : defaultAnnotationColor), + }; +}; diff --git a/src/plugins/event_annotation/public/components/group_editor_controls/annotation_list.tsx b/src/plugins/event_annotation/public/components/group_editor_controls/annotation_list.tsx new file mode 100644 index 0000000000000..f29638e202548 --- /dev/null +++ b/src/plugins/event_annotation/public/components/group_editor_controls/annotation_list.tsx @@ -0,0 +1,132 @@ +/* + * 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 { css } from '@emotion/react'; +import { DragContext, DragDrop, DropTargetSwapDuplicateCombine } from '@kbn/dom-drag-drop'; +import { + DimensionButton, + DimensionTrigger, + EmptyDimensionButton, +} from '@kbn/visualization-ui-components/public'; +import React, { useCallback, useContext, useEffect, useState } from 'react'; +import { v4 as uuidv4 } from 'uuid'; +import { euiThemeVars } from '@kbn/ui-theme'; +import { i18n } from '@kbn/i18n'; +import { createCopiedAnnotation, EventAnnotationConfig } from '../../../common'; +import { getAnnotationAccessor } from '..'; + +export const AnnotationList = ({ + annotations, + selectAnnotation, + update: updateAnnotations, +}: { + annotations: EventAnnotationConfig[]; + selectAnnotation: (annotation: EventAnnotationConfig) => void; + update: (annotations: EventAnnotationConfig[]) => void; +}) => { + const [newAnnotationId, setNewAnnotationId] = useState(uuidv4()); + useEffect(() => { + setNewAnnotationId(uuidv4()); + }, [annotations.length]); + + const { dragging } = useContext(DragContext); + + const addAnnotationText = i18n.translate('eventAnnotation.annotationList.add', { + defaultMessage: 'Add annotation', + }); + + const addNewAnnotation = useCallback( + (sourceAnnotationId?: string) => { + const source = sourceAnnotationId + ? annotations.find(({ id }) => id === sourceAnnotationId) + : undefined; + const newAnnotation = createCopiedAnnotation( + newAnnotationId, + new Date().toISOString(), + source + ); + + if (!source) { + selectAnnotation(newAnnotation); + } + updateAnnotations([...annotations, newAnnotation]); + }, + [annotations, newAnnotationId, selectAnnotation, updateAnnotations] + ); + + return ( +
+ {annotations.map((annotation, index) => ( +
+ + selectAnnotation(annotation)} + onRemoveClick={() => + updateAnnotations(annotations.filter(({ id }) => id !== annotation.id)) + } + accessorConfig={getAnnotationAccessor(annotation)} + label={annotation.label} + > + + + +
+ ))} + +
+ addNewAnnotation(sourceId)} + > + addNewAnnotation()} + /> + +
+
+ ); +}; diff --git a/src/plugins/event_annotation/public/components/group_editor_controls/get_annotation_accessor.ts b/src/plugins/event_annotation/public/components/group_editor_controls/get_annotation_accessor.ts new file mode 100644 index 0000000000000..59cdfe15d0814 --- /dev/null +++ b/src/plugins/event_annotation/public/components/group_editor_controls/get_annotation_accessor.ts @@ -0,0 +1,32 @@ +/* + * 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 type { AccessorConfig } from '@kbn/visualization-ui-components/public'; +import type { EventAnnotationConfig } from '../../../common'; +import { + defaultAnnotationColor, + defaultAnnotationRangeColor, + isRangeAnnotationConfig, +} from '../../event_annotation_service/helpers'; +import { annotationsIconSet } from '../annotation_editor_controls'; + +export const getAnnotationAccessor = (annotation: EventAnnotationConfig): AccessorConfig => { + const annotationIcon = !isRangeAnnotationConfig(annotation) + ? annotationsIconSet.find((option) => option.value === annotation?.icon) || + annotationsIconSet.find((option) => option.value === 'triangle') + : undefined; + const icon = annotationIcon?.icon ?? annotationIcon?.value; + return { + columnId: annotation.id, + triggerIconType: annotation.isHidden ? 'invisible' : icon ? 'custom' : 'color', + customIcon: icon, + color: + annotation?.color || + (isRangeAnnotationConfig(annotation) ? defaultAnnotationRangeColor : defaultAnnotationColor), + }; +}; diff --git a/src/plugins/event_annotation/public/components/group_editor_controls/group_editor_controls.test.tsx b/src/plugins/event_annotation/public/components/group_editor_controls/group_editor_controls.test.tsx new file mode 100644 index 0000000000000..d3b9f384a185f --- /dev/null +++ b/src/plugins/event_annotation/public/components/group_editor_controls/group_editor_controls.test.tsx @@ -0,0 +1,235 @@ +/* + * 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, { ChangeEvent, FormEvent } from 'react'; +import { EventAnnotationGroupConfig, getDefaultManualAnnotation } from '../../../common'; +import { ReactWrapper } from 'enzyme'; +import { mountWithIntl } from '@kbn/test-jest-helpers'; +import { GroupEditorControls } from './group_editor_controls'; +import { EuiTextAreaProps, EuiTextProps } from '@elastic/eui'; +import type { DataView } from '@kbn/data-views-plugin/common'; +import { act } from 'react-dom/test-utils'; +import type { QueryInputServices } from '@kbn/visualization-ui-components/public'; +import { AnnotationEditorControls, ENABLE_INDIVIDUAL_ANNOTATION_EDITING } from '..'; + +jest.mock('@elastic/eui', () => { + return { + ...jest.requireActual('@elastic/eui'), + EuiDatePicker: () => <>, // for some reason this component caused an infinite loop when the props updated + }; +}); + +describe('event annotation group editor', () => { + const dataViewId = 'my-index-pattern'; + const adHocDataViewId = 'ad-hoc'; + const adHocDataViewSpec = { + id: adHocDataViewId, + title: 'Ad Hoc Data View', + }; + + const group: EventAnnotationGroupConfig = { + annotations: [], + description: '', + tags: [], + indexPatternId: dataViewId, + title: 'My group', + ignoreGlobalFilters: false, + dataViewSpec: adHocDataViewSpec, + }; + + let wrapper: ReactWrapper; + let updateMock: jest.Mock; + let setSelectedAnnotationMock: jest.Mock; + + const TagSelector = (_props: { onTagsSelected: (tags: string[]) => void }) =>
; + + beforeEach(async () => { + updateMock = jest.fn(); + setSelectedAnnotationMock = jest.fn(); + + wrapper = mountWithIntl( + + Promise.resolve({ + id: spec.id, + title: spec.title, + toSpec: () => spec, + } as unknown as DataView) + } + queryInputServices={{} as QueryInputServices} + showValidation={false} + /> + ); + + await act(async () => { + await new Promise((resolve) => setImmediate(resolve)); + wrapper.update(); + }); + }); + + it('reports group updates', () => { + ( + wrapper.find( + "EuiFieldText[data-test-subj='annotationGroupTitle']" + ) as ReactWrapper + ).prop('onChange')!({ + target: { + value: 'im a new title!', + } as Partial as EventTarget, + } as FormEvent); + + ( + wrapper.find( + "EuiTextArea[data-test-subj='annotationGroupDescription']" + ) as ReactWrapper + ).prop('onChange')!({ + target: { + value: 'im a new description!', + }, + } as ChangeEvent); + + act(() => { + wrapper.find(TagSelector).prop('onTagsSelected')(['im a new tag!']); + }); + + // TODO - reenable data view selection tests when ENABLE_INDIVIDUAL_ANNOTATION_EDITING is set to true! + // this will happen in https://github.com/elastic/kibana/issues/158774 + + // const setDataViewId = (id: string) => + // ( + // wrapper.find( + // "EuiSelect[data-test-subj='annotationDataViewSelection']" + // ) as ReactWrapper + // ).prop('onChange')!({ target: { value: id } } as React.ChangeEvent); + + // setDataViewId(dataViewId); + // setDataViewId(adHocDataViewId); + + expect(updateMock.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + Object { + "annotations": Array [], + "dataViewSpec": Object { + "id": "ad-hoc", + "title": "Ad Hoc Data View", + }, + "description": "", + "ignoreGlobalFilters": false, + "indexPatternId": "my-index-pattern", + "tags": Array [], + "title": "im a new title!", + }, + ], + Array [ + Object { + "annotations": Array [], + "dataViewSpec": Object { + "id": "ad-hoc", + "title": "Ad Hoc Data View", + }, + "description": "im a new description!", + "ignoreGlobalFilters": false, + "indexPatternId": "my-index-pattern", + "tags": Array [], + "title": "My group", + }, + ], + Array [ + Object { + "annotations": Array [], + "dataViewSpec": Object { + "id": "ad-hoc", + "title": "Ad Hoc Data View", + }, + "description": "", + "ignoreGlobalFilters": false, + "indexPatternId": "my-index-pattern", + "tags": Array [ + "im a new tag!", + ], + "title": "My group", + }, + ], + ] + `); + }); + + if (ENABLE_INDIVIDUAL_ANNOTATION_EDITING) { + it('adds a new annotation group', () => { + act(() => { + wrapper.find('button[data-test-subj="addAnnotation"]').simulate('click'); + }); + + expect(updateMock).toHaveBeenCalledTimes(2); + const newAnnotations = (updateMock.mock.calls[0][0] as EventAnnotationGroupConfig) + .annotations; + expect(newAnnotations.length).toBe(group.annotations.length + 1); + expect(wrapper.exists(AnnotationEditorControls)); // annotation controls opened + }); + + it('incorporates annotation updates into group', () => { + const annotations = [ + getDefaultManualAnnotation('1', ''), + getDefaultManualAnnotation('2', ''), + ]; + + act(() => { + wrapper.setProps({ + selectedAnnotation: annotations[0], + group: { ...group, annotations }, + }); + }); + + wrapper.find(AnnotationEditorControls).prop('onAnnotationChange')({ + ...annotations[0], + color: 'newColor', + }); + + expect(updateMock).toHaveBeenCalledTimes(1); + expect(updateMock.mock.calls[0][0].annotations[0].color).toBe('newColor'); + expect(setSelectedAnnotationMock).toHaveBeenCalledTimes(1); + }); + + it('removes an annotation from a group', () => { + const annotations = [ + getDefaultManualAnnotation('1', ''), + getDefaultManualAnnotation('2', ''), + ]; + + act(() => { + wrapper.setProps({ + group: { ...group, annotations }, + }); + }); + + act(() => { + wrapper + .find('button[data-test-subj="indexPattern-dimension-remove"]') + .last() + .simulate('click'); + }); + + expect(updateMock).toHaveBeenCalledTimes(1); + expect(updateMock.mock.calls[0][0].annotations).toEqual(annotations.slice(0, 1)); + }); + } +}); diff --git a/src/plugins/event_annotation/public/components/group_editor_controls/group_editor_controls.tsx b/src/plugins/event_annotation/public/components/group_editor_controls/group_editor_controls.tsx new file mode 100644 index 0000000000000..c0a07a6fcb72b --- /dev/null +++ b/src/plugins/event_annotation/public/components/group_editor_controls/group_editor_controls.tsx @@ -0,0 +1,212 @@ +/* + * 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 { + EuiFieldText, + EuiForm, + EuiFormRow, + EuiSelect, + EuiText, + EuiTextArea, + EuiTitle, +} from '@elastic/eui'; +import { css } from '@emotion/react'; +import type { DataView, DataViewSpec } from '@kbn/data-views-plugin/common'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { SavedObjectsTaggingApiUiComponent } from '@kbn/saved-objects-tagging-oss-plugin/public'; +import { euiThemeVars } from '@kbn/ui-theme'; +import { QueryInputServices } from '@kbn/visualization-ui-components/public'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import { EVENT_ANNOTATION_APP_NAME } from '../../../common/constants'; +import { EventAnnotationConfig, EventAnnotationGroupConfig } from '../../../common'; +import { AnnotationEditorControls } from '../annotation_editor_controls'; +import { AnnotationList } from './annotation_list'; + +export const ENABLE_INDIVIDUAL_ANNOTATION_EDITING = false; + +const isTitleValid = (title: string) => Boolean(title.length); + +export const isGroupValid = (group: EventAnnotationGroupConfig) => isTitleValid(group.title); + +export const GroupEditorControls = ({ + group, + update, + setSelectedAnnotation: _setSelectedAnnotation, + selectedAnnotation, + TagSelector, + dataViews: globalDataViews, + createDataView, + queryInputServices, + showValidation, +}: { + group: EventAnnotationGroupConfig; + update: (group: EventAnnotationGroupConfig) => void; + selectedAnnotation: EventAnnotationConfig | undefined; + setSelectedAnnotation: (annotation: EventAnnotationConfig) => void; + TagSelector: SavedObjectsTaggingApiUiComponent['SavedObjectSaveModalTagSelector']; + dataViews: DataView[]; + createDataView: (spec: DataViewSpec) => Promise; + queryInputServices: QueryInputServices; + showValidation: boolean; +}) => { + // save the spec for the life of the component since the user might change their mind after selecting another data view + const [adHocDataView, setAdHocDataView] = useState(); + + useEffect(() => { + if (group.dataViewSpec) { + createDataView(group.dataViewSpec).then(setAdHocDataView); + } + }, [createDataView, group.dataViewSpec]); + + const setSelectedAnnotation = useCallback( + (newSelection: EventAnnotationConfig) => { + update({ + ...group, + annotations: group.annotations.map((annotation) => + annotation.id === newSelection.id ? newSelection : annotation + ), + }); + _setSelectedAnnotation(newSelection); + }, + [_setSelectedAnnotation, group, update] + ); + + const dataViews = useMemo(() => { + const items = [...globalDataViews]; + if (adHocDataView) { + items.push(adHocDataView); + } + return items; + }, [adHocDataView, globalDataViews]); + + const currentDataView = useMemo( + () => dataViews.find((dataView) => dataView.id === group.indexPatternId) || dataViews[0], + [dataViews, group.indexPatternId] + ); + + return !selectedAnnotation ? ( + <> + +

+ +

+
+ + + + update({ + ...group, + title: value, + }) + } + /> + + + + + } + > + + update({ + ...group, + description: value, + }) + } + /> + + + + update({ + ...group, + tags, + }) + } + /> + + {ENABLE_INDIVIDUAL_ANNOTATION_EDITING && ( + <> + + ({ + value, + text: name ?? title, + }))} + value={group.indexPatternId} + onChange={({ target: { value } }) => + update({ + ...group, + indexPatternId: value, + dataViewSpec: + value === adHocDataView?.id ? adHocDataView.toSpec(false) : undefined, + }) + } + /> + + + update({ ...group, annotations: newAnnotations })} + /> + + + )} + + + ) : ( + setSelectedAnnotation({ ...selectedAnnotation, ...changes })} + dataView={currentDataView} + getDefaultRangeEnd={(rangeStart) => rangeStart} + queryInputServices={queryInputServices} + appName={EVENT_ANNOTATION_APP_NAME} + /> + ); +}; diff --git a/src/plugins/event_annotation/public/components/group_editor_controls/index.ts b/src/plugins/event_annotation/public/components/group_editor_controls/index.ts new file mode 100644 index 0000000000000..b9db18b4682f4 --- /dev/null +++ b/src/plugins/event_annotation/public/components/group_editor_controls/index.ts @@ -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 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 * from './group_editor_controls'; diff --git a/src/plugins/event_annotation/public/components/group_editor_flyout.test.tsx b/src/plugins/event_annotation/public/components/group_editor_flyout.test.tsx new file mode 100644 index 0000000000000..7f732aa882ffb --- /dev/null +++ b/src/plugins/event_annotation/public/components/group_editor_flyout.test.tsx @@ -0,0 +1,134 @@ +/* + * 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 { EuiButton, EuiFlyout } from '@elastic/eui'; +import { EventAnnotationGroupConfig, getDefaultManualAnnotation } from '../../common'; +import { taggingApiMock } from '@kbn/saved-objects-tagging-oss-plugin/public/api.mock'; +import { shallow, ShallowWrapper } from 'enzyme'; +import React from 'react'; +import { GroupEditorControls } from './group_editor_controls'; +import { GroupEditorFlyout } from './group_editor_flyout'; +import { DataView } from '@kbn/data-views-plugin/common'; +import type { QueryInputServices } from '@kbn/visualization-ui-components/public'; + +const simulateButtonClick = (component: ShallowWrapper, selector: string) => { + (component.find(selector) as ShallowWrapper[0]>).prop('onClick')!( + {} as any + ); +}; + +const SELECTORS = { + SAVE_BUTTON: '[data-test-subj="saveAnnotationGroup"]', + CANCEL_BUTTON: '[data-test-subj="cancelGroupEdit"]', + BACK_BUTTON: '[data-test-subj="backToGroupSettings"]', +}; + +const assertGroupEditingState = (component: ShallowWrapper) => { + expect(component.exists(SELECTORS.SAVE_BUTTON)).toBeTruthy(); + expect(component.exists(SELECTORS.CANCEL_BUTTON)).toBeTruthy(); + expect(component.exists(SELECTORS.BACK_BUTTON)).toBeFalsy(); +}; + +const assertAnnotationEditingState = (component: ShallowWrapper) => { + expect(component.exists(SELECTORS.BACK_BUTTON)).toBeTruthy(); + expect(component.exists(SELECTORS.SAVE_BUTTON)).toBeFalsy(); + expect(component.exists(SELECTORS.CANCEL_BUTTON)).toBeFalsy(); +}; + +describe('group editor flyout', () => { + const annotation = getDefaultManualAnnotation('my-id', 'some-timestamp'); + + const group: EventAnnotationGroupConfig = { + annotations: [annotation], + description: '', + tags: [], + indexPatternId: 'some-id', + title: 'My group', + ignoreGlobalFilters: false, + }; + + const mockTaggingApi = taggingApiMock.create(); + + let component: ShallowWrapper; + let onSave: jest.Mock; + let onClose: jest.Mock; + let updateGroup: jest.Mock; + + beforeEach(() => { + onSave = jest.fn(); + onClose = jest.fn(); + updateGroup = jest.fn(); + component = shallow( + + ); + }); + + it('renders controls', () => { + expect(component.find(GroupEditorControls).props()).toMatchSnapshot(); + }); + it('signals close', () => { + component.find(EuiFlyout).prop('onClose')({} as MouseEvent); + simulateButtonClick(component, SELECTORS.CANCEL_BUTTON); + + expect(onClose).toHaveBeenCalledTimes(2); + }); + it('signals save', () => { + simulateButtonClick(component, SELECTORS.SAVE_BUTTON); + + expect(onSave).toHaveBeenCalledTimes(1); + }); + it("doesn't save invalid group config", () => { + component.setProps({ + group: { ...group, title: '' }, + }); + + simulateButtonClick(component, SELECTORS.SAVE_BUTTON); + + expect(onSave).not.toHaveBeenCalled(); + }); + it('reports group updates', () => { + const newGroup = { ...group, description: 'new description' }; + component.find(GroupEditorControls).prop('update')(newGroup); + + expect(updateGroup).toHaveBeenCalledWith(newGroup); + }); + test('specific annotation editing', () => { + assertGroupEditingState(component); + + component.find(GroupEditorControls).prop('setSelectedAnnotation')(annotation); + + assertAnnotationEditingState(component); + + component.find(SELECTORS.BACK_BUTTON).simulate('click'); + + assertGroupEditingState(component); + }); + it('removes active annotation instead of signaling close', () => { + component.find(GroupEditorControls).prop('setSelectedAnnotation')(annotation); + + assertAnnotationEditingState(component); + + component.find(EuiFlyout).prop('onClose')({} as MouseEvent); + + assertGroupEditingState(component); + }); +}); diff --git a/src/plugins/event_annotation/public/components/group_editor_flyout.tsx b/src/plugins/event_annotation/public/components/group_editor_flyout.tsx new file mode 100644 index 0000000000000..3e296951542a8 --- /dev/null +++ b/src/plugins/event_annotation/public/components/group_editor_flyout.tsx @@ -0,0 +1,146 @@ +/* + * 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 { + EuiFlyout, + EuiFlyoutHeader, + EuiTitle, + EuiFlyoutBody, + EuiFlyoutFooter, + EuiFlexGroup, + EuiFlexItem, + EuiButtonEmpty, + EuiButton, + htmlIdGenerator, +} from '@elastic/eui'; +import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { SavedObjectsTaggingApi } from '@kbn/saved-objects-tagging-oss-plugin/public'; +import { DataView, DataViewSpec } from '@kbn/data-views-plugin/common'; +import type { QueryInputServices } from '@kbn/visualization-ui-components/public'; +import { EventAnnotationConfig, EventAnnotationGroupConfig } from '../../common'; +import { GroupEditorControls, isGroupValid } from './group_editor_controls'; + +export const GroupEditorFlyout = ({ + group, + updateGroup, + onClose: parentOnClose, + onSave, + savedObjectsTagging, + dataViews, + createDataView, + queryInputServices, +}: { + group: EventAnnotationGroupConfig; + updateGroup: (newGroup: EventAnnotationGroupConfig) => void; + onClose: () => void; + onSave: () => void; + savedObjectsTagging: SavedObjectsTaggingApi; + dataViews: DataView[]; + createDataView: (spec: DataViewSpec) => Promise; + queryInputServices: QueryInputServices; +}) => { + const flyoutHeadingId = useMemo(() => htmlIdGenerator()(), []); + const flyoutBodyOverflowRef = useRef(null); + useEffect(() => { + if (!flyoutBodyOverflowRef.current) { + flyoutBodyOverflowRef.current = document.querySelector('.euiFlyoutBody__overflow'); + } + }, []); + + const [hasAttemptedSave, setHasAttemptedSave] = useState(false); + + const resetContentScroll = useCallback( + () => flyoutBodyOverflowRef.current && flyoutBodyOverflowRef.current.scroll(0, 0), + [] + ); + + const [selectedAnnotation, _setSelectedAnnotation] = useState(); + const setSelectedAnnotation = useCallback( + (newValue: EventAnnotationConfig | undefined) => { + if ((!newValue && selectedAnnotation) || (newValue && !selectedAnnotation)) + resetContentScroll(); + _setSelectedAnnotation(newValue); + }, + [resetContentScroll, selectedAnnotation] + ); + + const onClose = () => (selectedAnnotation ? setSelectedAnnotation(undefined) : parentOnClose()); + + return ( + + + +

+ +

+
+
+ + + + + + + + {selectedAnnotation ? ( + + setSelectedAnnotation(undefined)} + > + + + + ) : ( + <> + + + + + + + { + setHasAttemptedSave(true); + + if (isGroupValid(group)) { + onSave(); + } + }} + > + + + + + )} + + +
+ ); +}; diff --git a/src/plugins/event_annotation/public/components/index.ts b/src/plugins/event_annotation/public/components/index.ts new file mode 100644 index 0000000000000..5433625c6344b --- /dev/null +++ b/src/plugins/event_annotation/public/components/index.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 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 { AnnotationEditorControls, annotationsIconSet } from './annotation_editor_controls'; + +export * from './group_editor_controls'; + +export * from './get_annotation_accessor'; diff --git a/src/plugins/event_annotation/public/components/table_list.test.tsx b/src/plugins/event_annotation/public/components/table_list.test.tsx new file mode 100644 index 0000000000000..b6d20e64dc8d4 --- /dev/null +++ b/src/plugins/event_annotation/public/components/table_list.test.tsx @@ -0,0 +1,222 @@ +/* + * 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 { + EventAnnotationGroupTableList, + SAVED_OBJECTS_LIMIT_SETTING, + SAVED_OBJECTS_PER_PAGE_SETTING, +} from './table_list'; +import { + TableListViewTable, + type UserContentCommonSchema, +} from '@kbn/content-management-table-list-view-table'; +import { EventAnnotationServiceType } from '../event_annotation_service/types'; +import { IUiSettingsClient } from '@kbn/core-ui-settings-browser'; +import { shallow, ShallowWrapper } from 'enzyme'; +import { EventAnnotationGroupConfig, EVENT_ANNOTATION_GROUP_TYPE } from '../../common'; +import { taggingApiMock } from '@kbn/saved-objects-tagging-oss-plugin/public/mocks'; + +import { act } from 'react-dom/test-utils'; +import { GroupEditorFlyout } from './group_editor_flyout'; +import { DataView } from '@kbn/data-views-plugin/common'; +import { QueryInputServices } from '@kbn/visualization-ui-components/public'; +import { toastsServiceMock } from '@kbn/core-notifications-browser-mocks/src/toasts_service.mock'; +import { IToasts } from '@kbn/core-notifications-browser'; + +describe('annotation list view', () => { + const adHocDVId = 'ad-hoc'; + + const group: EventAnnotationGroupConfig = { + annotations: [], + description: '', + tags: [], + indexPatternId: adHocDVId, + title: 'My group', + ignoreGlobalFilters: false, + dataViewSpec: { + id: adHocDVId, + title: 'Ad hoc data view', + }, + }; + + let wrapper: ShallowWrapper; + let mockEventAnnotationService: EventAnnotationServiceType; + let mockToasts: IToasts; + + beforeEach(() => { + mockEventAnnotationService = { + findAnnotationGroupContent: jest.fn(), + deleteAnnotationGroups: jest.fn(), + loadAnnotationGroup: jest.fn().mockResolvedValue(group), + updateAnnotationGroup: jest.fn(() => Promise.resolve()), + } as Partial as EventAnnotationServiceType; + + const mockUiSettings = { + get: jest.fn( + (key) => + ({ + [SAVED_OBJECTS_LIMIT_SETTING]: 30, + [SAVED_OBJECTS_PER_PAGE_SETTING]: 10, + }[key]) + ), + } as Partial as IUiSettingsClient; + + mockToasts = toastsServiceMock.createStartContract(); + + wrapper = shallow( + {}, + setPageDataTestSubject: () => {}, + }} + dataViews={[ + { + id: 'some-id', + title: 'Some data view', + } as DataView, + ]} + createDataView={() => Promise.resolve({} as DataView)} + queryInputServices={{} as QueryInputServices} + toasts={mockToasts} + navigateToLens={() => {}} + /> + ); + }); + + it('searches for groups', () => { + const searchQuery = 'My Search Query'; + const references = [{ id: 'first_id', type: 'sometype' }]; + const referencesToExclude = [{ id: 'second_id', type: 'sometype' }]; + wrapper.find(TableListViewTable).prop('findItems')(searchQuery, { + references, + referencesToExclude, + }); + + expect(mockEventAnnotationService.findAnnotationGroupContent).toHaveBeenCalledWith( + 'My Search Query', + 30, + [{ id: 'first_id', type: 'sometype' }], + [{ id: 'second_id', type: 'sometype' }] + ); + }); + + describe('deleting groups', () => { + it('prevent deleting when user is missing perms', () => { + wrapper.setProps({ visualizeCapabilities: { delete: false } }); + + expect(wrapper.find(TableListViewTable).prop('deleteItems')).toBeUndefined(); + }); + + it('deletes groups using the service', () => { + expect(wrapper.find(TableListViewTable).prop('deleteItems')).toBeDefined(); + + wrapper.find(TableListViewTable).prop('deleteItems')!([ + { + id: 'some-id-1', + references: [ + { + type: 'index-pattern', + name: 'metrics-*', + id: 'metrics-*', + }, + ], + type: EVENT_ANNOTATION_GROUP_TYPE, + updatedAt: '', + attributes: { + title: 'group1', + }, + }, + { + id: 'some-id-2', + references: [], + type: EVENT_ANNOTATION_GROUP_TYPE, + updatedAt: '', + attributes: { + title: 'group2', + }, + }, + ]); + + expect((mockEventAnnotationService.deleteAnnotationGroups as jest.Mock).mock.calls) + .toMatchInlineSnapshot(` + Array [ + Array [ + Array [ + "some-id-1", + "some-id-2", + ], + ], + ] + `); + }); + }); + + describe('editing groups', () => { + it('prevents editing when user is missing perms', () => { + wrapper.setProps({ visualizeCapabilities: { save: false } }); + + expect(wrapper.find(TableListViewTable).prop('deleteItems')).toBeUndefined(); + }); + + it('edits existing group', async () => { + expect(wrapper.find(GroupEditorFlyout).exists()).toBeFalsy(); + const initialBouncerValue = wrapper.find(TableListViewTable).prop('refreshListBouncer'); + + act(() => { + wrapper.find(TableListViewTable).prop('editItem')!({ + id: '1234', + } as UserContentCommonSchema); + }); + + // wait one tick to give promise time to settle + await new Promise((resolve) => setTimeout(resolve, 0)); + + expect(mockEventAnnotationService.loadAnnotationGroup).toHaveBeenCalledWith('1234'); + + expect(wrapper.find(GroupEditorFlyout).exists()).toBeTruthy(); + + const updatedGroup = { ...group, tags: ['my-new-tag'] }; + + wrapper.find(GroupEditorFlyout).prop('updateGroup')(updatedGroup); + + wrapper.find(GroupEditorFlyout).prop('onSave')(); + + await new Promise((resolve) => setTimeout(resolve, 0)); + + expect(mockEventAnnotationService.updateAnnotationGroup).toHaveBeenCalledWith( + updatedGroup, + '1234' + ); + + expect(wrapper.find(GroupEditorFlyout).exists()).toBeFalsy(); + expect(wrapper.find(TableListViewTable).prop('refreshListBouncer')).not.toBe( + initialBouncerValue + ); // (should refresh list) + }); + + it('opens editor when title is clicked', async () => { + act(() => { + wrapper.find(TableListViewTable).prop('onClickTitle')!({ + id: '1234', + } as UserContentCommonSchema); + }); + + await new Promise((resolve) => setTimeout(resolve, 0)); + + expect(wrapper.find(GroupEditorFlyout).exists()).toBeTruthy(); + }); + }); +}); diff --git a/src/plugins/event_annotation/public/components/table_list.tsx b/src/plugins/event_annotation/public/components/table_list.tsx new file mode 100644 index 0000000000000..84535f2ffefc5 --- /dev/null +++ b/src/plugins/event_annotation/public/components/table_list.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, { useCallback, useState } from 'react'; +import { TableListViewTable } from '@kbn/content-management-table-list-view-table'; +import type { TableListTabParentProps } from '@kbn/content-management-tabbed-table-list-view'; +import { i18n } from '@kbn/i18n'; +import type { IUiSettingsClient } from '@kbn/core-ui-settings-browser'; +import { SavedObjectsFindOptionsReference } from '@kbn/core-saved-objects-api-browser'; +import { SavedObjectsTaggingApi } from '@kbn/saved-objects-tagging-oss-plugin/public'; +import { DataView, DataViewSpec } from '@kbn/data-views-plugin/common'; +import type { QueryInputServices } from '@kbn/visualization-ui-components/public'; +import { IToasts } from '@kbn/core-notifications-browser'; +import { EuiButton, EuiEmptyPrompt, EuiTitle } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { EventAnnotationGroupConfig } from '../../common'; +import type { EventAnnotationServiceType } from '../event_annotation_service/types'; +import { EventAnnotationGroupContent } from '../../common/types'; +import { GroupEditorFlyout } from './group_editor_flyout'; + +export const SAVED_OBJECTS_LIMIT_SETTING = 'savedObjects:listingLimit'; +export const SAVED_OBJECTS_PER_PAGE_SETTING = 'savedObjects:perPage'; + +const getCustomColumn = (dataViews: DataView[]) => { + const dataViewNameMap = Object.fromEntries( + dataViews.map((dataView) => [dataView.id, dataView.name ?? dataView.title]) + ); + + return { + field: 'dataView', + name: i18n.translate('eventAnnotation.tableList.dataView', { + defaultMessage: 'Data view', + }), + sortable: false, + width: '150px', + render: (_field: string, record: EventAnnotationGroupContent) => ( +
+ {record.attributes.dataViewSpec + ? record.attributes.dataViewSpec.name + : dataViewNameMap[record.attributes.indexPatternId]} +
+ ), + }; +}; + +export const EventAnnotationGroupTableList = ({ + uiSettings, + eventAnnotationService, + visualizeCapabilities, + savedObjectsTagging, + parentProps, + dataViews, + createDataView, + queryInputServices, + toasts, + navigateToLens, +}: { + uiSettings: IUiSettingsClient; + eventAnnotationService: EventAnnotationServiceType; + visualizeCapabilities: Record>; + savedObjectsTagging: SavedObjectsTaggingApi; + parentProps: TableListTabParentProps; + dataViews: DataView[]; + createDataView: (spec: DataViewSpec) => Promise; + queryInputServices: QueryInputServices; + toasts: IToasts; + navigateToLens: () => void; +}) => { + const listingLimit = uiSettings.get(SAVED_OBJECTS_LIMIT_SETTING); + const initialPageSize = uiSettings.get(SAVED_OBJECTS_PER_PAGE_SETTING); + + const [refreshListBouncer, setRefreshListBouncer] = useState(false); + + const refreshList = useCallback(() => { + setRefreshListBouncer(!refreshListBouncer); + }, [refreshListBouncer]); + + const fetchItems = useCallback( + ( + searchTerm: string, + { + references, + referencesToExclude, + }: { + references?: SavedObjectsFindOptionsReference[]; + referencesToExclude?: SavedObjectsFindOptionsReference[]; + } = {} + ) => { + // todo - allow page size changes + return eventAnnotationService.findAnnotationGroupContent( + searchTerm, + listingLimit, // TODO is this right? + references, + referencesToExclude + ); + }, + [eventAnnotationService, listingLimit] + ); + + const editItem = useCallback( + ({ id }: EventAnnotationGroupContent) => { + if (visualizeCapabilities.save) { + eventAnnotationService + .loadAnnotationGroup(id) + .then((group) => setGroupToEditInfo({ group, id })); + } + }, + [eventAnnotationService, visualizeCapabilities.save] + ); + + const [groupToEditInfo, setGroupToEditInfo] = useState<{ + group: EventAnnotationGroupConfig; + id: string; + }>(); + + const flyout = groupToEditInfo ? ( + setGroupToEditInfo({ group: newGroup, id: groupToEditInfo.id })} + onClose={() => setGroupToEditInfo(undefined)} + onSave={() => + (groupToEditInfo.id + ? eventAnnotationService.updateAnnotationGroup(groupToEditInfo.group, groupToEditInfo.id) + : eventAnnotationService.createAnnotationGroup(groupToEditInfo.group) + ).then(() => { + setGroupToEditInfo(undefined); + toasts.addSuccess(`Saved "${groupToEditInfo.group.title}"`); + refreshList(); + }) + } + savedObjectsTagging={savedObjectsTagging} + dataViews={dataViews} + createDataView={createDataView} + queryInputServices={queryInputServices} + /> + ) : undefined; + + return ( + <> + + refreshListBouncer={refreshListBouncer} + tableCaption={i18n.translate('eventAnnotation.tableList.listTitle', { + defaultMessage: 'Annotation Library', + })} + findItems={fetchItems} + deleteItems={ + visualizeCapabilities.delete + ? (items) => eventAnnotationService.deleteAnnotationGroups(items.map(({ id }) => id)) + : undefined + } + editItem={editItem} + listingLimit={listingLimit} + initialPageSize={initialPageSize} + initialFilter={''} + customTableColumn={getCustomColumn(dataViews)} + emptyPrompt={ + +

+ +

+ + } + body={ +

+ +

+ } + actions={ + + + + } + iconType="flag" + /> + } + entityName={i18n.translate('eventAnnotation.tableList.entityName', { + defaultMessage: 'annotation group', + })} + entityNamePlural={i18n.translate('eventAnnotation.tableList.entityNamePlural', { + defaultMessage: 'annotation groups', + })} + onClickTitle={editItem} + {...parentProps} + /> + {flyout} + + ); +}; diff --git a/src/plugins/event_annotation/public/event_annotation_service/__snapshots__/service.test.ts.snap b/src/plugins/event_annotation/public/event_annotation_service/__snapshots__/service.test.ts.snap index 73073c0e7ea16..0bd643095dcdc 100644 --- a/src/plugins/event_annotation/public/event_annotation_service/__snapshots__/service.test.ts.snap +++ b/src/plugins/event_annotation/public/event_annotation_service/__snapshots__/service.test.ts.snap @@ -1,5 +1,80 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`Event Annotation Service findAnnotationGroupContent should retrieve saved objects and format them 1`] = ` +Object { + "hits": Array [ + Object { + "attributes": Object { + "dataViewSpec": undefined, + "description": undefined, + "indexPatternId": undefined, + "title": undefined, + }, + "id": "nonExistingGroup", + "references": Array [], + "type": undefined, + "updatedAt": "", + }, + Object { + "attributes": Object { + "dataViewSpec": undefined, + "description": "", + "indexPatternId": "ipid", + "title": "groupTitle", + }, + "id": undefined, + "references": Array [ + Object { + "id": "ipid", + "name": "ipid", + "type": "index-pattern", + }, + Object { + "id": "some-tag", + "name": "some-tag", + "type": "tag", + }, + ], + "type": "event-annotation-group", + "updatedAt": "", + }, + Object { + "attributes": Object { + "dataViewSpec": undefined, + "description": undefined, + "indexPatternId": "ipid", + "title": "groupTitle", + }, + "id": "multiAnnotations", + "references": Array [ + Object { + "id": "ipid", + "name": "ipid", + "type": "index-pattern", + }, + ], + "type": "event-annotation-group", + "updatedAt": "", + }, + Object { + "attributes": Object { + "dataViewSpec": Object { + "id": "my-id", + }, + "description": undefined, + "indexPatternId": "my-id", + "title": "groupTitle", + }, + "id": "multiAnnotations", + "references": Array [], + "type": "event-annotation-group", + "updatedAt": "", + }, + ], + "total": 10, +} +`; + exports[`Event Annotation Service loadAnnotationGroup should properly load an annotation group with a multiple annotation 1`] = ` Object { "annotations": undefined, @@ -7,7 +82,7 @@ Object { "description": undefined, "ignoreGlobalFilters": undefined, "indexPatternId": "ipid", - "tags": undefined, + "tags": Array [], "title": "groupTitle", } `; diff --git a/src/plugins/event_annotation/public/event_annotation_service/helpers.ts b/src/plugins/event_annotation/public/event_annotation_service/helpers.ts index afe64a5a47eb2..31dabb34cdc09 100644 --- a/src/plugins/event_annotation/public/event_annotation_service/helpers.ts +++ b/src/plugins/event_annotation/public/event_annotation_service/helpers.ts @@ -5,7 +5,6 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ -import { i18n } from '@kbn/i18n'; import { euiLightVars } from '@kbn/ui-theme'; import { EventAnnotationConfig, @@ -17,13 +16,6 @@ export const defaultAnnotationColor = euiLightVars.euiColorAccent; // Do not compute it live as dependencies will add tens of Kbs to the plugin export const defaultAnnotationRangeColor = `#F04E981A`; // defaultAnnotationColor with opacity 0.1 -export const defaultAnnotationLabel = i18n.translate( - 'eventAnnotation.manualAnnotation.defaultAnnotationLabel', - { - defaultMessage: 'Event', - } -); - export const isRangeAnnotationConfig = ( annotation?: EventAnnotationConfig ): annotation is RangeEventAnnotationConfig => { diff --git a/src/plugins/event_annotation/public/event_annotation_service/service.test.ts b/src/plugins/event_annotation/public/event_annotation_service/service.test.ts index c131ca288a88d..905435bc4f4d0 100644 --- a/src/plugins/event_annotation/public/event_annotation_service/service.test.ts +++ b/src/plugins/event_annotation/public/event_annotation_service/service.test.ts @@ -6,6 +6,7 @@ * Side Public License, v 1. */ +import { SavedObjectsFindResponse } from '@kbn/core-saved-objects-api-browser'; import { CoreStart, SimpleSavedObject } from '@kbn/core/public'; import { coreMock } from '@kbn/core/public/mocks'; import { SavedObjectsManagementPluginStart } from '@kbn/saved-objects-management-plugin/public'; @@ -41,6 +42,11 @@ const annotationGroupResolveMocks: Record = name: 'ipid', type: 'index-pattern', }, + { + id: 'some-tag', + name: 'some-tag', + type: 'tag', + }, ], } as Partial as AnnotationGroupSavedObject, multiAnnotations: { @@ -138,6 +144,10 @@ describe('Event Annotation Service', () => { const typedId = id as keyof typeof annotationGroupResolveMocks; return annotationGroupResolveMocks[typedId]; }); + (core.savedObjects.client.find as jest.Mock).mockResolvedValue({ + total: 10, + savedObjects: Object.values(annotationGroupResolveMocks), + } as Pick, 'total' | 'savedObjects'>); (core.savedObjects.client.bulkCreate as jest.Mock).mockImplementation(() => { return annotationResolveMocks.multiAnnotations; }); @@ -474,7 +484,9 @@ describe('Event Annotation Service', () => { "description": "", "ignoreGlobalFilters": false, "indexPatternId": "ipid", - "tags": Array [], + "tags": Array [ + "some-tag", + ], "title": "groupTitle", } `); @@ -490,16 +502,53 @@ describe('Event Annotation Service', () => { expect(group.indexPatternId).toBe(group.dataViewSpec?.id); }); }); - // describe.skip('deleteAnnotationGroup', () => { - // it('deletes annotation group along with annotations that reference them', async () => { - // await eventAnnotationService.deleteAnnotationGroup('multiAnnotations'); - // expect(core.savedObjects.client.bulkDelete).toHaveBeenCalledWith([ - // { id: 'multiAnnotations', type: 'event-annotation-group' }, - // { id: 'annotation1', type: 'event-annotation' }, - // { id: 'annotation2', type: 'event-annotation' }, - // ]); - // }); - // }); + describe('findAnnotationGroupContent', () => { + it('should retrieve saved objects and format them', async () => { + const searchTerm = 'my search'; + + const content = await eventAnnotationService.findAnnotationGroupContent(searchTerm, 20, [ + { type: 'mytype', id: '1234' }, + ]); + + expect(content).toMatchSnapshot(); + + expect((core.savedObjects.client.find as jest.Mock).mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + Object { + "defaultSearchOperator": "AND", + "hasNoReference": undefined, + "hasReference": Array [ + Object { + "id": "1234", + "type": "mytype", + }, + ], + "page": 1, + "perPage": 20, + "search": "my search*", + "searchFields": Array [ + "title^3", + "description", + ], + "type": Array [ + "event-annotation-group", + ], + }, + ], + ] + `); + }); + }); + describe('deleteAnnotationGroups', () => { + it('deletes annotation group along with annotations that reference them', async () => { + await eventAnnotationService.deleteAnnotationGroups(['id1', 'id2']); + expect(core.savedObjects.client.bulkDelete).toHaveBeenCalledWith([ + { id: 'id1', type: 'event-annotation-group' }, + { id: 'id2', type: 'event-annotation-group' }, + ]); + }); + }); describe('createAnnotationGroup', () => { it('creates annotation group along with annotations', async () => { const annotations = [ @@ -509,7 +558,7 @@ describe('Event Annotation Service', () => { await eventAnnotationService.createAnnotationGroup({ title: 'newGroupTitle', description: 'my description', - tags: ['my', 'many', 'tags'], + tags: ['tag1', 'tag2', 'tag3'], indexPatternId: 'ipid', ignoreGlobalFilters: false, annotations, @@ -519,7 +568,6 @@ describe('Event Annotation Service', () => { { title: 'newGroupTitle', description: 'my description', - tags: ['my', 'many', 'tags'], ignoreGlobalFilters: false, dataViewSpec: null, annotations, @@ -531,6 +579,21 @@ describe('Event Annotation Service', () => { name: 'event-annotation-group_dataView-ref-ipid', type: 'index-pattern', }, + { + id: 'tag1', + name: 'tag1', + type: 'tag', + }, + { + id: 'tag2', + name: 'tag2', + type: 'tag', + }, + { + id: 'tag3', + name: 'tag3', + type: 'tag', + }, ], } ); @@ -555,11 +618,10 @@ describe('Event Annotation Service', () => { { title: 'newTitle', description: '', - tags: [], annotations: [], dataViewSpec: null, ignoreGlobalFilters: false, - }, + } as EventAnnotationGroupAttributes, { references: [ { @@ -572,72 +634,4 @@ describe('Event Annotation Service', () => { ); }); }); - // describe.skip('updateAnnotations', () => { - // const upsert = [ - // { - // id: 'annotation2', - // label: 'Query based event', - // icon: 'triangle', - // color: 'red', - // type: 'query', - // timeField: 'timestamp', - // key: { - // type: 'point_in_time', - // }, - // lineStyle: 'dashed', - // lineWidth: 3, - // filter: { type: 'kibana_query', query: '', language: 'kuery' }, - // }, - // { - // id: 'annotation4', - // label: 'Query based event', - // type: 'query', - // timeField: 'timestamp', - // key: { - // type: 'point_in_time', - // }, - // filter: { type: 'kibana_query', query: '', language: 'kuery' }, - // }, - // ] as EventAnnotationConfig[]; - // it('updates annotations - deletes annotations', async () => { - // await eventAnnotationService.updateAnnotations('multiAnnotations', { - // delete: ['annotation1', 'annotation2'], - // }); - // expect(core.savedObjects.client.bulkDelete).toHaveBeenCalledWith([ - // { id: 'annotation1', type: 'event-annotation' }, - // { id: 'annotation2', type: 'event-annotation' }, - // ]); - // }); - // it('updates annotations - inserts new annotations', async () => { - // await eventAnnotationService.updateAnnotations('multiAnnotations', { upsert }); - // expect(core.savedObjects.client.bulkCreate).toHaveBeenCalledWith([ - // { - // id: 'annotation2', - // type: 'event-annotation', - // attributes: upsert[0], - // overwrite: true, - // references: [ - // { - // id: 'multiAnnotations', - // name: 'event-annotation-group-ref-annotation2', - // type: 'event-annotation-group', - // }, - // ], - // }, - // { - // id: 'annotation4', - // type: 'event-annotation', - // attributes: upsert[1], - // overwrite: true, - // references: [ - // { - // id: 'multiAnnotations', - // name: 'event-annotation-group-ref-annotation4', - // type: 'event-annotation-group', - // }, - // ], - // }, - // ]); - // }); - // }); }); diff --git a/src/plugins/event_annotation/public/event_annotation_service/service.tsx b/src/plugins/event_annotation/public/event_annotation_service/service.tsx index a5ac2e265b0f7..65c2b9146df1c 100644 --- a/src/plugins/event_annotation/public/event_annotation_service/service.tsx +++ b/src/plugins/event_annotation/public/event_annotation_service/service.tsx @@ -10,9 +10,18 @@ import React from 'react'; import { partition } from 'lodash'; import { queryToAst } from '@kbn/data-plugin/common'; import { ExpressionAstExpression } from '@kbn/expressions-plugin/common'; -import { CoreStart, SavedObjectReference, SavedObjectsClientContract } from '@kbn/core/public'; +import { + CoreStart, + SavedObjectReference, + SavedObjectsClientContract, + SavedObjectsFindOptions, + SavedObjectsFindOptionsReference, + SimpleSavedObject, +} from '@kbn/core/public'; import { SavedObjectsManagementPluginStart } from '@kbn/saved-objects-management-plugin/public'; import { DataViewPersistableStateService } from '@kbn/data-views-plugin/common'; +import { defaultAnnotationLabel } from '../../common/manual_event_annotation'; +import { EventAnnotationGroupContent } from '../../common/types'; import { EventAnnotationConfig, EventAnnotationGroupAttributes, @@ -23,7 +32,6 @@ import { EventAnnotationServiceType } from './types'; import { defaultAnnotationColor, defaultAnnotationRangeColor, - defaultAnnotationLabel, isRangeAnnotationConfig, isQueryAnnotationConfig, } from './helpers'; @@ -39,18 +47,9 @@ export function getEventAnnotationService( ): EventAnnotationServiceType { const client: SavedObjectsClientContract = core.savedObjects.client; - const loadAnnotationGroup = async ( - savedObjectId: string - ): Promise => { - const savedObject = await client.get( - EVENT_ANNOTATION_GROUP_TYPE, - savedObjectId - ); - - if (savedObject.error) { - throw savedObject.error; - } - + const mapSavedObjectToGroupConfig = ( + savedObject: SimpleSavedObject + ): EventAnnotationGroupConfig => { const adHocDataViewSpec = savedObject.attributes.dataViewSpec ? DataViewPersistableStateService.inject( savedObject.attributes.dataViewSpec, @@ -61,7 +60,7 @@ export function getEventAnnotationService( return { title: savedObject.attributes.title, description: savedObject.attributes.description, - tags: savedObject.attributes.tags, + tags: savedObject.references.filter((ref) => ref.type === 'tag').map(({ id }) => id), ignoreGlobalFilters: savedObject.attributes.ignoreGlobalFilters, indexPatternId: adHocDataViewSpec ? adHocDataViewSpec.id! @@ -71,6 +70,71 @@ export function getEventAnnotationService( }; }; + const mapSavedObjectToGroupContent = ( + savedObject: SimpleSavedObject + ): EventAnnotationGroupContent => { + const groupConfig = mapSavedObjectToGroupConfig(savedObject); + + return { + id: savedObject.id, + references: savedObject.references, + type: savedObject.type, + updatedAt: savedObject.updatedAt ? savedObject.updatedAt : '', + attributes: { + title: groupConfig.title, + description: groupConfig.description, + indexPatternId: groupConfig.indexPatternId, + dataViewSpec: groupConfig.dataViewSpec, + }, + }; + }; + + const loadAnnotationGroup = async ( + savedObjectId: string + ): Promise => { + const savedObject = await client.get( + EVENT_ANNOTATION_GROUP_TYPE, + savedObjectId + ); + + if (savedObject.error) { + throw savedObject.error; + } + + return mapSavedObjectToGroupConfig(savedObject); + }; + + const findAnnotationGroupContent = async ( + searchTerm: string, + pageSize: number, + references?: SavedObjectsFindOptionsReference[], + referencesToExclude?: SavedObjectsFindOptionsReference[] + ): Promise<{ total: number; hits: EventAnnotationGroupContent[] }> => { + const searchOptions: SavedObjectsFindOptions = { + type: [EVENT_ANNOTATION_GROUP_TYPE], + searchFields: ['title^3', 'description'], + search: searchTerm ? `${searchTerm}*` : undefined, + perPage: pageSize, + page: 1, + defaultSearchOperator: 'AND' as const, + hasReference: references, + hasNoReference: referencesToExclude, + }; + + const { total, savedObjects } = await client.find( + searchOptions + ); + + return { + total, + hits: savedObjects.map(mapSavedObjectToGroupContent), + }; + }; + + const deleteAnnotationGroups = async (ids: string[]): Promise => { + await client.bulkDelete([...ids.map((id) => ({ type: EVENT_ANNOTATION_GROUP_TYPE, id }))]); + }; + const extractDataViewInformation = (group: EventAnnotationGroupConfig) => { let { dataViewSpec = null } = group; @@ -99,20 +163,35 @@ export function getEventAnnotationService( return { references, dataViewSpec }; }; - const createAnnotationGroup = async ( + const getAnnotationGroupAttributesAndReferences = ( group: EventAnnotationGroupConfig - ): Promise<{ id: string }> => { + ): { attributes: EventAnnotationGroupAttributes; references: SavedObjectReference[] } => { const { references, dataViewSpec } = extractDataViewInformation(group); const { title, description, tags, ignoreGlobalFilters, annotations } = group; + references.push( + ...tags.map((tag) => ({ + id: tag, + name: tag, + type: 'tag', + })) + ); + + return { + attributes: { title, description, ignoreGlobalFilters, annotations, dataViewSpec }, + references, + }; + }; + + const createAnnotationGroup = async ( + group: EventAnnotationGroupConfig + ): Promise<{ id: string }> => { + const { attributes, references } = getAnnotationGroupAttributesAndReferences(group); + const groupSavedObjectId = ( - await client.create( - EVENT_ANNOTATION_GROUP_TYPE, - { title, description, tags, ignoreGlobalFilters, annotations, dataViewSpec }, - { - references, - } - ) + await client.create(EVENT_ANNOTATION_GROUP_TYPE, attributes, { + references, + }) ).id; return { id: groupSavedObjectId }; @@ -122,17 +201,11 @@ export function getEventAnnotationService( group: EventAnnotationGroupConfig, annotationGroupId: string ): Promise => { - const { references, dataViewSpec } = extractDataViewInformation(group); - const { title, description, tags, ignoreGlobalFilters, annotations } = group; + const { attributes, references } = getAnnotationGroupAttributesAndReferences(group); - await client.update( - EVENT_ANNOTATION_GROUP_TYPE, - annotationGroupId, - { title, description, tags, ignoreGlobalFilters, annotations, dataViewSpec }, - { - references, - } - ); + await client.update(EVENT_ANNOTATION_GROUP_TYPE, annotationGroupId, attributes, { + references, + }); }; const checkHasAnnotationGroups = async (): Promise => { @@ -148,6 +221,8 @@ export function getEventAnnotationService( loadAnnotationGroup, updateAnnotationGroup, createAnnotationGroup, + deleteAnnotationGroups, + findAnnotationGroupContent, renderEventAnnotationGroupSavedObjectFinder: (props) => { return ( Promise; + findAnnotationGroupContent: ( + searchTerm: string, + pageSize: number, + references?: SavedObjectsFindOptionsReference[], + referencesToExclude?: SavedObjectsFindOptionsReference[] + ) => Promise<{ total: number; hits: EventAnnotationGroupContent[] }>; + deleteAnnotationGroups: (ids: string[]) => Promise; createAnnotationGroup: (group: EventAnnotationGroupConfig) => Promise<{ id: string }>; updateAnnotationGroup: ( group: EventAnnotationGroupConfig, diff --git a/src/plugins/event_annotation/public/get_table_list.tsx b/src/plugins/event_annotation/public/get_table_list.tsx new file mode 100644 index 0000000000000..5226d31894d15 --- /dev/null +++ b/src/plugins/event_annotation/public/get_table_list.tsx @@ -0,0 +1,61 @@ +/* + * 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, { FC } from 'react'; +import { toMountPoint } from '@kbn/kibana-react-plugin/public'; +import { FormattedRelative } from '@kbn/i18n-react'; +import { TableListViewKibanaProvider } from '@kbn/content-management-table-list-view-table'; +import { type TableListTabParentProps } from '@kbn/content-management-tabbed-table-list-view'; +import type { CoreStart } from '@kbn/core-lifecycle-browser'; +import type { SavedObjectsTaggingApi } from '@kbn/saved-objects-tagging-oss-plugin/public'; +import type { DataView, DataViewSpec } from '@kbn/data-views-plugin/common'; +import type { QueryInputServices } from '@kbn/visualization-ui-components/public'; +import { RootDragDropProvider } from '@kbn/dom-drag-drop'; +import type { EventAnnotationServiceType } from './event_annotation_service/types'; +import { EventAnnotationGroupTableList } from './components/table_list'; + +export interface EventAnnotationListingPageServices { + core: CoreStart; + savedObjectsTagging: SavedObjectsTaggingApi; + eventAnnotationService: EventAnnotationServiceType; + PresentationUtilContextProvider: FC; + dataViews: DataView[]; + createDataView: (spec: DataViewSpec) => Promise; + queryInputServices: QueryInputServices; +} + +export const getTableList = ( + parentProps: TableListTabParentProps, + services: EventAnnotationListingPageServices +) => { + return ( + + + services.core.application.navigateToApp('lens')} + /> + + + ); +}; diff --git a/src/plugins/event_annotation/public/index.ts b/src/plugins/event_annotation/public/index.ts index 58f6e2c7c9f22..1b96e39546bcb 100644 --- a/src/plugins/event_annotation/public/index.ts +++ b/src/plugins/event_annotation/public/index.ts @@ -21,3 +21,8 @@ export { isManualPointAnnotationConfig, isQueryAnnotationConfig, } from './event_annotation_service/helpers'; +export { + AnnotationEditorControls, + annotationsIconSet, +} from './components/annotation_editor_controls'; +export { getAnnotationAccessor } from './components/get_annotation_accessor'; diff --git a/src/plugins/event_annotation/public/plugin.ts b/src/plugins/event_annotation/public/plugin.ts index 4d390f308a474..576f8a3b2a8f0 100644 --- a/src/plugins/event_annotation/public/plugin.ts +++ b/src/plugins/event_annotation/public/plugin.ts @@ -6,10 +6,17 @@ * Side Public License, v 1. */ -import { Plugin, CoreSetup, CoreStart } from '@kbn/core/public'; -import { ExpressionsSetup } from '@kbn/expressions-plugin/public'; -import { SavedObjectsManagementPluginStart } from '@kbn/saved-objects-management-plugin/public'; -import { DataPublicPluginStart } from '@kbn/data-plugin/public'; +import type { Plugin, CoreSetup, CoreStart } from '@kbn/core/public'; +import type { PresentationUtilPluginStart } from '@kbn/presentation-util-plugin/public'; +import type { SavedObjectTaggingPluginStart } from '@kbn/saved-objects-tagging-plugin/public'; +import type { ExpressionsSetup } from '@kbn/expressions-plugin/public'; +import { Storage } from '@kbn/kibana-utils-plugin/public'; +import type { SavedObjectsManagementPluginStart } from '@kbn/saved-objects-management-plugin/public'; +import type { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public/types'; +import type { DataPublicPluginStart } from '@kbn/data-plugin/public'; +import type { VisualizationsSetup } from '@kbn/visualizations-plugin/public'; +import type { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public'; +import { i18n } from '@kbn/i18n'; import { EventAnnotationService } from './event_annotation_service'; import { manualPointEventAnnotation, @@ -18,14 +25,21 @@ import { eventAnnotationGroup, } from '../common'; import { getFetchEventAnnotations } from './fetch_event_annotations'; +import type { EventAnnotationListingPageServices } from './get_table_list'; +import { ANNOTATIONS_LISTING_VIEW_ID } from '../common/constants'; export interface EventAnnotationStartDependencies { savedObjectsManagement: SavedObjectsManagementPluginStart; data: DataPublicPluginStart; + savedObjectsTagging: SavedObjectTaggingPluginStart; + presentationUtil: PresentationUtilPluginStart; + dataViews: DataViewsPublicPluginStart; + unifiedSearch: UnifiedSearchPublicPluginStart; } interface SetupDependencies { expressions: ExpressionsSetup; + visualizations: VisualizationsSetup; } /** @public */ @@ -47,6 +61,46 @@ export class EventAnnotationPlugin dependencies.expressions.registerFunction( getFetchEventAnnotations({ getStartServices: core.getStartServices }) ); + + dependencies.visualizations.listingViewRegistry.add({ + title: i18n.translate('eventAnnotation.listingViewTitle', { + defaultMessage: 'Annotation groups', + }), + id: ANNOTATIONS_LISTING_VIEW_ID, + getTableList: async (props) => { + const [coreStart, pluginsStart] = await core.getStartServices(); + + const eventAnnotationService = await new EventAnnotationService( + coreStart, + pluginsStart.savedObjectsManagement + ).getService(); + + const ids = await pluginsStart.dataViews.getIds(); + const dataViews = await Promise.all(ids.map((id) => pluginsStart.dataViews.get(id))); + + const services: EventAnnotationListingPageServices = { + core: coreStart, + savedObjectsTagging: pluginsStart.savedObjectsTagging, + eventAnnotationService, + PresentationUtilContextProvider: pluginsStart.presentationUtil.ContextProvider, + dataViews, + createDataView: pluginsStart.dataViews.create.bind(pluginsStart.dataViews), + queryInputServices: { + http: coreStart.http, + docLinks: coreStart.docLinks, + notifications: coreStart.notifications, + uiSettings: coreStart.uiSettings, + dataViews: pluginsStart.dataViews, + unifiedSearch: pluginsStart.unifiedSearch, + data: pluginsStart.data, + storage: new Storage(localStorage), + }, + }; + + const { getTableList } = await import('./get_table_list'); + return getTableList(props, services); + }, + }); } public start( diff --git a/src/plugins/event_annotation/server/plugin.ts b/src/plugins/event_annotation/server/plugin.ts index 0ae55744016e6..d5e2fee433230 100644 --- a/src/plugins/event_annotation/server/plugin.ts +++ b/src/plugins/event_annotation/server/plugin.ts @@ -16,7 +16,6 @@ import { queryPointEventAnnotation, } from '../common'; import { setupSavedObjects } from './saved_objects'; -// import { getFetchEventAnnotations } from './fetch_event_annotations'; interface SetupDependencies { expressions: ExpressionsServerSetup; diff --git a/src/plugins/event_annotation/server/saved_objects.ts b/src/plugins/event_annotation/server/saved_objects.ts index 768def6b27f79..ef357aae0c546 100644 --- a/src/plugins/event_annotation/server/saved_objects.ts +++ b/src/plugins/event_annotation/server/saved_objects.ts @@ -14,7 +14,8 @@ import { } from '@kbn/core/server'; import { DataViewPersistableStateService } from '@kbn/data-views-plugin/common'; -import { EVENT_ANNOTATION_GROUP_TYPE } from '../common/constants'; +import { VISUALIZE_APP_NAME } from '@kbn/visualizations-plugin/common/constants'; +import { ANNOTATIONS_LISTING_VIEW_ID, EVENT_ANNOTATION_GROUP_TYPE } from '../common/constants'; import { EventAnnotationGroupAttributes } from '../common/types'; export function setupSavedObjects(coreSetup: CoreSetup) { @@ -28,6 +29,11 @@ export function setupSavedObjects(coreSetup: CoreSetup) { defaultSearchField: 'title', importableAndExportable: true, getTitle: (obj: { attributes: EventAnnotationGroupAttributes }) => obj.attributes.title, + getInAppUrl: (obj: { id: string }) => ({ + // TODO link to specific object + path: `/app/${VISUALIZE_APP_NAME}#/${ANNOTATIONS_LISTING_VIEW_ID}`, + uiCapabilitiesPath: 'visualize.show', + }), }, migrations: () => { const dataViewMigrations = DataViewPersistableStateService.getAllMigrations(); diff --git a/src/plugins/event_annotation/tsconfig.json b/src/plugins/event_annotation/tsconfig.json index 562bf05259c44..d8d9d61af2ac3 100644 --- a/src/plugins/event_annotation/tsconfig.json +++ b/src/plugins/event_annotation/tsconfig.json @@ -21,8 +21,29 @@ "@kbn/ui-theme", "@kbn/saved-objects-finder-plugin", "@kbn/saved-objects-management-plugin", + "@kbn/saved-objects-tagging-plugin", + "@kbn/presentation-util-plugin", + "@kbn/content-management-table-list-view", + "@kbn/visualizations-plugin", + "@kbn/data-views-plugin", + "@kbn/visualization-ui-components", + "@kbn/chart-icons", + "@kbn/unified-field-list-plugin", + "@kbn/dom-drag-drop", "@kbn/i18n-react", - "@kbn/core-saved-objects-server" + "@kbn/core-saved-objects-server", + "@kbn/test-jest-helpers", + "@kbn/saved-objects-tagging-oss-plugin", + "@kbn/core-saved-objects-api-browser", + "@kbn/kibana-react-plugin", + "@kbn/core-lifecycle-browser", + "@kbn/kibana-utils-plugin", + "@kbn/unified-search-plugin", + "@kbn/content-management-table-list-view", + "@kbn/content-management-table-list-view-table", + "@kbn/content-management-tabbed-table-list-view", + "@kbn/core-notifications-browser", + "@kbn/core-notifications-browser-mocks", ], "exclude": [ "target/**/*", diff --git a/src/plugins/files_management/public/app.tsx b/src/plugins/files_management/public/app.tsx index 3ee4e5f52720c..08f942c644789 100644 --- a/src/plugins/files_management/public/app.tsx +++ b/src/plugins/files_management/public/app.tsx @@ -9,7 +9,7 @@ import type { FunctionComponent } from 'react'; import React, { useState } from 'react'; import { EuiButtonEmpty } from '@elastic/eui'; -import { TableListView, UserContentCommonSchema } from '@kbn/content-management-table-list'; +import { TableListView, UserContentCommonSchema } from '@kbn/content-management-table-list-view'; import numeral from '@elastic/numeral'; import type { FileJSON } from '@kbn/files-plugin/common'; @@ -42,8 +42,8 @@ export const App: FunctionComponent = () => { return (
- tableListTitle={i18nTexts.tableListTitle} - tableListDescription={i18nTexts.tableListDescription} + title={i18nTexts.tableListTitle} + description={i18nTexts.tableListDescription} titleColumnName={i18nTexts.titleColumnName} emptyPrompt={} entityName={i18nTexts.entityName} diff --git a/src/plugins/files_management/public/mount_management_section.tsx b/src/plugins/files_management/public/mount_management_section.tsx index 9c7091516d46e..229e1d2b306f6 100755 --- a/src/plugins/files_management/public/mount_management_section.tsx +++ b/src/plugins/files_management/public/mount_management_section.tsx @@ -17,7 +17,7 @@ import type { ManagementAppMountParams } from '@kbn/management-plugin/public'; import { TableListViewKibanaProvider, TableListViewKibanaDependencies, -} from '@kbn/content-management-table-list'; +} from '@kbn/content-management-table-list-view-table'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import type { StartDependencies } from './types'; import { App } from './app'; diff --git a/src/plugins/files_management/tsconfig.json b/src/plugins/files_management/tsconfig.json index d15175fae0470..28986030e75f8 100644 --- a/src/plugins/files_management/tsconfig.json +++ b/src/plugins/files_management/tsconfig.json @@ -9,7 +9,8 @@ "@kbn/files-plugin", "@kbn/management-plugin", "@kbn/i18n", - "@kbn/content-management-table-list", + "@kbn/content-management-table-list-view-table", + "@kbn/content-management-table-list-view", "@kbn/kibana-react-plugin", "@kbn/i18n-react", "@kbn/shared-ux-file-image", diff --git a/src/plugins/visualization_ui_components/common/index.ts b/src/plugins/visualization_ui_components/common/index.ts new file mode 100644 index 0000000000000..3f66f3b659d8a --- /dev/null +++ b/src/plugins/visualization_ui_components/common/index.ts @@ -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 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 {}; diff --git a/src/plugins/visualization_ui_components/common/types.ts b/src/plugins/visualization_ui_components/common/types.ts new file mode 100644 index 0000000000000..7da9f13aced8d --- /dev/null +++ b/src/plugins/visualization_ui_components/common/types.ts @@ -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 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 type LineStyle = 'solid' | 'dashed' | 'dotted'; diff --git a/src/plugins/visualization_ui_components/kibana.jsonc b/src/plugins/visualization_ui_components/kibana.jsonc index f7d42af513338..6a2ed84a93149 100644 --- a/src/plugins/visualization_ui_components/kibana.jsonc +++ b/src/plugins/visualization_ui_components/kibana.jsonc @@ -8,7 +8,11 @@ "browser": true, "requiredBundles": [ "unifiedSearch", - "unifiedFieldList" + "unifiedFieldList", + "dataViews" + ], + "extraPublicDirs": [ + "common" ] } } diff --git a/src/plugins/visualization_ui_components/public/components/dimension_buttons/dimension_button.tsx b/src/plugins/visualization_ui_components/public/components/dimension_buttons/dimension_button.tsx index 4a91d74ebe5f6..7c9cbb881c2f3 100644 --- a/src/plugins/visualization_ui_components/public/components/dimension_buttons/dimension_button.tsx +++ b/src/plugins/visualization_ui_components/public/components/dimension_buttons/dimension_button.tsx @@ -7,7 +7,14 @@ */ import React from 'react'; -import { EuiButtonIcon, EuiLink, EuiToolTip, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { + EuiButtonIcon, + EuiLink, + EuiToolTip, + EuiFlexGroup, + EuiFlexItem, + useEuiFontSize, +} from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { css } from '@emotion/react'; import { euiThemeVars } from '@kbn/ui-theme'; @@ -41,12 +48,38 @@ export function DimensionButton({ message?: Message; }) { return ( -
+
onClick(accessorConfig.columnId)} aria-label={triggerLinkA11yText(label)} @@ -82,7 +115,11 @@ export function DimensionButton({ })} onClick={() => onRemoveClick(accessorConfig.columnId)} css={css` + margin-right: ${euiThemeVars.euiSizeS}; + visibility: hidden; + opacity: 0; color: ${euiThemeVars.euiTextSubduedColor}; + &:hover { color: ${euiThemeVars.euiColorDangerText}; } diff --git a/src/plugins/visualization_ui_components/public/components/dimension_buttons/dimension_button_icon.tsx b/src/plugins/visualization_ui_components/public/components/dimension_buttons/dimension_button_icon.tsx index bcb3ddb1e44bb..6294c27254a56 100644 --- a/src/plugins/visualization_ui_components/public/components/dimension_buttons/dimension_button_icon.tsx +++ b/src/plugins/visualization_ui_components/public/components/dimension_buttons/dimension_button_icon.tsx @@ -9,10 +9,14 @@ import React from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiIcon } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import { css } from '@emotion/react'; +import { euiThemeVars } from '@kbn/ui-theme'; import type { AccessorConfig, Message } from './types'; const baseIconProps = { - className: 'lnsLayerPanel__colorIndicator', + css: css` + margin-left: ${euiThemeVars.euiSizeS}; + `, } as const; const getIconFromAccessorConfig = (accessorConfig: AccessorConfig) => ( diff --git a/src/plugins/visualization_ui_components/public/components/dimension_buttons/empty_button.tsx b/src/plugins/visualization_ui_components/public/components/dimension_buttons/empty_button.tsx new file mode 100644 index 0000000000000..e66d512e80f73 --- /dev/null +++ b/src/plugins/visualization_ui_components/public/components/dimension_buttons/empty_button.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 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 { css } from '@emotion/react'; +import { euiThemeVars } from '@kbn/ui-theme'; +import { EuiButtonEmpty } from '@elastic/eui'; +import { DimensionTrigger } from './trigger'; + +export const EmptyDimensionButton = ({ + label, + ariaLabel, + onClick, + dataTestSubj, + iconType, + ...otherProps // from Drag&Drop integration +}: { + label: React.ReactNode; + ariaLabel: string; + onClick: () => void; + dataTestSubj?: string; + iconType?: string; +}) => { + return ( + { + onClick(); + }} + > + + + ); +}; diff --git a/src/plugins/visualization_ui_components/public/components/dimension_buttons/index.ts b/src/plugins/visualization_ui_components/public/components/dimension_buttons/index.ts index 54df2c7911488..b3037035fb57b 100644 --- a/src/plugins/visualization_ui_components/public/components/dimension_buttons/index.ts +++ b/src/plugins/visualization_ui_components/public/components/dimension_buttons/index.ts @@ -8,4 +8,8 @@ export * from './dimension_button'; +export * from './empty_button'; + +export * from './trigger'; + export type { AccessorConfig } from './types'; diff --git a/src/plugins/visualization_ui_components/public/components/dimension_buttons/palette_indicator.tsx b/src/plugins/visualization_ui_components/public/components/dimension_buttons/palette_indicator.tsx index 5838a7e5c5236..ae627c4a11b15 100644 --- a/src/plugins/visualization_ui_components/public/components/dimension_buttons/palette_indicator.tsx +++ b/src/plugins/visualization_ui_components/public/components/dimension_buttons/palette_indicator.tsx @@ -8,14 +8,32 @@ import React from 'react'; import { EuiColorPaletteDisplay } from '@elastic/eui'; +import { css } from '@emotion/react'; +import { euiThemeVars } from '@kbn/ui-theme'; import type { AccessorConfig } from './types'; export function PaletteIndicator({ accessorConfig }: { accessorConfig: AccessorConfig }) { if (accessorConfig.triggerIconType !== 'colorBy' || !accessorConfig.palette) return null; return ( -
+
diff --git a/src/plugins/visualization_ui_components/public/components/dimension_buttons/trigger.tsx b/src/plugins/visualization_ui_components/public/components/dimension_buttons/trigger.tsx new file mode 100644 index 0000000000000..c050582332b05 --- /dev/null +++ b/src/plugins/visualization_ui_components/public/components/dimension_buttons/trigger.tsx @@ -0,0 +1,70 @@ +/* + * 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 { EuiText, EuiFlexItem } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import React from 'react'; +import { EuiTextProps } from '@elastic/eui/src/components/text/text'; +import { css } from '@emotion/react'; +import { euiThemeVars } from '@kbn/ui-theme'; + +export const defaultDimensionTriggerTooltip = ( +

+ {i18n.translate('visualizationUiComponents.configure.invalidConfigTooltip', { + defaultMessage: 'Invalid configuration.', + })} +
+ {i18n.translate('visualizationUiComponents.configure.invalidConfigTooltipClick', { + defaultMessage: 'Click for more details.', + })} +

+); + +export const DimensionTrigger = ({ + id, + label, + color, + dataTestSubj, +}: { + label: React.ReactNode; + id?: string; + color?: EuiTextProps['color']; + dataTestSubj?: string; +}) => { + return ( + + + + + {label} + + + + + ); +}; diff --git a/src/plugins/visualization_ui_components/public/components/index.ts b/src/plugins/visualization_ui_components/public/components/index.ts index 4de1805bc472a..e20879f9e9990 100644 --- a/src/plugins/visualization_ui_components/public/components/index.ts +++ b/src/plugins/visualization_ui_components/public/components/index.ts @@ -28,8 +28,14 @@ export * from './dimension_editor_section'; export * from './dimension_buttons'; +export * from './line_style_settings'; + +export * from './text_decoration_setting'; + export type { AccessorConfig } from './dimension_buttons'; export type { FieldOptionValue, FieldOption, DataType } from './field_picker'; export type { IconSet } from './icon_select'; + +export type { QueryInputServices } from './query_input'; diff --git a/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/shared/line_style_settings.tsx b/src/plugins/visualization_ui_components/public/components/line_style_settings.tsx similarity index 83% rename from x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/shared/line_style_settings.tsx rename to src/plugins/visualization_ui_components/public/components/line_style_settings.tsx index a479daeb75919..0b7f09b6a7444 100644 --- a/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/shared/line_style_settings.tsx +++ b/src/plugins/visualization_ui_components/public/components/line_style_settings.tsx @@ -1,8 +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. + * 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'; @@ -14,30 +15,28 @@ import { EuiFlexItem, EuiFormRow, } from '@elastic/eui'; -import { LineStyle } from '@kbn/expression-xy-plugin/common'; - -import { idPrefix } from '../dimension_editor'; +import { LineStyle } from '../../common/types'; interface LineStyleConfig { - lineStyle?: Exclude; + lineStyle?: LineStyle; lineWidth?: number; } export const LineStyleSettings = ({ currentConfig, setConfig, - isHorizontal, + idPrefix, }: { currentConfig?: LineStyleConfig; setConfig: (config: LineStyleConfig) => void; - isHorizontal: boolean; + idPrefix: string; }) => { return ( <> @@ -52,7 +51,7 @@ export const LineStyleSettings = ({ @@ -109,7 +110,7 @@ export function FilterQueryInput({ {/* Empty for spacing */} { setFilterPopoverOpen(!filterPopoverOpen); diff --git a/src/plugins/visualization_ui_components/public/components/text_decoration_setting.tsx b/src/plugins/visualization_ui_components/public/components/text_decoration_setting.tsx new file mode 100644 index 0000000000000..7f52fc1935e01 --- /dev/null +++ b/src/plugins/visualization_ui_components/public/components/text_decoration_setting.tsx @@ -0,0 +1,122 @@ +/* + * 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 { EuiButtonGroup, EuiFormRow } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import React, { useState } from 'react'; + +interface TextDecorationConfig { + textVisibility?: boolean; + textField?: string; +} + +function getSelectedOption( + { textField, textVisibility }: TextDecorationConfig = {}, + isQueryBased?: boolean +) { + if (!textVisibility) { + return 'none'; + } + if (isQueryBased && textField) { + return 'field'; + } + return 'name'; +} + +export function TextDecorationSetting({ + idPrefix, + currentConfig, + setConfig, + isQueryBased, + children, +}: { + idPrefix: string; + currentConfig?: TextDecorationConfig; + setConfig: (config: TextDecorationConfig) => void; + isQueryBased?: boolean; + /** A children render function for custom sub fields on textDecoration change */ + children?: (textDecoration: 'none' | 'name' | 'field') => JSX.Element | null; +}) { + // To model the temporary state for label based on field when user didn't pick up the field yet, + // use a local state + const [selectedVisibleOption, setVisibleOption] = useState<'none' | 'name' | 'field'>( + getSelectedOption(currentConfig, isQueryBased) + ); + const options = [ + { + id: `${idPrefix}none`, + label: i18n.translate('visualizationUiComponents.xyChart.lineMarker.textVisibility.none', { + defaultMessage: 'None', + }), + 'data-test-subj': 'lnsXY_textVisibility_none', + }, + { + id: `${idPrefix}name`, + label: i18n.translate('visualizationUiComponents.xyChart.lineMarker.textVisibility.name', { + defaultMessage: 'Name', + }), + 'data-test-subj': 'lnsXY_textVisibility_name', + }, + ]; + if (isQueryBased) { + options.push({ + id: `${idPrefix}field`, + label: i18n.translate('visualizationUiComponents.xyChart.lineMarker.textVisibility.field', { + defaultMessage: 'Field', + }), + 'data-test-subj': 'lnsXY_textVisibility_field', + }); + } + + return ( + +
+ { + const chosenOption = id.replace(idPrefix, '') as 'none' | 'name' | 'field'; + if (chosenOption === 'none') { + setConfig({ + textVisibility: false, + textField: undefined, + }); + } else if (chosenOption === 'name') { + setConfig({ + textVisibility: true, + textField: undefined, + }); + } else if (chosenOption === 'field') { + setConfig({ + textVisibility: Boolean(currentConfig?.textField), + }); + } + + setVisibleOption(chosenOption); + }} + isFullWidth + /> + {children?.(selectedVisibleOption)} +
+
+ ); +} diff --git a/src/plugins/visualization_ui_components/public/index.ts b/src/plugins/visualization_ui_components/public/index.ts index d0495697dad01..b8be5d3afbd78 100644 --- a/src/plugins/visualization_ui_components/public/index.ts +++ b/src/plugins/visualization_ui_components/public/index.ts @@ -28,16 +28,25 @@ export { isQueryValid, DimensionEditorSection, DimensionButton, + DimensionTrigger, + EmptyDimensionButton, + LineStyleSettings, + TextDecorationSetting, } from './components'; +export { isFieldLensCompatible } from './util'; + export type { DataType, FieldOptionValue, FieldOption, IconSet, AccessorConfig, + QueryInputServices, } from './components'; +export type { FormatFactory } from './types'; + export function plugin() { return new VisualizationUiComponentsPlugin(); } diff --git a/src/plugins/visualization_ui_components/public/types.ts b/src/plugins/visualization_ui_components/public/types.ts new file mode 100644 index 0000000000000..8d259c3b90f90 --- /dev/null +++ b/src/plugins/visualization_ui_components/public/types.ts @@ -0,0 +1,11 @@ +/* + * 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 type { IFieldFormat, SerializedFieldFormat } from '@kbn/field-formats-plugin/common'; + +export type FormatFactory = (mapping?: SerializedFieldFormat) => IFieldFormat; diff --git a/src/plugins/visualization_ui_components/public/util.ts b/src/plugins/visualization_ui_components/public/util.ts new file mode 100644 index 0000000000000..2eb71ee858759 --- /dev/null +++ b/src/plugins/visualization_ui_components/public/util.ts @@ -0,0 +1,12 @@ +/* + * 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 { DataViewField, isNestedField } from '@kbn/data-views-plugin/common'; + +export const isFieldLensCompatible = (field: DataViewField) => + !isNestedField(field) && (!!field.aggregatable || !!field.scripted); diff --git a/src/plugins/visualization_ui_components/tsconfig.json b/src/plugins/visualization_ui_components/tsconfig.json index 7175696eef5f5..cbafd595ecc62 100644 --- a/src/plugins/visualization_ui_components/tsconfig.json +++ b/src/plugins/visualization_ui_components/tsconfig.json @@ -24,7 +24,8 @@ "@kbn/core-doc-links-browser", "@kbn/core", "@kbn/ui-theme", - "@kbn/coloring" + "@kbn/coloring", + "@kbn/field-formats-plugin" ], "exclude": [ "target/**/*", diff --git a/src/plugins/visualizations/common/constants.ts b/src/plugins/visualizations/common/constants.ts index f3e2fd30c4288..7b6e18708f3f9 100644 --- a/src/plugins/visualizations/common/constants.ts +++ b/src/plugins/visualizations/common/constants.ts @@ -20,6 +20,7 @@ export const VISUALIZE_APP_NAME = 'visualize'; export const VisualizeConstants = { VISUALIZE_BASE_PATH: '/app/visualize', LANDING_PAGE_PATH: '/', + LANDING_PAGE_PATH_WITH_TAB: '/:activeTab', WIZARD_STEP_1_PAGE_PATH: '/new', WIZARD_STEP_2_PAGE_PATH: '/new/configure', CREATE_PATH: '/create', diff --git a/src/plugins/visualizations/public/mocks.ts b/src/plugins/visualizations/public/mocks.ts index 4c9ea92d83739..9bc31adccd3ba 100644 --- a/src/plugins/visualizations/public/mocks.ts +++ b/src/plugins/visualizations/public/mocks.ts @@ -35,6 +35,7 @@ const createSetupContract = (): VisualizationsSetup => ({ createBaseVisualization: jest.fn(), registerAlias: jest.fn(), visEditorsRegistry: { registerDefault: jest.fn(), register: jest.fn(), get: jest.fn() }, + listingViewRegistry: { add: jest.fn() }, }); const createStartContract = (): VisualizationsStart => ({ diff --git a/src/plugins/visualizations/public/plugin.ts b/src/plugins/visualizations/public/plugin.ts index 4e6d3d429eb1d..2b906620e5f8b 100644 --- a/src/plugins/visualizations/public/plugin.ts +++ b/src/plugins/visualizations/public/plugin.ts @@ -110,6 +110,7 @@ import { } from './services'; import { VisualizeConstants } from '../common/constants'; import { EditInLensAction } from './actions/edit_in_lens_action'; +import { ListingViewRegistry } from './types'; import { LATEST_VERSION, CONTENT_ID } from '../common/content_management'; /** @@ -118,8 +119,10 @@ import { LATEST_VERSION, CONTENT_ID } from '../common/content_management'; * @public */ -export type VisualizationsSetup = TypesSetup & { visEditorsRegistry: VisEditorsRegistry }; - +export type VisualizationsSetup = TypesSetup & { + visEditorsRegistry: VisEditorsRegistry; + listingViewRegistry: ListingViewRegistry; +}; export interface VisualizationsStart extends TypesStart { showNewVisModal: typeof showNewVisModal; } @@ -246,6 +249,7 @@ export class VisualizationsPlugin }; const start = createStartServicesGetter(core.getStartServices); + const listingViewRegistry: ListingViewRegistry = new Set(); const visEditorsRegistry = createVisEditorsRegistry(); core.application.register({ @@ -321,6 +325,7 @@ export class VisualizationsPlugin getKibanaVersion: () => this.initializerContext.env.packageInfo.version, spaces: pluginsStart.spaces, visEditorsRegistry, + listingViewRegistry, unifiedSearch: pluginsStart.unifiedSearch, }; @@ -388,6 +393,7 @@ export class VisualizationsPlugin return { ...this.types.setup(), visEditorsRegistry, + listingViewRegistry, }; } diff --git a/src/plugins/visualizations/public/types.ts b/src/plugins/visualizations/public/types.ts index c69f7894fa326..c23f40dc85bae 100644 --- a/src/plugins/visualizations/public/types.ts +++ b/src/plugins/visualizations/public/types.ts @@ -16,6 +16,7 @@ import { import type { ISearchSource } from '@kbn/data-plugin/common'; import { ExpressionAstExpression } from '@kbn/expressions-plugin/public'; +import type { TableListTab } from '@kbn/content-management-tabbed-table-list-view'; import type { Vis } from './vis'; import type { PersistedState } from './persisted_state'; import type { VisParams, SerializedVis } from '../common'; @@ -94,3 +95,5 @@ export interface VisEditorOptionsProps { setValidity(isValid: boolean): void; setTouched(isTouched: boolean): void; } + +export type ListingViewRegistry = Pick, 'add'>; diff --git a/src/plugins/visualizations/public/visualize_app/app.tsx b/src/plugins/visualizations/public/visualize_app/app.tsx index 0a4e46288616f..625c0846221a5 100644 --- a/src/plugins/visualizations/public/visualize_app/app.tsx +++ b/src/plugins/visualizations/public/visualize_app/app.tsx @@ -139,7 +139,11 @@ export const VisualizeApp = ({ onAppLeave }: VisualizeAppProps) => { diff --git a/src/plugins/visualizations/public/visualize_app/components/visualize_listing.tsx b/src/plugins/visualizations/public/visualize_app/components/visualize_listing.tsx index 8dd9885ef5520..e169a7ebaa034 100644 --- a/src/plugins/visualizations/public/visualize_app/components/visualize_listing.tsx +++ b/src/plugins/visualizations/public/visualize_app/components/visualize_listing.tsx @@ -8,20 +8,34 @@ import './visualize_listing.scss'; -import React, { useCallback, useRef, useMemo, useEffect, MouseEvent } from 'react'; +import React, { + useCallback, + useRef, + useMemo, + useEffect, + MouseEvent, + MutableRefObject, +} from 'react'; import { EuiCallOut, EuiLink, EuiSpacer } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; import useUnmount from 'react-use/lib/useUnmount'; import useMount from 'react-use/lib/useMount'; -import { useLocation } from 'react-router-dom'; +import { useLocation, useParams } from 'react-router-dom'; import type { SavedObjectReference } from '@kbn/core/public'; import { useKibana, useExecutionContext } from '@kbn/kibana-react-plugin/public'; -import { TableListView } from '@kbn/content-management-table-list'; +import { + TabbedTableListView, + type TableListTab, +} from '@kbn/content-management-tabbed-table-list-view'; import type { OpenContentEditorParams } from '@kbn/content-management-content-editor'; -import type { UserContentCommonSchema } from '@kbn/content-management-table-list'; +import { + type UserContentCommonSchema, + TableListViewProps, +} from '@kbn/content-management-table-list-view'; +import { TableListViewTable } from '@kbn/content-management-table-list-view-table'; import { findListItems } from '../../utils/saved_visualize_utils'; import { updateBasicSoAttributes } from '../../utils/saved_objects_utils/update_basic_attributes'; import { checkForDuplicateTitle } from '../../utils/saved_objects_utils/check_for_duplicate_title'; @@ -71,71 +85,38 @@ const toTableListViewSavedObject = (savedObject: Record): Visua }, }; }; +type CustomTableViewProps = Pick< + TableListViewProps, + | 'createItem' + | 'findItems' + | 'deleteItems' + | 'editItem' + | 'contentEditor' + | 'emptyPrompt' + | 'showEditActionForItem' +>; -export const VisualizeListing = () => { +const useTableListViewProps = ( + closeNewVisModal: MutableRefObject<() => void>, + listingLimit: number +): CustomTableViewProps => { const { services: { - core, application, - executionContext, - chrome, history, - toastNotifications, - stateTransferService, savedObjects, - uiSettings, - visualizeCapabilities, - dashboardCapabilities, - kbnUrlStateStorage, - overlays, savedObjectsTagging, + overlays, + toastNotifications, + visualizeCapabilities, }, } = useKibana(); - const { pathname } = useLocation(); - const closeNewVisModal = useRef(() => {}); - const visualizedUserContent = useRef(); - const listingLimit = uiSettings.get(SAVED_OBJECTS_LIMIT_SETTING); - const initialPageSize = uiSettings.get(SAVED_OBJECTS_PER_PAGE_SETTING); - - useExecutionContext(executionContext, { - type: 'application', - page: 'list', - }); - - useEffect(() => { - if (pathname === '/new') { - // In case the user navigated to the page via the /visualize/new URL we start the dialog immediately - closeNewVisModal.current = showNewVisModal({ - onClose: () => { - // In case the user came via a URL to this page, change the URL to the regular landing page URL after closing the modal - history.push(VisualizeConstants.LANDING_PAGE_PATH); - }, - }); - } else { - // close modal window if exists - closeNewVisModal.current(); - } - }, [history, pathname]); - useMount(() => { - // Reset editor state for all apps if the visualize listing page is loaded. - stateTransferService.clearEditorState(); - chrome.setBreadcrumbs([ - { - text: i18n.translate('visualizations.visualizeListingBreadcrumbsTitle', { - defaultMessage: 'Visualize Library', - }), - }, - ]); - chrome.docTitle.change( - i18n.translate('visualizations.listingPageTitle', { defaultMessage: 'Visualize Library' }) - ); - }); - useUnmount(() => closeNewVisModal.current()); + const visualizedUserContent = useRef(); const createNewVis = useCallback(() => { closeNewVisModal.current = showNewVisModal(); - }, []); + }, [closeNewVisModal]); const editItem = useCallback( ({ attributes: { editUrl, editApp } }: VisualizeUserContent) => { @@ -259,74 +240,172 @@ export const VisualizeListing = () => { [savedObjects.client, toastNotifications] ); - const calloutMessage = ( - { - event.preventDefault(); - application.navigateToUrl(application.getUrlForApp('dashboards')); - }} - > - -
- ), - }} - /> + const props: CustomTableViewProps = { + findItems: fetchItems, + deleteItems, + contentEditor: { + isReadonly: !visualizeCapabilities.save, + onSave: onContentEditorSave, + customValidators: contentEditorValidators, + }, + editItem, + emptyPrompt: noItemsFragment, + createItem: createNewVis, + showEditActionForItem: ({ attributes: { readOnly } }) => + visualizeCapabilities.save && !readOnly, + }; + + return props; +}; + +export const VisualizeListing = () => { + const { + services: { + application, + executionContext, + chrome, + history, + stateTransferService, + dashboardCapabilities, + uiSettings, + kbnUrlStateStorage, + listingViewRegistry, + }, + } = useKibana(); + const { pathname } = useLocation(); + const closeNewVisModal = useRef(() => {}); + + useExecutionContext(executionContext, { + type: 'application', + page: 'list', + }); + + useEffect(() => { + if (pathname === '/new') { + // In case the user navigated to the page via the /visualize/new URL we start the dialog immediately + closeNewVisModal.current = showNewVisModal({ + onClose: () => { + // In case the user came via a URL to this page, change the URL to the regular landing page URL after closing the modal + history.push(VisualizeConstants.LANDING_PAGE_PATH); + }, + }); + } else { + // close modal window if exists + closeNewVisModal.current(); + } + }, [history, pathname]); + + useMount(() => { + // Reset editor state for all apps if the visualize listing page is loaded. + stateTransferService.clearEditorState(); + chrome.setBreadcrumbs([ + { + text: i18n.translate('visualizations.visualizeListingBreadcrumbsTitle', { + defaultMessage: 'Visualize Library', + }), + }, + ]); + chrome.docTitle.change( + i18n.translate('visualizations.listingPageTitle', { defaultMessage: 'Visualize Library' }) + ); + }); + useUnmount(() => closeNewVisModal.current()); + + const listingLimit = uiSettings.get(SAVED_OBJECTS_LIMIT_SETTING); + const initialPageSize = uiSettings.get(SAVED_OBJECTS_PER_PAGE_SETTING); + + const tableViewProps = useTableListViewProps(closeNewVisModal, listingLimit); + + const visualizeLibraryTitle = i18n.translate('visualizations.listing.table.listTitle', { + defaultMessage: 'Visualize Library', + }); + + const visualizeTab: TableListTab = useMemo(() => { + const calloutMessage = ( + { + event.preventDefault(); + application.navigateToUrl(application.getUrlForApp('dashboards')); + }} + > + + + ), + }} + /> + ); + + return { + title: 'Visualizations', + id: 'visualizations', + getTableList: (propsFromParent) => ( + <> + {dashboardCapabilities.createNew && ( + <> + + + + )} + + id="vis" + // we allow users to create visualizations even if they can't save them + // for data exploration purposes + customTableColumn={getCustomColumn()} + listingLimit={listingLimit} + initialPageSize={initialPageSize} + initialFilter={''} + entityName={i18n.translate('visualizations.listing.table.entityName', { + defaultMessage: 'visualization', + })} + entityNamePlural={i18n.translate('visualizations.listing.table.entityNamePlural', { + defaultMessage: 'visualizations', + })} + getDetailViewLink={({ attributes: { editApp, editUrl, error } }) => + getVisualizeListItemLink(application, kbnUrlStateStorage, editApp, editUrl, error) + } + tableCaption={visualizeLibraryTitle} + {...tableViewProps} + {...propsFromParent} + /> + + ), + }; + }, [ + application, + dashboardCapabilities.createNew, + initialPageSize, + kbnUrlStateStorage, + listingLimit, + tableViewProps, + visualizeLibraryTitle, + ]); + + const tabs = useMemo( + () => [visualizeTab, ...Array.from(listingViewRegistry as Set)], + [listingViewRegistry, visualizeTab] ); + const { activeTab } = useParams<{ activeTab: string }>(); + return ( - - id="vis" + - visualizeCapabilities.save && !readOnly - } - customTableColumn={getCustomColumn()} - listingLimit={listingLimit} - initialPageSize={initialPageSize} - initialFilter={''} - contentEditor={{ - isReadonly: !visualizeCapabilities.save, - onSave: onContentEditorSave, - customValidators: contentEditorValidators, + title={visualizeLibraryTitle} + tabs={tabs} + activeTabId={activeTab} + changeActiveTab={(id) => { + application.navigateToUrl(`#/${id}`); }} - emptyPrompt={noItemsFragment} - entityName={i18n.translate('visualizations.listing.table.entityName', { - defaultMessage: 'visualization', - })} - entityNamePlural={i18n.translate('visualizations.listing.table.entityNamePlural', { - defaultMessage: 'visualizations', - })} - tableListTitle={i18n.translate('visualizations.listing.table.listTitle', { - defaultMessage: 'Visualize Library', - })} - getDetailViewLink={({ attributes: { editApp, editUrl, error, readOnly } }) => - readOnly - ? undefined - : getVisualizeListItemLink(core.application, kbnUrlStateStorage, editApp, editUrl, error) - } - > - {dashboardCapabilities.createNew && ( - <> - - - - )} - + /> ); }; diff --git a/src/plugins/visualizations/public/visualize_app/index.tsx b/src/plugins/visualizations/public/visualize_app/index.tsx index e432275c755e6..0dc41f8f35d07 100644 --- a/src/plugins/visualizations/public/visualize_app/index.tsx +++ b/src/plugins/visualizations/public/visualize_app/index.tsx @@ -17,7 +17,7 @@ import { toMountPoint, } from '@kbn/kibana-react-plugin/public'; import { FormattedRelative } from '@kbn/i18n-react'; -import { TableListViewKibanaProvider } from '@kbn/content-management-table-list'; +import { TableListViewKibanaProvider } from '@kbn/content-management-table-list-view-table'; import { VisualizeApp } from './app'; import { VisualizeServices } from './types'; import { addHelpMenuToAppChrome, addBadgeToAppChrome } from './utils'; diff --git a/src/plugins/visualizations/public/visualize_app/types.ts b/src/plugins/visualizations/public/visualize_app/types.ts index c340342dab8f0..8d86648e9d685 100644 --- a/src/plugins/visualizations/public/visualize_app/types.ts +++ b/src/plugins/visualizations/public/visualize_app/types.ts @@ -48,7 +48,7 @@ import type { VisParams, } from '..'; -import type { SavedVisState } from '../types'; +import type { ListingViewRegistry, SavedVisState } from '../types'; import type { createVisEmbeddableFromObject } from '../embeddable'; import type { VisEditorsRegistry } from '../vis_editors_registry'; @@ -113,6 +113,7 @@ export interface VisualizeServices extends CoreStart { spaces?: SpacesPluginStart; theme: ThemeServiceStart; visEditorsRegistry: VisEditorsRegistry; + listingViewRegistry: ListingViewRegistry; unifiedSearch: UnifiedSearchPublicPluginStart; } diff --git a/src/plugins/visualizations/tsconfig.json b/src/plugins/visualizations/tsconfig.json index dd5edc8961361..65e1053e4f7e1 100644 --- a/src/plugins/visualizations/tsconfig.json +++ b/src/plugins/visualizations/tsconfig.json @@ -42,7 +42,8 @@ "@kbn/i18n-react", "@kbn/safer-lodash-set", "@kbn/shared-ux-page-analytics-no-data", - "@kbn/content-management-table-list", + "@kbn/content-management-table-list-view", + "@kbn/content-management-tabbed-table-list-view", "@kbn/test-jest-helpers", "@kbn/analytics", "@kbn/content-management-content-editor", @@ -57,7 +58,10 @@ "@kbn/core-saved-objects-api-server", "@kbn/object-versioning", "@kbn/core-saved-objects-server", - "@kbn/core-saved-objects-utils-server" + "@kbn/core-saved-objects-utils-server", + "@kbn/content-management-table-list-view-table", + "@kbn/content-management-tabbed-table-list-view", + "@kbn/content-management-table-list-view" ], "exclude": [ "target/**/*", diff --git a/test/functional/page_objects/visualize_page.ts b/test/functional/page_objects/visualize_page.ts index bcfbc8caa9ce1..a08b950ce9853 100644 --- a/test/functional/page_objects/visualize_page.ts +++ b/test/functional/page_objects/visualize_page.ts @@ -69,6 +69,14 @@ export class VisualizePageObject extends FtrService { await this.common.navigateToApp('visualize'); } + public async selectVisualizationsTab() { + await this.listingTable.selectTab(1); + } + + public async selectAnnotationsTab() { + await this.listingTable.selectTab(2); + } + public async clickNewVisualization() { await this.listingTable.clickNewButton(); } diff --git a/test/functional/services/listing_table.ts b/test/functional/services/listing_table.ts index 96e9ba2e49d34..9bd2eb80132a8 100644 --- a/test/functional/services/listing_table.ts +++ b/test/functional/services/listing_table.ts @@ -204,6 +204,10 @@ export class ListingTableService extends FtrService { await this.testSubjects.click('deleteSelectedItems'); } + public async selectFirstItemInList() { + await this.find.clickByCssSelector('.euiTableCellContent .euiCheckbox__input'); + } + public async clickItemCheckbox(id: string) { await this.testSubjects.click(`checkboxSelectRow-${id}`); } @@ -213,9 +217,13 @@ export class ListingTableService extends FtrService { * @param name item name * @param id row id */ - public async deleteItem(name: string, id: string) { + public async deleteItem(name: string, id?: string) { await this.searchForItemWithName(name); - await this.clickItemCheckbox(id); + if (id) { + await this.clickItemCheckbox(id); + } else { + await this.selectFirstItemInList(); + } await this.clickDeleteSelected(); await this.common.clickConfirmOnModal(); } @@ -253,4 +261,8 @@ export class ListingTableService extends FtrService { timeout: 5000, }); } + + public async selectTab(which: number) { + await this.find.clickByCssSelector(`.euiTab:nth-child(${which})`); + } } diff --git a/tsconfig.base.json b/tsconfig.base.json index 5ab10d6e5de8e..6830598bf44a8 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -170,8 +170,12 @@ "@kbn/content-management-examples-plugin/*": ["examples/content_management_examples/*"], "@kbn/content-management-plugin": ["src/plugins/content_management"], "@kbn/content-management-plugin/*": ["src/plugins/content_management/*"], - "@kbn/content-management-table-list": ["packages/content-management/table_list"], - "@kbn/content-management-table-list/*": ["packages/content-management/table_list/*"], + "@kbn/content-management-tabbed-table-list-view": ["packages/content-management/tabbed_table_list_view"], + "@kbn/content-management-tabbed-table-list-view/*": ["packages/content-management/tabbed_table_list_view/*"], + "@kbn/content-management-table-list-view": ["packages/content-management/table_list_view"], + "@kbn/content-management-table-list-view/*": ["packages/content-management/table_list_view/*"], + "@kbn/content-management-table-list-view-table": ["packages/content-management/table_list_view_table"], + "@kbn/content-management-table-list-view-table/*": ["packages/content-management/table_list_view_table/*"], "@kbn/content-management-utils": ["packages/kbn-content-management-utils"], "@kbn/content-management-utils/*": ["packages/kbn-content-management-utils/*"], "@kbn/controls-example-plugin": ["examples/controls_example"], diff --git a/x-pack/plugins/graph/public/application.tsx b/x-pack/plugins/graph/public/application.tsx index 28b44b804373a..82e1a061c67c8 100644 --- a/x-pack/plugins/graph/public/application.tsx +++ b/x-pack/plugins/graph/public/application.tsx @@ -27,7 +27,7 @@ import { NavigationPublicPluginStart as NavigationStart } from '@kbn/navigation- import { Storage } from '@kbn/kibana-utils-plugin/public'; import { FormattedRelative } from '@kbn/i18n-react'; import { Start as InspectorPublicPluginStart } from '@kbn/inspector-plugin/public'; -import { TableListViewKibanaProvider } from '@kbn/content-management-table-list'; +import { TableListViewKibanaProvider } from '@kbn/content-management-table-list-view-table'; import './index.scss'; import('./font_awesome'); diff --git a/x-pack/plugins/graph/public/apps/listing_route.tsx b/x-pack/plugins/graph/public/apps/listing_route.tsx index a34a4bc5b591b..d5e7d2be00967 100644 --- a/x-pack/plugins/graph/public/apps/listing_route.tsx +++ b/x-pack/plugins/graph/public/apps/listing_route.tsx @@ -11,8 +11,8 @@ import { FormattedMessage, I18nProvider } from '@kbn/i18n-react'; import { EuiEmptyPrompt, EuiLink, EuiButton } from '@elastic/eui'; import { ApplicationStart } from '@kbn/core/public'; import { useHistory, useLocation } from 'react-router-dom'; -import { TableListView } from '@kbn/content-management-table-list'; -import type { UserContentCommonSchema } from '@kbn/content-management-table-list'; +import { TableListView } from '@kbn/content-management-table-list-view'; +import type { UserContentCommonSchema } from '@kbn/content-management-table-list-view'; import { deleteSavedWorkspace, findSavedWorkspace } from '../helpers/saved_workspace_utils'; import { getEditPath, getEditUrl, getNewPath, setBreadcrumbs } from '../services/url'; import { GraphWorkspaceSavedObject } from '../types'; @@ -111,7 +111,7 @@ export function ListingRoute({ entityNamePlural={i18n.translate('xpack.graph.listing.table.entityNamePlural', { defaultMessage: 'graphs', })} - tableListTitle={i18n.translate('xpack.graph.listing.graphsTitle', { + title={i18n.translate('xpack.graph.listing.graphsTitle', { defaultMessage: 'Graphs', })} getDetailViewLink={({ id }) => getEditUrl(addBasePath, { id })} diff --git a/x-pack/plugins/graph/tsconfig.json b/x-pack/plugins/graph/tsconfig.json index b91a6913f07ae..4ee1639a2d4f8 100644 --- a/x-pack/plugins/graph/tsconfig.json +++ b/x-pack/plugins/graph/tsconfig.json @@ -28,7 +28,6 @@ "@kbn/config-schema", "@kbn/i18n-react", "@kbn/inspector-plugin", - "@kbn/content-management-table-list", "@kbn/test-jest-helpers", "@kbn/data-views-plugin", "@kbn/es-query", @@ -44,6 +43,8 @@ "@kbn/content-management-plugin", "@kbn/core-saved-objects-api-server", "@kbn/object-versioning", + "@kbn/content-management-table-list-view-table", + "@kbn/content-management-table-list-view", ], "exclude": [ "target/**/*", diff --git a/x-pack/plugins/lens/common/constants.ts b/x-pack/plugins/lens/common/constants.ts index 8838b974ac4dc..5909dbcb48f4b 100644 --- a/x-pack/plugins/lens/common/constants.ts +++ b/x-pack/plugins/lens/common/constants.ts @@ -8,10 +8,10 @@ import rison from '@kbn/rison'; import type { RefreshInterval, TimeRange } from '@kbn/data-plugin/common/query'; import type { Filter } from '@kbn/es-query'; -import { i18n } from '@kbn/i18n'; export const PLUGIN_ID = 'lens'; export const APP_ID = 'lens'; +export const LENS_APP_NAME = 'lens'; export const LENS_EMBEDDABLE_TYPE = 'lens'; export const DOC_TYPE = 'lens'; export const NOT_INTERNATIONALIZED_PRODUCT_NAME = 'Lens Visualizations'; @@ -89,7 +89,3 @@ export function getEditPath( export function getFullPath(id?: string) { return `/app/${PLUGIN_ID}${id ? getEditPath(id) : getBasePath()}`; } - -export const LENS_APP_NAME = i18n.translate('xpack.lens.queryInput.appName', { - defaultMessage: 'Lens', -}); diff --git a/x-pack/plugins/lens/common/types.ts b/x-pack/plugins/lens/common/types.ts index 3c6830ba50b0d..ee89551dfc36a 100644 --- a/x-pack/plugins/lens/common/types.ts +++ b/x-pack/plugins/lens/common/types.ts @@ -9,7 +9,6 @@ import type { Filter, FilterMeta } from '@kbn/es-query'; import type { Position } from '@elastic/charts'; import type { $Values } from '@kbn/utility-types'; import type { CustomPaletteParams, PaletteOutput } from '@kbn/coloring'; -import type { IFieldFormat, SerializedFieldFormat } from '@kbn/field-formats-plugin/common'; import type { ColorMode } from '@kbn/charts-plugin/common'; import type { LegendSize } from '@kbn/visualizations-plugin/common'; import { CategoryDisplay, LegendDisplay, NumberDisplay, PieChartTypes } from './constants'; @@ -21,8 +20,7 @@ export type { AllowedPartitionOverrides } from '@kbn/expression-partition-vis-pl export type { AllowedSettingsOverrides } from '@kbn/charts-plugin/common'; export type { AllowedGaugeOverrides } from '@kbn/expression-gauge-plugin/common'; export type { AllowedXYOverrides } from '@kbn/expression-xy-plugin/common'; - -export type FormatFactory = (mapping?: SerializedFieldFormat) => IFieldFormat; +export type { FormatFactory } from '@kbn/visualization-ui-components/public'; export interface DateRange { fromDate: string; diff --git a/x-pack/plugins/lens/public/app_plugin/lens_top_nav.tsx b/x-pack/plugins/lens/public/app_plugin/lens_top_nav.tsx index 41b0fa567d567..ca5efceaf48bd 100644 --- a/x-pack/plugins/lens/public/app_plugin/lens_top_nav.tsx +++ b/x-pack/plugins/lens/public/app_plugin/lens_top_nav.tsx @@ -17,7 +17,7 @@ import { useKibana } from '@kbn/kibana-react-plugin/public'; import { DataViewPickerProps } from '@kbn/unified-search-plugin/public'; import moment from 'moment'; import { LENS_APP_LOCATOR } from '../../common/locator/locator'; -import { ENABLE_SQL } from '../../common/constants'; +import { ENABLE_SQL, LENS_APP_NAME } from '../../common/constants'; import { LensAppServices, LensTopNavActions, LensTopNavMenuProps } from './types'; import { toggleSettingsMenuOpen } from './settings_menu'; import { @@ -1096,7 +1096,7 @@ export const LensTopNavMenu = ({ showFilterBar={true} data-test-subj="lnsApp_topNav" screenTitle={'lens'} - appName={'lens'} + appName={LENS_APP_NAME} displayStyle="detached" className="hide-for-sharing" /> diff --git a/x-pack/plugins/lens/public/data_views_service/loader.ts b/x-pack/plugins/lens/public/data_views_service/loader.ts index abd8a48815122..ef48761d39a6e 100644 --- a/x-pack/plugins/lens/public/data_views_service/loader.ts +++ b/x-pack/plugins/lens/public/data_views_service/loader.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { isNestedField } from '@kbn/data-views-plugin/common'; +import { isFieldLensCompatible } from '@kbn/visualization-ui-components/public'; import type { DataViewsContract, DataView, DataViewSpec } from '@kbn/data-views-plugin/public'; import { keyBy } from 'lodash'; import { IndexPattern, IndexPatternField, IndexPatternMap, IndexPatternRef } from '../types'; @@ -30,7 +30,7 @@ export function convertDataViewIntoLensIndexPattern( restrictionRemapper: (name: string) => string = onRestrictionMapping ): IndexPattern { const newFields = dataView.fields - .filter((field) => !isNestedField(field) && (!!field.aggregatable || !!field.scripted)) + .filter(isFieldLensCompatible) .map((field): IndexPatternField => { // Convert the getters on the index pattern service into plain JSON const base = { diff --git a/x-pack/plugins/lens/public/datasources/form_based/dimension_panel/dimension_panel.test.tsx b/x-pack/plugins/lens/public/datasources/form_based/dimension_panel/dimension_panel.test.tsx index 234bc93e4798b..e13fc22464649 100644 --- a/x-pack/plugins/lens/public/datasources/form_based/dimension_panel/dimension_panel.test.tsx +++ b/x-pack/plugins/lens/public/datasources/form_based/dimension_panel/dimension_panel.test.tsx @@ -669,7 +669,7 @@ describe('FormBasedDimensionEditor', () => { act(() => { wrapper - .find('input[data-test-subj="column-label-edit"]') + .find('input[data-test-subj="name-input"]') .simulate('change', { target: { value: 'New Label' } }); }); @@ -773,7 +773,7 @@ describe('FormBasedDimensionEditor', () => { act(() => { wrapper - .find('input[data-test-subj="column-label-edit"]') + .find('input[data-test-subj="name-input"]') .simulate('change', { target: { value: 'Sum of bytes' } }); }); diff --git a/x-pack/plugins/lens/public/datasources/form_based/form_based.tsx b/x-pack/plugins/lens/public/datasources/form_based/form_based.tsx index 59c8096c7d269..d5779ef4ac81a 100644 --- a/x-pack/plugins/lens/public/datasources/form_based/form_based.tsx +++ b/x-pack/plugins/lens/public/datasources/form_based/form_based.tsx @@ -26,6 +26,7 @@ import type { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/ import { EuiButton } from '@elastic/eui'; import type { SharePluginStart } from '@kbn/share-plugin/public'; import type { DraggingIdentifier } from '@kbn/dom-drag-drop'; +import { DimensionTrigger } from '@kbn/visualization-ui-components/public'; import type { DatasourceDimensionEditorProps, DatasourceDimensionTriggerProps, @@ -99,7 +100,6 @@ import { DOCUMENT_FIELD_NAME } from '../../../common/constants'; import { isColumnOfType } from './operations/definitions/helpers'; import { LayerSettingsPanel } from './layer_settings'; import { FormBasedLayer } from '../..'; -import { DimensionTrigger } from '../../shared_components/dimension_trigger'; import { filterAndSortUserMessages } from '../../app_plugin/get_application_user_messages'; export type { OperationType, GenericIndexPatternColumn } from './operations'; export { deleteColumn } from './operations'; diff --git a/x-pack/plugins/lens/public/datasources/text_based/text_based_languages.tsx b/x-pack/plugins/lens/public/datasources/text_based/text_based_languages.tsx index dab66cac44fa5..90ce505b06700 100644 --- a/x-pack/plugins/lens/public/datasources/text_based/text_based_languages.tsx +++ b/x-pack/plugins/lens/public/datasources/text_based/text_based_languages.tsx @@ -19,6 +19,7 @@ import type { ExpressionsStart, DatatableColumnType } from '@kbn/expressions-plu import type { DataViewsPublicPluginStart, DataView } from '@kbn/data-views-plugin/public'; import type { DataPublicPluginStart } from '@kbn/data-plugin/public'; import { euiThemeVars } from '@kbn/ui-theme'; +import { DimensionTrigger } from '@kbn/visualization-ui-components/public'; import { DatasourceDimensionEditorProps, DatasourceDataPanelProps, @@ -42,7 +43,6 @@ import type { import { FieldSelect } from './field_select'; import type { Datasource, IndexPatternMap } from '../../types'; import { LayerPanel } from './layerpanel'; -import { DimensionTrigger } from '../../shared_components/dimension_trigger'; function getLayerReferenceName(layerId: string) { return `textBasedLanguages-datasource-layer-${layerId}`; diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/buttons/empty_dimension_button.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/buttons/empty_dimension_button.tsx index f0ef1508c5076..1e2b3ff4b4c05 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/buttons/empty_dimension_button.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/buttons/empty_dimension_button.tsx @@ -6,7 +6,6 @@ */ import React, { useMemo, useState, useEffect, useContext } from 'react'; -import { EuiButtonEmpty } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; import { i18n } from '@kbn/i18n'; import { @@ -16,6 +15,9 @@ import { DropType, DropTargetSwapDuplicateCombine, } from '@kbn/dom-drag-drop'; +import { EmptyDimensionButton as EmptyDimensionButtonInner } from '@kbn/visualization-ui-components/public'; +import { css } from '@emotion/react'; +import { euiThemeVars } from '@kbn/ui-theme'; import { isDraggedField } from '../../../../utils'; import { generateId } from '../../../../id_generator'; @@ -56,49 +58,32 @@ const defaultButtonLabels = { const DefaultEmptyButton = ({ columnId, group, onClick }: EmptyButtonProps) => { const { buttonAriaLabel, buttonLabel } = group.labels || {}; return ( - { - onClick(columnId); - }} - > - {buttonLabel || defaultButtonLabels.label} - + onClick(columnId)} + /> ); }; const SuggestedValueButton = ({ columnId, group, onClick }: EmptyButtonProps) => ( - + } + ariaLabel={i18n.translate('xpack.lens.indexPattern.suggestedValueAriaLabel', { defaultMessage: 'Suggested value: {value} for {groupLabel}', values: { value: group.suggestedValue?.(), groupLabel: group.groupLabel }, })} - data-test-subj="lns-empty-dimension-suggested-value" - onClick={() => { - onClick(columnId); - }} - > - - + dataTestSubj="lns-empty-dimension-suggested-value" + iconType="plusInCircleFilled" + onClick={() => onClick(columnId)} + /> ); export function EmptyDimensionButton({ @@ -205,7 +190,11 @@ export function EmptyDimensionButton({ onDrop={handleOnDrop} dropTypes={dropTypes} > -
+
{typeof group.suggestedValue?.() === 'number' ? ( ) : ( diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.scss b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.scss index 0efd82bc31063..3afcc0173ca2e 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.scss +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.scss @@ -41,19 +41,19 @@ } // Add border to the top of the next same panel - & + & { + &+& { border-top: $euiBorderThin; margin-top: 0; } - & > * { + &>* { margin-bottom: 0; } // Targeting EUI class as we are unable to apply a class to this element in component &, .euiFormRow__fieldWrapper { - & > * + * { + &>*+* { margin-top: $euiSizeS; } } @@ -64,121 +64,31 @@ padding: $euiSizeS $euiSize; } -.lnsLayerPanel__dimensionRemove { - margin-right: $euiSizeS; - opacity: 0; - - &:focus { - opacity: 1; - } -} - -.lnsLayerPanel__dimension { - @include euiFontSizeS; - border-radius: $euiBorderRadius; - display: flex; - align-items: center; - overflow: hidden; - min-height: $euiSizeXL; - position: relative; - - // NativeRenderer is messing this up - > div { - flex-grow: 1; - } - - &:hover, - &:focus { - .lnsLayerPanel__dimensionRemove { - visibility: visible; - opacity: 1; - transition: opacity $euiAnimSpeedFast ease-in-out; - } - } +.lnsLayerPanel__styleEditor { + padding: $euiSize; } -.lnsLayerPanel__dimension--empty { - border: $euiBorderWidthThin dashed $euiBorderColor !important; - - &:focus, - &:focus-within { - @include euiFocusRing; - } -} +// Start dimension style overrides .lnsLayerPanel__dimensionContainer { position: relative; - & + & { + &+& { margin-top: $euiSizeS; } } -.lnsLayerPanel__triggerText { - width: 100%; - padding: $euiSizeXS $euiSizeS; - word-break: break-word; - font-weight: $euiFontWeightRegular; -} - -.lnsLayerPanel__dimensionLink { - &:hover { - text-decoration: none; - } -} - -.lnsLayerPanel__triggerTextLabel { - transition: background-color $euiAnimSpeedFast ease-in-out; - - &:hover { - text-decoration: underline; - } -} - .domDragDrop-isReplacing { - .lnsLayerPanel__triggerText { + .dimensionTrigger__textLabel { text-decoration: line-through; } } -.lnsLayerPanel__triggerTextContent { - // Make EUI button content not centered - justify-content: flex-start; - padding: 0 !important; // sass-lint:disable-line no-important - color: $euiTextSubduedColor; -} - -.lnsLayerPanel__styleEditor { - padding: $euiSize; -} - -.lnsLayerPanel__colorIndicator { - margin-left: $euiSizeS; -} - -.lnsLayerPanel__paletteContainer { - position: absolute; - bottom: 0; - left: 0; - right: 0; -} - -.lnsLayerPanel__palette { - height: $euiSizeXS / 2; - border-radius: 0 0 ($euiBorderRadius - 1px) ($euiBorderRadius - 1px); - - &::after { - border: none; - } -} - // Added .lnsLayerPanel__dimension specificity required for animation style override .lnsLayerPanel__dimension .lnsLayerPanel__dimensionLink { - width: 100%; - &:focus { - @include passDownFocusRing('.lnsLayerPanel__triggerTextLabel'); + @include passDownFocusRing('.dimensionTrigger__textLabel'); background-color: transparent; text-decoration-thickness: $euiBorderWidthThin !important; } -} +} \ No newline at end of file diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.test.tsx index 18b5acd160aaf..eae473567922d 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.test.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.test.tsx @@ -21,6 +21,7 @@ import { mountWithProvider, } from '../../../mocks'; import { createIndexPatternServiceMock } from '../../../mocks/data_views_service_mock'; +import { DimensionButton } from '@kbn/visualization-ui-components/public'; jest.mock('../../../id_generator'); @@ -714,9 +715,7 @@ describe('LayerPanel', () => { expect(instance.exists('[data-test-subj="lns-fakeDimension"]')).toBeTruthy(); expect( - instance - .find('[data-test-subj="lns-fakeDimension"] .lnsLayerPanel__triggerTextLabel') - .text() + instance.find('[data-test-subj="lns-fakeDimension"] .dimensionTrigger__textLabel').text() ).toBe(fakeAccessorLabel); }); @@ -826,7 +825,7 @@ describe('LayerPanel', () => { const dragDropElement = instance .find('[data-test-subj="lnsGroup"] DragDrop') .first() - .find('.lnsLayerPanel__dimension') + .find(DimensionButton) .first(); dragDropElement.simulate('dragOver'); diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx index a7314f2c1b6b4..736a46a38630f 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx @@ -21,7 +21,7 @@ import { i18n } from '@kbn/i18n'; import { css } from '@emotion/react'; import { euiThemeVars } from '@kbn/ui-theme'; import { DragDropIdentifier, ReorderProvider, DropType } from '@kbn/dom-drag-drop'; -import { DimensionButton } from '@kbn/visualization-ui-components/public'; +import { DimensionButton, DimensionTrigger } from '@kbn/visualization-ui-components/public'; import { LayerActions } from './layer_actions'; import { IndexPatternServiceAPI } from '../../../data_views_service/service'; import { NativeRenderer } from '../../../native_renderer'; @@ -572,7 +572,6 @@ export function LayerPanel( indexPatterns={dataViews.indexPatterns} > - - - {group.fakeFinalAccessor.label} - - + />
)} diff --git a/x-pack/plugins/lens/public/shared_components/dimension_trigger/index.tsx b/x-pack/plugins/lens/public/shared_components/dimension_trigger/index.tsx deleted file mode 100644 index d705c50017a8d..0000000000000 --- a/x-pack/plugins/lens/public/shared_components/dimension_trigger/index.tsx +++ /dev/null @@ -1,50 +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 { EuiText, EuiFlexItem } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import React from 'react'; -import { EuiTextProps } from '@elastic/eui/src/components/text/text'; - -export const defaultDimensionTriggerTooltip = ( -

- {i18n.translate('xpack.lens.configure.invalidConfigTooltip', { - defaultMessage: 'Invalid configuration.', - })} -
- {i18n.translate('xpack.lens.configure.invalidConfigTooltipClick', { - defaultMessage: 'Click for more details.', - })} -

-); - -export const DimensionTrigger = ({ - id, - label, - color, - dataTestSubj, -}: { - label: string; - id: string; - color?: EuiTextProps['color']; - dataTestSubj?: string; -}) => { - return ( - - - - {label} - - - - ); -}; diff --git a/x-pack/plugins/lens/public/visualizations/xy/annotations/actions/index.ts b/x-pack/plugins/lens/public/visualizations/xy/annotations/actions/index.ts index b7bc603fae05f..b6e3f9e82d785 100644 --- a/x-pack/plugins/lens/public/visualizations/xy/annotations/actions/index.ts +++ b/x-pack/plugins/lens/public/visualizations/xy/annotations/actions/index.ts @@ -9,6 +9,8 @@ import type { CoreStart } from '@kbn/core/public'; import { EventAnnotationServiceType } from '@kbn/event-annotation-plugin/public'; import { SavedObjectTaggingPluginStart } from '@kbn/saved-objects-tagging-plugin/public'; import { DataViewsContract } from '@kbn/data-views-plugin/public'; +import { VISUALIZE_APP_NAME } from '@kbn/visualizations-plugin/common/constants'; +import { ANNOTATIONS_LISTING_VIEW_ID } from '@kbn/event-annotation-plugin/common'; import type { LayerAction, StateSetter } from '../../../../types'; import { XYState, XYAnnotationLayerConfig } from '../../types'; import { getUnlinkLayerAction } from './unlink_action'; @@ -51,6 +53,10 @@ export const createAnnotationActions = ({ toasts: core.notifications.toasts, savedObjectsTagging, dataViews, + goToAnnotationLibrary: () => + core.application.navigateToApp(VISUALIZE_APP_NAME, { + path: `#/${ANNOTATIONS_LISTING_VIEW_ID}`, + }), }) ); } diff --git a/x-pack/plugins/lens/public/visualizations/xy/annotations/actions/save_action.test.tsx b/x-pack/plugins/lens/public/visualizations/xy/annotations/actions/save_action.test.tsx index e5256ec49b78a..8505f9811749a 100644 --- a/x-pack/plugins/lens/public/visualizations/xy/annotations/actions/save_action.test.tsx +++ b/x-pack/plugins/lens/public/visualizations/xy/annotations/actions/save_action.test.tsx @@ -153,7 +153,7 @@ describe('annotation group save action', () => { toExpression: jest.fn(), toFetchExpression: jest.fn(), renderEventAnnotationGroupSavedObjectFinder: jest.fn(), - } as EventAnnotationServiceType, + } as Partial as EventAnnotationServiceType, toasts: toastsServiceMock.createStartContract(), modalOnSaveProps: { newTitle: 'my title', @@ -165,6 +165,7 @@ describe('annotation group save action', () => { onTitleDuplicate: () => {}, }, dataViews, + goToAnnotationLibrary: () => Promise.resolve(), }; }; diff --git a/x-pack/plugins/lens/public/visualizations/xy/annotations/actions/save_action.tsx b/x-pack/plugins/lens/public/visualizations/xy/annotations/actions/save_action.tsx index 0e66654af48c1..1b4ae5fd4958d 100644 --- a/x-pack/plugins/lens/public/visualizations/xy/annotations/actions/save_action.tsx +++ b/x-pack/plugins/lens/public/visualizations/xy/annotations/actions/save_action.tsx @@ -17,7 +17,7 @@ import { SavedObjectSaveModal, } from '@kbn/saved-objects-plugin/public'; import { EventAnnotationGroupConfig } from '@kbn/event-annotation-plugin/common'; -import { EuiIcon } from '@elastic/eui'; +import { EuiIcon, EuiLink } from '@elastic/eui'; import { type SavedObjectTaggingPluginStart } from '@kbn/saved-objects-tagging-plugin/public'; import { DataViewsContract } from '@kbn/data-views-plugin/public'; import type { LayerAction, StateSetter } from '../../../../types'; @@ -116,7 +116,7 @@ const saveAnnotationGroupToLibrary = async ( title: newTitle, description: newDescription, tags: newTags, - dataViewSpec: dataView.isPersisted() ? undefined : dataView.toSpec(), + dataViewSpec: dataView.isPersisted() ? undefined : dataView.toSpec(false), }; if (saveAsNew) { @@ -140,6 +140,7 @@ export const onSave = async ({ toasts, modalOnSaveProps: { newTitle, newDescription, newTags, closeModal, newCopyOnSave }, dataViews, + goToAnnotationLibrary, }: { state: XYState; layer: XYAnnotationLayerConfig; @@ -148,6 +149,7 @@ export const onSave = async ({ toasts: ToastsStart; modalOnSaveProps: ModalOnSaveProps; dataViews: DataViewsContract; + goToAnnotationLibrary: () => Promise; }) => { let savedInfo: Awaited>; try { @@ -203,9 +205,21 @@ export const onSave = async ({

annotation library, + link: ( + goToAnnotationLibrary()} + > + {i18n.translate( + 'xpack.lens.xyChart.annotations.saveAnnotationGroupToLibrary.annotationLibrary', + { + defaultMessage: 'annotation library', + } + )} + + ), }} />

, @@ -222,6 +236,7 @@ export const getSaveLayerAction = ({ toasts, savedObjectsTagging, dataViews, + goToAnnotationLibrary, }: { state: XYState; layer: XYAnnotationLayerConfig; @@ -230,6 +245,7 @@ export const getSaveLayerAction = ({ toasts: ToastsStart; savedObjectsTagging?: SavedObjectTaggingPluginStart; dataViews: DataViewsContract; + goToAnnotationLibrary: () => Promise; }): LayerAction => { const neverSaved = !isByReferenceAnnotationsLayer(layer); @@ -261,6 +277,7 @@ export const getSaveLayerAction = ({ toasts, modalOnSaveProps: props, dataViews, + goToAnnotationLibrary, }); }} title={neverSaved ? '' : layer.__lastSaved.title} diff --git a/x-pack/plugins/lens/public/visualizations/xy/annotations/helpers.tsx b/x-pack/plugins/lens/public/visualizations/xy/annotations/helpers.tsx index 6174017f3054b..3bf619cc76129 100644 --- a/x-pack/plugins/lens/public/visualizations/xy/annotations/helpers.tsx +++ b/x-pack/plugins/lens/public/visualizations/xy/annotations/helpers.tsx @@ -8,19 +8,19 @@ import { i18n } from '@kbn/i18n'; import moment from 'moment'; import { - defaultAnnotationColor, - defaultAnnotationRangeColor, + getAnnotationAccessor, isQueryAnnotationConfig, - isRangeAnnotationConfig, } from '@kbn/event-annotation-plugin/public'; -import { EventAnnotationConfig } from '@kbn/event-annotation-plugin/common'; +import { + createCopiedAnnotation, + EventAnnotationConfig, + getDefaultQueryAnnotation, +} from '@kbn/event-annotation-plugin/common'; import { IconChartBarAnnotations } from '@kbn/chart-icons'; import { LayerTypes } from '@kbn/expression-xy-plugin/public'; -import type { AccessorConfig } from '@kbn/visualization-ui-components/public'; import { isDraggedDataViewField } from '../../../utils'; import type { FramePublicAPI, Visualization } from '../../../types'; import { isHorizontalChart } from '../state_helpers'; -import { annotationsIconSet } from '../xy_config_panel/annotations_config_panel/icon_set'; import type { XYState, XYDataLayerConfig, XYAnnotationLayerConfig, XYLayerConfig } from '../types'; import { checkScaleOperation, @@ -129,50 +129,6 @@ export const getAnnotationsSupportedLayer = ( }; }; -const getDefaultManualAnnotation = (id: string, timestamp: string): EventAnnotationConfig => ({ - label: defaultAnnotationLabel, - type: 'manual', - key: { - type: 'point_in_time', - timestamp, - }, - icon: 'triangle', - id, -}); - -const getDefaultQueryAnnotation = ( - id: string, - fieldName: string, - timeField: string -): EventAnnotationConfig => ({ - filter: { - type: 'kibana_query', - query: `${fieldName}: *`, - language: 'kuery', - }, - timeField, - type: 'query', - key: { - type: 'point_in_time', - }, - id, - label: `${fieldName}: *`, -}); - -const createCopiedAnnotation = ( - newId: string, - timestamp: string, - source?: EventAnnotationConfig -): EventAnnotationConfig => { - if (!source) { - return getDefaultManualAnnotation(newId, timestamp); - } - return { - ...source, - id: newId, - }; -}; - export const onAnnotationDrop: Visualization['onDrop'] = ({ prevState, frame, @@ -446,26 +402,8 @@ export const setAnnotationsDimension: Visualization['setDimension'] = ( }; }; -export const getSingleColorAnnotationConfig = ( - annotation: EventAnnotationConfig -): AccessorConfig => { - const annotationIcon = !isRangeAnnotationConfig(annotation) - ? annotationsIconSet.find((option) => option.value === annotation?.icon) || - annotationsIconSet.find((option) => option.value === 'triangle') - : undefined; - const icon = annotationIcon?.icon ?? annotationIcon?.value; - return { - columnId: annotation.id, - triggerIconType: annotation.isHidden ? 'invisible' : icon ? 'custom' : 'color', - customIcon: icon, - color: - annotation?.color || - (isRangeAnnotationConfig(annotation) ? defaultAnnotationRangeColor : defaultAnnotationColor), - }; -}; - export const getAnnotationsAccessorColorConfig = (layer: XYAnnotationLayerConfig) => - layer.annotations.map((annotation) => getSingleColorAnnotationConfig(annotation)); + layer.annotations.map((annotation) => getAnnotationAccessor(annotation)); export const getAnnotationsConfiguration = ({ state, diff --git a/x-pack/plugins/lens/public/visualizations/xy/index.ts b/x-pack/plugins/lens/public/visualizations/xy/index.ts index 244d47cd5b114..6f590000d512a 100644 --- a/x-pack/plugins/lens/public/visualizations/xy/index.ts +++ b/x-pack/plugins/lens/public/visualizations/xy/index.ts @@ -30,7 +30,15 @@ export class XyVisualization { const { getXyVisualization } = await import('../../async_services'); const [ coreStart, - { charts, data, fieldFormats, eventAnnotation, unifiedSearch, savedObjectsTagging }, + { + charts, + data, + fieldFormats, + eventAnnotation, + unifiedSearch, + savedObjectsTagging, + dataViews, + }, ] = await core.getStartServices(); const [palettes, eventAnnotationService] = await Promise.all([ charts.palettes.getPalettes(), @@ -47,6 +55,7 @@ export class XyVisualization { useLegacyTimeAxis, kibanaTheme: core.theme, unifiedSearch, + dataViewsService: dataViews, savedObjectsTagging, }); }); diff --git a/x-pack/plugins/lens/public/visualizations/xy/to_expression.test.ts b/x-pack/plugins/lens/public/visualizations/xy/to_expression.test.ts index a1378a1442698..9cdf33c134c8c 100644 --- a/x-pack/plugins/lens/public/visualizations/xy/to_expression.test.ts +++ b/x-pack/plugins/lens/public/visualizations/xy/to_expression.test.ts @@ -20,6 +20,7 @@ import { LegendSize } from '@kbn/visualizations-plugin/common'; import { dataPluginMock } from '@kbn/data-plugin/public/mocks'; import { IStorageWrapper } from '@kbn/kibana-utils-plugin/public'; import { unifiedSearchPluginMock } from '@kbn/unified-search-plugin/public/mocks'; +import { DataViewsServicePublic } from '@kbn/data-views-plugin/public'; describe('#toExpression', () => { const xyVisualization = getXyVisualization({ @@ -32,6 +33,7 @@ describe('#toExpression', () => { storage: {} as IStorageWrapper, data: dataPluginMock.createStartContract(), unifiedSearch: unifiedSearchPluginMock.createStartContract(), + dataViewsService: {} as DataViewsServicePublic, }); let mockDatasource: ReturnType; let frame: ReturnType; diff --git a/x-pack/plugins/lens/public/visualizations/xy/visualization.test.tsx b/x-pack/plugins/lens/public/visualizations/xy/visualization.test.tsx index 6f720f0e21f2f..bcd5ed68aa38c 100644 --- a/x-pack/plugins/lens/public/visualizations/xy/visualization.test.tsx +++ b/x-pack/plugins/lens/public/visualizations/xy/visualization.test.tsx @@ -52,6 +52,7 @@ import { set } from '@kbn/safer-lodash-set'; import { SavedObjectReference } from '@kbn/core-saved-objects-api-server'; import { getAnnotationsLayers } from './visualization_helpers'; import { cloneDeep } from 'lodash'; +import { DataViewsServicePublic } from '@kbn/data-views-plugin/public'; const DATE_HISTORGRAM_COLUMN_ID = 'date_histogram_column'; const exampleAnnotation: EventAnnotationConfig = { @@ -108,6 +109,7 @@ const xyVisualization = getXyVisualization({ storage: {} as IStorageWrapper, data: dataPluginMock.createStartContract(), unifiedSearch: unifiedSearchPluginMock.createStartContract(), + dataViewsService: {} as DataViewsServicePublic, }); describe('xy_visualization', () => { diff --git a/x-pack/plugins/lens/public/visualizations/xy/visualization.tsx b/x-pack/plugins/lens/public/visualizations/xy/visualization.tsx index d4f6d4411b5a7..cdbd189790e07 100644 --- a/x-pack/plugins/lens/public/visualizations/xy/visualization.tsx +++ b/x-pack/plugins/lens/public/visualizations/xy/visualization.tsx @@ -14,7 +14,10 @@ import type { PaletteRegistry } from '@kbn/coloring'; import { IconChartBarReferenceLine, IconChartBarAnnotations } from '@kbn/chart-icons'; import { FieldFormatsStart } from '@kbn/field-formats-plugin/public'; import { CoreStart, SavedObjectReference, ThemeServiceStart } from '@kbn/core/public'; -import type { EventAnnotationServiceType } from '@kbn/event-annotation-plugin/public'; +import { + EventAnnotationServiceType, + getAnnotationAccessor, +} from '@kbn/event-annotation-plugin/public'; import { KibanaContextProvider, KibanaThemeProvider } from '@kbn/kibana-react-plugin/public'; import { VIS_EVENT_TO_TRIGGER } from '@kbn/visualizations-plugin/public'; import type { DataPublicPluginStart } from '@kbn/data-plugin/public'; @@ -24,7 +27,8 @@ import { LayerTypes } from '@kbn/expression-xy-plugin/public'; import { SavedObjectTaggingPluginStart } from '@kbn/saved-objects-tagging-plugin/public'; import { EventAnnotationGroupConfig } from '@kbn/event-annotation-plugin/common'; import { isEqual } from 'lodash'; -import type { AccessorConfig } from '@kbn/visualization-ui-components/public'; +import { type AccessorConfig, DimensionTrigger } from '@kbn/visualization-ui-components/public'; +import { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public'; import { generateId } from '../../id_generator'; import { isDraggedDataViewField, @@ -36,8 +40,8 @@ import { import { getSuggestions } from './xy_suggestions'; import { XyToolbar } from './xy_config_panel'; import { + DataDimensionEditor, DataDimensionEditorDataSectionExtra, - DimensionEditor, } from './xy_config_panel/dimension_editor'; import { LayerHeader, LayerHeaderContent } from './xy_config_panel/layer_header'; import type { @@ -79,7 +83,6 @@ import { getUniqueLabels, onAnnotationDrop, isDateHistogram, - getSingleColorAnnotationConfig, } from './annotations/helpers'; import { checkXAccessorCompatibility, @@ -105,7 +108,6 @@ import { groupAxesByType } from './axes_configuration'; import type { XYState } from './types'; import { ReferenceLinePanel } from './xy_config_panel/reference_line_config_panel'; import { AnnotationsPanel } from './xy_config_panel/annotations_config_panel'; -import { DimensionTrigger } from '../../shared_components/dimension_trigger'; import { defaultAnnotationLabel } from './annotations/helpers'; import { onDropForVisualization } from '../../editor_frame_service/editor_frame/config_panel/buttons/drop_targets_utils'; import { createAnnotationActions } from './annotations/actions'; @@ -127,6 +129,7 @@ export const getXyVisualization = ({ kibanaTheme, eventAnnotationService, unifiedSearch, + dataViewsService, savedObjectsTagging, }: { core: CoreStart; @@ -138,6 +141,7 @@ export const getXyVisualization = ({ useLegacyTimeAxis: boolean; kibanaTheme: ThemeServiceStart; unifiedSearch: UnifiedSearchPublicPluginStart; + dataViewsService: DataViewsPublicPluginStart; savedObjectsTagging?: SavedObjectTaggingPluginStart; }): Visualization => ({ id: XY_ID, @@ -651,9 +655,9 @@ export const getXyVisualization = ({ const dimensionEditor = isReferenceLayer(layer) ? ( ) : isAnnotationsLayer(layer) ? ( - + ) : ( - + ); render( @@ -1142,7 +1146,7 @@ function getVisualizationInfo( palette.push( ...layer.annotations .filter(({ isHidden }) => !isHidden) - .map((annotation) => getSingleColorAnnotationConfig(annotation).color) + .map((annotation) => getAnnotationAccessor(annotation).color) ); } diff --git a/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/annotations_config_panel/annotations_panel.tsx b/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/annotations_config_panel/annotations_panel.tsx index 9cb5f4d64079e..19bbcc7f4bfc8 100644 --- a/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/annotations_config_panel/annotations_panel.tsx +++ b/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/annotations_config_panel/annotations_panel.tsx @@ -5,59 +5,30 @@ * 2.0. */ -import './index.scss'; -import React, { useCallback, useEffect } from 'react'; -import { i18n } from '@kbn/i18n'; -import { EuiFormRow, EuiSwitch, EuiSwitchEvent, EuiButtonGroup, EuiSpacer } from '@elastic/eui'; -import type { PaletteRegistry } from '@kbn/coloring'; +import React, { useCallback, useEffect, useState } from 'react'; import type { DatatableUtilitiesService } from '@kbn/data-plugin/common'; -import { - defaultAnnotationColor, - defaultAnnotationRangeColor, - isQueryAnnotationConfig, - isRangeAnnotationConfig, -} from '@kbn/event-annotation-plugin/public'; -import { - EventAnnotationConfig, - PointInTimeEventAnnotationConfig, - QueryPointEventAnnotationConfig, -} from '@kbn/event-annotation-plugin/common'; +import { AnnotationEditorControls } from '@kbn/event-annotation-plugin/public'; +import { EventAnnotationConfig } from '@kbn/event-annotation-plugin/common'; +import { useKibana } from '@kbn/kibana-react-plugin/public'; +import { useDebouncedValue } from '@kbn/visualization-ui-components/public'; +import { DataViewsPublicPluginStart, DataView } from '@kbn/data-views-plugin/public'; import moment from 'moment'; -import { useExistingFieldsReader } from '@kbn/unified-field-list-plugin/public'; -import { - IconSelectSetting, - FieldOption, - FieldOptionValue, - FieldPicker, - NameInput, - useDebouncedValue, - DimensionEditorSection, - ColorPicker, -} from '@kbn/visualization-ui-components/public'; -import { FormatFactory } from '../../../../../common/types'; -import { isHorizontalChart } from '../../state_helpers'; -import { defaultAnnotationLabel, defaultRangeAnnotationLabel } from '../../annotations/helpers'; -import { TextDecorationSetting } from '../shared/marker_decoration_settings'; -import { LineStyleSettings } from '../shared/line_style_settings'; +import { search } from '@kbn/data-plugin/public'; +import { LENS_APP_NAME } from '../../../../../common/constants'; +import { DONT_CLOSE_DIMENSION_CONTAINER_ON_CLICK_CLASS } from '../../../../utils'; +import { LensAppServices } from '../../../../app_plugin/types'; import { updateLayer } from '..'; -import { annotationsIconSet } from './icon_set'; -import type { VisualizationDimensionEditorProps } from '../../../../types'; -import type { State, XYState, XYAnnotationLayerConfig } from '../../types'; -import { ConfigPanelManualAnnotation } from './manual_annotation_panel'; -import { ConfigPanelQueryAnnotation } from './query_annotation_panel'; -import { TooltipSection } from './tooltip_annotation_panel'; -import { sanitizeProperties, toLineAnnotationColor } from './helpers'; +import type { FramePublicAPI, VisualizationDimensionEditorProps } from '../../../../types'; +import type { State, XYState, XYAnnotationLayerConfig, XYDataLayerConfig } from '../../types'; +import { isDataLayer } from '../../visualization_helpers'; export const AnnotationsPanel = ( props: VisualizationDimensionEditorProps & { datatableUtilities: DatatableUtilitiesService; - formatFactory: FormatFactory; - paletteService: PaletteRegistry; + dataViewsService: DataViewsPublicPluginStart; } ) => { const { state, setState, layerId, accessor, frame } = props; - const isHorizontal = isHorizontalChart(state.layers); - const { hasFieldData } = useExistingFieldsReader(); const { inputValue: localState, handleInputChange: setLocalState } = useDebouncedValue({ value: state, @@ -71,26 +42,15 @@ export const AnnotationsPanel = ( const currentAnnotation = localLayer.annotations?.find((c) => c.id === accessor); - const isQueryBased = isQueryAnnotationConfig(currentAnnotation); - const isRange = isRangeAnnotationConfig(currentAnnotation); - const [queryInputShouldOpen, setQueryInputShouldOpen] = React.useState(false); - useEffect(() => { - setQueryInputShouldOpen(!isQueryBased); - }, [isQueryBased]); - - const setAnnotations = useCallback( - (annotation: Partial | undefined) => { + const setAnnotation = useCallback( + (annotation: EventAnnotationConfig) => { if (annotation == null) { return; } const newConfigs = [...(localLayer.annotations || [])]; const existingIndex = newConfigs.findIndex((c) => c.id === accessor); if (existingIndex !== -1) { - const existingConfig = newConfigs[existingIndex]; - newConfigs[existingIndex] = sanitizeProperties({ - ...existingConfig, - ...annotation, - }); + newConfigs[existingIndex] = annotation; } else { throw new Error( 'should never happen because annotation is created before config panel is opened' @@ -101,305 +61,81 @@ export const AnnotationsPanel = ( [accessor, index, localState, localLayer, setLocalState] ); - return ( - <> - - - { - const typeFromId = id.replace( - 'lens_xyChart_annotation_', - '' - ) as EventAnnotationConfig['type']; - if (currentAnnotation?.type === typeFromId) { - return; - } - if (typeFromId === 'query') { - const currentIndexPattern = - frame.dataViews.indexPatterns[localLayer.indexPatternId]; - // If coming from a range type, it requires some additional resets - const additionalRangeResets = isRangeAnnotationConfig(currentAnnotation) - ? { - label: - currentAnnotation.label === defaultRangeAnnotationLabel - ? defaultAnnotationLabel - : currentAnnotation.label, - color: toLineAnnotationColor(currentAnnotation.color), - } - : {}; - return setAnnotations({ - type: typeFromId, - timeField: - (currentIndexPattern.timeFieldName || - // fallback to the first avaiable date field in the dataView - currentIndexPattern.fields.find(({ type: fieldType }) => fieldType === 'date') - ?.displayName) ?? - '', - key: { type: 'point_in_time' }, - ...additionalRangeResets, - }); - } - // From query to manual annotation - return setAnnotations({ - type: typeFromId, - key: { type: 'point_in_time', timestamp: moment().toISOString() }, - }); - }} - isFullWidth - /> - - {isQueryBased ? ( - - ) : ( - - )} - - - { - setAnnotations({ label: value }); - }} - /> - {!isRange && ( - <> - setAnnotations({ icon })} - defaultIcon="triangle" - currentIcon={currentAnnotation?.icon} - customIconSet={annotationsIconSet} - /> - - {(textDecorationSelected) => { - if (textDecorationSelected !== 'field') { - return null; - } - const currentIndexPattern = - frame.dataViews.indexPatterns[localLayer.indexPatternId]; - const options = currentIndexPattern.fields - .filter(({ displayName, type }) => displayName && type !== 'document') - .map( - (field) => - ({ - label: field.displayName, - value: { - type: 'field', - field: field.name, - dataType: field.type, - }, - exists: hasFieldData(currentIndexPattern.id, field.name), - compatible: true, - 'data-test-subj': `lnsXY-annotation-fieldOption-${field.name}`, - } as FieldOption) - ); - const selectedField = (currentAnnotation as QueryPointEventAnnotationConfig) - .textField; + const [currentDataView, setCurrentDataView] = useState(); + + useEffect(() => { + const updateDataView = async () => { + let dataView: DataView; + const availableIds = await props.dataViewsService.getIds(); + if (availableIds.includes(localLayer.indexPatternId)) { + dataView = await props.dataViewsService.get(localLayer.indexPatternId); + } else { + dataView = await props.dataViewsService.create( + frame.dataViews.indexPatterns[localLayer.indexPatternId].spec + ); + } + setCurrentDataView(dataView); + }; + + updateDataView(); + }, [frame.dataViews.indexPatterns, localLayer.indexPatternId, props.dataViewsService]); - const fieldIsValid = selectedField - ? Boolean(currentIndexPattern.getFieldByName(selectedField)) - : true; - return ( - <> - - - - ); - }} - - - - )} - {isRange && ( - - { - setAnnotations({ - outside: id === `lens_xyChart_fillStyle_outside`, - }); - }} - isFullWidth - /> - - )} + const queryInputServices = useKibana().services; - - setAnnotations({ isHidden: ev.target.checked })} - /> - - {isQueryBased && currentAnnotation && ( - - - - - - )} - - ); -}; + if (!currentAnnotation) { + throw new Error('Annotation not found... this should never happen!'); + } -const ConfigPanelGenericSwitch = ({ - label, - ['data-test-subj']: dataTestSubj, - value, - onChange, -}: { - label: string; - 'data-test-subj': string; - value: boolean; - onChange: (event: EuiSwitchEvent) => void; -}) => ( - - setAnnotation(newAnnotation)} + dataView={currentDataView} + getDefaultRangeEnd={(rangeStart) => + getEndTimestamp( + props.datatableUtilities, + rangeStart, + frame, + localState.layers.filter(isDataLayer) + ) + } + queryInputServices={queryInputServices} + calendarClassName={DONT_CLOSE_DIMENSION_CONTAINER_ON_CLICK_CLASS} + appName={LENS_APP_NAME} /> - -); + ) : null; +}; + +const getEndTimestamp = ( + datatableUtilities: DatatableUtilitiesService, + startTime: string, + { activeData, dateRange }: FramePublicAPI, + dataLayers: XYDataLayerConfig[] +) => { + const startTimeNumber = moment(startTime).valueOf(); + const dateRangeFraction = + (moment(dateRange.toDate).valueOf() - moment(dateRange.fromDate).valueOf()) * 0.1; + const fallbackValue = moment(startTimeNumber + dateRangeFraction).toISOString(); + const dataLayersId = dataLayers.map(({ layerId }) => layerId); + if ( + !dataLayersId.length || + !activeData || + Object.entries(activeData) + .filter(([key]) => dataLayersId.includes(key)) + .every(([, { rows }]) => !rows || !rows.length) + ) { + return fallbackValue; + } + const xColumn = activeData?.[dataLayersId[0]].columns.find( + (column) => column.id === dataLayers[0].xAccessor + ); + if (!xColumn) { + return fallbackValue; + } + + const dateInterval = datatableUtilities.getDateHistogramMeta(xColumn)?.interval; + if (!dateInterval) return fallbackValue; + const intervalDuration = search.aggs.parseInterval(dateInterval); + if (!intervalDuration) return fallbackValue; + return moment(startTimeNumber + 3 * intervalDuration.as('milliseconds')).toISOString(); +}; diff --git a/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/annotations_config_panel/helpers.ts b/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/annotations_config_panel/helpers.ts deleted file mode 100644 index 0eb80c1018dc2..0000000000000 --- a/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/annotations_config_panel/helpers.ts +++ /dev/null @@ -1,116 +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 { search } from '@kbn/data-plugin/public'; -import { transparentize } from '@elastic/eui'; -import type { DatatableUtilitiesService } from '@kbn/data-plugin/common'; -import type { - EventAnnotationConfig, - RangeEventAnnotationConfig, - PointInTimeEventAnnotationConfig, - QueryPointEventAnnotationConfig, -} from '@kbn/event-annotation-plugin/common'; -import { - defaultAnnotationColor, - defaultAnnotationRangeColor, - isQueryAnnotationConfig, - isRangeAnnotationConfig, -} from '@kbn/event-annotation-plugin/public'; -import chroma from 'chroma-js'; -import { pick } from 'lodash'; -import moment from 'moment'; -import type { FramePublicAPI } from '../../../../types'; -import type { XYDataLayerConfig } from '../../types'; - -export const toRangeAnnotationColor = (color = defaultAnnotationColor) => { - return chroma(transparentize(color, 0.1)).hex().toUpperCase(); -}; - -export const toLineAnnotationColor = (color = defaultAnnotationRangeColor) => { - return chroma(transparentize(color, 1)).hex().toUpperCase(); -}; - -export const getEndTimestamp = ( - datatableUtilities: DatatableUtilitiesService, - startTime: string, - { activeData, dateRange }: FramePublicAPI, - dataLayers: XYDataLayerConfig[] -) => { - const startTimeNumber = moment(startTime).valueOf(); - const dateRangeFraction = - (moment(dateRange.toDate).valueOf() - moment(dateRange.fromDate).valueOf()) * 0.1; - const fallbackValue = moment(startTimeNumber + dateRangeFraction).toISOString(); - const dataLayersId = dataLayers.map(({ layerId }) => layerId); - if ( - !dataLayersId.length || - !activeData || - Object.entries(activeData) - .filter(([key]) => dataLayersId.includes(key)) - .every(([, { rows }]) => !rows || !rows.length) - ) { - return fallbackValue; - } - const xColumn = activeData?.[dataLayersId[0]].columns.find( - (column) => column.id === dataLayers[0].xAccessor - ); - if (!xColumn) { - return fallbackValue; - } - - const dateInterval = datatableUtilities.getDateHistogramMeta(xColumn)?.interval; - if (!dateInterval) return fallbackValue; - const intervalDuration = search.aggs.parseInterval(dateInterval); - if (!intervalDuration) return fallbackValue; - return moment(startTimeNumber + 3 * intervalDuration.as('milliseconds')).toISOString(); -}; - -export const sanitizeProperties = (annotation: EventAnnotationConfig) => { - if (isRangeAnnotationConfig(annotation)) { - const rangeAnnotation: RangeEventAnnotationConfig = pick(annotation, [ - 'type', - 'label', - 'key', - 'id', - 'isHidden', - 'color', - 'outside', - ]); - return rangeAnnotation; - } - if (isQueryAnnotationConfig(annotation)) { - const lineAnnotation: QueryPointEventAnnotationConfig = pick(annotation, [ - 'type', - 'id', - 'label', - 'key', - 'timeField', - 'isHidden', - 'lineStyle', - 'lineWidth', - 'color', - 'icon', - 'textVisibility', - 'textField', - 'filter', - 'extraFields', - ]); - return lineAnnotation; - } - const lineAnnotation: PointInTimeEventAnnotationConfig = pick(annotation, [ - 'type', - 'id', - 'label', - 'key', - 'isHidden', - 'lineStyle', - 'lineWidth', - 'color', - 'icon', - 'textVisibility', - ]); - return lineAnnotation; -}; diff --git a/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/annotations_config_panel/icon_set.ts b/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/annotations_config_panel/icon_set.ts deleted file mode 100644 index 3e67cd8e5c14e..0000000000000 --- a/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/annotations_config_panel/icon_set.ts +++ /dev/null @@ -1,106 +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 { i18n } from '@kbn/i18n'; -import { AvailableAnnotationIcon } from '@kbn/event-annotation-plugin/common'; -import { IconTriangle, IconCircle } from '@kbn/chart-icons'; -import { type IconSet } from '@kbn/visualization-ui-components/public'; - -export const annotationsIconSet: IconSet = [ - { - value: 'asterisk', - label: i18n.translate('xpack.lens.xyChart.iconSelect.asteriskIconLabel', { - defaultMessage: 'Asterisk', - }), - }, - { - value: 'alert', - label: i18n.translate('xpack.lens.xyChart.iconSelect.alertIconLabel', { - defaultMessage: 'Alert', - }), - }, - { - value: 'bell', - label: i18n.translate('xpack.lens.xyChart.iconSelect.bellIconLabel', { - defaultMessage: 'Bell', - }), - }, - { - value: 'bolt', - label: i18n.translate('xpack.lens.xyChart.iconSelect.boltIconLabel', { - defaultMessage: 'Bolt', - }), - }, - { - value: 'bug', - label: i18n.translate('xpack.lens.xyChart.iconSelect.bugIconLabel', { - defaultMessage: 'Bug', - }), - }, - { - value: 'circle', - label: i18n.translate('xpack.lens.xyChart.iconSelect.circleIconLabel', { - defaultMessage: 'Circle', - }), - icon: IconCircle, - canFill: true, - }, - - { - value: 'editorComment', - label: i18n.translate('xpack.lens.xyChart.iconSelect.commentIconLabel', { - defaultMessage: 'Comment', - }), - }, - { - value: 'flag', - label: i18n.translate('xpack.lens.xyChart.iconSelect.flagIconLabel', { - defaultMessage: 'Flag', - }), - }, - { - value: 'heart', - label: i18n.translate('xpack.lens.xyChart.iconSelect.heartLabel', { defaultMessage: 'Heart' }), - }, - { - value: 'mapMarker', - label: i18n.translate('xpack.lens.xyChart.iconSelect.mapMarkerLabel', { - defaultMessage: 'Map Marker', - }), - }, - { - value: 'pinFilled', - label: i18n.translate('xpack.lens.xyChart.iconSelect.mapPinLabel', { - defaultMessage: 'Map Pin', - }), - }, - { - value: 'starEmpty', - label: i18n.translate('xpack.lens.xyChart.iconSelect.starLabel', { defaultMessage: 'Star' }), - }, - { - value: 'starFilled', - label: i18n.translate('xpack.lens.xyChart.iconSelect.starFilledLabel', { - defaultMessage: 'Star filled', - }), - }, - { - value: 'tag', - label: i18n.translate('xpack.lens.xyChart.iconSelect.tagIconLabel', { - defaultMessage: 'Tag', - }), - }, - { - value: 'triangle', - label: i18n.translate('xpack.lens.xyChart.iconSelect.triangleIconLabel', { - defaultMessage: 'Triangle', - }), - icon: IconTriangle, - shouldRotate: true, - canFill: true, - }, -]; diff --git a/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/annotations_config_panel/index.test.tsx b/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/annotations_config_panel/index.test.tsx deleted file mode 100644 index 92fa248979fc3..0000000000000 --- a/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/annotations_config_panel/index.test.tsx +++ /dev/null @@ -1,668 +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 from 'react'; -import { mountWithIntl as mount } from '@kbn/test-jest-helpers'; -import { createDatatableUtilitiesMock } from '@kbn/data-plugin/common/mocks'; -import { LayerTypes } from '@kbn/expression-xy-plugin/public'; -import { AnnotationsPanel } from '.'; -import { FramePublicAPI } from '../../../../types'; -import { DatasourcePublicAPI } from '../../../..'; -import { createMockFramePublicAPI } from '../../../../mocks'; -import { State, XYState } from '../../types'; -import { Position } from '@elastic/charts'; -import { chartPluginMock } from '@kbn/charts-plugin/public/mocks'; -import moment from 'moment'; -import { EventAnnotationConfig } from '@kbn/event-annotation-plugin/common'; -import { createMockDataViewsState } from '../../../../data_views_service/mocks'; -import { createMockedIndexPattern } from '../../../../datasources/form_based/mocks'; -import { act } from 'react-dom/test-utils'; -import { EuiButtonGroup } from '@elastic/eui'; - -jest.mock('lodash', () => { - const original = jest.requireActual('lodash'); - - return { - ...original, - debounce: (fn: unknown) => fn, - }; -}); - -jest.mock('@kbn/unified-search-plugin/public', () => ({ - QueryStringInput: () => { - return 'QueryStringInput'; - }, -})); - -const customLineStaticAnnotation: EventAnnotationConfig = { - id: 'ann1', - type: 'manual', - key: { type: 'point_in_time' as const, timestamp: '2022-03-18T08:25:00.000Z' }, - label: 'Event', - icon: 'triangle' as const, - color: 'red', - lineStyle: 'dashed' as const, - lineWidth: 3, -}; - -describe('AnnotationsPanel', () => { - const datatableUtilities = createDatatableUtilitiesMock(); - let frame: FramePublicAPI; - - function testState(): State { - return { - legend: { isVisible: true, position: Position.Right }, - valueLabels: 'hide', - preferredSeriesType: 'bar', - layers: [ - { - layerType: LayerTypes.ANNOTATIONS, - layerId: 'annotation', - indexPatternId: 'indexPattern1', - annotations: [customLineStaticAnnotation], - ignoreGlobalFilters: true, - }, - ], - }; - } - - beforeEach(() => { - frame = createMockFramePublicAPI({ datasourceLayers: {} }); - }); - - describe('Dimension Editor', () => { - test('shows correct options for line annotations', () => { - const state = testState(); - const component = mount( - - ); - - expect( - component.find('EuiDatePicker[data-test-subj="lns-xyAnnotation-time"]').prop('selected') - ).toEqual(moment('2022-03-18T08:25:00.000Z')); - expect( - component.find('EuiDatePicker[data-test-subj="lns-xyAnnotation-fromTime"]').exists() - ).toBeFalsy(); - expect( - component.find('EuiDatePicker[data-test-subj="lns-xyAnnotation-toTime"]').exists() - ).toBeFalsy(); - expect( - component.find('EuiSwitch[data-test-subj="lns-xyAnnotation-rangeSwitch"]').prop('checked') - ).toEqual(false); - expect( - component.find('EuiFieldText[data-test-subj="column-label-edit"]').prop('value') - ).toEqual('Event'); - expect( - component.find('EuiComboBox[data-test-subj="lns-icon-select"]').prop('selectedOptions') - ).toEqual([{ label: 'Triangle', value: 'triangle' }]); - expect(component.find('TextDecorationSetting').exists()).toBeTruthy(); - expect(component.find('LineStyleSettings').exists()).toBeTruthy(); - expect( - component.find('EuiButtonGroup[data-test-subj="lns-xyAnnotation-fillStyle"]').exists() - ).toBeFalsy(); - }); - test('shows correct options for range annotations', () => { - const state = testState(); - state.layers[0] = { - annotations: [ - { - color: 'red', - icon: 'triangle', - id: 'ann1', - type: 'manual', - isHidden: undefined, - key: { - endTimestamp: '2022-03-21T10:49:00.000Z', - timestamp: '2022-03-18T08:25:00.000Z', - type: 'range', - }, - label: 'Event range', - lineStyle: 'dashed', - lineWidth: 3, - }, - ], - layerId: 'annotation', - layerType: 'annotations', - indexPatternId: 'indexPattern1', - ignoreGlobalFilters: true, - }; - const component = mount( - - ); - - expect( - component.find('EuiDatePicker[data-test-subj="lns-xyAnnotation-fromTime"]').prop('selected') - ).toEqual(moment('2022-03-18T08:25:00.000Z')); - expect( - component.find('EuiDatePicker[data-test-subj="lns-xyAnnotation-toTime"]').prop('selected') - ).toEqual(moment('2022-03-21T10:49:00.000Z')); - expect( - component.find('EuiDatePicker[data-test-subj="lns-xyAnnotation-time"]').exists() - ).toBeFalsy(); - expect( - component.find('EuiSwitch[data-test-subj="lns-xyAnnotation-rangeSwitch"]').prop('checked') - ).toEqual(true); - expect( - component.find('EuiFieldText[data-test-subj="column-label-edit"]').prop('value') - ).toEqual('Event range'); - expect(component.find('EuiComboBox[data-test-subj="lns-icon-select"]').exists()).toBeFalsy(); - expect(component.find('TextDecorationSetting').exists()).toBeFalsy(); - expect(component.find('LineStyleSettings').exists()).toBeFalsy(); - expect(component.find('[data-test-subj="lns-xyAnnotation-fillStyle"]').exists()).toBeTruthy(); - }); - - test('calculates correct endTimstamp and transparent color when switching for range annotation and back', () => { - const state = testState(); - const setState = jest.fn(); - const component = mount( - - ); - - component.find('button[data-test-subj="lns-xyAnnotation-rangeSwitch"]').simulate('click'); - - expect(setState).toBeCalledWith({ - ...state, - layers: [ - { - annotations: [ - { - color: '#FF00001A', - id: 'ann1', - isHidden: undefined, - label: 'Event range', - type: 'manual', - key: { - endTimestamp: '2022-03-21T10:49:00.000Z', - timestamp: '2022-03-18T08:25:00.000Z', - type: 'range', - }, - }, - ], - indexPatternId: 'indexPattern1', - layerId: 'annotation', - layerType: 'annotations', - ignoreGlobalFilters: true, - }, - ], - }); - component.find('button[data-test-subj="lns-xyAnnotation-rangeSwitch"]').simulate('click'); - expect(setState).toBeCalledWith({ - ...state, - layers: [ - { - annotations: [ - { - color: '#FF0000', - id: 'ann1', - isHidden: undefined, - key: { - timestamp: '2022-03-18T08:25:00.000Z', - type: 'point_in_time', - }, - label: 'Event', - type: 'manual', - }, - ], - indexPatternId: 'indexPattern1', - layerId: 'annotation', - layerType: 'annotations', - ignoreGlobalFilters: true, - }, - ], - }); - }); - - test('shows correct options for query based', () => { - const state = testState(); - const indexPattern = createMockedIndexPattern(); - state.layers[0] = { - annotations: [ - { - color: 'red', - icon: 'triangle', - id: 'ann1', - type: 'query', - isHidden: undefined, - timeField: 'timestamp', - key: { - type: 'point_in_time', - }, - label: 'Query based event', - lineStyle: 'dashed', - lineWidth: 3, - filter: { type: 'kibana_query', query: '', language: 'kuery' }, - }, - ], - layerId: 'annotation', - layerType: 'annotations', - indexPatternId: indexPattern.id, - ignoreGlobalFilters: true, - }; - const frameMock = createMockFramePublicAPI({ - datasourceLayers: {}, - dataViews: createMockDataViewsState({ - indexPatterns: { [indexPattern.id]: indexPattern }, - }), - }); - - const component = mount( - - ); - - expect( - component.find('[data-test-subj="lnsXY-annotation-query-based-field-picker"]').exists() - ).toBeTruthy(); - expect( - component.find('[data-test-subj="lnsXY-annotation-query-based-query-input"]').exists() - ).toBeTruthy(); - - // The provided indexPattern has 2 date fields - expect( - component - .find('[data-test-subj="lnsXY-annotation-query-based-field-picker"]') - .at(0) - .prop('options') - ).toHaveLength(2); - // When in query mode a new "field" option is added to the previous 2 ones - expect( - component.find('[data-test-subj="lns-lineMarker-text-visibility"]').at(0).prop('options') - ).toHaveLength(3); - expect( - component.find('[data-test-subj="lnsXY-annotation-tooltip-add_field"]').exists() - ).toBeTruthy(); - }); - - test('should prefill timeField with the default time field when switching to query based annotations', () => { - const state = testState(); - const indexPattern = createMockedIndexPattern(); - state.layers[0] = { - annotations: [customLineStaticAnnotation], - layerId: 'annotation', - layerType: 'annotations', - ignoreGlobalFilters: true, - indexPatternId: indexPattern.id, - }; - const frameMock = createMockFramePublicAPI({ - datasourceLayers: {}, - dataViews: createMockDataViewsState({ - indexPatterns: { [indexPattern.id]: indexPattern }, - }), - }); - - const setState = jest.fn(); - - const component = mount( - - ); - - act(() => { - component - .find(`[data-test-subj="lns-xyAnnotation-placementType"]`) - .find(EuiButtonGroup) - .prop('onChange')!('lens_xyChart_annotation_query'); - }); - component.update(); - - expect(setState).toHaveBeenCalledWith( - expect.objectContaining({ - layers: [ - expect.objectContaining({ - annotations: [expect.objectContaining({ timeField: 'timestamp' })], - }), - ], - }) - ); - }); - - test('should avoid to retain specific manual configurations when switching to query based annotations', () => { - const state = testState(); - const indexPattern = createMockedIndexPattern(); - state.layers[0] = { - annotations: [customLineStaticAnnotation], - layerId: 'annotation', - layerType: 'annotations', - ignoreGlobalFilters: true, - indexPatternId: indexPattern.id, - }; - const frameMock = createMockFramePublicAPI({ - datasourceLayers: {}, - dataViews: createMockDataViewsState({ - indexPatterns: { [indexPattern.id]: indexPattern }, - }), - }); - - const setState = jest.fn(); - - const component = mount( - - ); - - act(() => { - component - .find(`[data-test-subj="lns-xyAnnotation-placementType"]`) - .find(EuiButtonGroup) - .prop('onChange')!('lens_xyChart_annotation_query'); - }); - component.update(); - - expect(setState).toHaveBeenCalledWith( - expect.objectContaining({ - layers: [ - expect.objectContaining({ - annotations: [ - expect.objectContaining({ - key: expect.not.objectContaining({ timestamp: expect.any('string') }), - }), - ], - }), - ], - }) - ); - }); - - test('should avoid to retain range manual configurations when switching to query based annotations', () => { - const state = testState(); - const indexPattern = createMockedIndexPattern(); - state.layers[0] = { - annotations: [ - { - color: 'red', - icon: 'triangle', - id: 'ann1', - type: 'manual', - isHidden: undefined, - key: { - endTimestamp: '2022-03-21T10:49:00.000Z', - timestamp: '2022-03-18T08:25:00.000Z', - type: 'range', - }, - label: 'Event range', - lineStyle: 'dashed', - lineWidth: 3, - }, - ], - layerId: 'annotation', - layerType: 'annotations', - ignoreGlobalFilters: true, - indexPatternId: indexPattern.id, - }; - const frameMock = createMockFramePublicAPI({ - datasourceLayers: {}, - dataViews: createMockDataViewsState({ - indexPatterns: { [indexPattern.id]: indexPattern }, - }), - }); - - const setState = jest.fn(); - - const component = mount( - - ); - - act(() => { - component - .find(`[data-test-subj="lns-xyAnnotation-placementType"]`) - .find(EuiButtonGroup) - .prop('onChange')!('lens_xyChart_annotation_query'); - }); - component.update(); - - expect(setState).toHaveBeenCalledWith( - expect.objectContaining({ - layers: [ - expect.objectContaining({ - annotations: [ - expect.objectContaining({ label: expect.not.stringContaining('Event range') }), - ], - }), - ], - }) - ); - }); - - test('should set a default tiemstamp when switching from query based to manual annotations', () => { - const state = testState(); - const indexPattern = createMockedIndexPattern(); - state.layers[0] = { - annotations: [ - { - color: 'red', - icon: 'triangle', - id: 'ann1', - type: 'query', - isHidden: undefined, - timeField: 'timestamp', - key: { - type: 'point_in_time', - }, - label: 'Query based event', - lineStyle: 'dashed', - lineWidth: 3, - filter: { type: 'kibana_query', query: '', language: 'kuery' }, - }, - ], - layerId: 'annotation', - layerType: 'annotations', - indexPatternId: indexPattern.id, - ignoreGlobalFilters: true, - }; - const frameMock = createMockFramePublicAPI({ - datasourceLayers: {}, - dataViews: createMockDataViewsState({ - indexPatterns: { [indexPattern.id]: indexPattern }, - }), - }); - - const setState = jest.fn(); - - const component = mount( - - ); - - act(() => { - component - .find(`[data-test-subj="lns-xyAnnotation-placementType"]`) - .find(EuiButtonGroup) - .prop('onChange')!('lens_xyChart_annotation_manual'); - }); - component.update(); - - expect(setState).toHaveBeenCalledWith( - expect.objectContaining({ - layers: [ - expect.objectContaining({ - annotations: [ - expect.objectContaining({ - key: { type: 'point_in_time', timestamp: expect.any(String) }, - }), - ], - }), - ], - }) - ); - - // also check query specific props are not carried over - expect(setState).toHaveBeenCalledWith( - expect.objectContaining({ - layers: [ - expect.objectContaining({ - annotations: [expect.not.objectContaining({ timeField: 'timestamp' })], - }), - ], - }) - ); - }); - - test('should fallback to the first date field available in the dataView if not time-based', () => { - const state = testState(); - const indexPattern = createMockedIndexPattern({ timeFieldName: '' }); - state.layers[0] = { - annotations: [customLineStaticAnnotation], - layerId: 'annotation', - layerType: 'annotations', - ignoreGlobalFilters: true, - indexPatternId: indexPattern.id, - }; - const frameMock = createMockFramePublicAPI({ - datasourceLayers: {}, - dataViews: createMockDataViewsState({ - indexPatterns: { [indexPattern.id]: indexPattern }, - }), - }); - - const setState = jest.fn(); - - const component = mount( - - ); - - act(() => { - component - .find(`[data-test-subj="lns-xyAnnotation-placementType"]`) - .find(EuiButtonGroup) - .prop('onChange')!('lens_xyChart_annotation_query'); - }); - component.update(); - - expect(setState).toHaveBeenCalledWith( - expect.objectContaining({ - layers: [ - expect.objectContaining({ - annotations: [expect.objectContaining({ timeField: 'timestampLabel' })], - }), - ], - }) - ); - }); - }); -}); diff --git a/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/annotations_config_panel/types.ts b/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/annotations_config_panel/types.ts deleted file mode 100644 index f446afb6be265..0000000000000 --- a/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/annotations_config_panel/types.ts +++ /dev/null @@ -1,15 +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 { - PointInTimeEventAnnotationConfig, - RangeEventAnnotationConfig, -} from '@kbn/event-annotation-plugin/common'; - -export type ManualEventAnnotationType = - | PointInTimeEventAnnotationConfig - | RangeEventAnnotationConfig; diff --git a/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/dimension_editor.tsx b/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/dimension_editor.tsx index f5d19edd81c5e..4ce681eedbd51 100644 --- a/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/dimension_editor.tsx +++ b/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/dimension_editor.tsx @@ -9,7 +9,6 @@ import React, { useCallback, useMemo } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiButtonGroup, EuiFormRow, htmlIdGenerator } from '@elastic/eui'; import type { PaletteRegistry } from '@kbn/coloring'; -import type { DatatableUtilitiesService } from '@kbn/data-plugin/common'; import { useDebouncedValue } from '@kbn/visualization-ui-components/public'; import { ColorPicker } from '@kbn/visualization-ui-components/public'; import type { VisualizationDimensionEditorProps } from '../../../types'; @@ -17,9 +16,7 @@ import { State, XYState, XYDataLayerConfig, YConfig, YAxisMode } from '../types' import { FormatFactory } from '../../../../common/types'; import { getSeriesColor, isHorizontalChart } from '../state_helpers'; import { PalettePicker } from '../../../shared_components'; -import { getDataLayers, isAnnotationsLayer, isReferenceLayer } from '../visualization_helpers'; -import { ReferenceLinePanel } from './reference_line_config_panel'; -import { AnnotationsPanel } from './annotations_config_panel'; +import { getDataLayers } from '../visualization_helpers'; import { CollapseSetting } from '../../../shared_components/collapse_setting'; import { getSortedAccessors } from '../to_expression'; import { getColorAssignments, getAssignedColorConfig } from '../color_assignment'; @@ -42,26 +39,6 @@ export function updateLayer( export const idPrefix = htmlIdGenerator()(); -export function DimensionEditor( - props: VisualizationDimensionEditorProps & { - datatableUtilities: DatatableUtilitiesService; - formatFactory: FormatFactory; - paletteService: PaletteRegistry; - } -) { - const { state, layerId } = props; - const index = state.layers.findIndex((l) => l.layerId === layerId); - const layer = state.layers[index]; - if (isAnnotationsLayer(layer)) { - return ; - } - - if (isReferenceLayer(layer)) { - return ; - } - return ; -} - export function DataDimensionEditor( props: VisualizationDimensionEditorProps & { formatFactory: FormatFactory; diff --git a/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/reference_line_config_panel/reference_line_panel.tsx b/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/reference_line_config_panel/reference_line_panel.tsx index d160777eeaa4d..6f6b4807b6f96 100644 --- a/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/reference_line_config_panel/reference_line_panel.tsx +++ b/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/reference_line_config_panel/reference_line_panel.tsx @@ -14,6 +14,8 @@ import { useDebouncedValue, IconSelectSetting, ColorPicker, + LineStyleSettings, + TextDecorationSetting, } from '@kbn/visualization-ui-components/public'; import type { VisualizationDimensionEditorProps } from '../../../../types'; import { State, XYState, XYReferenceLineLayerConfig, YConfig } from '../../types'; @@ -22,11 +24,7 @@ import { FormatFactory } from '../../../../../common/types'; import { updateLayer } from '..'; import { idPrefix } from '../dimension_editor'; import { isHorizontalChart } from '../../state_helpers'; -import { - MarkerDecorationPosition, - TextDecorationSetting, -} from '../shared/marker_decoration_settings'; -import { LineStyleSettings } from '../shared/line_style_settings'; +import { MarkerDecorationPosition } from '../shared/marker_decoration_settings'; import { referenceLineIconsSet } from './icon_set'; import { defaultReferenceLineColor } from '../../color_assignment'; @@ -77,7 +75,11 @@ export const ReferenceLinePanel = ( return ( <> - + setConfig({ icon })} currentIcon={localConfig?.icon} @@ -88,11 +90,7 @@ export const ReferenceLinePanel = ( setConfig={setConfig} currentConfig={localConfig} /> - + { textField?: string; } -function getSelectedOption( - { textField, textVisibility }: MarkerDecorationConfig = {}, - isQueryBased?: boolean -) { - if (!textVisibility) { - return 'none'; - } - if (isQueryBased && textField) { - return 'field'; - } - return 'name'; -} - -export function TextDecorationSetting({ - currentConfig, - setConfig, - isQueryBased, - children, -}: { - currentConfig?: MarkerDecorationConfig; - setConfig: (config: MarkerDecorationConfig) => void; - isQueryBased?: boolean; - /** A children render function for custom sub fields on textDecoration change */ - children?: (textDecoration: 'none' | 'name' | 'field') => JSX.Element | null; -}) { - // To model the temporary state for label based on field when user didn't pick up the field yet, - // use a local state - const [selectedVisibleOption, setVisibleOption] = useState<'none' | 'name' | 'field'>( - getSelectedOption(currentConfig, isQueryBased) - ); - const options = [ - { - id: `${idPrefix}none`, - label: i18n.translate('xpack.lens.xyChart.lineMarker.textVisibility.none', { - defaultMessage: 'None', - }), - 'data-test-subj': 'lnsXY_textVisibility_none', - }, - { - id: `${idPrefix}name`, - label: i18n.translate('xpack.lens.xyChart.lineMarker.textVisibility.name', { - defaultMessage: 'Name', - }), - 'data-test-subj': 'lnsXY_textVisibility_name', - }, - ]; - if (isQueryBased) { - options.push({ - id: `${idPrefix}field`, - label: i18n.translate('xpack.lens.xyChart.lineMarker.textVisibility.field', { - defaultMessage: 'Field', - }), - 'data-test-subj': 'lnsXY_textVisibility_field', - }); - } - - return ( - -
- { - const chosenOption = id.replace(idPrefix, '') as 'none' | 'name' | 'field'; - if (chosenOption === 'none') { - setConfig({ - textVisibility: false, - textField: undefined, - }); - } else if (chosenOption === 'name') { - setConfig({ - textVisibility: true, - textField: undefined, - }); - } else if (chosenOption === 'field') { - setConfig({ - textVisibility: Boolean(currentConfig?.textField), - }); - } - - setVisibleOption(chosenOption); - }} - isFullWidth - /> - {children?.(selectedVisibleOption)} -
-
- ); -} - export function MarkerDecorationPosition({ currentConfig, setConfig, diff --git a/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/xy_config_panel.test.tsx b/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/xy_config_panel.test.tsx index 6caa8238808e9..252c3de6b8e57 100644 --- a/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/xy_config_panel.test.tsx +++ b/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/xy_config_panel.test.tsx @@ -8,9 +8,8 @@ import React from 'react'; import { mountWithIntl as mount, shallowWithIntl as shallow } from '@kbn/test-jest-helpers'; import { EuiButtonGroupProps, EuiButtonGroup } from '@elastic/eui'; -import { createDatatableUtilitiesMock } from '@kbn/data-plugin/common/mocks'; import { XyToolbar } from '.'; -import { DimensionEditor } from './dimension_editor'; +import { DataDimensionEditor } from './dimension_editor'; import { AxisSettingsPopover } from './axis_settings_popover'; import { FramePublicAPI, DatasourcePublicAPI } from '../../../types'; import { State, XYState, XYDataLayerConfig } from '../types'; @@ -254,12 +253,10 @@ describe('XY Config panels', () => { }); describe('Dimension Editor', () => { - const datatableUtilities = createDatatableUtilitiesMock(); - test('shows the correct axis side options when in horizontal mode', () => { const state = testState(); const component = mount( - { ...state, layers: [{ ...state.layers[0], seriesType: 'bar_horizontal' } as XYDataLayerConfig], }} - datatableUtilities={datatableUtilities} formatFactory={jest.fn()} paletteService={chartPluginMock.createPaletteRegistry()} panelRef={React.createRef()} @@ -290,14 +286,13 @@ describe('XY Config panels', () => { test('shows the default axis side options when not in horizontal mode', () => { const state = testState(); const component = mount( - { ], } as XYState; const component = mount( - { accessor="bar" groupId="left" state={state} - datatableUtilities={datatableUtilities} formatFactory={jest.fn()} paletteService={chartPluginMock.createPaletteRegistry()} panelRef={React.createRef()} @@ -376,7 +370,7 @@ describe('XY Config panels', () => { } as XYState; const component = mount( - { accessor="bar" groupId="left" state={state} - datatableUtilities={datatableUtilities} formatFactory={jest.fn()} paletteService={chartPluginMock.createPaletteRegistry()} panelRef={React.createRef()} @@ -422,7 +415,7 @@ describe('XY Config panels', () => { } as XYState; const component = mount( - { accessor="bar" groupId="left" state={state} - datatableUtilities={datatableUtilities} formatFactory={jest.fn()} paletteService={chartPluginMock.createPaletteRegistry()} panelRef={React.createRef()} diff --git a/x-pack/plugins/lens/public/visualizations/xy/xy_suggestions.test.ts b/x-pack/plugins/lens/public/visualizations/xy/xy_suggestions.test.ts index 0007493f3b8eb..8417d02d79995 100644 --- a/x-pack/plugins/lens/public/visualizations/xy/xy_suggestions.test.ts +++ b/x-pack/plugins/lens/public/visualizations/xy/xy_suggestions.test.ts @@ -25,6 +25,7 @@ import { coreMock, themeServiceMock } from '@kbn/core/public/mocks'; import { dataPluginMock } from '@kbn/data-plugin/public/mocks'; import { IStorageWrapper } from '@kbn/kibana-utils-plugin/public'; import { unifiedSearchPluginMock } from '@kbn/unified-search-plugin/public/mocks'; +import { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public'; jest.mock('../../id_generator'); @@ -38,6 +39,7 @@ const xyVisualization = getXyVisualization({ storage: {} as IStorageWrapper, data: dataPluginMock.createStartContract(), unifiedSearch: unifiedSearchPluginMock.createStartContract(), + dataViewsService: {} as DataViewsPublicPluginStart, }); describe('xy_suggestions', () => { diff --git a/x-pack/plugins/maps/public/render_app.tsx b/x-pack/plugins/maps/public/render_app.tsx index c4bddf5a71541..3897e38222eef 100644 --- a/x-pack/plugins/maps/public/render_app.tsx +++ b/x-pack/plugins/maps/public/render_app.tsx @@ -15,7 +15,7 @@ import { ExitFullScreenButtonKibanaProvider } from '@kbn/shared-ux-button-exit-f import { KibanaThemeProvider, toMountPoint } from '@kbn/kibana-react-plugin/public'; import { FormattedRelative } from '@kbn/i18n-react'; import type { SavedObjectTaggingPluginStart } from '@kbn/saved-objects-tagging-plugin/public'; -import { TableListViewKibanaProvider } from '@kbn/content-management-table-list'; +import { TableListViewKibanaProvider } from '@kbn/content-management-table-list-view-table'; import { getCoreChrome, getCoreI18n, diff --git a/x-pack/plugins/maps/public/routes/list_page/maps_list_view.tsx b/x-pack/plugins/maps/public/routes/list_page/maps_list_view.tsx index d98444057097d..93fc858f1c9d8 100644 --- a/x-pack/plugins/maps/public/routes/list_page/maps_list_view.tsx +++ b/x-pack/plugins/maps/public/routes/list_page/maps_list_view.tsx @@ -9,8 +9,8 @@ import React, { useCallback, memo, useEffect } from 'react'; import type { SavedObjectsFindOptionsReference, ScopedHistory } from '@kbn/core/public'; import { METRIC_TYPE } from '@kbn/analytics'; import { i18n } from '@kbn/i18n'; -import { TableListView } from '@kbn/content-management-table-list'; -import type { UserContentCommonSchema } from '@kbn/content-management-table-list'; +import { TableListView } from '@kbn/content-management-table-list-view'; +import type { UserContentCommonSchema } from '@kbn/content-management-table-list-view'; import type { MapItem } from '../../../common/content_management'; import { APP_ID, APP_NAME, getEditPath, MAP_PATH } from '../../../common/constants'; @@ -133,7 +133,7 @@ function MapsListViewComp({ history }: Props) { entityNamePlural={i18n.translate('xpack.maps.mapListing.entityNamePlural', { defaultMessage: 'maps', })} - tableListTitle={APP_NAME} + title={APP_NAME} onClickTitle={({ id }) => history.push(getEditPath(id))} /> ); diff --git a/x-pack/plugins/maps/tsconfig.json b/x-pack/plugins/maps/tsconfig.json index 6f24d4d7d3c5e..b0e27ee323dba 100644 --- a/x-pack/plugins/maps/tsconfig.json +++ b/x-pack/plugins/maps/tsconfig.json @@ -50,7 +50,6 @@ "@kbn/field-formats-plugin", "@kbn/shared-ux-button-exit-full-screen", "@kbn/i18n-react", - "@kbn/content-management-table-list", "@kbn/react-field", "@kbn/analytics", "@kbn/mapbox-gl", @@ -71,6 +70,8 @@ "@kbn/core-saved-objects-server", "@kbn/maps-vector-tile-utils", "@kbn/core-http-common", + "@kbn/content-management-table-list-view-table", + "@kbn/content-management-table-list-view", ], "exclude": [ "target/**/*", diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index 009cb044b4c3e..65e1cae7b8ec6 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -20922,7 +20922,6 @@ "xpack.lens.legacyUrlConflict.objectNoun": "Visualisation Lens", "xpack.lens.lensSavedObjectLabel": "Visualisation Lens", "xpack.lens.lineMarker.positionRequirementTooltip": "Vous devez sélectionner une icône ou afficher le nom pour pouvoir en modifier la position.", - "xpack.lens.lineMarker.textVisibility": "Décoration du texte", "xpack.lens.metric.addLayer": "Visualisation", "xpack.lens.metric.breakdownBy": "Répartir par", "xpack.lens.metric.color": "Couleur", @@ -21041,7 +21040,6 @@ "xpack.lens.pieChart.visualOptionsLabel": "Options visuelles", "xpack.lens.primaryMetric.headingLabel": "Valeur", "xpack.lens.primaryMetric.label": "Indicateur principal", - "xpack.lens.queryInput.appName": "Lens", "xpack.lens.reporting.shareContextMenu.csvReportsButtonLabel": "Téléchargement CSV", "xpack.lens.resetLayerAriaLabel": "Effacer le calque", "xpack.lens.saveDuplicateRejectedDescription": "La confirmation d'enregistrement avec un doublon de titre a été rejetée.", @@ -21157,22 +21155,7 @@ "xpack.lens.xyChart.addDataLayerLabel": "Visualisation", "xpack.lens.xyChart.addReferenceLineLayerLabel": "Lignes de référence", "xpack.lens.xyChart.addReferenceLineLayerLabelDisabledHelp": "Ajouter des données pour activer le calque de référence", - "xpack.lens.xyChart.annotation.hide": "Masquer l’annotation", - "xpack.lens.xyChart.annotation.manual": "Date statique", - "xpack.lens.xyChart.annotation.query": "Requête personnalisée", - "xpack.lens.xyChart.annotation.queryField": "Champ de date cible", - "xpack.lens.xyChart.annotation.queryInput": "Requête sur les annotations", - "xpack.lens.xyChart.annotation.tooltip": "Afficher les champs supplémentaires", - "xpack.lens.xyChart.annotation.tooltip.addField": "Ajouter un champ", - "xpack.lens.xyChart.annotation.tooltip.deleteButtonLabel": "Supprimer", - "xpack.lens.xyChart.annotation.tooltip.noFields": "Aucune sélection", - "xpack.lens.xyChart.annotationDate": "Date de l’annotation", - "xpack.lens.xyChart.annotationDate.from": "De", - "xpack.lens.xyChart.annotationDate.placementType": "Type de placement", - "xpack.lens.xyChart.annotationDate.to": "À", "xpack.lens.xyChart.annotationError.timeFieldEmpty": "Le champ temporel est manquant", - "xpack.lens.xyChart.appearance": "Apparence", - "xpack.lens.xyChart.applyAsRange": "Appliquer en tant que plage", "xpack.lens.xyChart.axisOrientation.angled": "En angle", "xpack.lens.xyChart.axisOrientation.horizontal": "Horizontal", "xpack.lens.xyChart.axisOrientation.label": "Orientation", @@ -21195,9 +21178,6 @@ "xpack.lens.xyChart.fill.label": "Remplir", "xpack.lens.xyChart.fill.none": "Aucun", "xpack.lens.xyChart.fillOpacityLabel": "Opacité de remplissage", - "xpack.lens.xyChart.fillStyle": "Remplir", - "xpack.lens.xyChart.fillStyle.inside": "Intérieur", - "xpack.lens.xyChart.fillStyle.outside": "Extérieur", "xpack.lens.xyChart.Gridlines": "Quadrillage", "xpack.lens.xyChart.horizontalAxisLabel": "Axe horizontal", "xpack.lens.xyChart.horizontalLeftAxisLabel": "Axe supérieur horizontal", @@ -21207,17 +21187,10 @@ "xpack.lens.xyChart.iconSelect.bellIconLabel": "Cloche", "xpack.lens.xyChart.iconSelect.boltIconLabel": "Éclair", "xpack.lens.xyChart.iconSelect.bugIconLabel": "Bug", - "xpack.lens.xyChart.iconSelect.circleIconLabel": "Cercle", "xpack.lens.xyChart.iconSelect.commentIconLabel": "Commentaire", "xpack.lens.xyChart.iconSelect.flagIconLabel": "Drapeau", - "xpack.lens.xyChart.iconSelect.heartLabel": "Cœur", - "xpack.lens.xyChart.iconSelect.mapMarkerLabel": "Repère", - "xpack.lens.xyChart.iconSelect.mapPinLabel": "Punaise", "xpack.lens.xyChart.iconSelect.noIconLabel": "Aucun", - "xpack.lens.xyChart.iconSelect.starFilledLabel": "Étoile remplie", - "xpack.lens.xyChart.iconSelect.starLabel": "Étoile", "xpack.lens.xyChart.iconSelect.tagIconLabel": "Balise", - "xpack.lens.xyChart.iconSelect.triangleIconLabel": "Triangle", "xpack.lens.xyChart.layerAnnotation": "Annotation", "xpack.lens.xyChart.layerAnnotationsIgnoreTitle": "Calques ignorant les filtres globaux", "xpack.lens.xyChart.layerAnnotationsLabel": "Annotations", @@ -21233,13 +21206,6 @@ "xpack.lens.xyChart.lineColor.label": "Couleur", "xpack.lens.xyChart.lineMarker.auto": "Auto", "xpack.lens.xyChart.lineMarker.position": "Position de la décoration", - "xpack.lens.xyChart.lineMarker.textVisibility.field": "Champ", - "xpack.lens.xyChart.lineMarker.textVisibility.name": "Nom", - "xpack.lens.xyChart.lineMarker.textVisibility.none": "Aucun", - "xpack.lens.xyChart.lineStyle.dashed": "Tirets", - "xpack.lens.xyChart.lineStyle.dotted": "Pointillé", - "xpack.lens.xyChart.lineStyle.label": "Ligne", - "xpack.lens.xyChart.lineStyle.solid": "Uni", "xpack.lens.xyChart.markerPosition.above": "Haut", "xpack.lens.xyChart.markerPosition.below": "Bas", "xpack.lens.xyChart.markerPosition.left": "Gauche", @@ -21248,7 +21214,6 @@ "xpack.lens.xyChart.missingValuesLabelHelpText": "Par défaut, les graphiques en aires et en courbes masquent les blancs entre les données. Pour remplir le blanc, effectuez une sélection.", "xpack.lens.xyChart.missingValuesStyle": "Afficher sous la forme d’une ligne pointillée", "xpack.lens.xyChart.nestUnderRoot": "Ensemble de données entier", - "xpack.lens.xyChart.placement": "Placement", "xpack.lens.xyChart.rightAxisDisabledHelpText": "Ce paramètre s'applique uniquement lorsque l'axe de droite est activé.", "xpack.lens.xyChart.rightAxisLabel": "Axe de droite", "xpack.lens.xyChart.scaleLinear": "Linéaire", @@ -21258,7 +21223,6 @@ "xpack.lens.xyChart.showCurrenTimeMarker": "Afficher le repère de temps actuel", "xpack.lens.xyChart.showEnzones": "Afficher les marqueurs de données partielles", "xpack.lens.xyChart.splitSeries": "Répartition", - "xpack.lens.xyChart.tooltip": "Infobulle", "xpack.lens.xyChart.topAxisDisabledHelpText": "Ce paramètre s'applique uniquement lorsque l'axe du haut est activé.", "xpack.lens.xyChart.topAxisLabel": "Axe du haut", "xpack.lens.xyChart.valuesHistogramDisabledHelpText": "Ce paramètre ne peut pas être modifié dans les histogrammes.", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 44eebedd74528..30ca83889d63f 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -20922,7 +20922,6 @@ "xpack.lens.legacyUrlConflict.objectNoun": "レンズビジュアライゼーション", "xpack.lens.lensSavedObjectLabel": "レンズビジュアライゼーション", "xpack.lens.lineMarker.positionRequirementTooltip": "位置を変更するには、アイコンを選択するか、名前を表示する必要があります", - "xpack.lens.lineMarker.textVisibility": "テキスト装飾", "xpack.lens.metric.addLayer": "ビジュアライゼーション", "xpack.lens.metric.breakdownBy": "内訳の基準", "xpack.lens.metric.color": "色", @@ -21041,7 +21040,6 @@ "xpack.lens.pieChart.visualOptionsLabel": "視覚オプション", "xpack.lens.primaryMetric.headingLabel": "値", "xpack.lens.primaryMetric.label": "主メトリック", - "xpack.lens.queryInput.appName": "レンズ", "xpack.lens.reporting.shareContextMenu.csvReportsButtonLabel": "CSVダウンロード", "xpack.lens.resetLayerAriaLabel": "レイヤーをクリア", "xpack.lens.saveDuplicateRejectedDescription": "重複ファイルの保存確認が拒否されました", @@ -21157,22 +21155,7 @@ "xpack.lens.xyChart.addDataLayerLabel": "ビジュアライゼーション", "xpack.lens.xyChart.addReferenceLineLayerLabel": "基準線", "xpack.lens.xyChart.addReferenceLineLayerLabelDisabledHelp": "一部のデータを追加して、基準レイヤーを有効にする", - "xpack.lens.xyChart.annotation.hide": "注釈を非表示", - "xpack.lens.xyChart.annotation.manual": "固定日付", - "xpack.lens.xyChart.annotation.query": "カスタムクエリ", - "xpack.lens.xyChart.annotation.queryField": "ターゲット日付フィールド", - "xpack.lens.xyChart.annotation.queryInput": "注釈クエリ", - "xpack.lens.xyChart.annotation.tooltip": "追加フィールドを表示", - "xpack.lens.xyChart.annotation.tooltip.addField": "フィールドの追加", - "xpack.lens.xyChart.annotation.tooltip.deleteButtonLabel": "削除", - "xpack.lens.xyChart.annotation.tooltip.noFields": "選択されていません", - "xpack.lens.xyChart.annotationDate": "注釈日", - "xpack.lens.xyChart.annotationDate.from": "開始:", - "xpack.lens.xyChart.annotationDate.placementType": "配置タイプ", - "xpack.lens.xyChart.annotationDate.to": "終了:", "xpack.lens.xyChart.annotationError.timeFieldEmpty": "時刻フィールドがありません", - "xpack.lens.xyChart.appearance": "見た目", - "xpack.lens.xyChart.applyAsRange": "範囲として適用", "xpack.lens.xyChart.axisOrientation.angled": "傾斜", "xpack.lens.xyChart.axisOrientation.horizontal": "横", "xpack.lens.xyChart.axisOrientation.label": "向き", @@ -21195,9 +21178,6 @@ "xpack.lens.xyChart.fill.label": "塗りつぶし", "xpack.lens.xyChart.fill.none": "なし", "xpack.lens.xyChart.fillOpacityLabel": "塗りつぶしの透明度", - "xpack.lens.xyChart.fillStyle": "塗りつぶし", - "xpack.lens.xyChart.fillStyle.inside": "内部", - "xpack.lens.xyChart.fillStyle.outside": "外側", "xpack.lens.xyChart.Gridlines": "グリッド線", "xpack.lens.xyChart.horizontalAxisLabel": "横軸", "xpack.lens.xyChart.horizontalLeftAxisLabel": "横上軸", @@ -21207,17 +21187,10 @@ "xpack.lens.xyChart.iconSelect.bellIconLabel": "ベル", "xpack.lens.xyChart.iconSelect.boltIconLabel": "ボルト", "xpack.lens.xyChart.iconSelect.bugIconLabel": "バグ", - "xpack.lens.xyChart.iconSelect.circleIconLabel": "円", "xpack.lens.xyChart.iconSelect.commentIconLabel": "コメント", "xpack.lens.xyChart.iconSelect.flagIconLabel": "旗", - "xpack.lens.xyChart.iconSelect.heartLabel": "ハート", - "xpack.lens.xyChart.iconSelect.mapMarkerLabel": "マップマーカー", - "xpack.lens.xyChart.iconSelect.mapPinLabel": "マップピン", "xpack.lens.xyChart.iconSelect.noIconLabel": "なし", - "xpack.lens.xyChart.iconSelect.starFilledLabel": "塗りつぶされた星", - "xpack.lens.xyChart.iconSelect.starLabel": "星", "xpack.lens.xyChart.iconSelect.tagIconLabel": "タグ", - "xpack.lens.xyChart.iconSelect.triangleIconLabel": "三角形", "xpack.lens.xyChart.layerAnnotation": "注釈", "xpack.lens.xyChart.layerAnnotationsIgnoreTitle": "グローバルフィルターを無視するレイヤー", "xpack.lens.xyChart.layerAnnotationsLabel": "注釈", @@ -21233,13 +21206,6 @@ "xpack.lens.xyChart.lineColor.label": "色", "xpack.lens.xyChart.lineMarker.auto": "自動", "xpack.lens.xyChart.lineMarker.position": "装飾位置", - "xpack.lens.xyChart.lineMarker.textVisibility.field": "フィールド", - "xpack.lens.xyChart.lineMarker.textVisibility.name": "名前", - "xpack.lens.xyChart.lineMarker.textVisibility.none": "なし", - "xpack.lens.xyChart.lineStyle.dashed": "鎖線", - "xpack.lens.xyChart.lineStyle.dotted": "点線", - "xpack.lens.xyChart.lineStyle.label": "折れ線", - "xpack.lens.xyChart.lineStyle.solid": "塗りつぶし", "xpack.lens.xyChart.markerPosition.above": "トップ", "xpack.lens.xyChart.markerPosition.below": "一番下", "xpack.lens.xyChart.markerPosition.left": "左", @@ -21248,7 +21214,6 @@ "xpack.lens.xyChart.missingValuesLabelHelpText": "デフォルトでは、面グラフと折れ線グラフはデータのギャップが非表示になります。ギャップを埋めるには、選択します。", "xpack.lens.xyChart.missingValuesStyle": "点線として表示", "xpack.lens.xyChart.nestUnderRoot": "データセット全体", - "xpack.lens.xyChart.placement": "配置", "xpack.lens.xyChart.rightAxisDisabledHelpText": "この設定は、右の軸が有効であるときにのみ適用されます。", "xpack.lens.xyChart.rightAxisLabel": "右の軸", "xpack.lens.xyChart.scaleLinear": "線形", @@ -21258,7 +21223,6 @@ "xpack.lens.xyChart.showCurrenTimeMarker": "現在時刻マーカーを表示", "xpack.lens.xyChart.showEnzones": "部分データマーカーを表示", "xpack.lens.xyChart.splitSeries": "内訳", - "xpack.lens.xyChart.tooltip": "ツールチップ", "xpack.lens.xyChart.topAxisDisabledHelpText": "この設定は、上の軸が有効であるときにのみ適用されます。", "xpack.lens.xyChart.topAxisLabel": "上の軸", "xpack.lens.xyChart.valuesHistogramDisabledHelpText": "この設定はヒストグラムで変更できません。", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 73eb81fb3bc38..212f1bed8ff47 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -20922,7 +20922,6 @@ "xpack.lens.legacyUrlConflict.objectNoun": "Lens 可视化", "xpack.lens.lensSavedObjectLabel": "Lens 可视化", "xpack.lens.lineMarker.positionRequirementTooltip": "必须选择图标或显示名称才能更改其位置", - "xpack.lens.lineMarker.textVisibility": "文本装饰", "xpack.lens.metric.addLayer": "可视化", "xpack.lens.metric.breakdownBy": "细分方式", "xpack.lens.metric.color": "颜色", @@ -21041,7 +21040,6 @@ "xpack.lens.pieChart.visualOptionsLabel": "视觉选项", "xpack.lens.primaryMetric.headingLabel": "值", "xpack.lens.primaryMetric.label": "主要指标", - "xpack.lens.queryInput.appName": "Lens", "xpack.lens.reporting.shareContextMenu.csvReportsButtonLabel": "CSV 下载", "xpack.lens.resetLayerAriaLabel": "清除图层", "xpack.lens.saveDuplicateRejectedDescription": "已拒绝使用重复标题保存确认", @@ -21157,22 +21155,7 @@ "xpack.lens.xyChart.addDataLayerLabel": "可视化", "xpack.lens.xyChart.addReferenceLineLayerLabel": "参考线", "xpack.lens.xyChart.addReferenceLineLayerLabelDisabledHelp": "添加一些数据以启用参考图层", - "xpack.lens.xyChart.annotation.hide": "隐藏标注", - "xpack.lens.xyChart.annotation.manual": "静态日期", - "xpack.lens.xyChart.annotation.query": "定制查询", - "xpack.lens.xyChart.annotation.queryField": "目标日期字段", - "xpack.lens.xyChart.annotation.queryInput": "标注查询", - "xpack.lens.xyChart.annotation.tooltip": "显示其他字段", - "xpack.lens.xyChart.annotation.tooltip.addField": "添加字段", - "xpack.lens.xyChart.annotation.tooltip.deleteButtonLabel": "删除", - "xpack.lens.xyChart.annotation.tooltip.noFields": "未选择任何内容", - "xpack.lens.xyChart.annotationDate": "标注日期", - "xpack.lens.xyChart.annotationDate.from": "自", - "xpack.lens.xyChart.annotationDate.placementType": "位置类型", - "xpack.lens.xyChart.annotationDate.to": "至", "xpack.lens.xyChart.annotationError.timeFieldEmpty": "缺少时间字段", - "xpack.lens.xyChart.appearance": "外观", - "xpack.lens.xyChart.applyAsRange": "应用为范围", "xpack.lens.xyChart.axisOrientation.angled": "带角度", "xpack.lens.xyChart.axisOrientation.horizontal": "水平", "xpack.lens.xyChart.axisOrientation.label": "方向", @@ -21195,9 +21178,6 @@ "xpack.lens.xyChart.fill.label": "填充", "xpack.lens.xyChart.fill.none": "无", "xpack.lens.xyChart.fillOpacityLabel": "填充透明度", - "xpack.lens.xyChart.fillStyle": "填充", - "xpack.lens.xyChart.fillStyle.inside": "内部", - "xpack.lens.xyChart.fillStyle.outside": "外部", "xpack.lens.xyChart.Gridlines": "网格线", "xpack.lens.xyChart.horizontalAxisLabel": "水平轴", "xpack.lens.xyChart.horizontalLeftAxisLabel": "水平顶轴", @@ -21207,17 +21187,10 @@ "xpack.lens.xyChart.iconSelect.bellIconLabel": "钟铃", "xpack.lens.xyChart.iconSelect.boltIconLabel": "闪电", "xpack.lens.xyChart.iconSelect.bugIconLabel": "昆虫", - "xpack.lens.xyChart.iconSelect.circleIconLabel": "圆形", "xpack.lens.xyChart.iconSelect.commentIconLabel": "注释", "xpack.lens.xyChart.iconSelect.flagIconLabel": "旗帜", - "xpack.lens.xyChart.iconSelect.heartLabel": "心形", - "xpack.lens.xyChart.iconSelect.mapMarkerLabel": "地图标记", - "xpack.lens.xyChart.iconSelect.mapPinLabel": "地图图钉", "xpack.lens.xyChart.iconSelect.noIconLabel": "无", - "xpack.lens.xyChart.iconSelect.starFilledLabel": "星形填充", - "xpack.lens.xyChart.iconSelect.starLabel": "五角星", "xpack.lens.xyChart.iconSelect.tagIconLabel": "标签", - "xpack.lens.xyChart.iconSelect.triangleIconLabel": "三角形", "xpack.lens.xyChart.layerAnnotation": "标注", "xpack.lens.xyChart.layerAnnotationsIgnoreTitle": "忽略全局筛选的图层", "xpack.lens.xyChart.layerAnnotationsLabel": "标注", @@ -21233,13 +21206,6 @@ "xpack.lens.xyChart.lineColor.label": "颜色", "xpack.lens.xyChart.lineMarker.auto": "自动", "xpack.lens.xyChart.lineMarker.position": "装饰位置", - "xpack.lens.xyChart.lineMarker.textVisibility.field": "字段", - "xpack.lens.xyChart.lineMarker.textVisibility.name": "名称", - "xpack.lens.xyChart.lineMarker.textVisibility.none": "无", - "xpack.lens.xyChart.lineStyle.dashed": "虚线", - "xpack.lens.xyChart.lineStyle.dotted": "点线", - "xpack.lens.xyChart.lineStyle.label": "折线图", - "xpack.lens.xyChart.lineStyle.solid": "纯色", "xpack.lens.xyChart.markerPosition.above": "顶部", "xpack.lens.xyChart.markerPosition.below": "底部", "xpack.lens.xyChart.markerPosition.left": "左", @@ -21248,7 +21214,6 @@ "xpack.lens.xyChart.missingValuesLabelHelpText": "默认情况下,面积图和折线图会隐藏数据中的缺口。要填充缺口,请进行选择。", "xpack.lens.xyChart.missingValuesStyle": "显示为虚线", "xpack.lens.xyChart.nestUnderRoot": "整个数据集", - "xpack.lens.xyChart.placement": "位置", "xpack.lens.xyChart.rightAxisDisabledHelpText": "此设置仅在启用右轴时应用。", "xpack.lens.xyChart.rightAxisLabel": "右轴", "xpack.lens.xyChart.scaleLinear": "线性", @@ -21258,7 +21223,6 @@ "xpack.lens.xyChart.showCurrenTimeMarker": "显示当前时间标记", "xpack.lens.xyChart.showEnzones": "显示部分数据标记", "xpack.lens.xyChart.splitSeries": "细目", - "xpack.lens.xyChart.tooltip": "工具提示", "xpack.lens.xyChart.topAxisDisabledHelpText": "此设置仅在启用顶轴时应用。", "xpack.lens.xyChart.topAxisLabel": "顶轴", "xpack.lens.xyChart.valuesHistogramDisabledHelpText": "不能在直方图上更改此设置。", diff --git a/x-pack/test/functional/apps/lens/group6/annotations.ts b/x-pack/test/functional/apps/lens/group6/annotations.ts index 4bec636f52625..aa3b409d21fb0 100644 --- a/x-pack/test/functional/apps/lens/group6/annotations.ts +++ b/x-pack/test/functional/apps/lens/group6/annotations.ts @@ -22,6 +22,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const retry = getService('retry'); const toastsService = getService('toasts'); const testSubjects = getService('testSubjects'); + const listingTable = getService('listingTable'); const from = 'Sep 19, 2015 @ 06:31:44.000'; const to = 'Sep 23, 2015 @ 18:31:44.000'; @@ -156,7 +157,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const toastContents = await toastsService.getToastContent(1); expect(toastContents).to.be( - `Saved "${ANNOTATION_GROUP_TITLE}"\nView or manage in the annotation library` + `Saved "${ANNOTATION_GROUP_TITLE}"\nView or manage in the annotation library.` ); await PageObjects.lens.save(FIRST_VIS_TITLE); @@ -181,15 +182,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); it('should remove layer for deleted annotation group', async () => { - // TODO - delete from listing page instead - - await PageObjects.settings.navigateTo(); - await PageObjects.settings.clickKibanaSavedObjects(); - await PageObjects.savedObjects.searchForObject(ANNOTATION_GROUP_TITLE); - await PageObjects.savedObjects.clickCheckboxByTitle(ANNOTATION_GROUP_TITLE); - await PageObjects.savedObjects.clickDelete({ confirmDelete: true }); - await PageObjects.visualize.gotoVisualizationLandingPage(); + await PageObjects.visualize.selectAnnotationsTab(); + await listingTable.deleteItem(ANNOTATION_GROUP_TITLE); + await PageObjects.visualize.selectVisualizationsTab(); await PageObjects.visualize.loadSavedVisualization(FIRST_VIS_TITLE, { navigateToVisualize: false, }); diff --git a/x-pack/test/functional/page_objects/lens_page.ts b/x-pack/test/functional/page_objects/lens_page.ts index 7bbcc5dc73321..4370e7d143779 100644 --- a/x-pack/test/functional/page_objects/lens_page.ts +++ b/x-pack/test/functional/page_objects/lens_page.ts @@ -128,7 +128,7 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont extraFields?: string[]; }) { // type * in the query editor - const queryInput = await testSubjects.find('lnsXY-annotation-query-based-query-input'); + const queryInput = await testSubjects.find('annotation-query-based-query-input'); await queryInput.type(opts.queryString); await testSubjects.click('indexPattern-filters-existingFilterTrigger'); await this.selectOptionFromComboBox( @@ -773,7 +773,7 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont }, async editDimensionLabel(label: string) { - await testSubjects.setValue('column-label-edit', label, { clearWithKeyboard: true }); + await testSubjects.setValue('name-input', label, { clearWithKeyboard: true }); }, async editDimensionFormat(format: string, options?: { decimals?: number; prefix?: string }) { await this.selectOptionFromComboBox('indexPattern-dimension-format', format); diff --git a/yarn.lock b/yarn.lock index 365d7ef74d20e..0d175c93031de 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3180,7 +3180,15 @@ version "0.0.0" uid "" -"@kbn/content-management-table-list@link:packages/content-management/table_list": +"@kbn/content-management-tabbed-table-list-view@link:packages/content-management/tabbed_table_list_view": + version "0.0.0" + uid "" + +"@kbn/content-management-table-list-view-table@link:packages/content-management/table_list_view_table": + version "0.0.0" + uid "" + +"@kbn/content-management-table-list-view@link:packages/content-management/table_list_view": version "0.0.0" uid ""