From 72742d0d5031a9f8c9605458e51bc40e639cda05 Mon Sep 17 00:00:00 2001 From: Anton Dosov Date: Mon, 16 Aug 2021 11:18:34 +0200 Subject: [PATCH 01/21] [FieldFormats] Example plugin (#108070) --- .github/CODEOWNERS | 1 + examples/field_formats_example/README.md | 10 + examples/field_formats_example/kibana.json | 12 + examples/field_formats_example/public/app.tsx | 208 ++++++++++++++++++ .../examples/1_using_existing_format.ts | 34 +++ .../examples/2_creating_custom_formatter.ts | 64 ++++++ .../3_creating_custom_format_editor.tsx | 52 +++++ .../field_formats_example/public/formats.png | Bin 0 -> 40118 bytes .../field_formats_example/public/index.ts | 12 + .../field_formats_example/public/plugin.tsx | 103 +++++++++ examples/field_formats_example/tsconfig.json | 23 ++ .../public/index.ts | 5 +- test/examples/config.js | 1 + test/examples/field_formats/index.ts | 41 ++++ 14 files changed, 565 insertions(+), 1 deletion(-) create mode 100755 examples/field_formats_example/README.md create mode 100644 examples/field_formats_example/kibana.json create mode 100644 examples/field_formats_example/public/app.tsx create mode 100644 examples/field_formats_example/public/examples/1_using_existing_format.ts create mode 100644 examples/field_formats_example/public/examples/2_creating_custom_formatter.ts create mode 100644 examples/field_formats_example/public/examples/3_creating_custom_format_editor.tsx create mode 100644 examples/field_formats_example/public/formats.png create mode 100755 examples/field_formats_example/public/index.ts create mode 100755 examples/field_formats_example/public/plugin.tsx create mode 100644 examples/field_formats_example/tsconfig.json create mode 100644 test/examples/field_formats/index.ts diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index d98cde7b48c21..7ee84b6bc9e8d 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -45,6 +45,7 @@ /examples/ui_actions_explorer/ @elastic/kibana-app-services /examples/url_generators_examples/ @elastic/kibana-app-services /examples/url_generators_explorer/ @elastic/kibana-app-services +/examples/field_formats_example/ @elastic/kibana-app-services /packages/elastic-datemath/ @elastic/kibana-app-services /packages/kbn-interpreter/ @elastic/kibana-app-services /src/plugins/bfetch/ @elastic/kibana-app-services diff --git a/examples/field_formats_example/README.md b/examples/field_formats_example/README.md new file mode 100755 index 0000000000000..ee16922dfeb97 --- /dev/null +++ b/examples/field_formats_example/README.md @@ -0,0 +1,10 @@ +## Field formats example + +Field formats is a service used by index patterns for applying custom formatting to values in a document. +Field formats service can also be used separately from index patterns. + +This example plugin shows: + +1. How field formats can be used for formatting values +2. How to create a custom field format and make it available in index pattern field editor +3. How to create a custom editor for a custom field formatter diff --git a/examples/field_formats_example/kibana.json b/examples/field_formats_example/kibana.json new file mode 100644 index 0000000000000..a33375e661197 --- /dev/null +++ b/examples/field_formats_example/kibana.json @@ -0,0 +1,12 @@ +{ + "id": "fieldFormatsExample", + "version": "1.0.0", + "kibanaVersion": "kibana", + "ui": true, + "owner": { + "name": "App Services", + "githubTeam": "kibana-app-services" + }, + "description": "A plugin that demonstrates field formats usage", + "requiredPlugins": ["developerExamples", "fieldFormats", "indexPatternFieldEditor", "data"] +} diff --git a/examples/field_formats_example/public/app.tsx b/examples/field_formats_example/public/app.tsx new file mode 100644 index 0000000000000..ce4995672d227 --- /dev/null +++ b/examples/field_formats_example/public/app.tsx @@ -0,0 +1,208 @@ +/* + * 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 { + EuiBasicTable, + EuiCallOut, + EuiCode, + EuiCodeBlock, + EuiLink, + EuiPage, + EuiPageBody, + EuiPageContent, + EuiPageContentBody, + EuiPageHeader, + EuiPageHeaderSection, + EuiSpacer, + EuiText, + EuiTitle, +} from '@elastic/eui'; +import { FieldFormatsStart } from '../../../src/plugins/field_formats/public'; +import * as example1 from './examples/1_using_existing_format'; +import * as example2 from './examples/2_creating_custom_formatter'; +// @ts-ignore +import example1SampleCode from '!!raw-loader!./examples/1_using_existing_format'; +// @ts-ignore +import example2SampleCode from '!!raw-loader!./examples/2_creating_custom_formatter'; +// @ts-ignore +import example3SampleCode from '!!raw-loader!./examples/3_creating_custom_format_editor'; + +export interface Deps { + fieldFormats: FieldFormatsStart; + + /** + * Just for demo purposes + */ + openIndexPatternNumberFieldEditor: () => void; +} + +const UsingAnExistingFieldFormatExample: React.FC<{ deps: Deps }> = (props) => { + const sample = example1.getSample(props.deps.fieldFormats); + + return ( + <> + +

+ This example shows how to use existing field formatter to format values. As an example, we + have a following sample configuration{' '} + {JSON.stringify(example1.sampleSerializedFieldFormat)} representing a{' '} + bytes + field formatter with a 0.00b pattern. +

+
+ + {example1SampleCode} + + + + ); +}; + +const CreatingCustomFieldFormat: React.FC<{ deps: Deps }> = (props) => { + const sample = example2.getSample(props.deps.fieldFormats); + + return ( + <> + +

+ This example shows how to create a custom field formatter. As an example, we create a + currency formatter and then display some values as USD. +

+
+ + {example2SampleCode} + + + + + +

+ Currency formatter that we've just created is already integrated with index patterns. + It can be applied to any numeric field of any index pattern.{' '} + props.deps.openIndexPatternNumberFieldEditor()}> + Open index pattern field editor to give it a try. + +

+
+ + ); +}; + +const CreatingCustomFieldFormatEditor: React.FC<{ deps: Deps }> = (props) => { + return ( + <> + +

+ This example shows how to create a custom field formatter editor. As an example, we will + create a format editor for the currency formatter created in the previous section. This + custom editor will allow to select either USD or EUR{' '} + currency. +

+
+ + {example3SampleCode} + + + +

+ Currency formatter and its custom editor are integrated with index patterns. It can be + applied to any numeric field of any index pattern.{' '} + props.deps.openIndexPatternNumberFieldEditor()}> + Open index pattern field editor to give it a try. + +

+
+ + ); +}; + +export const App: React.FC<{ deps: Deps }> = (props) => { + return ( + + + + + +

Field formats examples

+
+
+
+ + +
+ +

Using an existing field format

+
+ + +
+ + +
+ +

Creating a custom field format

+
+ + +
+ + +
+ +

Creating a custom field format editor

+
+ + +
+
+
+
+
+ ); +}; diff --git a/examples/field_formats_example/public/examples/1_using_existing_format.ts b/examples/field_formats_example/public/examples/1_using_existing_format.ts new file mode 100644 index 0000000000000..1f534e79d7568 --- /dev/null +++ b/examples/field_formats_example/public/examples/1_using_existing_format.ts @@ -0,0 +1,34 @@ +/* + * 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 { SerializedFieldFormat } from '../../../../src/plugins/field_formats/common'; +import { FieldFormatsStart } from '../../../../src/plugins/field_formats/public'; + +// 1. Assume we have an existing field format configuration serialized and saved somewhere +// In this case it is `bytes` field formatter with a configured `'0.00b'` pattern +// NOTE: the `params` field is not type checked and a consumer has to know the `param` format that a particular `formatId` expects, +// https://github.com/elastic/kibana/issues/108158 +export const sampleSerializedFieldFormat: SerializedFieldFormat<{ pattern: string }> = { + id: 'bytes', + params: { + pattern: '0.00b', + }, +}; + +export function getSample(fieldFormats: FieldFormatsStart) { + // 2. we create a field format instance from an existing configuration + const fieldFormat = fieldFormats.deserialize(sampleSerializedFieldFormat); + + // 3. now we can use it to convert values + const pairs = [1000, 100000, 100000000].map((value) => ({ + raw: value, + formatted: fieldFormat.convert(value), + })); + + return pairs; +} diff --git a/examples/field_formats_example/public/examples/2_creating_custom_formatter.ts b/examples/field_formats_example/public/examples/2_creating_custom_formatter.ts new file mode 100644 index 0000000000000..5cc61558a8490 --- /dev/null +++ b/examples/field_formats_example/public/examples/2_creating_custom_formatter.ts @@ -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 { KBN_FIELD_TYPES } from '@kbn/field-types'; +import { FieldFormat, SerializedFieldFormat } from '../../../../src/plugins/field_formats/common'; +import { FieldFormatsSetup, FieldFormatsStart } from '../../../../src/plugins/field_formats/public'; + +// 1. Create a custom formatter by extending {@link FieldFormat} +export class ExampleCurrencyFormat extends FieldFormat { + static id = 'example-currency'; + static title = 'Currency (example)'; + + // 2. Specify field types that this formatter supports + static fieldType = KBN_FIELD_TYPES.NUMBER; + + // Or pass an array in case supports multiple types + // static fieldType = [KBN_FIELD_TYPES.NUMBER, KBN_FIELD_TYPES.DATE]; + + // 3. This formats support a `currency` param. Use `EUR` as a default. + getParamDefaults() { + return { + currency: 'EUR', + }; + } + + // 4. Implement a conversion function + textConvert = (val: unknown) => { + if (typeof val !== 'number') return `${val}`; + + return new Intl.NumberFormat(undefined, { + style: 'currency', + currency: this.param('currency'), + }).format(val); + }; +} + +export function registerExampleFormat(fieldFormats: FieldFormatsSetup) { + // 5. Register a field format. This should happen in setup plugin lifecycle phase. + fieldFormats.register([ExampleCurrencyFormat]); +} + +// 6. Now let's apply the formatter to some sample values +export function getSample(fieldFormats: FieldFormatsStart) { + const exampleSerializedFieldFormat: SerializedFieldFormat<{ currency: string }> = { + id: 'example-currency', + params: { + currency: 'USD', + }, + }; + + const fieldFormat = fieldFormats.deserialize(exampleSerializedFieldFormat); + + const pairs = [1000, 100000, 100000000].map((value) => ({ + raw: value, + formatted: fieldFormat.convert(value), + })); + + return pairs; +} diff --git a/examples/field_formats_example/public/examples/3_creating_custom_format_editor.tsx b/examples/field_formats_example/public/examples/3_creating_custom_format_editor.tsx new file mode 100644 index 0000000000000..dc2135c94985c --- /dev/null +++ b/examples/field_formats_example/public/examples/3_creating_custom_format_editor.tsx @@ -0,0 +1,52 @@ +/* + * 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 { EuiFormRow, EuiSelect } from '@elastic/eui'; +import { + FieldFormatEditor, + FieldFormatEditorFactory, + IndexPatternFieldEditorSetup, +} from '../../../../src/plugins/index_pattern_field_editor/public'; +import { ExampleCurrencyFormat } from './2_creating_custom_formatter'; + +// 1. Create an editor component +// NOTE: the `params` field is not type checked and a consumer has to know the `param` format that a particular `formatId` expects, +// https://github.com/elastic/kibana/issues/108158 +const ExampleCurrencyFormatEditor: FieldFormatEditor<{ currency: string }> = (props) => { + return ( + + { + props.onChange({ + currency: e.target.value, + }); + }} + /> + + ); +}; + +// 2. Make sure it has a `formatId` that corresponds to format's id +ExampleCurrencyFormatEditor.formatId = ExampleCurrencyFormat.id; + +// 3. Wrap editor component in a factory. This is needed to support and encourage code-splitting. +const ExampleCurrencyFormatEditorFactory: FieldFormatEditorFactory<{ + currency: string; +}> = async () => ExampleCurrencyFormatEditor; +ExampleCurrencyFormatEditorFactory.formatId = ExampleCurrencyFormatEditor.formatId; + +export function registerExampleFormatEditor(indexPatternFieldEditor: IndexPatternFieldEditorSetup) { + // 4. Register a field editor. This should happen in setup plugin lifecycle phase. + indexPatternFieldEditor.fieldFormatEditors.register(ExampleCurrencyFormatEditorFactory); +} diff --git a/examples/field_formats_example/public/formats.png b/examples/field_formats_example/public/formats.png new file mode 100644 index 0000000000000000000000000000000000000000..2d8d8a17e145042c69300bc2a51cbbfc22808ffe GIT binary patch literal 40118 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 { + public setup(core: CoreSetup, deps: SetupDeps) { + registerExampleFormat(deps.fieldFormats); + registerExampleFormatEditor(deps.indexPatternFieldEditor); + + // just for demonstration purposes: + // opens a field editor using default index pattern and first number field + const openIndexPatternNumberFieldEditor = async () => { + const [, plugins] = await core.getStartServices(); + const indexPattern = await plugins.data.indexPatterns.getDefault(); + if (!indexPattern) { + alert('Creating at least one index pattern to continue with this example'); + return; + } + + const numberField = indexPattern + .getNonScriptedFields() + .find((f) => !f.name.startsWith('_') && f.type === KBN_FIELD_TYPES.NUMBER); + + if (!numberField) { + alert( + 'Default index pattern needs at least a single field of type `number` to continue with this example' + ); + return; + } + + plugins.indexPatternFieldEditor.openEditor({ + ctx: { + indexPattern, + }, + fieldName: numberField.name, + }); + }; + + // Register an application into the side navigation menu + core.application.register({ + id: 'fieldFormatsExample', + title: 'Field formats example', + navLinkStatus: AppNavLinkStatus.hidden, + async mount({ element }: AppMountParameters) { + const [, plugins] = await core.getStartServices(); + ReactDOM.render( + , + element + ); + return () => ReactDOM.unmountComponentAtNode(element); + }, + }); + + // This section is only needed to get this example plugin to show up in our Developer Examples. + deps.developerExamples.register({ + appId: 'fieldFormatsExample', + title: 'Field formats example', + description: `Learn how to use an existing field formats or how to create a custom one`, + image: img, + }); + } + public start(core: CoreStart) { + return {}; + } + public stop() {} +} diff --git a/examples/field_formats_example/tsconfig.json b/examples/field_formats_example/tsconfig.json new file mode 100644 index 0000000000000..a0b609c95bae1 --- /dev/null +++ b/examples/field_formats_example/tsconfig.json @@ -0,0 +1,23 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "./target", + "skipLibCheck": true + }, + "include": [ + "index.ts", + "common/**/*.ts", + "public/**/*.ts", + "public/**/*.tsx", + "server/**/*.ts", + "../../typings/**/*" + ], + "exclude": [], + "references": [ + { "path": "../../src/core/tsconfig.json" }, + { "path": "../developer_examples/tsconfig.json" }, + { "path": "../../src/plugins/field_formats/tsconfig.json" }, + { "path": "../../src/plugins/data/tsconfig.json" }, + { "path": "../../src/plugins/index_pattern_field_editor/tsconfig.json" } + ] +} diff --git a/src/plugins/index_pattern_field_editor/public/index.ts b/src/plugins/index_pattern_field_editor/public/index.ts index 80ead500c3d9d..6546dabcb2c44 100644 --- a/src/plugins/index_pattern_field_editor/public/index.ts +++ b/src/plugins/index_pattern_field_editor/public/index.ts @@ -20,7 +20,10 @@ import { IndexPatternFieldEditorPlugin } from './plugin'; -export { PluginStart as IndexPatternFieldEditorStart } from './types'; +export type { + PluginSetup as IndexPatternFieldEditorSetup, + PluginStart as IndexPatternFieldEditorStart, +} from './types'; export { DefaultFormatEditor } from './components/field_format_editor/editors/default/default'; export { FieldFormatEditorFactory, FieldFormatEditor, FormatEditorProps } from './components'; diff --git a/test/examples/config.js b/test/examples/config.js index 85068740af463..8f123e9e932dc 100644 --- a/test/examples/config.js +++ b/test/examples/config.js @@ -30,6 +30,7 @@ export default async function ({ readConfigFile }) { require.resolve('./routing'), require.resolve('./expressions_explorer'), require.resolve('./index_pattern_field_editor_example'), + require.resolve('./field_formats'), ], services: { ...functionalConfig.get('services'), diff --git a/test/examples/field_formats/index.ts b/test/examples/field_formats/index.ts new file mode 100644 index 0000000000000..82f66aed3b299 --- /dev/null +++ b/test/examples/field_formats/index.ts @@ -0,0 +1,41 @@ +/* + * 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 expect from '@kbn/expect'; +import { FtrProviderContext } from 'test/functional/ftr_provider_context'; + +// eslint-disable-next-line import/no-default-export +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const testSubjects = getService('testSubjects'); + const PageObjects = getPageObjects(['common']); + + describe('Field formats example', function () { + before(async () => { + this.tags('ciGroup2'); + await PageObjects.common.navigateToApp('fieldFormatsExample'); + }); + + it('renders field formats example 1', async () => { + const formattedValues = await Promise.all( + (await testSubjects.findAll('example1 sample formatted')).map((wrapper) => + wrapper.getVisibleText() + ) + ); + expect(formattedValues).to.eql(['1000.00B', '97.66KB', '95.37MB']); + }); + + it('renders field formats example 2', async () => { + const formattedValues = await Promise.all( + (await testSubjects.findAll('example2 sample formatted')).map((wrapper) => + wrapper.getVisibleText() + ) + ); + expect(formattedValues).to.eql(['$1,000.00', '$100,000.00', '$100,000,000.00']); + }); + }); +} From 45ec64f99e6777db148c1506d96c57c49789675e Mon Sep 17 00:00:00 2001 From: Matthias Wilhelm Date: Mon, 16 Aug 2021 11:39:20 +0200 Subject: [PATCH 02/21] [Discover][Context] Add functional test, convert to TypeScript (#108481) --- ...t_navigation.js => _context_navigation.ts} | 4 ++- .../{_date_nanos.js => _date_nanos.ts} | 3 ++- ...amp.js => _date_nanos_custom_timestamp.ts} | 3 ++- ..._navigation.js => _discover_navigation.ts} | 3 ++- .../apps/context/{_filters.js => _filters.ts} | 4 ++- .../apps/context/{_size.js => _size.ts} | 25 ++++++++++++++++++- .../apps/context/{index.js => index.ts} | 4 ++- 7 files changed, 39 insertions(+), 7 deletions(-) rename test/functional/apps/context/{_context_navigation.js => _context_navigation.ts} (94%) rename test/functional/apps/context/{_date_nanos.js => _date_nanos.ts} (95%) rename test/functional/apps/context/{_date_nanos_custom_timestamp.js => _date_nanos_custom_timestamp.ts} (93%) rename test/functional/apps/context/{_discover_navigation.js => _discover_navigation.ts} (97%) rename test/functional/apps/context/{_filters.js => _filters.ts} (95%) rename test/functional/apps/context/{_size.js => _size.ts} (72%) rename test/functional/apps/context/{index.js => index.ts} (94%) diff --git a/test/functional/apps/context/_context_navigation.js b/test/functional/apps/context/_context_navigation.ts similarity index 94% rename from test/functional/apps/context/_context_navigation.js rename to test/functional/apps/context/_context_navigation.ts index 7f72d44c50ea0..9b8d33208dfb1 100644 --- a/test/functional/apps/context/_context_navigation.js +++ b/test/functional/apps/context/_context_navigation.ts @@ -6,6 +6,8 @@ * Side Public License, v 1. */ +import { FtrProviderContext } from '../../ftr_provider_context'; + const TEST_FILTER_COLUMN_NAMES = [ [ 'agent', @@ -14,7 +16,7 @@ const TEST_FILTER_COLUMN_NAMES = [ ['extension', 'jpg'], ]; -export default function ({ getService, getPageObjects }) { +export default function ({ getService, getPageObjects }: FtrProviderContext) { const retry = getService('retry'); const browser = getService('browser'); const docTable = getService('docTable'); diff --git a/test/functional/apps/context/_date_nanos.js b/test/functional/apps/context/_date_nanos.ts similarity index 95% rename from test/functional/apps/context/_date_nanos.js rename to test/functional/apps/context/_date_nanos.ts index 35e38abc6fdb7..6b7b28a4c7690 100644 --- a/test/functional/apps/context/_date_nanos.js +++ b/test/functional/apps/context/_date_nanos.ts @@ -7,12 +7,13 @@ */ import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../ftr_provider_context'; const TEST_INDEX_PATTERN = 'date-nanos'; const TEST_DEFAULT_CONTEXT_SIZE = 1; const TEST_STEP_SIZE = 3; -export default function ({ getService, getPageObjects }) { +export default function ({ getService, getPageObjects }: FtrProviderContext) { const kibanaServer = getService('kibanaServer'); const docTable = getService('docTable'); const security = getService('security'); diff --git a/test/functional/apps/context/_date_nanos_custom_timestamp.js b/test/functional/apps/context/_date_nanos_custom_timestamp.ts similarity index 93% rename from test/functional/apps/context/_date_nanos_custom_timestamp.js rename to test/functional/apps/context/_date_nanos_custom_timestamp.ts index a933c2f20b163..43714804a1912 100644 --- a/test/functional/apps/context/_date_nanos_custom_timestamp.js +++ b/test/functional/apps/context/_date_nanos_custom_timestamp.ts @@ -7,12 +7,13 @@ */ import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../ftr_provider_context'; const TEST_INDEX_PATTERN = 'date_nanos_custom_timestamp'; const TEST_DEFAULT_CONTEXT_SIZE = 1; const TEST_STEP_SIZE = 3; -export default function ({ getService, getPageObjects }) { +export default function ({ getService, getPageObjects }: FtrProviderContext) { const kibanaServer = getService('kibanaServer'); const docTable = getService('docTable'); const security = getService('security'); diff --git a/test/functional/apps/context/_discover_navigation.js b/test/functional/apps/context/_discover_navigation.ts similarity index 97% rename from test/functional/apps/context/_discover_navigation.js rename to test/functional/apps/context/_discover_navigation.ts index a09be8b35ba8f..1b8300f3345b1 100644 --- a/test/functional/apps/context/_discover_navigation.js +++ b/test/functional/apps/context/_discover_navigation.ts @@ -7,6 +7,7 @@ */ import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../ftr_provider_context'; const TEST_COLUMN_NAMES = ['@message']; const TEST_FILTER_COLUMN_NAMES = [ @@ -14,7 +15,7 @@ const TEST_FILTER_COLUMN_NAMES = [ ['geo.src', 'IN'], ]; -export default function ({ getService, getPageObjects }) { +export default function ({ getService, getPageObjects }: FtrProviderContext) { const retry = getService('retry'); const docTable = getService('docTable'); const filterBar = getService('filterBar'); diff --git a/test/functional/apps/context/_filters.js b/test/functional/apps/context/_filters.ts similarity index 95% rename from test/functional/apps/context/_filters.js rename to test/functional/apps/context/_filters.ts index fe4abc97a364c..d07cf802b7ebb 100644 --- a/test/functional/apps/context/_filters.js +++ b/test/functional/apps/context/_filters.ts @@ -6,13 +6,15 @@ * Side Public License, v 1. */ +import { FtrProviderContext } from '../../ftr_provider_context'; + const TEST_INDEX_PATTERN = 'logstash-*'; const TEST_ANCHOR_ID = 'AU_x3_BrGFA8no6QjjaI'; const TEST_ANCHOR_FILTER_FIELD = 'geo.src'; const TEST_ANCHOR_FILTER_VALUE = 'IN'; const TEST_COLUMN_NAMES = ['extension', 'geo.src']; -export default function ({ getService, getPageObjects }) { +export default function ({ getService, getPageObjects }: FtrProviderContext) { const docTable = getService('docTable'); const filterBar = getService('filterBar'); const retry = getService('retry'); diff --git a/test/functional/apps/context/_size.js b/test/functional/apps/context/_size.ts similarity index 72% rename from test/functional/apps/context/_size.js rename to test/functional/apps/context/_size.ts index 604f3b67be425..b11af7cd5c72f 100644 --- a/test/functional/apps/context/_size.js +++ b/test/functional/apps/context/_size.ts @@ -6,15 +6,18 @@ * Side Public License, v 1. */ +import { FtrProviderContext } from '../../ftr_provider_context'; + const TEST_INDEX_PATTERN = 'logstash-*'; const TEST_ANCHOR_ID = 'AU_x3_BrGFA8no6QjjaI'; const TEST_DEFAULT_CONTEXT_SIZE = 2; const TEST_STEP_SIZE = 2; -export default function ({ getService, getPageObjects }) { +export default function ({ getService, getPageObjects }: FtrProviderContext) { const kibanaServer = getService('kibanaServer'); const retry = getService('retry'); const docTable = getService('docTable'); + const browser = getService('browser'); const PageObjects = getPageObjects(['context']); let expectedRowLength = 2 * TEST_DEFAULT_CONTEXT_SIZE + 1; @@ -70,5 +73,25 @@ export default function ({ getService, getPageObjects }) { } ); }); + + it('should show 101 records when 50 newer and 50 older records are requests', async function () { + const predecessorCountPicker = await PageObjects.context.getPredecessorCountPicker(); + await predecessorCountPicker.clearValueWithKeyboard(); + await predecessorCountPicker.pressKeys('50'); + await predecessorCountPicker.pressKeys(browser.keys.ENTER); + + const successorCountPicker = await PageObjects.context.getSuccessorCountPicker(); + await successorCountPicker.clearValueWithKeyboard(); + await successorCountPicker.pressKeys('50'); + await successorCountPicker.pressKeys(browser.keys.ENTER); + + await retry.waitFor( + `number of rows displayed after clicking load more successors is ${expectedRowLength}`, + async function () { + const rows = await docTable.getRowsText(); + return rows.length === 101; + } + ); + }); }); } diff --git a/test/functional/apps/context/index.js b/test/functional/apps/context/index.ts similarity index 94% rename from test/functional/apps/context/index.js rename to test/functional/apps/context/index.ts index 031171a58718b..1320a22aad09b 100644 --- a/test/functional/apps/context/index.js +++ b/test/functional/apps/context/index.ts @@ -6,7 +6,9 @@ * Side Public License, v 1. */ -export default function ({ getService, getPageObjects, loadTestFile }) { +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function ({ getService, getPageObjects, loadTestFile }: FtrProviderContext) { const browser = getService('browser'); const esArchiver = getService('esArchiver'); const PageObjects = getPageObjects(['common']); From cb4069272f73a35d9f74c64784692f88e7bb3d71 Mon Sep 17 00:00:00 2001 From: Uladzislau Lasitsa Date: Mon, 16 Aug 2021 13:01:14 +0300 Subject: [PATCH 03/21] Reduce calling canFilter significantly (#108515) * Reduce calling canFIlter * Fix lint Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../vis_type_pie/public/utils/get_legend_actions.tsx | 4 ++-- .../vis_type_xy/public/utils/get_legend_actions.tsx | 8 +++++--- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/plugins/vis_type_pie/public/utils/get_legend_actions.tsx b/src/plugins/vis_type_pie/public/utils/get_legend_actions.tsx index 19164d0aefe52..4ffc458bfd401 100644 --- a/src/plugins/vis_type_pie/public/utils/get_legend_actions.tsx +++ b/src/plugins/vis_type_pie/public/utils/get_legend_actions.tsx @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, useMemo } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiContextMenuPanelDescriptor, EuiIcon, EuiPopover, EuiContextMenu } from '@elastic/eui'; @@ -29,7 +29,7 @@ export const getLegendActions = ( return ({ series: [pieSeries] }) => { const [popoverOpen, setPopoverOpen] = useState(false); const [isfilterable, setIsfilterable] = useState(true); - const filterData = getFilterEventData(pieSeries); + const filterData = useMemo(() => getFilterEventData(pieSeries), [pieSeries]); useEffect(() => { (async () => setIsfilterable(await canFilter(filterData, actions)))(); diff --git a/src/plugins/vis_type_xy/public/utils/get_legend_actions.tsx b/src/plugins/vis_type_xy/public/utils/get_legend_actions.tsx index 2ea55cf58359b..c125d8dd075ed 100644 --- a/src/plugins/vis_type_xy/public/utils/get_legend_actions.tsx +++ b/src/plugins/vis_type_xy/public/utils/get_legend_actions.tsx @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import React, { useState } from 'react'; +import React, { useState, useEffect, useMemo } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiContextMenuPanelDescriptor, EuiIcon, EuiPopover, EuiContextMenu } from '@elastic/eui'; @@ -24,9 +24,11 @@ export const getLegendActions = ( const [popoverOpen, setPopoverOpen] = useState(false); const [isfilterable, setIsfilterable] = useState(false); const series = xySeries as XYChartSeriesIdentifier; - const filterData = getFilterEventData(series); + const filterData = useMemo(() => getFilterEventData(series), [series]); - (async () => setIsfilterable(await canFilter(filterData)))(); + useEffect(() => { + (async () => setIsfilterable(await canFilter(filterData)))(); + }, [filterData]); if (!isfilterable || !filterData) { return null; From c2d5d1b6c26ea6658a7dadc27f5f860db3f4d5d1 Mon Sep 17 00:00:00 2001 From: Pablo Machado Date: Mon, 16 Aug 2021 12:15:21 +0200 Subject: [PATCH 04/21] Improve security alerts t-grid loading and empty state (#108527) --- .../components/t_grid/integrated/index.tsx | 107 +++++++++++------- .../timelines/public/container/index.tsx | 3 +- 2 files changed, 70 insertions(+), 40 deletions(-) diff --git a/x-pack/plugins/timelines/public/components/t_grid/integrated/index.tsx b/x-pack/plugins/timelines/public/components/t_grid/integrated/index.tsx index d2dfef542aca1..6b83039f3a545 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/integrated/index.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/integrated/index.tsx @@ -8,18 +8,20 @@ import type { AlertConsumers as AlertConsumersTyped } from '@kbn/rule-data-utils'; // @ts-expect-error import { AlertConsumers as AlertConsumersNonTyped } from '@kbn/rule-data-utils/target_node/alerts_as_data_rbac'; -import { EuiFlexGroup, EuiFlexItem, EuiPanel } from '@elastic/eui'; +import { EuiEmptyPrompt, EuiFlexGroup, EuiFlexItem, EuiPanel, EuiProgress } from '@elastic/eui'; import { isEmpty } from 'lodash/fp'; import React, { useEffect, useMemo, useState } from 'react'; import styled from 'styled-components'; import { useDispatch } from 'react-redux'; +import { FormattedMessage } from '@kbn/i18n/react'; import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; import { Direction, EntityType } from '../../../../common/search_strategy'; import type { DocValueFields } from '../../../../common/search_strategy'; import type { CoreStart } from '../../../../../../../src/core/public'; import type { BrowserFields } from '../../../../common/search_strategy/index_fields'; import { TGridCellAction, TimelineId, TimelineTabs } from '../../../../common/types/timeline'; + import type { CellValueElementProps, ColumnHeaderOptions, @@ -66,6 +68,7 @@ const TitleText = styled.span` const StyledEuiPanel = styled(EuiPanel)<{ $isFullScreen: boolean }>` display: flex; flex-direction: column; + position: relative; ${({ $isFullScreen }) => $isFullScreen && @@ -310,6 +313,8 @@ const TGridIntegratedComponent: React.FC = ({ return ( + {loading && } + {canQueryTimeline ? ( <> = ({ - -