From 1ab5b4ab8bdfd2214a83f1e2271b0b7cf2133952 Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Fri, 26 Jun 2020 12:04:42 +0100 Subject: [PATCH 01/13] [alerting] migrates the old `alerting` consumer to be `alerts` (#69982) This PR migrates all old alerts with the `alerting` consumer to have `alerts` instead. This is because in 7.9 we changed the feature ID and we need these to remain in sync otherwise the RBAC work (https://github.com/elastic/kibana/pull/67157) will break old alerts. --- .../alerts/server/saved_objects/index.ts | 2 + .../server/saved_objects/migrations.test.ts | 87 +++++ .../alerts/server/saved_objects/migrations.ts | 49 +++ .../alerting_api_integration/common/config.ts | 1 + .../tests/actions/type_not_enabled.ts | 4 +- .../spaces_only/tests/alerting/index.ts | 1 + .../spaces_only/tests/alerting/migrations.ts | 34 ++ .../{alerting => actions}/data.json | 0 .../functional/es_archives/alerts/data.json | 41 +++ .../es_archives/alerts/mappings.json | 345 ++++++++++++++++++ 10 files changed, 562 insertions(+), 2 deletions(-) create mode 100644 x-pack/plugins/alerts/server/saved_objects/migrations.test.ts create mode 100644 x-pack/plugins/alerts/server/saved_objects/migrations.ts create mode 100644 x-pack/test/alerting_api_integration/spaces_only/tests/alerting/migrations.ts rename x-pack/test/functional/es_archives/{alerting => actions}/data.json (100%) create mode 100644 x-pack/test/functional/es_archives/alerts/data.json create mode 100644 x-pack/test/functional/es_archives/alerts/mappings.json diff --git a/x-pack/plugins/alerts/server/saved_objects/index.ts b/x-pack/plugins/alerts/server/saved_objects/index.ts index c98d9bcbd9ae5..06ce8d673e6b7 100644 --- a/x-pack/plugins/alerts/server/saved_objects/index.ts +++ b/x-pack/plugins/alerts/server/saved_objects/index.ts @@ -6,6 +6,7 @@ import { SavedObjectsServiceSetup } from 'kibana/server'; import mappings from './mappings.json'; +import { getMigrations } from './migrations'; import { EncryptedSavedObjectsPluginSetup } from '../../../encrypted_saved_objects/server'; export function setupSavedObjects( @@ -16,6 +17,7 @@ export function setupSavedObjects( name: 'alert', hidden: true, namespaceType: 'single', + migrations: getMigrations(encryptedSavedObjects), mappings: mappings.alert, }); diff --git a/x-pack/plugins/alerts/server/saved_objects/migrations.test.ts b/x-pack/plugins/alerts/server/saved_objects/migrations.test.ts new file mode 100644 index 0000000000000..38cda5a9a0f7c --- /dev/null +++ b/x-pack/plugins/alerts/server/saved_objects/migrations.test.ts @@ -0,0 +1,87 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import uuid from 'uuid'; +import { getMigrations } from './migrations'; +import { RawAlert } from '../types'; +import { SavedObjectUnsanitizedDoc } from 'kibana/server'; +import { encryptedSavedObjectsMock } from '../../../encrypted_saved_objects/server/mocks'; +import { migrationMocks } from 'src/core/server/mocks'; + +const { log } = migrationMocks.createContext(); +const encryptedSavedObjectsSetup = encryptedSavedObjectsMock.createSetup(); + +describe('7.9.0', () => { + beforeEach(() => { + jest.resetAllMocks(); + encryptedSavedObjectsSetup.createMigration.mockImplementation( + (shouldMigrateWhenPredicate, migration) => migration + ); + }); + + test('changes nothing on alerts by other plugins', () => { + const migration790 = getMigrations(encryptedSavedObjectsSetup)['7.9.0']; + const alert = getMockData({}); + expect(migration790(alert, { log })).toMatchObject(alert); + + expect(encryptedSavedObjectsSetup.createMigration).toHaveBeenCalledWith( + expect.any(Function), + expect.any(Function) + ); + }); + + test('migrates the consumer for alerting', () => { + const migration790 = getMigrations(encryptedSavedObjectsSetup)['7.9.0']; + const alert = getMockData({ + consumer: 'alerting', + }); + expect(migration790(alert, { log })).toMatchObject({ + ...alert, + attributes: { + ...alert.attributes, + consumer: 'alerts', + }, + }); + }); +}); + +function getMockData( + overwrites: Record = {} +): SavedObjectUnsanitizedDoc { + return { + attributes: { + enabled: true, + name: 'abc', + tags: ['foo'], + alertTypeId: '123', + consumer: 'bar', + apiKey: '', + apiKeyOwner: '', + schedule: { interval: '10s' }, + throttle: null, + params: { + bar: true, + }, + muteAll: false, + mutedInstanceIds: [], + createdBy: new Date().toISOString(), + updatedBy: new Date().toISOString(), + createdAt: new Date().toISOString(), + actions: [ + { + group: 'default', + actionRef: '1', + actionTypeId: '1', + params: { + foo: true, + }, + }, + ], + ...overwrites, + }, + id: uuid.v4(), + type: 'alert', + }; +} diff --git a/x-pack/plugins/alerts/server/saved_objects/migrations.ts b/x-pack/plugins/alerts/server/saved_objects/migrations.ts new file mode 100644 index 0000000000000..142102dd711c7 --- /dev/null +++ b/x-pack/plugins/alerts/server/saved_objects/migrations.ts @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { + SavedObjectMigrationMap, + SavedObjectUnsanitizedDoc, + SavedObjectMigrationFn, +} from '../../../../../src/core/server'; +import { RawAlert } from '../types'; +import { EncryptedSavedObjectsPluginSetup } from '../../../encrypted_saved_objects/server'; + +export function getMigrations( + encryptedSavedObjects: EncryptedSavedObjectsPluginSetup +): SavedObjectMigrationMap { + return { + '7.9.0': changeAlertingConsumer(encryptedSavedObjects), + }; +} + +/** + * In v7.9.0 we changed the Alerting plugin so it uses the `consumer` value of `alerts` + * prior to that we were using `alerting` and we need to keep these in sync + */ +function changeAlertingConsumer( + encryptedSavedObjects: EncryptedSavedObjectsPluginSetup +): SavedObjectMigrationFn { + const consumerMigration = new Map(); + consumerMigration.set('alerting', 'alerts'); + + return encryptedSavedObjects.createMigration( + function shouldBeMigrated(doc): doc is SavedObjectUnsanitizedDoc { + return consumerMigration.has(doc.attributes.consumer); + }, + (doc: SavedObjectUnsanitizedDoc): SavedObjectUnsanitizedDoc => { + const { + attributes: { consumer }, + } = doc; + return { + ...doc, + attributes: { + ...doc.attributes, + consumer: consumerMigration.get(consumer) ?? consumer, + }, + }; + } + ); +} diff --git a/x-pack/test/alerting_api_integration/common/config.ts b/x-pack/test/alerting_api_integration/common/config.ts index bc209e2bb4925..0877fdc949dc4 100644 --- a/x-pack/test/alerting_api_integration/common/config.ts +++ b/x-pack/test/alerting_api_integration/common/config.ts @@ -82,6 +82,7 @@ export function createTestConfig(name: string, options: CreateTestConfigOptions) 'localhost', 'some.non.existent.com', ])}`, + '--xpack.encryptedSavedObjects.encryptionKey="wuGNaIhoMpk5sO4UBxgr3NyW1sFcLgIf"', `--xpack.actions.enabledActionTypes=${JSON.stringify(enabledActionTypes)}`, '--xpack.eventLog.logEntries=true', `--xpack.actions.preconfigured=${JSON.stringify({ diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/type_not_enabled.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/type_not_enabled.ts index 912b0dc339a21..b8963d72ead50 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/type_not_enabled.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/type_not_enabled.ts @@ -17,8 +17,8 @@ export default function typeNotEnabledTests({ getService }: FtrProviderContext) describe('actionType not enabled', () => { // loads action PREWRITTEN_ACTION_ID with actionType DISABLED_ACTION_TYPE - before(() => esArchiver.load('alerting')); - after(() => esArchiver.unload('alerting')); + before(() => esArchiver.load('actions')); + after(() => esArchiver.unload('actions')); it('should handle create action with disabled actionType request appropriately', async () => { const response = await supertest.post(`/api/actions/action`).set('kbn-xsrf', 'foo').send({ diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/index.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/index.ts index a0c4da361bd38..2fc35ddaa3c61 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/index.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/index.ts @@ -26,5 +26,6 @@ export default function alertingTests({ loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./alerts_space1')); loadTestFile(require.resolve('./alerts_default_space')); loadTestFile(require.resolve('./builtin_alert_types')); + loadTestFile(require.resolve('./migrations')); }); } diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/migrations.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/migrations.ts new file mode 100644 index 0000000000000..fc61f59d129d7 --- /dev/null +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/migrations.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; +import { getUrlPrefix } from '../../../common/lib'; +import { FtrProviderContext } from '../../../common/ftr_provider_context'; + +// eslint-disable-next-line import/no-default-export +export default function createGetTests({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + + describe('migrations', () => { + before(async () => { + await esArchiver.load('alerts'); + }); + + after(async () => { + await esArchiver.unload('alerts'); + }); + + it('7.9.0 migrates the `alerting` consumer to be the `alerts`', async () => { + const response = await supertest.get( + `${getUrlPrefix(``)}/api/alerts/alert/74f3e6d7-b7bb-477d-ac28-92ee22728e6e` + ); + + expect(response.status).to.eql(200); + expect(response.body.consumer).to.equal('alerts'); + }); + }); +} diff --git a/x-pack/test/functional/es_archives/alerting/data.json b/x-pack/test/functional/es_archives/actions/data.json similarity index 100% rename from x-pack/test/functional/es_archives/alerting/data.json rename to x-pack/test/functional/es_archives/actions/data.json diff --git a/x-pack/test/functional/es_archives/alerts/data.json b/x-pack/test/functional/es_archives/alerts/data.json new file mode 100644 index 0000000000000..3703473606ea2 --- /dev/null +++ b/x-pack/test/functional/es_archives/alerts/data.json @@ -0,0 +1,41 @@ +{ + "type": "doc", + "value": { + "id": "alert:74f3e6d7-b7bb-477d-ac28-92ee22728e6e", + "index": ".kibana_1", + "source": { + "alert": { + "actions": [ + ], + "alertTypeId": "example.always-firing", + "apiKey": "QIUT8u0/kbOakEHSj50jDpVR90MrqOxanEscboYOoa8PxQvcA5jfHash+fqH3b+KNjJ1LpnBcisGuPkufY9j1e32gKzwGZV5Bfys87imHvygJvIM8uKiFF8bQ8Y4NTaxOJO9fAmZPrFy07ZcQMCAQz+DUTgBFqs=", + "apiKeyOwner": "elastic", + "consumer": "alerting", + "createdAt": "2020-06-17T15:35:38.497Z", + "createdBy": "elastic", + "enabled": true, + "muteAll": false, + "mutedInstanceIds": [ + ], + "name": "always-firing-alert", + "params": { + }, + "schedule": { + "interval": "1m" + }, + "scheduledTaskId": "329798f0-b0b0-11ea-9510-fdf248d5f2a4", + "tags": [ + ], + "throttle": null, + "updatedBy": "elastic" + }, + "migrationVersion": { + "alert": "7.8.0" + }, + "references": [ + ], + "type": "alert", + "updated_at": "2020-06-17T15:35:39.839Z" + } + } +} \ No newline at end of file diff --git a/x-pack/test/functional/es_archives/alerts/mappings.json b/x-pack/test/functional/es_archives/alerts/mappings.json new file mode 100644 index 0000000000000..287d9a79a68cf --- /dev/null +++ b/x-pack/test/functional/es_archives/alerts/mappings.json @@ -0,0 +1,345 @@ +{ + "type": "index", + "value": { + "aliases": { + ".kibana": { + } + }, + "index": ".kibana_1", + "mappings": { + "_meta": { + "migrationMappingPropertyHashes": { + "action": "6e96ac5e648f57523879661ea72525b7", + "action_task_params": "a9d49f184ee89641044be0ca2950fa3a", + "alert": "7b44fba6773e37c806ce290ea9b7024e", + "apm-indices": "9bb9b2bf1fa636ed8619cbab5ce6a1dd", + "apm-telemetry": "3525d7c22c42bc80f5e6e9cb3f2b26a2", + "application_usage_totals": "c897e4310c5f24b07caaff3db53ae2c1", + "application_usage_transactional": "965839e75f809fefe04f92dc4d99722a", + "canvas-element": "7390014e1091044523666d97247392fc", + "canvas-workpad": "b0a1706d356228dbdcb4a17e6b9eb231", + "cases": "32aa96a6d3855ddda53010ae2048ac22", + "cases-comments": "c2061fb929f585df57425102fa928b4b", + "cases-configure": "42711cbb311976c0687853f4c1354572", + "cases-user-actions": "32277330ec6b721abe3b846cfd939a71", + "config": "ae24d22d5986d04124cc6568f771066f", + "dashboard": "d00f614b29a80360e1190193fd333bab", + "file-upload-telemetry": "0ed4d3e1983d1217a30982630897092e", + "graph-workspace": "cd7ba1330e6682e9cc00b78850874be1", + "index-pattern": "66eccb05066c5a89924f48a9e9736499", + "infrastructure-ui-source": "ddc0ecb18383f6b26101a2fadb2dab0c", + "inventory-view": "88fc7e12fd1b45b6f0787323ce4f18d2", + "kql-telemetry": "d12a98a6f19a2d273696597547e064ee", + "lens": "d33c68a69ff1e78c9888dedd2164ac22", + "lens-ui-telemetry": "509bfa5978586998e05f9e303c07a327", + "map": "23d7aa4a720d4938ccde3983f87bd58d", + "maps-telemetry": "bfd39d88aadadb4be597ea984d433dbe", + "metrics-explorer-view": "428e319af3e822c80a84cf87123ca35c", + "migrationVersion": "4a1746014a75ade3a714e1db5763276f", + "ml-telemetry": "257fd1d4b4fdbb9cb4b8a3b27da201e9", + "namespace": "2f4316de49999235636386fe51dc06c1", + "namespaces": "2f4316de49999235636386fe51dc06c1", + "query": "11aaeb7f5f7fa5bb43f25e18ce26e7d9", + "references": "7997cf5a56cc02bdc9c93361bde732b0", + "sample-data-telemetry": "7d3cfeb915303c9641c59681967ffeb4", + "search": "181661168bbadd1eff5902361e2a0d5c", + "space": "c5ca8acafa0beaa4d08d014a97b6bc6b", + "telemetry": "36a616f7026dfa617d6655df850fe16d", + "timelion-sheet": "9a2a2748877c7a7b582fef201ab1d4cf", + "todo": "082a2cc96a590268344d5cd74c159ac4", + "tsvb-validation-telemetry": "3a37ef6c8700ae6fc97d5c7da00e9215", + "type": "2f4316de49999235636386fe51dc06c1", + "ui-metric": "0d409297dc5ebe1e3a1da691c6ee32e3", + "updated_at": "00da57df13e94e9d98437d13ace4bfe0", + "upgrade-assistant-reindex-operation": "296a89039fc4260292be36b1b005d8f2", + "upgrade-assistant-telemetry": "56702cec857e0a9dacfb696655b4ff7b", + "uptime-dynamic-settings": "fcdb453a30092f022f2642db29523d80", + "url": "b675c3be8d76ecf029294d51dc7ec65d", + "visualization": "52d7a13ad68a150c4525b292d23e12cc" + } + }, + "dynamic": "strict", + "properties": { + "action": { + "properties": { + "actionTypeId": { + "type": "keyword" + }, + "config": { + "enabled": false, + "type": "object" + }, + "name": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + }, + "secrets": { + "type": "binary" + } + } + }, + "action_task_params": { + "properties": { + "actionId": { + "type": "keyword" + }, + "apiKey": { + "type": "binary" + }, + "params": { + "enabled": false, + "type": "object" + } + } + }, + "alert": { + "properties": { + "actions": { + "properties": { + "actionRef": { + "type": "keyword" + }, + "actionTypeId": { + "type": "keyword" + }, + "group": { + "type": "keyword" + }, + "params": { + "enabled": false, + "type": "object" + } + }, + "type": "nested" + }, + "alertTypeId": { + "type": "keyword" + }, + "apiKey": { + "type": "binary" + }, + "apiKeyOwner": { + "type": "keyword" + }, + "consumer": { + "type": "keyword" + }, + "createdAt": { + "type": "date" + }, + "createdBy": { + "type": "keyword" + }, + "enabled": { + "type": "boolean" + }, + "muteAll": { + "type": "boolean" + }, + "mutedInstanceIds": { + "type": "keyword" + }, + "name": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + }, + "params": { + "enabled": false, + "type": "object" + }, + "schedule": { + "properties": { + "interval": { + "type": "keyword" + } + } + }, + "scheduledTaskId": { + "type": "keyword" + }, + "tags": { + "type": "keyword" + }, + "throttle": { + "type": "keyword" + }, + "updatedBy": { + "type": "keyword" + } + } + }, + "config": { + "dynamic": "true", + "properties": { + "buildNum": { + "type": "keyword" + } + } + }, + "migrationVersion": { + "dynamic": "true", + "properties": { + "alert": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "config": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "space": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "namespace": { + "type": "keyword" + }, + "namespaces": { + "type": "keyword" + }, + "query": { + "properties": { + "description": { + "type": "text" + }, + "filters": { + "enabled": false, + "type": "object" + }, + "query": { + "properties": { + "language": { + "type": "keyword" + }, + "query": { + "index": false, + "type": "keyword" + } + } + }, + "timefilter": { + "enabled": false, + "type": "object" + }, + "title": { + "type": "text" + } + } + }, + "references": { + "properties": { + "id": { + "type": "keyword" + }, + "name": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + }, + "type": "nested" + }, + "search": { + "properties": { + "columns": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "hits": { + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "sort": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "space": { + "properties": { + "_reserved": { + "type": "boolean" + }, + "color": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "disabledFeatures": { + "type": "keyword" + }, + "imageUrl": { + "index": false, + "type": "text" + }, + "initials": { + "type": "keyword" + }, + "name": { + "fields": { + "keyword": { + "ignore_above": 2048, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "type": { + "type": "keyword" + }, + "updated_at": { + "type": "date" + } + } + }, + "settings": { + "index": { + "auto_expand_replicas": "0-1", + "number_of_replicas": "1", + "number_of_shards": "1" + } + } + } +} \ No newline at end of file From 3e7c3801ab223ba187f1f42696858ada99162c5e Mon Sep 17 00:00:00 2001 From: Vadim Dalecky Date: Fri, 26 Jun 2020 13:20:58 +0200 Subject: [PATCH 02/13] =?UTF-8?q?refactor:=20=F0=9F=92=A1=20fix=20typo=20i?= =?UTF-8?q?n=20embeddable=20(#69417)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Elastic Machine --- .../embeddable/public/lib/embeddables/embeddable.tsx | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/plugins/embeddable/public/lib/embeddables/embeddable.tsx b/src/plugins/embeddable/public/lib/embeddables/embeddable.tsx index 9c544e86e189a..fcecf117d7d52 100644 --- a/src/plugins/embeddable/public/lib/embeddables/embeddable.tsx +++ b/src/plugins/embeddable/public/lib/embeddables/embeddable.tsx @@ -51,8 +51,7 @@ export abstract class Embeddable< // to update input when the parent changes. private parentSubscription?: Rx.Subscription; - // TODO: Rename to destroyed. - private destoyed: boolean = false; + private destroyed: boolean = false; constructor(input: TEmbeddableInput, output: TEmbeddableOutput, parent?: IContainer) { this.id = input.id; @@ -123,7 +122,7 @@ export abstract class Embeddable< } public updateInput(changes: Partial): void { - if (this.destoyed) { + if (this.destroyed) { throw new Error('Embeddable has been destroyed'); } if (this.parent) { @@ -135,7 +134,7 @@ export abstract class Embeddable< } public render(domNode: HTMLElement | Element): void { - if (this.destoyed) { + if (this.destroyed) { throw new Error('Embeddable has been destroyed'); } return; @@ -155,7 +154,7 @@ export abstract class Embeddable< * implementors to add any additional clean up tasks, like unmounting and unsubscribing. */ public destroy(): void { - this.destoyed = true; + this.destroyed = true; this.input$.complete(); this.output$.complete(); From d511bb2c9b466f07467d85e9d2261d24d54381e2 Mon Sep 17 00:00:00 2001 From: Mikhail Shustov Date: Fri, 26 Jun 2020 14:39:46 +0300 Subject: [PATCH 03/13] move Metrics API to start (#69787) * move metrics to start * update plugins accordingly * update docs * update legacy code Co-authored-by: Elastic Machine --- .../kibana-plugin-core-server.coresetup.md | 1 - ...na-plugin-core-server.coresetup.metrics.md | 13 ------ .../kibana-plugin-core-server.corestart.md | 1 + ...na-plugin-core-server.corestart.metrics.md | 12 ++++++ .../core/server/kibana-plugin-core-server.md | 2 +- ...rver.metricsservicesetup.getopsmetrics_.md | 24 ----------- ...-plugin-core-server.metricsservicesetup.md | 9 ---- src/core/server/index.ts | 6 +-- src/core/server/internal_types.ts | 4 +- src/core/server/legacy/legacy_service.test.ts | 2 - src/core/server/legacy/legacy_service.ts | 6 +-- .../server/metrics/metrics_service.mock.ts | 41 +++++++++++++------ .../server/metrics/metrics_service.test.ts | 12 +++--- src/core/server/metrics/metrics_service.ts | 13 +++--- src/core/server/metrics/types.ts | 6 +-- src/core/server/mocks.ts | 4 +- src/core/server/plugins/plugin_context.ts | 6 +-- src/core/server/server.api.md | 12 ++++-- src/core/server/server.ts | 6 +-- .../status/routes/api/register_stats.js | 2 +- .../server/__snapshots__/index.test.ts.snap | 2 +- .../server/index.test.ts | 26 +----------- .../kibana_usage_collection/server/plugin.ts | 19 +++++---- src/plugins/telemetry/server/plugin.ts | 2 +- 24 files changed, 98 insertions(+), 133 deletions(-) delete mode 100644 docs/development/core/server/kibana-plugin-core-server.coresetup.metrics.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.corestart.metrics.md delete mode 100644 docs/development/core/server/kibana-plugin-core-server.metricsservicesetup.getopsmetrics_.md diff --git a/docs/development/core/server/kibana-plugin-core-server.coresetup.md b/docs/development/core/server/kibana-plugin-core-server.coresetup.md index e9ed5b830b691..32221a320d2a1 100644 --- a/docs/development/core/server/kibana-plugin-core-server.coresetup.md +++ b/docs/development/core/server/kibana-plugin-core-server.coresetup.md @@ -22,7 +22,6 @@ export interface CoreSetupStartServicesAccessor<TPluginsStart, TStart> | [StartServicesAccessor](./kibana-plugin-core-server.startservicesaccessor.md) | | [http](./kibana-plugin-core-server.coresetup.http.md) | HttpServiceSetup & {
resources: HttpResources;
} | [HttpServiceSetup](./kibana-plugin-core-server.httpservicesetup.md) | | [logging](./kibana-plugin-core-server.coresetup.logging.md) | LoggingServiceSetup | [LoggingServiceSetup](./kibana-plugin-core-server.loggingservicesetup.md) | -| [metrics](./kibana-plugin-core-server.coresetup.metrics.md) | MetricsServiceSetup | [MetricsServiceSetup](./kibana-plugin-core-server.metricsservicesetup.md) | | [savedObjects](./kibana-plugin-core-server.coresetup.savedobjects.md) | SavedObjectsServiceSetup | [SavedObjectsServiceSetup](./kibana-plugin-core-server.savedobjectsservicesetup.md) | | [status](./kibana-plugin-core-server.coresetup.status.md) | StatusServiceSetup | [StatusServiceSetup](./kibana-plugin-core-server.statusservicesetup.md) | | [uiSettings](./kibana-plugin-core-server.coresetup.uisettings.md) | UiSettingsServiceSetup | [UiSettingsServiceSetup](./kibana-plugin-core-server.uisettingsservicesetup.md) | diff --git a/docs/development/core/server/kibana-plugin-core-server.coresetup.metrics.md b/docs/development/core/server/kibana-plugin-core-server.coresetup.metrics.md deleted file mode 100644 index 77c9e867ef8ea..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.coresetup.metrics.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [CoreSetup](./kibana-plugin-core-server.coresetup.md) > [metrics](./kibana-plugin-core-server.coresetup.metrics.md) - -## CoreSetup.metrics property - -[MetricsServiceSetup](./kibana-plugin-core-server.metricsservicesetup.md) - -Signature: - -```typescript -metrics: MetricsServiceSetup; -``` diff --git a/docs/development/core/server/kibana-plugin-core-server.corestart.md b/docs/development/core/server/kibana-plugin-core-server.corestart.md index 6a6bacf1eef40..acd23f0f47386 100644 --- a/docs/development/core/server/kibana-plugin-core-server.corestart.md +++ b/docs/development/core/server/kibana-plugin-core-server.corestart.md @@ -19,6 +19,7 @@ export interface CoreStart | [capabilities](./kibana-plugin-core-server.corestart.capabilities.md) | CapabilitiesStart | [CapabilitiesStart](./kibana-plugin-core-server.capabilitiesstart.md) | | [elasticsearch](./kibana-plugin-core-server.corestart.elasticsearch.md) | ElasticsearchServiceStart | [ElasticsearchServiceStart](./kibana-plugin-core-server.elasticsearchservicestart.md) | | [http](./kibana-plugin-core-server.corestart.http.md) | HttpServiceStart | [HttpServiceStart](./kibana-plugin-core-server.httpservicestart.md) | +| [metrics](./kibana-plugin-core-server.corestart.metrics.md) | MetricsServiceStart | | | [savedObjects](./kibana-plugin-core-server.corestart.savedobjects.md) | SavedObjectsServiceStart | [SavedObjectsServiceStart](./kibana-plugin-core-server.savedobjectsservicestart.md) | | [uiSettings](./kibana-plugin-core-server.corestart.uisettings.md) | UiSettingsServiceStart | [UiSettingsServiceStart](./kibana-plugin-core-server.uisettingsservicestart.md) | diff --git a/docs/development/core/server/kibana-plugin-core-server.corestart.metrics.md b/docs/development/core/server/kibana-plugin-core-server.corestart.metrics.md new file mode 100644 index 0000000000000..a51c2f842c346 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.corestart.metrics.md @@ -0,0 +1,12 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [CoreStart](./kibana-plugin-core-server.corestart.md) > [metrics](./kibana-plugin-core-server.corestart.metrics.md) + +## CoreStart.metrics property + + +Signature: + +```typescript +metrics: MetricsServiceStart; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.md b/docs/development/core/server/kibana-plugin-core-server.md index 29c340bc390f2..74422c82fc9ea 100644 --- a/docs/development/core/server/kibana-plugin-core-server.md +++ b/docs/development/core/server/kibana-plugin-core-server.md @@ -112,7 +112,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [LoggerFactory](./kibana-plugin-core-server.loggerfactory.md) | The single purpose of LoggerFactory interface is to define a way to retrieve a context-based logger instance. | | [LoggingServiceSetup](./kibana-plugin-core-server.loggingservicesetup.md) | Provides APIs to plugins for customizing the plugin's logger. | | [LogMeta](./kibana-plugin-core-server.logmeta.md) | Contextual metadata | -| [MetricsServiceSetup](./kibana-plugin-core-server.metricsservicesetup.md) | APIs to retrieves metrics gathered and exposed by the core platform. | +| [MetricsServiceSetup](./kibana-plugin-core-server.metricsservicesetup.md) | | | [NodesVersionCompatibility](./kibana-plugin-core-server.nodesversioncompatibility.md) | | | [OnPostAuthToolkit](./kibana-plugin-core-server.onpostauthtoolkit.md) | A tool set defining an outcome of OnPostAuth interceptor for incoming request. | | [OnPreAuthToolkit](./kibana-plugin-core-server.onpreauthtoolkit.md) | A tool set defining an outcome of OnPreAuth interceptor for incoming request. | diff --git a/docs/development/core/server/kibana-plugin-core-server.metricsservicesetup.getopsmetrics_.md b/docs/development/core/server/kibana-plugin-core-server.metricsservicesetup.getopsmetrics_.md deleted file mode 100644 index 61107fbf20ad9..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.metricsservicesetup.getopsmetrics_.md +++ /dev/null @@ -1,24 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [MetricsServiceSetup](./kibana-plugin-core-server.metricsservicesetup.md) > [getOpsMetrics$](./kibana-plugin-core-server.metricsservicesetup.getopsmetrics_.md) - -## MetricsServiceSetup.getOpsMetrics$ property - -Retrieve an observable emitting the [OpsMetrics](./kibana-plugin-core-server.opsmetrics.md) gathered. The observable will emit an initial value during core's `start` phase, and a new value every fixed interval of time, based on the `opts.interval` configuration property. - -Signature: - -```typescript -getOpsMetrics$: () => Observable; -``` - -## Example - - -```ts -core.metrics.getOpsMetrics$().subscribe(metrics => { - // do something with the metrics -}) - -``` - diff --git a/docs/development/core/server/kibana-plugin-core-server.metricsservicesetup.md b/docs/development/core/server/kibana-plugin-core-server.metricsservicesetup.md index 00045aeac74b4..0bec919797b6f 100644 --- a/docs/development/core/server/kibana-plugin-core-server.metricsservicesetup.md +++ b/docs/development/core/server/kibana-plugin-core-server.metricsservicesetup.md @@ -4,17 +4,8 @@ ## MetricsServiceSetup interface -APIs to retrieves metrics gathered and exposed by the core platform. - Signature: ```typescript export interface MetricsServiceSetup ``` - -## Properties - -| Property | Type | Description | -| --- | --- | --- | -| [getOpsMetrics$](./kibana-plugin-core-server.metricsservicesetup.getopsmetrics_.md) | () => Observable<OpsMetrics> | Retrieve an observable emitting the [OpsMetrics](./kibana-plugin-core-server.opsmetrics.md) gathered. The observable will emit an initial value during core's start phase, and a new value every fixed interval of time, based on the opts.interval configuration property. | - diff --git a/src/core/server/index.ts b/src/core/server/index.ts index e0afd5e57f041..7520111bf33ac 100644 --- a/src/core/server/index.ts +++ b/src/core/server/index.ts @@ -60,7 +60,7 @@ import { } from './saved_objects'; import { CapabilitiesSetup, CapabilitiesStart } from './capabilities'; import { UuidServiceSetup } from './uuid'; -import { MetricsServiceSetup } from './metrics'; +import { MetricsServiceStart } from './metrics'; import { StatusServiceSetup } from './status'; import { LoggingServiceSetup, @@ -403,8 +403,6 @@ export interface CoreSetup { contracts: new Map([['plugin-id', 'plugin-value']]), }, rendering: renderingServiceMock, - metrics: metricsServiceMock.createInternalSetupContract(), uuid: uuidSetup, status: statusServiceMock.createInternalSetupContract(), logging: loggingServiceMock.createInternalSetupContract(), diff --git a/src/core/server/legacy/legacy_service.ts b/src/core/server/legacy/legacy_service.ts index be737f6593c02..a544bad6c0e41 100644 --- a/src/core/server/legacy/legacy_service.ts +++ b/src/core/server/legacy/legacy_service.ts @@ -276,6 +276,9 @@ export class LegacyService implements CoreService { createSerializer: startDeps.core.savedObjects.createSerializer, getTypeRegistry: startDeps.core.savedObjects.getTypeRegistry, }, + metrics: { + getOpsMetrics$: startDeps.core.metrics.getOpsMetrics$, + }, uiSettings: { asScopedToClient: startDeps.core.uiSettings.asScopedToClient }, }; @@ -312,9 +315,6 @@ export class LegacyService implements CoreService { logging: { configure: (config$) => setupDeps.core.logging.configure([], config$), }, - metrics: { - getOpsMetrics$: setupDeps.core.metrics.getOpsMetrics$, - }, savedObjects: { setClientFactoryProvider: setupDeps.core.savedObjects.setClientFactoryProvider, addClientWrapper: setupDeps.core.savedObjects.addClientWrapper, diff --git a/src/core/server/metrics/metrics_service.mock.ts b/src/core/server/metrics/metrics_service.mock.ts index cc53a4e27d571..769f6ee2a549a 100644 --- a/src/core/server/metrics/metrics_service.mock.ts +++ b/src/core/server/metrics/metrics_service.mock.ts @@ -16,29 +16,46 @@ * specific language governing permissions and limitations * under the License. */ - +import { BehaviorSubject } from 'rxjs'; import { MetricsService } from './metrics_service'; import { InternalMetricsServiceSetup, InternalMetricsServiceStart, - MetricsServiceSetup, MetricsServiceStart, } from './types'; -const createSetupContractMock = () => { - const setupContract: jest.Mocked = { - getOpsMetrics$: jest.fn(), - }; - return setupContract; -}; - const createInternalSetupContractMock = () => { - const setupContract: jest.Mocked = createSetupContractMock(); + const setupContract: jest.Mocked = {}; return setupContract; }; const createStartContractMock = () => { - const startContract: jest.Mocked = {}; + const startContract: jest.Mocked = { + getOpsMetrics$: jest.fn(), + }; + startContract.getOpsMetrics$.mockReturnValue( + new BehaviorSubject({ + process: { + memory: { + heap: { total_in_bytes: 1, used_in_bytes: 1, size_limit: 1 }, + resident_set_size_in_bytes: 1, + }, + event_loop_delay: 1, + pid: 1, + uptime_in_millis: 1, + }, + os: { + platform: 'darwin' as const, + platformRelease: 'test', + load: { '1m': 1, '5m': 1, '15m': 1 }, + memory: { total_in_bytes: 1, free_in_bytes: 1, used_in_bytes: 1 }, + uptime_in_millis: 1, + }, + response_times: { avg_in_millis: 1, max_in_millis: 1 }, + requests: { disconnects: 1, total: 1, statusCodes: { '200': 1 } }, + concurrent_connections: 1, + }) + ); return startContract; }; @@ -60,7 +77,7 @@ const createMock = () => { export const metricsServiceMock = { create: createMock, - createSetupContract: createSetupContractMock, + createSetupContract: createStartContractMock, createStartContract: createStartContractMock, createInternalSetupContract: createInternalSetupContractMock, createInternalStartContract: createInternalStartContractMock, diff --git a/src/core/server/metrics/metrics_service.test.ts b/src/core/server/metrics/metrics_service.test.ts index b3cc06ffca1d2..f2019de7b6cab 100644 --- a/src/core/server/metrics/metrics_service.test.ts +++ b/src/core/server/metrics/metrics_service.test.ts @@ -75,8 +75,8 @@ describe('MetricsService', () => { it('resets the collector after each collection', async () => { mockOpsCollector.collect.mockResolvedValue(dummyMetrics); - const { getOpsMetrics$ } = await metricsService.setup({ http: httpMock }); - await metricsService.start(); + await metricsService.setup({ http: httpMock }); + const { getOpsMetrics$ } = await metricsService.start(); // `advanceTimersByTime` only ensure the interval handler is executed // however the `reset` call is executed after the async call to `collect` @@ -109,8 +109,8 @@ describe('MetricsService', () => { describe('#stop', () => { it('stops the metrics interval', async () => { - const { getOpsMetrics$ } = await metricsService.setup({ http: httpMock }); - await metricsService.start(); + await metricsService.setup({ http: httpMock }); + const { getOpsMetrics$ } = await metricsService.start(); expect(mockOpsCollector.collect).toHaveBeenCalledTimes(1); @@ -125,8 +125,8 @@ describe('MetricsService', () => { }); it('completes the metrics observable', async () => { - const { getOpsMetrics$ } = await metricsService.setup({ http: httpMock }); - await metricsService.start(); + await metricsService.setup({ http: httpMock }); + const { getOpsMetrics$ } = await metricsService.start(); let completed = false; diff --git a/src/core/server/metrics/metrics_service.ts b/src/core/server/metrics/metrics_service.ts index 0ea9d00792600..f28fb21aaac0d 100644 --- a/src/core/server/metrics/metrics_service.ts +++ b/src/core/server/metrics/metrics_service.ts @@ -45,12 +45,7 @@ export class MetricsService public async setup({ http }: MetricsServiceSetupDeps): Promise { this.metricsCollector = new OpsMetricsCollector(http.server); - - const metricsObservable = this.metrics$.asObservable(); - - return { - getOpsMetrics$: () => metricsObservable, - }; + return {}; } public async start(): Promise { @@ -68,7 +63,11 @@ export class MetricsService this.refreshMetrics(); }, config.interval.asMilliseconds()); - return {}; + const metricsObservable = this.metrics$.asObservable(); + + return { + getOpsMetrics$: () => metricsObservable, + }; } private async refreshMetrics() { diff --git a/src/core/server/metrics/types.ts b/src/core/server/metrics/types.ts index 5c8f18fff380d..cbf0acacd6bab 100644 --- a/src/core/server/metrics/types.ts +++ b/src/core/server/metrics/types.ts @@ -20,12 +20,14 @@ import { Observable } from 'rxjs'; import { OpsProcessMetrics, OpsOsMetrics, OpsServerMetrics } from './collectors'; +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface MetricsServiceSetup {} /** * APIs to retrieves metrics gathered and exposed by the core platform. * * @public */ -export interface MetricsServiceSetup { +export interface MetricsServiceStart { /** * Retrieve an observable emitting the {@link OpsMetrics} gathered. * The observable will emit an initial value during core's `start` phase, and a new value every fixed interval of time, @@ -40,8 +42,6 @@ export interface MetricsServiceSetup { */ getOpsMetrics$: () => Observable; } -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface MetricsServiceStart {} export type InternalMetricsServiceSetup = MetricsServiceSetup; export type InternalMetricsServiceStart = MetricsServiceStart; diff --git a/src/core/server/mocks.ts b/src/core/server/mocks.ts index 2ac5bd98f7ed4..4491942951c50 100644 --- a/src/core/server/mocks.ts +++ b/src/core/server/mocks.ts @@ -129,7 +129,6 @@ function createCoreSetupMock({ http: httpMock, savedObjects: savedObjectsServiceMock.createInternalSetupContract(), status: statusServiceMock.createSetupContract(), - metrics: metricsServiceMock.createSetupContract(), uiSettings: uiSettingsMock, uuid: uuidServiceMock.createSetupContract(), logging: loggingServiceMock.createSetupContract(), @@ -146,6 +145,7 @@ function createCoreStartMock() { capabilities: capabilitiesServiceMock.createStartContract(), elasticsearch: elasticsearchServiceMock.createStart(), http: httpServiceMock.createStartContract(), + metrics: metricsServiceMock.createStartContract(), savedObjects: savedObjectsServiceMock.createStartContract(), uiSettings: uiSettingsServiceMock.createStartContract(), }; @@ -159,7 +159,6 @@ function createInternalCoreSetupMock() { context: contextServiceMock.createSetupContract(), elasticsearch: elasticsearchServiceMock.createInternalSetup(), http: httpServiceMock.createInternalSetupContract(), - metrics: metricsServiceMock.createInternalSetupContract(), savedObjects: savedObjectsServiceMock.createInternalSetupContract(), status: statusServiceMock.createInternalSetupContract(), uuid: uuidServiceMock.createSetupContract(), @@ -176,6 +175,7 @@ function createInternalCoreStartMock() { capabilities: capabilitiesServiceMock.createStartContract(), elasticsearch: elasticsearchServiceMock.createStart(), http: httpServiceMock.createInternalStartContract(), + metrics: metricsServiceMock.createStartContract(), savedObjects: savedObjectsServiceMock.createInternalStartContract(), uiSettings: uiSettingsServiceMock.createStartContract(), }; diff --git a/src/core/server/plugins/plugin_context.ts b/src/core/server/plugins/plugin_context.ts index 32bc8dc088cad..4643789d99a88 100644 --- a/src/core/server/plugins/plugin_context.ts +++ b/src/core/server/plugins/plugin_context.ts @@ -169,9 +169,6 @@ export function createPluginSetupContext( logging: { configure: (config$) => deps.logging.configure(['plugins', plugin.name], config$), }, - metrics: { - getOpsMetrics$: deps.metrics.getOpsMetrics$, - }, savedObjects: { setClientFactoryProvider: deps.savedObjects.setClientFactoryProvider, addClientWrapper: deps.savedObjects.addClientWrapper, @@ -225,6 +222,9 @@ export function createPluginStartContext( createSerializer: deps.savedObjects.createSerializer, getTypeRegistry: deps.savedObjects.getTypeRegistry, }, + metrics: { + getOpsMetrics$: deps.metrics.getOpsMetrics$, + }, uiSettings: { asScopedToClient: deps.uiSettings.asScopedToClient, }, diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index 00ec217bc8586..108826ad61aa2 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -712,8 +712,6 @@ export interface CoreSetup Observable; } // @public (undocumented) diff --git a/src/core/server/server.ts b/src/core/server/server.ts index 3bbcd0e37e142..dc37b77c57c92 100644 --- a/src/core/server/server.ts +++ b/src/core/server/server.ts @@ -150,7 +150,7 @@ export class Server { savedObjects: savedObjectsSetup, }); - const metricsSetup = await this.metrics.setup({ http: httpSetup }); + await this.metrics.setup({ http: httpSetup }); const renderingSetup = await this.rendering.setup({ http: httpSetup, @@ -181,7 +181,6 @@ export class Server { status: statusSetup, uiSettings: uiSettingsSetup, uuid: uuidSetup, - metrics: metricsSetup, rendering: renderingSetup, httpResources: httpResourcesSetup, logging: loggingSetup, @@ -211,12 +210,14 @@ export class Server { }); const capabilitiesStart = this.capabilities.start(); const uiSettingsStart = await this.uiSettings.start(); + const metricsStart = await this.metrics.start(); const httpStart = this.http.getStartContract(); this.coreStart = { capabilities: capabilitiesStart, elasticsearch: elasticsearchStart, http: httpStart, + metrics: metricsStart, savedObjects: savedObjectsStart, uiSettings: uiSettingsStart, }; @@ -236,7 +237,6 @@ export class Server { await this.rendering.start({ legacy: this.legacy, }); - await this.metrics.start(); return this.coreStart; } diff --git a/src/legacy/server/status/routes/api/register_stats.js b/src/legacy/server/status/routes/api/register_stats.js index 09957e61f74d3..0221c7e0ea085 100644 --- a/src/legacy/server/status/routes/api/register_stats.js +++ b/src/legacy/server/status/routes/api/register_stats.js @@ -54,7 +54,7 @@ export function registerStatsApi(usageCollection, server, config, kbnServer) { /* kibana_stats gets singled out from the collector set as it is used * for health-checking Kibana and fetch does not rely on fetching data * from ES */ - server.newPlatform.setup.core.metrics.getOpsMetrics$().subscribe((metrics) => { + server.newPlatform.start.core.metrics.getOpsMetrics$().subscribe((metrics) => { lastMetrics = { ...metrics, timestamp: new Date().toISOString(), diff --git a/src/plugins/kibana_usage_collection/server/__snapshots__/index.test.ts.snap b/src/plugins/kibana_usage_collection/server/__snapshots__/index.test.ts.snap index 41c4c33b53c8d..f07912eff02b7 100644 --- a/src/plugins/kibana_usage_collection/server/__snapshots__/index.test.ts.snap +++ b/src/plugins/kibana_usage_collection/server/__snapshots__/index.test.ts.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`kibana_usage_collection Runs the setup method without issues 1`] = `true`; +exports[`kibana_usage_collection Runs the setup method without issues 1`] = `false`; exports[`kibana_usage_collection Runs the setup method without issues 2`] = `true`; diff --git a/src/plugins/kibana_usage_collection/server/index.test.ts b/src/plugins/kibana_usage_collection/server/index.test.ts index c2680fef01caa..d4b065896c88c 100644 --- a/src/plugins/kibana_usage_collection/server/index.test.ts +++ b/src/plugins/kibana_usage_collection/server/index.test.ts @@ -17,7 +17,6 @@ * under the License. */ -import { BehaviorSubject } from 'rxjs'; import { coreMock, savedObjectsRepositoryMock, @@ -47,30 +46,6 @@ describe('kibana_usage_collection', () => { test('Runs the setup method without issues', () => { const coreSetup = coreMock.createSetup(); - coreSetup.metrics.getOpsMetrics$.mockImplementation( - () => - new BehaviorSubject({ - process: { - memory: { - heap: { total_in_bytes: 1, used_in_bytes: 1, size_limit: 1 }, - resident_set_size_in_bytes: 1, - }, - event_loop_delay: 1, - pid: 1, - uptime_in_millis: 1, - }, - os: { - platform: 'darwin' as const, - platformRelease: 'test', - load: { '1m': 1, '5m': 1, '15m': 1 }, - memory: { total_in_bytes: 1, free_in_bytes: 1, used_in_bytes: 1 }, - uptime_in_millis: 1, - }, - response_times: { avg_in_millis: 1, max_in_millis: 1 }, - requests: { disconnects: 1, total: 1, statusCodes: { '200': 1 } }, - concurrent_connections: 1, - }) - ); expect(pluginInstance.setup(coreSetup, { usageCollection })).toBe(undefined); usageCollectors.forEach(({ isReady }) => { @@ -86,6 +61,7 @@ describe('kibana_usage_collection', () => { coreStart.uiSettings.asScopedToClient.mockImplementation(() => uiSettingsServiceMock.createClient() ); + expect(pluginInstance.start(coreStart)).toBe(undefined); usageCollectors.forEach(({ isReady }) => { expect(isReady()).toBe(true); // All should return true at this point diff --git a/src/plugins/kibana_usage_collection/server/plugin.ts b/src/plugins/kibana_usage_collection/server/plugin.ts index 64d5367100236..803a9146bd08f 100644 --- a/src/plugins/kibana_usage_collection/server/plugin.ts +++ b/src/plugins/kibana_usage_collection/server/plugin.ts @@ -18,18 +18,18 @@ */ import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; -import { Observable } from 'rxjs'; +import { Subject, Observable } from 'rxjs'; import { PluginInitializerContext, CoreSetup, Plugin, - MetricsServiceSetup, ISavedObjectsRepository, IUiSettingsClient, SharedGlobalConfig, SavedObjectsClient, CoreStart, SavedObjectsServiceSetup, + OpsMetrics, } from '../../../core/server'; import { registerApplicationUsageCollector, @@ -49,16 +49,18 @@ export class KibanaUsageCollectionPlugin implements Plugin { private readonly legacyConfig$: Observable; private savedObjectsClient?: ISavedObjectsRepository; private uiSettingsClient?: IUiSettingsClient; + private metric$: Subject; constructor(initializerContext: PluginInitializerContext) { this.legacyConfig$ = initializerContext.config.legacy.globalConfig$; + this.metric$ = new Subject(); } public setup( - { savedObjects, metrics, getStartServices }: CoreSetup, + { savedObjects }: CoreSetup, { usageCollection }: KibanaUsageCollectionPluginsDepsSetup ) { - this.registerUsageCollectors(usageCollection, metrics, (opts) => + this.registerUsageCollectors(usageCollection, this.metric$, (opts) => savedObjects.registerType(opts) ); } @@ -68,19 +70,22 @@ export class KibanaUsageCollectionPlugin implements Plugin { this.savedObjectsClient = savedObjects.createInternalRepository(); const savedObjectsClient = new SavedObjectsClient(this.savedObjectsClient); this.uiSettingsClient = uiSettings.asScopedToClient(savedObjectsClient); + core.metrics.getOpsMetrics$().subscribe(this.metric$); } - public stop() {} + public stop() { + this.metric$.complete(); + } private registerUsageCollectors( usageCollection: UsageCollectionSetup, - metrics: MetricsServiceSetup, + metric$: Subject, registerType: SavedObjectsRegisterType ) { const getSavedObjectsClient = () => this.savedObjectsClient; const getUiSettingsClient = () => this.uiSettingsClient; - registerOpsStatsCollector(usageCollection, metrics.getOpsMetrics$()); + registerOpsStatsCollector(usageCollection, metric$); registerKibanaUsageCollector(usageCollection, this.legacyConfig$); registerManagementUsageCollector(usageCollection, getUiSettingsClient); registerUiMetricUsageCollector(usageCollection, registerType, getSavedObjectsClient); diff --git a/src/plugins/telemetry/server/plugin.ts b/src/plugins/telemetry/server/plugin.ts index e555c40d25592..6c8888feafc1f 100644 --- a/src/plugins/telemetry/server/plugin.ts +++ b/src/plugins/telemetry/server/plugin.ts @@ -75,7 +75,7 @@ export class TelemetryPlugin implements Plugin { } public async setup( - { elasticsearch, http, savedObjects, metrics }: CoreSetup, + { elasticsearch, http, savedObjects }: CoreSetup, { usageCollection, telemetryCollectionManager }: TelemetryPluginsSetup ) { const currentKibanaVersion = this.currentKibanaVersion; From 52223da44fd91119b1d8bada483d4963ff771755 Mon Sep 17 00:00:00 2001 From: Devon Thomson Date: Fri, 26 Jun 2020 07:55:12 -0400 Subject: [PATCH 04/13] prep state transfer for passing embeddables by value to editor and back (#69991) Co-authored-by: Elastic Machine --- .../application/dashboard_app_controller.tsx | 14 ++++++-- src/plugins/embeddable/public/index.ts | 2 +- .../lib/actions/edit_panel_action.test.tsx | 2 +- .../public/lib/actions/edit_panel_action.ts | 6 ++-- .../embeddable_state_transfer.test.ts | 8 ++--- .../embeddable_state_transfer.ts | 20 ++++++------ .../public/lib/state_transfer/index.ts | 2 +- .../public/lib/state_transfer/types.ts | 32 ++++++++++++++----- src/plugins/embeddable/public/mocks.tsx | 4 +-- .../public/wizard/new_vis_modal.test.tsx | 2 +- .../public/wizard/new_vis_modal.tsx | 2 +- .../public/application/editor/editor.js | 2 +- .../lens/public/app_plugin/mounter.tsx | 2 +- 13 files changed, 61 insertions(+), 37 deletions(-) diff --git a/src/plugins/dashboard/public/application/dashboard_app_controller.tsx b/src/plugins/dashboard/public/application/dashboard_app_controller.tsx index 3c559a6cde211..b52bf5bf02b7b 100644 --- a/src/plugins/dashboard/public/application/dashboard_app_controller.tsx +++ b/src/plugins/dashboard/public/application/dashboard_app_controller.tsx @@ -60,6 +60,7 @@ import { ViewMode, SavedObjectEmbeddableInput, ContainerOutput, + EmbeddableInput, } from '../../../embeddable/public'; import { NavAction, SavedDashboardPanel } from '../types'; @@ -430,9 +431,16 @@ export class DashboardAppController { .getStateTransfer(scopedHistory()) .getIncomingEmbeddablePackage(); if (incomingState) { - container.addNewEmbeddable(incomingState.type, { - savedObjectId: incomingState.id, - }); + if ('id' in incomingState) { + container.addNewEmbeddable(incomingState.type, { + savedObjectId: incomingState.id, + }); + } else if ('input' in incomingState) { + container.addNewEmbeddable( + incomingState.type, + incomingState.input + ); + } } } diff --git a/src/plugins/embeddable/public/index.ts b/src/plugins/embeddable/public/index.ts index 1d1dc79121937..35fbfe2e0aa38 100644 --- a/src/plugins/embeddable/public/index.ts +++ b/src/plugins/embeddable/public/index.ts @@ -69,7 +69,7 @@ export { isRangeSelectTriggerContext, isValueClickTriggerContext, EmbeddableStateTransfer, - EmbeddableOriginatingAppState, + EmbeddableEditorState, EmbeddablePackageState, EmbeddableRenderer, EmbeddableRendererProps, diff --git a/src/plugins/embeddable/public/lib/actions/edit_panel_action.test.tsx b/src/plugins/embeddable/public/lib/actions/edit_panel_action.test.tsx index 4b602efb02717..594a7ad73c396 100644 --- a/src/plugins/embeddable/public/lib/actions/edit_panel_action.test.tsx +++ b/src/plugins/embeddable/public/lib/actions/edit_panel_action.test.tsx @@ -59,7 +59,7 @@ test('redirects to app using state transfer', async () => { const embeddable = new EditableEmbeddable({ id: '123', viewMode: ViewMode.EDIT }, true); embeddable.getOutput = jest.fn(() => ({ editApp: 'ultraVisualize', editPath: '/123' })); await action.execute({ embeddable }); - expect(stateTransferMock.navigateToWithOriginatingApp).toHaveBeenCalledWith('ultraVisualize', { + expect(stateTransferMock.navigateToEditor).toHaveBeenCalledWith('ultraVisualize', { path: '/123', state: { originatingApp: 'superCoolCurrentApp' }, }); diff --git a/src/plugins/embeddable/public/lib/actions/edit_panel_action.ts b/src/plugins/embeddable/public/lib/actions/edit_panel_action.ts index d983dc9f41853..9177a77d547b0 100644 --- a/src/plugins/embeddable/public/lib/actions/edit_panel_action.ts +++ b/src/plugins/embeddable/public/lib/actions/edit_panel_action.ts @@ -24,7 +24,7 @@ import { take } from 'rxjs/operators'; import { ViewMode } from '../types'; import { EmbeddableFactoryNotFoundError } from '../errors'; import { EmbeddableStart } from '../../plugin'; -import { IEmbeddable, EmbeddableOriginatingAppState, EmbeddableStateTransfer } from '../..'; +import { IEmbeddable, EmbeddableEditorState, EmbeddableStateTransfer } from '../..'; export const ACTION_EDIT_PANEL = 'editPanel'; @@ -35,7 +35,7 @@ interface ActionContext { interface NavigationContext { app: string; path: string; - state?: EmbeddableOriginatingAppState; + state?: EmbeddableEditorState; } export class EditPanelAction implements Action { @@ -88,7 +88,7 @@ export class EditPanelAction implements Action { const appTarget = this.getAppTarget(context); if (appTarget) { if (this.stateTransfer && appTarget.state) { - await this.stateTransfer.navigateToWithOriginatingApp(appTarget.app, { + await this.stateTransfer.navigateToEditor(appTarget.app, { path: appTarget.path, state: appTarget.state, }); diff --git a/src/plugins/embeddable/public/lib/state_transfer/embeddable_state_transfer.test.ts b/src/plugins/embeddable/public/lib/state_transfer/embeddable_state_transfer.test.ts index 0d5ae6be68185..b7dd95ccba32c 100644 --- a/src/plugins/embeddable/public/lib/state_transfer/embeddable_state_transfer.test.ts +++ b/src/plugins/embeddable/public/lib/state_transfer/embeddable_state_transfer.test.ts @@ -38,7 +38,7 @@ describe('embeddable state transfer', () => { }); it('can send an outgoing originating app state', async () => { - await stateTransfer.navigateToWithOriginatingApp(destinationApp, { state: { originatingApp } }); + await stateTransfer.navigateToEditor(destinationApp, { state: { originatingApp } }); expect(application.navigateToApp).toHaveBeenCalledWith('superUltraVisualize', { state: { originatingApp: 'superUltraTestDashboard' }, }); @@ -50,7 +50,7 @@ describe('embeddable state transfer', () => { application.navigateToApp, (historyMock as unknown) as ScopedHistory ); - await stateTransfer.navigateToWithOriginatingApp(destinationApp, { + await stateTransfer.navigateToEditor(destinationApp, { state: { originatingApp }, appendToExistingState: true, }); @@ -94,7 +94,7 @@ describe('embeddable state transfer', () => { application.navigateToApp, (historyMock as unknown) as ScopedHistory ); - const fetchedState = stateTransfer.getIncomingOriginatingApp(); + const fetchedState = stateTransfer.getIncomingEditorState(); expect(fetchedState).toEqual({ originatingApp: 'extremeSportsKibana' }); }); @@ -104,7 +104,7 @@ describe('embeddable state transfer', () => { application.navigateToApp, (historyMock as unknown) as ScopedHistory ); - const fetchedState = stateTransfer.getIncomingOriginatingApp(); + const fetchedState = stateTransfer.getIncomingEditorState(); expect(fetchedState).toBeUndefined(); }); diff --git a/src/plugins/embeddable/public/lib/state_transfer/embeddable_state_transfer.ts b/src/plugins/embeddable/public/lib/state_transfer/embeddable_state_transfer.ts index 57b425d2df45c..8f70e5a66c478 100644 --- a/src/plugins/embeddable/public/lib/state_transfer/embeddable_state_transfer.ts +++ b/src/plugins/embeddable/public/lib/state_transfer/embeddable_state_transfer.ts @@ -20,8 +20,8 @@ import { cloneDeep } from 'lodash'; import { ScopedHistory, ApplicationStart } from '../../../../../core/public'; import { - EmbeddableOriginatingAppState, - isEmbeddableOriginatingAppState, + EmbeddableEditorState, + isEmbeddableEditorState, EmbeddablePackageState, isEmbeddablePackageState, } from './types'; @@ -39,16 +39,16 @@ export class EmbeddableStateTransfer { ) {} /** - * Fetches an {@link EmbeddableOriginatingAppState | originating app} argument from the scoped + * Fetches an {@link EmbeddableEditorState | originating app} argument from the scoped * history's location state. * * @param history - the scoped history to fetch from * @param options.keysToRemoveAfterFetch - an array of keys to be removed from the state after they are retrieved */ - public getIncomingOriginatingApp(options?: { + public getIncomingEditorState(options?: { keysToRemoveAfterFetch?: string[]; - }): EmbeddableOriginatingAppState | undefined { - return this.getIncomingState(isEmbeddableOriginatingAppState, { + }): EmbeddableEditorState | undefined { + return this.getIncomingState(isEmbeddableEditorState, { keysToRemoveAfterFetch: options?.keysToRemoveAfterFetch, }); } @@ -70,17 +70,17 @@ export class EmbeddableStateTransfer { /** * A wrapper around the {@link ApplicationStart.navigateToApp} method which navigates to the specified appId - * with {@link EmbeddableOriginatingAppState | originating app state} + * with {@link EmbeddableEditorState | embeddable editor state} */ - public async navigateToWithOriginatingApp( + public async navigateToEditor( appId: string, options?: { path?: string; - state: EmbeddableOriginatingAppState; + state: EmbeddableEditorState; appendToExistingState?: boolean; } ): Promise { - await this.navigateToWithState(appId, options); + await this.navigateToWithState(appId, options); } /** diff --git a/src/plugins/embeddable/public/lib/state_transfer/index.ts b/src/plugins/embeddable/public/lib/state_transfer/index.ts index e51efc5dcca26..7daa7a0ea81d6 100644 --- a/src/plugins/embeddable/public/lib/state_transfer/index.ts +++ b/src/plugins/embeddable/public/lib/state_transfer/index.ts @@ -18,4 +18,4 @@ */ export { EmbeddableStateTransfer } from './embeddable_state_transfer'; -export { EmbeddableOriginatingAppState, EmbeddablePackageState } from './types'; +export { EmbeddableEditorState, EmbeddablePackageState } from './types'; diff --git a/src/plugins/embeddable/public/lib/state_transfer/types.ts b/src/plugins/embeddable/public/lib/state_transfer/types.ts index 8eae441d1be23..a6721784302ac 100644 --- a/src/plugins/embeddable/public/lib/state_transfer/types.ts +++ b/src/plugins/embeddable/public/lib/state_transfer/types.ts @@ -17,33 +17,49 @@ * under the License. */ +import { EmbeddableInput } from '..'; + /** * Represents a state package that contains the last active app id. * @public */ -export interface EmbeddableOriginatingAppState { +export interface EmbeddableEditorState { originatingApp: string; + byValueMode?: boolean; + valueInput?: EmbeddableInput; } -export function isEmbeddableOriginatingAppState( - state: unknown -): state is EmbeddableOriginatingAppState { +export function isEmbeddableEditorState(state: unknown): state is EmbeddableEditorState { return ensureFieldOfTypeExists('originatingApp', state, 'string'); } /** - * Represents a state package that contains all fields necessary to create an embeddable in a container. + * Represents a state package that contains all fields necessary to create an embeddable by reference in a container. * @public */ -export interface EmbeddablePackageState { +export interface EmbeddablePackageByReferenceState { type: string; id: string; } +/** + * Represents a state package that contains all fields necessary to create an embeddable by value in a container. + * @public + */ +export interface EmbeddablePackageByValueState { + type: string; + input: EmbeddableInput; +} + +export type EmbeddablePackageState = + | EmbeddablePackageByReferenceState + | EmbeddablePackageByValueState; + export function isEmbeddablePackageState(state: unknown): state is EmbeddablePackageState { return ( - ensureFieldOfTypeExists('type', state, 'string') && - ensureFieldOfTypeExists('id', state, 'string') + (ensureFieldOfTypeExists('type', state, 'string') && + ensureFieldOfTypeExists('id', state, 'string')) || + ensureFieldOfTypeExists('input', state, 'object') ); } diff --git a/src/plugins/embeddable/public/mocks.tsx b/src/plugins/embeddable/public/mocks.tsx index 49910525c7ab1..6d94af1f22829 100644 --- a/src/plugins/embeddable/public/mocks.tsx +++ b/src/plugins/embeddable/public/mocks.tsx @@ -78,9 +78,9 @@ export const createEmbeddablePanelMock = ({ export const createEmbeddableStateTransferMock = (): Partial => { return { - getIncomingOriginatingApp: jest.fn(), + getIncomingEditorState: jest.fn(), getIncomingEmbeddablePackage: jest.fn(), - navigateToWithOriginatingApp: jest.fn(), + navigateToEditor: jest.fn(), navigateToWithEmbeddablePackage: jest.fn(), }; }; diff --git a/src/plugins/visualizations/public/wizard/new_vis_modal.test.tsx b/src/plugins/visualizations/public/wizard/new_vis_modal.test.tsx index dd89e98fb8fe5..f48febfef5b43 100644 --- a/src/plugins/visualizations/public/wizard/new_vis_modal.test.tsx +++ b/src/plugins/visualizations/public/wizard/new_vis_modal.test.tsx @@ -165,7 +165,7 @@ describe('NewVisModal', () => { ); const visButton = wrapper.find('button[data-test-subj="visType-visWithAliasUrl"]'); visButton.simulate('click'); - expect(stateTransfer.navigateToWithOriginatingApp).toBeCalledWith('otherApp', { + expect(stateTransfer.navigateToEditor).toBeCalledWith('otherApp', { path: '#/aliasUrl', state: { originatingApp: 'coolJestTestApp' }, }); diff --git a/src/plugins/visualizations/public/wizard/new_vis_modal.tsx b/src/plugins/visualizations/public/wizard/new_vis_modal.tsx index 84a5bca0ed0ed..1d01900ceffc2 100644 --- a/src/plugins/visualizations/public/wizard/new_vis_modal.tsx +++ b/src/plugins/visualizations/public/wizard/new_vis_modal.tsx @@ -172,7 +172,7 @@ class NewVisModal extends React.Component originatingApp; const visStateToEditorState = () => { diff --git a/x-pack/plugins/lens/public/app_plugin/mounter.tsx b/x-pack/plugins/lens/public/app_plugin/mounter.tsx index 7a33241792a58..1ee618a31a698 100644 --- a/x-pack/plugins/lens/public/app_plugin/mounter.tsx +++ b/x-pack/plugins/lens/public/app_plugin/mounter.tsx @@ -38,7 +38,7 @@ export async function mountApp( const stateTransfer = embeddable?.getStateTransfer(params.history); const { originatingApp } = - stateTransfer?.getIncomingOriginatingApp({ keysToRemoveAfterFetch: ['originatingApp'] }) || {}; + stateTransfer?.getIncomingEditorState({ keysToRemoveAfterFetch: ['originatingApp'] }) || {}; const instance = await createEditorFrame(); From b3b5dab00d630872ccf4ac1a56c35150acc642b4 Mon Sep 17 00:00:00 2001 From: Anton Dosov Date: Fri, 26 Jun 2020 14:05:17 +0200 Subject: [PATCH 05/13] Api reference docs for state_containers and state_sync (#67354) Adds state_containers and state_sync to api_extractor improves TSDoc definitions for those plugins adds changes to api_extractor script to support common/ folder and runs docs generation sequentially to not get OOM. Co-authored-by: Elastic Machine --- .../common/state_containers/index.md | 12 ++ ...utils-common-state_containers.basestate.md | 13 ++ ...state_containers.basestatecontainer.get.md | 13 ++ ...mon-state_containers.basestatecontainer.md | 22 +++ ...state_containers.basestatecontainer.set.md | 13 ++ ...te_containers.basestatecontainer.state_.md | 13 ++ ...tils-common-state_containers.comparator.md | 13 ++ ...a_utils-common-state_containers.connect.md | 13 ++ ...n-state_containers.createstatecontainer.md | 24 +++ ...state_containers.createstatecontainer_1.md | 25 +++ ...state_containers.createstatecontainer_2.md | 27 +++ ...ners.createstatecontaineroptions.freeze.md | 25 +++ ..._containers.createstatecontaineroptions.md | 20 +++ ...ainers.createstatecontainerreacthelpers.md | 22 +++ ..._utils-common-state_containers.dispatch.md | 13 ++ ...mon-state_containers.ensurepureselector.md | 12 ++ ...n-state_containers.ensurepuretransition.md | 12 ++ ...common-state_containers.mapstatetoprops.md | 13 ++ ...ns-kibana_utils-common-state_containers.md | 52 ++++++ ...tils-common-state_containers.middleware.md | 13 ++ ...ls-common-state_containers.pureselector.md | 12 ++ ...ate_containers.pureselectorstoselectors.md | 14 ++ ...state_containers.pureselectortoselector.md | 12 ++ ...a_utils-common-state_containers.reducer.md | 13 ++ ...s.reduxlikestatecontainer.addmiddleware.md | 11 ++ ...ainers.reduxlikestatecontainer.dispatch.md | 11 ++ ...ainers.reduxlikestatecontainer.getstate.md | 11 ++ ...tate_containers.reduxlikestatecontainer.md | 25 +++ ...tainers.reduxlikestatecontainer.reducer.md | 11 ++ ....reduxlikestatecontainer.replacereducer.md | 11 ++ ...iners.reduxlikestatecontainer.subscribe.md | 11 ++ ..._utils-common-state_containers.selector.md | 12 ++ ...-common-state_containers.statecontainer.md | 21 +++ ...ate_containers.statecontainer.selectors.md | 11 ++ ...e_containers.statecontainer.transitions.md | 11 ++ ...tils-common-state_containers.unboxstate.md | 13 ++ ...n-state_containers.usecontainerselector.md | 13 ++ ...mmon-state_containers.usecontainerstate.md | 13 ++ .../kibana_utils/public/state_sync/index.md | 12 ++ ...lic-state_sync.createkbnurlstatestorage.md | 16 ++ ...e_sync.createsessionstoragestatestorage.md | 13 ++ ...c-state_sync.ikbnurlstatestorage.cancel.md | 13 ++ ...-state_sync.ikbnurlstatestorage.change_.md | 11 ++ ...ic-state_sync.ikbnurlstatestorage.flush.md | 15 ++ ...blic-state_sync.ikbnurlstatestorage.get.md | 11 ++ ...s-public-state_sync.ikbnurlstatestorage.md | 24 +++ ...blic-state_sync.ikbnurlstatestorage.set.md | 13 ++ ...-state_sync.inullablebasestatecontainer.md | 24 +++ ...te_sync.inullablebasestatecontainer.set.md | 11 ++ ...te_sync.isessionstoragestatestorage.get.md | 11 ++ ...-state_sync.isessionstoragestatestorage.md | 21 +++ ...te_sync.isessionstoragestatestorage.set.md | 11 ++ ...-public-state_sync.istatestorage.cancel.md | 13 ++ ...public-state_sync.istatestorage.change_.md | 13 ++ ...ils-public-state_sync.istatestorage.get.md | 13 ++ ...a_utils-public-state_sync.istatestorage.md | 25 +++ ...ils-public-state_sync.istatestorage.set.md | 13 ++ ...tils-public-state_sync.istatesyncconfig.md | 22 +++ ...te_sync.istatesyncconfig.statecontainer.md | 13 ++ ...tate_sync.istatesyncconfig.statestorage.md | 15 ++ ...-state_sync.istatesyncconfig.storagekey.md | 13 ++ ...a_utils-public-state_sync.isyncstateref.md | 20 +++ ...s-public-state_sync.isyncstateref.start.md | 13 ++ ...ls-public-state_sync.isyncstateref.stop.md | 13 ++ ...-plugins-kibana_utils-public-state_sync.md | 48 ++++++ ...-public-state_sync.startsyncstatefntype.md | 12 ++ ...s-public-state_sync.stopsyncstatefntype.md | 12 ++ ...ibana_utils-public-state_sync.syncstate.md | 93 +++++++++++ ...bana_utils-public-state_sync.syncstates.md | 42 +++++ src/dev/run_check_published_api_changes.ts | 56 +++++-- .../common/state_containers/common.api.md | 156 ++++++++++++++++++ .../create_state_container.ts | 43 ++++- .../create_state_container_react_helpers.ts | 23 ++- .../common/state_containers/index.ts | 40 ++++- .../common/state_containers/types.ts | 106 +++++++++++- .../kibana_utils/public/state_sync/index.ts | 21 +++ .../public/state_sync/public.api.md | 97 +++++++++++ .../public/state_sync/state_sync.ts | 68 +++++--- .../create_kbn_url_state_storage.ts | 30 +++- .../create_session_storage_state_storage.ts | 12 ++ .../state_sync_state_storage/types.ts | 5 +- .../kibana_utils/public/state_sync/types.ts | 25 ++- 82 files changed, 1816 insertions(+), 71 deletions(-) create mode 100644 docs/development/plugins/kibana_utils/common/state_containers/index.md create mode 100644 docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.basestate.md create mode 100644 docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.basestatecontainer.get.md create mode 100644 docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.basestatecontainer.md create mode 100644 docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.basestatecontainer.set.md create mode 100644 docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.basestatecontainer.state_.md create mode 100644 docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.comparator.md create mode 100644 docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.connect.md create mode 100644 docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.createstatecontainer.md create mode 100644 docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.createstatecontainer_1.md create mode 100644 docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.createstatecontainer_2.md create mode 100644 docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.createstatecontaineroptions.freeze.md create mode 100644 docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.createstatecontaineroptions.md create mode 100644 docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.createstatecontainerreacthelpers.md create mode 100644 docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.dispatch.md create mode 100644 docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.ensurepureselector.md create mode 100644 docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.ensurepuretransition.md create mode 100644 docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.mapstatetoprops.md create mode 100644 docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.md create mode 100644 docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.middleware.md create mode 100644 docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.pureselector.md create mode 100644 docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.pureselectorstoselectors.md create mode 100644 docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.pureselectortoselector.md create mode 100644 docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.reducer.md create mode 100644 docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.reduxlikestatecontainer.addmiddleware.md create mode 100644 docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.reduxlikestatecontainer.dispatch.md create mode 100644 docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.reduxlikestatecontainer.getstate.md create mode 100644 docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.reduxlikestatecontainer.md create mode 100644 docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.reduxlikestatecontainer.reducer.md create mode 100644 docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.reduxlikestatecontainer.replacereducer.md create mode 100644 docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.reduxlikestatecontainer.subscribe.md create mode 100644 docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.selector.md create mode 100644 docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.statecontainer.md create mode 100644 docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.statecontainer.selectors.md create mode 100644 docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.statecontainer.transitions.md create mode 100644 docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.unboxstate.md create mode 100644 docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.usecontainerselector.md create mode 100644 docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.usecontainerstate.md create mode 100644 docs/development/plugins/kibana_utils/public/state_sync/index.md create mode 100644 docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.createkbnurlstatestorage.md create mode 100644 docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.createsessionstoragestatestorage.md create mode 100644 docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.cancel.md create mode 100644 docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.change_.md create mode 100644 docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.flush.md create mode 100644 docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.get.md create mode 100644 docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.md create mode 100644 docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.set.md create mode 100644 docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.inullablebasestatecontainer.md create mode 100644 docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.inullablebasestatecontainer.set.md create mode 100644 docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.isessionstoragestatestorage.get.md create mode 100644 docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.isessionstoragestatestorage.md create mode 100644 docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.isessionstoragestatestorage.set.md create mode 100644 docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.istatestorage.cancel.md create mode 100644 docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.istatestorage.change_.md create mode 100644 docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.istatestorage.get.md create mode 100644 docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.istatestorage.md create mode 100644 docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.istatestorage.set.md create mode 100644 docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.istatesyncconfig.md create mode 100644 docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.istatesyncconfig.statecontainer.md create mode 100644 docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.istatesyncconfig.statestorage.md create mode 100644 docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.istatesyncconfig.storagekey.md create mode 100644 docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.isyncstateref.md create mode 100644 docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.isyncstateref.start.md create mode 100644 docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.isyncstateref.stop.md create mode 100644 docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.md create mode 100644 docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.startsyncstatefntype.md create mode 100644 docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.stopsyncstatefntype.md create mode 100644 docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.syncstate.md create mode 100644 docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.syncstates.md create mode 100644 src/plugins/kibana_utils/common/state_containers/common.api.md create mode 100644 src/plugins/kibana_utils/public/state_sync/public.api.md diff --git a/docs/development/plugins/kibana_utils/common/state_containers/index.md b/docs/development/plugins/kibana_utils/common/state_containers/index.md new file mode 100644 index 0000000000000..b4e1071ceb732 --- /dev/null +++ b/docs/development/plugins/kibana_utils/common/state_containers/index.md @@ -0,0 +1,12 @@ + + +[Home](./index.md) + +## API Reference + +## Packages + +| Package | Description | +| --- | --- | +| [kibana-plugin-plugins-kibana\_utils-common-state\_containers](./kibana-plugin-plugins-kibana_utils-common-state_containers.md) | State containers are Redux-store-like objects meant to help you manage state in your services or apps. Refer to [guides and examples](https://github.com/elastic/kibana/tree/master/src/plugins/kibana_utils/docs/state_containers) for more info | + diff --git a/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.basestate.md b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.basestate.md new file mode 100644 index 0000000000000..92893afc02bef --- /dev/null +++ b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.basestate.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-common-state\_containers](./kibana-plugin-plugins-kibana_utils-common-state_containers.md) > [BaseState](./kibana-plugin-plugins-kibana_utils-common-state_containers.basestate.md) + +## BaseState type + +Base [StateContainer](./kibana-plugin-plugins-kibana_utils-common-state_containers.statecontainer.md) state shape + +Signature: + +```typescript +export declare type BaseState = object; +``` diff --git a/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.basestatecontainer.get.md b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.basestatecontainer.get.md new file mode 100644 index 0000000000000..b939954d92aa6 --- /dev/null +++ b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.basestatecontainer.get.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-common-state\_containers](./kibana-plugin-plugins-kibana_utils-common-state_containers.md) > [BaseStateContainer](./kibana-plugin-plugins-kibana_utils-common-state_containers.basestatecontainer.md) > [get](./kibana-plugin-plugins-kibana_utils-common-state_containers.basestatecontainer.get.md) + +## BaseStateContainer.get property + +Retrieves current state from the container + +Signature: + +```typescript +get: () => State; +``` diff --git a/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.basestatecontainer.md b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.basestatecontainer.md new file mode 100644 index 0000000000000..66c25c87f5e37 --- /dev/null +++ b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.basestatecontainer.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-common-state\_containers](./kibana-plugin-plugins-kibana_utils-common-state_containers.md) > [BaseStateContainer](./kibana-plugin-plugins-kibana_utils-common-state_containers.basestatecontainer.md) + +## BaseStateContainer interface + +Base state container shape without transitions or selectors + +Signature: + +```typescript +export interface BaseStateContainer +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [get](./kibana-plugin-plugins-kibana_utils-common-state_containers.basestatecontainer.get.md) | () => State | Retrieves current state from the container | +| [set](./kibana-plugin-plugins-kibana_utils-common-state_containers.basestatecontainer.set.md) | (state: State) => void | Sets state into container | +| [state$](./kibana-plugin-plugins-kibana_utils-common-state_containers.basestatecontainer.state_.md) | Observable<State> | of state | + diff --git a/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.basestatecontainer.set.md b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.basestatecontainer.set.md new file mode 100644 index 0000000000000..ed4ff365adfb3 --- /dev/null +++ b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.basestatecontainer.set.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-common-state\_containers](./kibana-plugin-plugins-kibana_utils-common-state_containers.md) > [BaseStateContainer](./kibana-plugin-plugins-kibana_utils-common-state_containers.basestatecontainer.md) > [set](./kibana-plugin-plugins-kibana_utils-common-state_containers.basestatecontainer.set.md) + +## BaseStateContainer.set property + +Sets state into container + +Signature: + +```typescript +set: (state: State) => void; +``` diff --git a/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.basestatecontainer.state_.md b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.basestatecontainer.state_.md new file mode 100644 index 0000000000000..35838fa53d539 --- /dev/null +++ b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.basestatecontainer.state_.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-common-state\_containers](./kibana-plugin-plugins-kibana_utils-common-state_containers.md) > [BaseStateContainer](./kibana-plugin-plugins-kibana_utils-common-state_containers.basestatecontainer.md) > [state$](./kibana-plugin-plugins-kibana_utils-common-state_containers.basestatecontainer.state_.md) + +## BaseStateContainer.state$ property + + of state + +Signature: + +```typescript +state$: Observable; +``` diff --git a/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.comparator.md b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.comparator.md new file mode 100644 index 0000000000000..12af33756fb19 --- /dev/null +++ b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.comparator.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-common-state\_containers](./kibana-plugin-plugins-kibana_utils-common-state_containers.md) > [Comparator](./kibana-plugin-plugins-kibana_utils-common-state_containers.comparator.md) + +## Comparator type + +Used to compare state. see [useContainerSelector](./kibana-plugin-plugins-kibana_utils-common-state_containers.usecontainerselector.md) + +Signature: + +```typescript +export declare type Comparator = (previous: Result, current: Result) => boolean; +``` diff --git a/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.connect.md b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.connect.md new file mode 100644 index 0000000000000..e05f1fb392fe6 --- /dev/null +++ b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.connect.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-common-state\_containers](./kibana-plugin-plugins-kibana_utils-common-state_containers.md) > [Connect](./kibana-plugin-plugins-kibana_utils-common-state_containers.connect.md) + +## Connect type + +Similar to `connect` from react-redux, allows to map state from state container to component's props + +Signature: + +```typescript +export declare type Connect = (mapStateToProp: MapStateToProps>) => (component: ComponentType) => FC>; +``` diff --git a/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.createstatecontainer.md b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.createstatecontainer.md new file mode 100644 index 0000000000000..cc43b59676dc1 --- /dev/null +++ b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.createstatecontainer.md @@ -0,0 +1,24 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-common-state\_containers](./kibana-plugin-plugins-kibana_utils-common-state_containers.md) > [createStateContainer](./kibana-plugin-plugins-kibana_utils-common-state_containers.createstatecontainer.md) + +## createStateContainer() function + +Creates a state container without transitions and without selectors. + +Signature: + +```typescript +export declare function createStateContainer(defaultState: State): ReduxLikeStateContainer; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| defaultState | State | initial state | + +Returns: + +`ReduxLikeStateContainer` + diff --git a/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.createstatecontainer_1.md b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.createstatecontainer_1.md new file mode 100644 index 0000000000000..794bf63588312 --- /dev/null +++ b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.createstatecontainer_1.md @@ -0,0 +1,25 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-common-state\_containers](./kibana-plugin-plugins-kibana_utils-common-state_containers.md) > [createStateContainer](./kibana-plugin-plugins-kibana_utils-common-state_containers.createstatecontainer_1.md) + +## createStateContainer() function + +Creates a state container with transitions, but without selectors + +Signature: + +```typescript +export declare function createStateContainer(defaultState: State, pureTransitions: PureTransitions): ReduxLikeStateContainer; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| defaultState | State | initial state | +| pureTransitions | PureTransitions | state transitions configuration object. Map of . | + +Returns: + +`ReduxLikeStateContainer` + diff --git a/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.createstatecontainer_2.md b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.createstatecontainer_2.md new file mode 100644 index 0000000000000..1946baae202f1 --- /dev/null +++ b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.createstatecontainer_2.md @@ -0,0 +1,27 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-common-state\_containers](./kibana-plugin-plugins-kibana_utils-common-state_containers.md) > [createStateContainer](./kibana-plugin-plugins-kibana_utils-common-state_containers.createstatecontainer_2.md) + +## createStateContainer() function + +Creates a state container with transitions and selectors + +Signature: + +```typescript +export declare function createStateContainer(defaultState: State, pureTransitions: PureTransitions, pureSelectors: PureSelectors, options?: CreateStateContainerOptions): ReduxLikeStateContainer; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| defaultState | State | initial state | +| pureTransitions | PureTransitions | state transitions configuration object. Map of . | +| pureSelectors | PureSelectors | state selectors configuration object. Map of . | +| options | CreateStateContainerOptions | state container options [CreateStateContainerOptions](./kibana-plugin-plugins-kibana_utils-common-state_containers.createstatecontaineroptions.md) | + +Returns: + +`ReduxLikeStateContainer` + diff --git a/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.createstatecontaineroptions.freeze.md b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.createstatecontaineroptions.freeze.md new file mode 100644 index 0000000000000..4f772c7c54d08 --- /dev/null +++ b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.createstatecontaineroptions.freeze.md @@ -0,0 +1,25 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-common-state\_containers](./kibana-plugin-plugins-kibana_utils-common-state_containers.md) > [CreateStateContainerOptions](./kibana-plugin-plugins-kibana_utils-common-state_containers.createstatecontaineroptions.md) > [freeze](./kibana-plugin-plugins-kibana_utils-common-state_containers.createstatecontaineroptions.freeze.md) + +## CreateStateContainerOptions.freeze property + +Function to use when freezing state. Supply identity function. If not provided, default deepFreeze is use. + +Signature: + +```typescript +freeze?: (state: T) => T; +``` + +## Example + +If you expect that your state will be mutated externally an you cannot prevent that + +```ts +{ + freeze: state => state, +} + +``` + diff --git a/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.createstatecontaineroptions.md b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.createstatecontaineroptions.md new file mode 100644 index 0000000000000..d328d306e93e1 --- /dev/null +++ b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.createstatecontaineroptions.md @@ -0,0 +1,20 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-common-state\_containers](./kibana-plugin-plugins-kibana_utils-common-state_containers.md) > [CreateStateContainerOptions](./kibana-plugin-plugins-kibana_utils-common-state_containers.createstatecontaineroptions.md) + +## CreateStateContainerOptions interface + +State container options + +Signature: + +```typescript +export interface CreateStateContainerOptions +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [freeze](./kibana-plugin-plugins-kibana_utils-common-state_containers.createstatecontaineroptions.freeze.md) | <T>(state: T) => T | Function to use when freezing state. Supply identity function. If not provided, default deepFreeze is use. | + diff --git a/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.createstatecontainerreacthelpers.md b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.createstatecontainerreacthelpers.md new file mode 100644 index 0000000000000..a6076490c2746 --- /dev/null +++ b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.createstatecontainerreacthelpers.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-common-state\_containers](./kibana-plugin-plugins-kibana_utils-common-state_containers.md) > [createStateContainerReactHelpers](./kibana-plugin-plugins-kibana_utils-common-state_containers.createstatecontainerreacthelpers.md) + +## createStateContainerReactHelpers variable + +Creates helpers for using [State Containers](./kibana-plugin-plugins-kibana_utils-common-state_containers.statecontainer.md) with react Refer to [guide](https://github.com/elastic/kibana/blob/master/src/plugins/kibana_utils/docs/state_containers/react.md) for details + +Signature: + +```typescript +createStateContainerReactHelpers: >() => { + Provider: React.Provider; + Consumer: React.Consumer; + context: React.Context; + useContainer: () => Container; + useState: () => UnboxState; + useTransitions: () => Container["transitions"]; + useSelector: (selector: (state: UnboxState) => Result, comparator?: Comparator) => Result; + connect: Connect>; +} +``` diff --git a/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.dispatch.md b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.dispatch.md new file mode 100644 index 0000000000000..d4057a549bb0d --- /dev/null +++ b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.dispatch.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-common-state\_containers](./kibana-plugin-plugins-kibana_utils-common-state_containers.md) > [Dispatch](./kibana-plugin-plugins-kibana_utils-common-state_containers.dispatch.md) + +## Dispatch type + +Redux like dispatch + +Signature: + +```typescript +export declare type Dispatch = (action: T) => void; +``` diff --git a/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.ensurepureselector.md b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.ensurepureselector.md new file mode 100644 index 0000000000000..5e4e86ad82d53 --- /dev/null +++ b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.ensurepureselector.md @@ -0,0 +1,12 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-common-state\_containers](./kibana-plugin-plugins-kibana_utils-common-state_containers.md) > [EnsurePureSelector](./kibana-plugin-plugins-kibana_utils-common-state_containers.ensurepureselector.md) + +## EnsurePureSelector type + + +Signature: + +```typescript +export declare type EnsurePureSelector = Ensure>; +``` diff --git a/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.ensurepuretransition.md b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.ensurepuretransition.md new file mode 100644 index 0000000000000..0e621e989346b --- /dev/null +++ b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.ensurepuretransition.md @@ -0,0 +1,12 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-common-state\_containers](./kibana-plugin-plugins-kibana_utils-common-state_containers.md) > [EnsurePureTransition](./kibana-plugin-plugins-kibana_utils-common-state_containers.ensurepuretransition.md) + +## EnsurePureTransition type + + +Signature: + +```typescript +export declare type EnsurePureTransition = Ensure>; +``` diff --git a/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.mapstatetoprops.md b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.mapstatetoprops.md new file mode 100644 index 0000000000000..8e6a49ac72742 --- /dev/null +++ b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.mapstatetoprops.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-common-state\_containers](./kibana-plugin-plugins-kibana_utils-common-state_containers.md) > [MapStateToProps](./kibana-plugin-plugins-kibana_utils-common-state_containers.mapstatetoprops.md) + +## MapStateToProps type + +State container state to component props mapper. See [Connect](./kibana-plugin-plugins-kibana_utils-common-state_containers.connect.md) + +Signature: + +```typescript +export declare type MapStateToProps = (state: State) => StateProps; +``` diff --git a/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.md b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.md new file mode 100644 index 0000000000000..e74ff2c6885be --- /dev/null +++ b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.md @@ -0,0 +1,52 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-common-state\_containers](./kibana-plugin-plugins-kibana_utils-common-state_containers.md) + +## kibana-plugin-plugins-kibana\_utils-common-state\_containers package + +State containers are Redux-store-like objects meant to help you manage state in your services or apps. Refer to [guides and examples](https://github.com/elastic/kibana/tree/master/src/plugins/kibana_utils/docs/state_containers) for more info + +## Functions + +| Function | Description | +| --- | --- | +| [createStateContainer(defaultState)](./kibana-plugin-plugins-kibana_utils-common-state_containers.createstatecontainer.md) | Creates a state container without transitions and without selectors. | +| [createStateContainer(defaultState, pureTransitions)](./kibana-plugin-plugins-kibana_utils-common-state_containers.createstatecontainer_1.md) | Creates a state container with transitions, but without selectors | +| [createStateContainer(defaultState, pureTransitions, pureSelectors, options)](./kibana-plugin-plugins-kibana_utils-common-state_containers.createstatecontainer_2.md) | Creates a state container with transitions and selectors | + +## Interfaces + +| Interface | Description | +| --- | --- | +| [BaseStateContainer](./kibana-plugin-plugins-kibana_utils-common-state_containers.basestatecontainer.md) | Base state container shape without transitions or selectors | +| [CreateStateContainerOptions](./kibana-plugin-plugins-kibana_utils-common-state_containers.createstatecontaineroptions.md) | State container options | +| [ReduxLikeStateContainer](./kibana-plugin-plugins-kibana_utils-common-state_containers.reduxlikestatecontainer.md) | Fully featured state container which matches Redux store interface. Extends [StateContainer](./kibana-plugin-plugins-kibana_utils-common-state_containers.statecontainer.md) Allows to use state container with redux libraries | +| [StateContainer](./kibana-plugin-plugins-kibana_utils-common-state_containers.statecontainer.md) | Fully featured state container with [Selectors](./kibana-plugin-plugins-kibana_utils-common-state_containers.selector.md) and . Extends [BaseStateContainer](./kibana-plugin-plugins-kibana_utils-common-state_containers.basestatecontainer.md) | + +## Variables + +| Variable | Description | +| --- | --- | +| [createStateContainerReactHelpers](./kibana-plugin-plugins-kibana_utils-common-state_containers.createstatecontainerreacthelpers.md) | Creates helpers for using [State Containers](./kibana-plugin-plugins-kibana_utils-common-state_containers.statecontainer.md) with react Refer to [guide](https://github.com/elastic/kibana/blob/master/src/plugins/kibana_utils/docs/state_containers/react.md) for details | +| [useContainerSelector](./kibana-plugin-plugins-kibana_utils-common-state_containers.usecontainerselector.md) | React hook to apply selector to state container to extract only needed information. Will re-render your component only when the section changes. | +| [useContainerState](./kibana-plugin-plugins-kibana_utils-common-state_containers.usecontainerstate.md) | React hooks that returns the latest state of a [StateContainer](./kibana-plugin-plugins-kibana_utils-common-state_containers.statecontainer.md). | + +## Type Aliases + +| Type Alias | Description | +| --- | --- | +| [BaseState](./kibana-plugin-plugins-kibana_utils-common-state_containers.basestate.md) | Base [StateContainer](./kibana-plugin-plugins-kibana_utils-common-state_containers.statecontainer.md) state shape | +| [Comparator](./kibana-plugin-plugins-kibana_utils-common-state_containers.comparator.md) | Used to compare state. see [useContainerSelector](./kibana-plugin-plugins-kibana_utils-common-state_containers.usecontainerselector.md) | +| [Connect](./kibana-plugin-plugins-kibana_utils-common-state_containers.connect.md) | Similar to connect from react-redux, allows to map state from state container to component's props | +| [Dispatch](./kibana-plugin-plugins-kibana_utils-common-state_containers.dispatch.md) | Redux like dispatch | +| [EnsurePureSelector](./kibana-plugin-plugins-kibana_utils-common-state_containers.ensurepureselector.md) | | +| [EnsurePureTransition](./kibana-plugin-plugins-kibana_utils-common-state_containers.ensurepuretransition.md) | | +| [MapStateToProps](./kibana-plugin-plugins-kibana_utils-common-state_containers.mapstatetoprops.md) | State container state to component props mapper. See [Connect](./kibana-plugin-plugins-kibana_utils-common-state_containers.connect.md) | +| [Middleware](./kibana-plugin-plugins-kibana_utils-common-state_containers.middleware.md) | Redux like Middleware | +| [PureSelector](./kibana-plugin-plugins-kibana_utils-common-state_containers.pureselector.md) | | +| [PureSelectorsToSelectors](./kibana-plugin-plugins-kibana_utils-common-state_containers.pureselectorstoselectors.md) | | +| [PureSelectorToSelector](./kibana-plugin-plugins-kibana_utils-common-state_containers.pureselectortoselector.md) | | +| [Reducer](./kibana-plugin-plugins-kibana_utils-common-state_containers.reducer.md) | Redux like Reducer | +| [Selector](./kibana-plugin-plugins-kibana_utils-common-state_containers.selector.md) | | +| [UnboxState](./kibana-plugin-plugins-kibana_utils-common-state_containers.unboxstate.md) | Utility type for inferring state shape from [StateContainer](./kibana-plugin-plugins-kibana_utils-common-state_containers.statecontainer.md) | + diff --git a/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.middleware.md b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.middleware.md new file mode 100644 index 0000000000000..574b83306dc95 --- /dev/null +++ b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.middleware.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-common-state\_containers](./kibana-plugin-plugins-kibana_utils-common-state_containers.md) > [Middleware](./kibana-plugin-plugins-kibana_utils-common-state_containers.middleware.md) + +## Middleware type + +Redux like Middleware + +Signature: + +```typescript +export declare type Middleware = (store: Pick, 'getState' | 'dispatch'>) => (next: (action: TransitionDescription) => TransitionDescription | any) => Dispatch; +``` diff --git a/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.pureselector.md b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.pureselector.md new file mode 100644 index 0000000000000..6ac07cba446f5 --- /dev/null +++ b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.pureselector.md @@ -0,0 +1,12 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-common-state\_containers](./kibana-plugin-plugins-kibana_utils-common-state_containers.md) > [PureSelector](./kibana-plugin-plugins-kibana_utils-common-state_containers.pureselector.md) + +## PureSelector type + + +Signature: + +```typescript +export declare type PureSelector = (state: State) => Selector; +``` diff --git a/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.pureselectorstoselectors.md b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.pureselectorstoselectors.md new file mode 100644 index 0000000000000..82a91f7c87e17 --- /dev/null +++ b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.pureselectorstoselectors.md @@ -0,0 +1,14 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-common-state\_containers](./kibana-plugin-plugins-kibana_utils-common-state_containers.md) > [PureSelectorsToSelectors](./kibana-plugin-plugins-kibana_utils-common-state_containers.pureselectorstoselectors.md) + +## PureSelectorsToSelectors type + + +Signature: + +```typescript +export declare type PureSelectorsToSelectors = { + [K in keyof T]: PureSelectorToSelector>; +}; +``` diff --git a/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.pureselectortoselector.md b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.pureselectortoselector.md new file mode 100644 index 0000000000000..5c12afd1cd971 --- /dev/null +++ b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.pureselectortoselector.md @@ -0,0 +1,12 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-common-state\_containers](./kibana-plugin-plugins-kibana_utils-common-state_containers.md) > [PureSelectorToSelector](./kibana-plugin-plugins-kibana_utils-common-state_containers.pureselectortoselector.md) + +## PureSelectorToSelector type + + +Signature: + +```typescript +export declare type PureSelectorToSelector> = ReturnType>; +``` diff --git a/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.reducer.md b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.reducer.md new file mode 100644 index 0000000000000..519e6ce7d7cfb --- /dev/null +++ b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.reducer.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-common-state\_containers](./kibana-plugin-plugins-kibana_utils-common-state_containers.md) > [Reducer](./kibana-plugin-plugins-kibana_utils-common-state_containers.reducer.md) + +## Reducer type + +Redux like Reducer + +Signature: + +```typescript +export declare type Reducer = (state: State, action: TransitionDescription) => State; +``` diff --git a/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.reduxlikestatecontainer.addmiddleware.md b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.reduxlikestatecontainer.addmiddleware.md new file mode 100644 index 0000000000000..e90da05e30d87 --- /dev/null +++ b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.reduxlikestatecontainer.addmiddleware.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-common-state\_containers](./kibana-plugin-plugins-kibana_utils-common-state_containers.md) > [ReduxLikeStateContainer](./kibana-plugin-plugins-kibana_utils-common-state_containers.reduxlikestatecontainer.md) > [addMiddleware](./kibana-plugin-plugins-kibana_utils-common-state_containers.reduxlikestatecontainer.addmiddleware.md) + +## ReduxLikeStateContainer.addMiddleware property + +Signature: + +```typescript +addMiddleware: (middleware: Middleware) => void; +``` diff --git a/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.reduxlikestatecontainer.dispatch.md b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.reduxlikestatecontainer.dispatch.md new file mode 100644 index 0000000000000..7a9755ee3b65c --- /dev/null +++ b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.reduxlikestatecontainer.dispatch.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-common-state\_containers](./kibana-plugin-plugins-kibana_utils-common-state_containers.md) > [ReduxLikeStateContainer](./kibana-plugin-plugins-kibana_utils-common-state_containers.reduxlikestatecontainer.md) > [dispatch](./kibana-plugin-plugins-kibana_utils-common-state_containers.reduxlikestatecontainer.dispatch.md) + +## ReduxLikeStateContainer.dispatch property + +Signature: + +```typescript +dispatch: (action: TransitionDescription) => void; +``` diff --git a/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.reduxlikestatecontainer.getstate.md b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.reduxlikestatecontainer.getstate.md new file mode 100644 index 0000000000000..86e1c6dd34cd6 --- /dev/null +++ b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.reduxlikestatecontainer.getstate.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-common-state\_containers](./kibana-plugin-plugins-kibana_utils-common-state_containers.md) > [ReduxLikeStateContainer](./kibana-plugin-plugins-kibana_utils-common-state_containers.reduxlikestatecontainer.md) > [getState](./kibana-plugin-plugins-kibana_utils-common-state_containers.reduxlikestatecontainer.getstate.md) + +## ReduxLikeStateContainer.getState property + +Signature: + +```typescript +getState: () => State; +``` diff --git a/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.reduxlikestatecontainer.md b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.reduxlikestatecontainer.md new file mode 100644 index 0000000000000..0e08119c1eae4 --- /dev/null +++ b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.reduxlikestatecontainer.md @@ -0,0 +1,25 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-common-state\_containers](./kibana-plugin-plugins-kibana_utils-common-state_containers.md) > [ReduxLikeStateContainer](./kibana-plugin-plugins-kibana_utils-common-state_containers.reduxlikestatecontainer.md) + +## ReduxLikeStateContainer interface + +Fully featured state container which matches Redux store interface. Extends [StateContainer](./kibana-plugin-plugins-kibana_utils-common-state_containers.statecontainer.md) Allows to use state container with redux libraries + +Signature: + +```typescript +export interface ReduxLikeStateContainer extends StateContainer +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [addMiddleware](./kibana-plugin-plugins-kibana_utils-common-state_containers.reduxlikestatecontainer.addmiddleware.md) | (middleware: Middleware<State>) => void | | +| [dispatch](./kibana-plugin-plugins-kibana_utils-common-state_containers.reduxlikestatecontainer.dispatch.md) | (action: TransitionDescription) => void | | +| [getState](./kibana-plugin-plugins-kibana_utils-common-state_containers.reduxlikestatecontainer.getstate.md) | () => State | | +| [reducer](./kibana-plugin-plugins-kibana_utils-common-state_containers.reduxlikestatecontainer.reducer.md) | Reducer<State> | | +| [replaceReducer](./kibana-plugin-plugins-kibana_utils-common-state_containers.reduxlikestatecontainer.replacereducer.md) | (nextReducer: Reducer<State>) => void | | +| [subscribe](./kibana-plugin-plugins-kibana_utils-common-state_containers.reduxlikestatecontainer.subscribe.md) | (listener: (state: State) => void) => () => void | | + diff --git a/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.reduxlikestatecontainer.reducer.md b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.reduxlikestatecontainer.reducer.md new file mode 100644 index 0000000000000..49eabf19340f2 --- /dev/null +++ b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.reduxlikestatecontainer.reducer.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-common-state\_containers](./kibana-plugin-plugins-kibana_utils-common-state_containers.md) > [ReduxLikeStateContainer](./kibana-plugin-plugins-kibana_utils-common-state_containers.reduxlikestatecontainer.md) > [reducer](./kibana-plugin-plugins-kibana_utils-common-state_containers.reduxlikestatecontainer.reducer.md) + +## ReduxLikeStateContainer.reducer property + +Signature: + +```typescript +reducer: Reducer; +``` diff --git a/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.reduxlikestatecontainer.replacereducer.md b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.reduxlikestatecontainer.replacereducer.md new file mode 100644 index 0000000000000..2582d31d9adc4 --- /dev/null +++ b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.reduxlikestatecontainer.replacereducer.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-common-state\_containers](./kibana-plugin-plugins-kibana_utils-common-state_containers.md) > [ReduxLikeStateContainer](./kibana-plugin-plugins-kibana_utils-common-state_containers.reduxlikestatecontainer.md) > [replaceReducer](./kibana-plugin-plugins-kibana_utils-common-state_containers.reduxlikestatecontainer.replacereducer.md) + +## ReduxLikeStateContainer.replaceReducer property + +Signature: + +```typescript +replaceReducer: (nextReducer: Reducer) => void; +``` diff --git a/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.reduxlikestatecontainer.subscribe.md b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.reduxlikestatecontainer.subscribe.md new file mode 100644 index 0000000000000..15139a7bd9f3e --- /dev/null +++ b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.reduxlikestatecontainer.subscribe.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-common-state\_containers](./kibana-plugin-plugins-kibana_utils-common-state_containers.md) > [ReduxLikeStateContainer](./kibana-plugin-plugins-kibana_utils-common-state_containers.reduxlikestatecontainer.md) > [subscribe](./kibana-plugin-plugins-kibana_utils-common-state_containers.reduxlikestatecontainer.subscribe.md) + +## ReduxLikeStateContainer.subscribe property + +Signature: + +```typescript +subscribe: (listener: (state: State) => void) => () => void; +``` diff --git a/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.selector.md b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.selector.md new file mode 100644 index 0000000000000..5c143551d130b --- /dev/null +++ b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.selector.md @@ -0,0 +1,12 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-common-state\_containers](./kibana-plugin-plugins-kibana_utils-common-state_containers.md) > [Selector](./kibana-plugin-plugins-kibana_utils-common-state_containers.selector.md) + +## Selector type + + +Signature: + +```typescript +export declare type Selector = (...args: Args) => Result; +``` diff --git a/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.statecontainer.md b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.statecontainer.md new file mode 100644 index 0000000000000..23ec1c8e5be01 --- /dev/null +++ b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.statecontainer.md @@ -0,0 +1,21 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-common-state\_containers](./kibana-plugin-plugins-kibana_utils-common-state_containers.md) > [StateContainer](./kibana-plugin-plugins-kibana_utils-common-state_containers.statecontainer.md) + +## StateContainer interface + +Fully featured state container with [Selectors](./kibana-plugin-plugins-kibana_utils-common-state_containers.selector.md) and . Extends [BaseStateContainer](./kibana-plugin-plugins-kibana_utils-common-state_containers.basestatecontainer.md) + +Signature: + +```typescript +export interface StateContainer extends BaseStateContainer +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [selectors](./kibana-plugin-plugins-kibana_utils-common-state_containers.statecontainer.selectors.md) | Readonly<PureSelectorsToSelectors<PureSelectors>> | | +| [transitions](./kibana-plugin-plugins-kibana_utils-common-state_containers.statecontainer.transitions.md) | Readonly<PureTransitionsToTransitions<PureTransitions>> | | + diff --git a/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.statecontainer.selectors.md b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.statecontainer.selectors.md new file mode 100644 index 0000000000000..2afac07b59e39 --- /dev/null +++ b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.statecontainer.selectors.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-common-state\_containers](./kibana-plugin-plugins-kibana_utils-common-state_containers.md) > [StateContainer](./kibana-plugin-plugins-kibana_utils-common-state_containers.statecontainer.md) > [selectors](./kibana-plugin-plugins-kibana_utils-common-state_containers.statecontainer.selectors.md) + +## StateContainer.selectors property + +Signature: + +```typescript +selectors: Readonly>; +``` diff --git a/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.statecontainer.transitions.md b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.statecontainer.transitions.md new file mode 100644 index 0000000000000..4712d3287beef --- /dev/null +++ b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.statecontainer.transitions.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-common-state\_containers](./kibana-plugin-plugins-kibana_utils-common-state_containers.md) > [StateContainer](./kibana-plugin-plugins-kibana_utils-common-state_containers.statecontainer.md) > [transitions](./kibana-plugin-plugins-kibana_utils-common-state_containers.statecontainer.transitions.md) + +## StateContainer.transitions property + +Signature: + +```typescript +transitions: Readonly>; +``` diff --git a/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.unboxstate.md b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.unboxstate.md new file mode 100644 index 0000000000000..d4f99841456d7 --- /dev/null +++ b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.unboxstate.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-common-state\_containers](./kibana-plugin-plugins-kibana_utils-common-state_containers.md) > [UnboxState](./kibana-plugin-plugins-kibana_utils-common-state_containers.unboxstate.md) + +## UnboxState type + +Utility type for inferring state shape from [StateContainer](./kibana-plugin-plugins-kibana_utils-common-state_containers.statecontainer.md) + +Signature: + +```typescript +export declare type UnboxState> = Container extends StateContainer ? T : never; +``` diff --git a/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.usecontainerselector.md b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.usecontainerselector.md new file mode 100644 index 0000000000000..fe5f30a9c8472 --- /dev/null +++ b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.usecontainerselector.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-common-state\_containers](./kibana-plugin-plugins-kibana_utils-common-state_containers.md) > [useContainerSelector](./kibana-plugin-plugins-kibana_utils-common-state_containers.usecontainerselector.md) + +## useContainerSelector variable + +React hook to apply selector to state container to extract only needed information. Will re-render your component only when the section changes. + +Signature: + +```typescript +useContainerSelector: , Result>(container: Container, selector: (state: UnboxState) => Result, comparator?: Comparator) => Result +``` diff --git a/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.usecontainerstate.md b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.usecontainerstate.md new file mode 100644 index 0000000000000..7cef47c58f9d9 --- /dev/null +++ b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.usecontainerstate.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-common-state\_containers](./kibana-plugin-plugins-kibana_utils-common-state_containers.md) > [useContainerState](./kibana-plugin-plugins-kibana_utils-common-state_containers.usecontainerstate.md) + +## useContainerState variable + +React hooks that returns the latest state of a [StateContainer](./kibana-plugin-plugins-kibana_utils-common-state_containers.statecontainer.md). + +Signature: + +```typescript +useContainerState: >(container: Container) => UnboxState +``` diff --git a/docs/development/plugins/kibana_utils/public/state_sync/index.md b/docs/development/plugins/kibana_utils/public/state_sync/index.md new file mode 100644 index 0000000000000..4b345d9130bd5 --- /dev/null +++ b/docs/development/plugins/kibana_utils/public/state_sync/index.md @@ -0,0 +1,12 @@ + + +[Home](./index.md) + +## API Reference + +## Packages + +| Package | Description | +| --- | --- | +| [kibana-plugin-plugins-kibana\_utils-public-state\_sync](./kibana-plugin-plugins-kibana_utils-public-state_sync.md) | State syncing utilities are a set of helpers for syncing your application state with URL or browser storage.They are designed to work together with state containers (). But state containers are not required.State syncing utilities include:- util which: - Subscribes to state changes and pushes them to state storage. - Optionally subscribes to state storage changes and pushes them to state. - Two types of storage compatible with syncState: - - Serializes state and persists it to URL's query param in rison or hashed format. Listens for state updates in the URL and pushes them back to state. - - Serializes state and persists it to browser storage.Refer [here](https://github.com/elastic/kibana/tree/master/src/plugins/kibana_utils/docs/state_sync) for a complete guide and examples | + diff --git a/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.createkbnurlstatestorage.md b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.createkbnurlstatestorage.md new file mode 100644 index 0000000000000..22f70ce22b574 --- /dev/null +++ b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.createkbnurlstatestorage.md @@ -0,0 +1,16 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-public-state\_sync](./kibana-plugin-plugins-kibana_utils-public-state_sync.md) > [createKbnUrlStateStorage](./kibana-plugin-plugins-kibana_utils-public-state_sync.createkbnurlstatestorage.md) + +## createKbnUrlStateStorage variable + +Creates [IKbnUrlStateStorage](./kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.md) state storage + +Signature: + +```typescript +createKbnUrlStateStorage: ({ useHash, history }?: { + useHash: boolean; + history?: History | undefined; +}) => IKbnUrlStateStorage +``` diff --git a/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.createsessionstoragestatestorage.md b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.createsessionstoragestatestorage.md new file mode 100644 index 0000000000000..dccff93ad1724 --- /dev/null +++ b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.createsessionstoragestatestorage.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-public-state\_sync](./kibana-plugin-plugins-kibana_utils-public-state_sync.md) > [createSessionStorageStateStorage](./kibana-plugin-plugins-kibana_utils-public-state_sync.createsessionstoragestatestorage.md) + +## createSessionStorageStateStorage variable + +Creates [ISessionStorageStateStorage](./kibana-plugin-plugins-kibana_utils-public-state_sync.isessionstoragestatestorage.md) [guide](https://github.com/elastic/kibana/blob/master/src/plugins/kibana_utils/docs/state_sync/storages/session_storage.md) + +Signature: + +```typescript +createSessionStorageStateStorage: (storage?: Storage) => ISessionStorageStateStorage +``` diff --git a/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.cancel.md b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.cancel.md new file mode 100644 index 0000000000000..29a511d57d7bd --- /dev/null +++ b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.cancel.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-public-state\_sync](./kibana-plugin-plugins-kibana_utils-public-state_sync.md) > [IKbnUrlStateStorage](./kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.md) > [cancel](./kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.cancel.md) + +## IKbnUrlStateStorage.cancel property + +cancels any pending url updates + +Signature: + +```typescript +cancel: () => void; +``` diff --git a/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.change_.md b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.change_.md new file mode 100644 index 0000000000000..2b55f2aca70c8 --- /dev/null +++ b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.change_.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-public-state\_sync](./kibana-plugin-plugins-kibana_utils-public-state_sync.md) > [IKbnUrlStateStorage](./kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.md) > [change$](./kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.change_.md) + +## IKbnUrlStateStorage.change$ property + +Signature: + +```typescript +change$: (key: string) => Observable; +``` diff --git a/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.flush.md b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.flush.md new file mode 100644 index 0000000000000..e0e6aa9be4368 --- /dev/null +++ b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.flush.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-public-state\_sync](./kibana-plugin-plugins-kibana_utils-public-state_sync.md) > [IKbnUrlStateStorage](./kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.md) > [flush](./kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.flush.md) + +## IKbnUrlStateStorage.flush property + +synchronously runs any pending url updates returned boolean indicates if change occurred + +Signature: + +```typescript +flush: (opts?: { + replace?: boolean; + }) => boolean; +``` diff --git a/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.get.md b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.get.md new file mode 100644 index 0000000000000..0eb60c21fbbbf --- /dev/null +++ b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.get.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-public-state\_sync](./kibana-plugin-plugins-kibana_utils-public-state_sync.md) > [IKbnUrlStateStorage](./kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.md) > [get](./kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.get.md) + +## IKbnUrlStateStorage.get property + +Signature: + +```typescript +get: (key: string) => State | null; +``` diff --git a/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.md b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.md new file mode 100644 index 0000000000000..56cefebd2acfe --- /dev/null +++ b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.md @@ -0,0 +1,24 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-public-state\_sync](./kibana-plugin-plugins-kibana_utils-public-state_sync.md) > [IKbnUrlStateStorage](./kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.md) + +## IKbnUrlStateStorage interface + +KbnUrlStateStorage is a state storage for [syncState()](./kibana-plugin-plugins-kibana_utils-public-state_sync.syncstate.md) utility which: 1. Keeps state in sync with the URL. 2. Serializes data and stores it in the URL in one of the supported formats: \* Rison encoded. \* Hashed URL: In URL we store only the hash from the serialized state, but the state itself is stored in sessionStorage. See kibana's advanced option for more context state:storeInSessionStorage 3. Takes care of listening to the URL updates and notifies state about the updates. 4. Takes care of batching URL updates to prevent redundant browser history records. [GUIDE](https://github.com/elastic/kibana/blob/master/src/plugins/kibana_utils/docs/state_sync/storages/kbn_url_storage.md) + +Signature: + +```typescript +export interface IKbnUrlStateStorage extends IStateStorage +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [cancel](./kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.cancel.md) | () => void | cancels any pending url updates | +| [change$](./kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.change_.md) | <State = unknown>(key: string) => Observable<State | null> | | +| [flush](./kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.flush.md) | (opts?: {
replace?: boolean;
}) => boolean | synchronously runs any pending url updates returned boolean indicates if change occurred | +| [get](./kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.get.md) | <State = unknown>(key: string) => State | null | | +| [set](./kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.set.md) | <State>(key: string, state: State, opts?: {
replace: boolean;
}) => Promise<string | undefined> | | + diff --git a/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.set.md b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.set.md new file mode 100644 index 0000000000000..2eab44d344414 --- /dev/null +++ b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.set.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-public-state\_sync](./kibana-plugin-plugins-kibana_utils-public-state_sync.md) > [IKbnUrlStateStorage](./kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.md) > [set](./kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.set.md) + +## IKbnUrlStateStorage.set property + +Signature: + +```typescript +set: (key: string, state: State, opts?: { + replace: boolean; + }) => Promise; +``` diff --git a/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.inullablebasestatecontainer.md b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.inullablebasestatecontainer.md new file mode 100644 index 0000000000000..ca69609936405 --- /dev/null +++ b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.inullablebasestatecontainer.md @@ -0,0 +1,24 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-public-state\_sync](./kibana-plugin-plugins-kibana_utils-public-state_sync.md) > [INullableBaseStateContainer](./kibana-plugin-plugins-kibana_utils-public-state_sync.inullablebasestatecontainer.md) + +## INullableBaseStateContainer interface + +Extension of with one constraint: set state should handle `null` as incoming state + +Signature: + +```typescript +export interface INullableBaseStateContainer extends BaseStateContainer +``` + +## Remarks + +State container for stateSync() have to accept "null" for example, set() implementation could handle null and fallback to some default state this is required to handle edge case, when state in storage becomes empty and syncing is in progress. state container will be notified about about storage becoming empty with null passed in + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [set](./kibana-plugin-plugins-kibana_utils-public-state_sync.inullablebasestatecontainer.set.md) | (state: State | null) => void | | + diff --git a/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.inullablebasestatecontainer.set.md b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.inullablebasestatecontainer.set.md new file mode 100644 index 0000000000000..dd2978f59484a --- /dev/null +++ b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.inullablebasestatecontainer.set.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-public-state\_sync](./kibana-plugin-plugins-kibana_utils-public-state_sync.md) > [INullableBaseStateContainer](./kibana-plugin-plugins-kibana_utils-public-state_sync.inullablebasestatecontainer.md) > [set](./kibana-plugin-plugins-kibana_utils-public-state_sync.inullablebasestatecontainer.set.md) + +## INullableBaseStateContainer.set property + +Signature: + +```typescript +set: (state: State | null) => void; +``` diff --git a/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.isessionstoragestatestorage.get.md b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.isessionstoragestatestorage.get.md new file mode 100644 index 0000000000000..83131c77132ce --- /dev/null +++ b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.isessionstoragestatestorage.get.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-public-state\_sync](./kibana-plugin-plugins-kibana_utils-public-state_sync.md) > [ISessionStorageStateStorage](./kibana-plugin-plugins-kibana_utils-public-state_sync.isessionstoragestatestorage.md) > [get](./kibana-plugin-plugins-kibana_utils-public-state_sync.isessionstoragestatestorage.get.md) + +## ISessionStorageStateStorage.get property + +Signature: + +```typescript +get: (key: string) => State | null; +``` diff --git a/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.isessionstoragestatestorage.md b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.isessionstoragestatestorage.md new file mode 100644 index 0000000000000..7792bc3932f95 --- /dev/null +++ b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.isessionstoragestatestorage.md @@ -0,0 +1,21 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-public-state\_sync](./kibana-plugin-plugins-kibana_utils-public-state_sync.md) > [ISessionStorageStateStorage](./kibana-plugin-plugins-kibana_utils-public-state_sync.isessionstoragestatestorage.md) + +## ISessionStorageStateStorage interface + +[IStateStorage](./kibana-plugin-plugins-kibana_utils-public-state_sync.istatestorage.md) for storing state in browser [guide](https://github.com/elastic/kibana/blob/master/src/plugins/kibana_utils/docs/state_sync/storages/session_storage.md) + +Signature: + +```typescript +export interface ISessionStorageStateStorage extends IStateStorage +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [get](./kibana-plugin-plugins-kibana_utils-public-state_sync.isessionstoragestatestorage.get.md) | <State = unknown>(key: string) => State | null | | +| [set](./kibana-plugin-plugins-kibana_utils-public-state_sync.isessionstoragestatestorage.set.md) | <State>(key: string, state: State) => void | | + diff --git a/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.isessionstoragestatestorage.set.md b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.isessionstoragestatestorage.set.md new file mode 100644 index 0000000000000..04b0ab01f0d13 --- /dev/null +++ b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.isessionstoragestatestorage.set.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-public-state\_sync](./kibana-plugin-plugins-kibana_utils-public-state_sync.md) > [ISessionStorageStateStorage](./kibana-plugin-plugins-kibana_utils-public-state_sync.isessionstoragestatestorage.md) > [set](./kibana-plugin-plugins-kibana_utils-public-state_sync.isessionstoragestatestorage.set.md) + +## ISessionStorageStateStorage.set property + +Signature: + +```typescript +set: (key: string, state: State) => void; +``` diff --git a/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.istatestorage.cancel.md b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.istatestorage.cancel.md new file mode 100644 index 0000000000000..ce771d52a6e60 --- /dev/null +++ b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.istatestorage.cancel.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-public-state\_sync](./kibana-plugin-plugins-kibana_utils-public-state_sync.md) > [IStateStorage](./kibana-plugin-plugins-kibana_utils-public-state_sync.istatestorage.md) > [cancel](./kibana-plugin-plugins-kibana_utils-public-state_sync.istatestorage.cancel.md) + +## IStateStorage.cancel property + +Optional method to cancel any pending activity syncState() will call it, if it is provided by IStateStorage + +Signature: + +```typescript +cancel?: () => void; +``` diff --git a/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.istatestorage.change_.md b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.istatestorage.change_.md new file mode 100644 index 0000000000000..ed6672a3d83c6 --- /dev/null +++ b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.istatestorage.change_.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-public-state\_sync](./kibana-plugin-plugins-kibana_utils-public-state_sync.md) > [IStateStorage](./kibana-plugin-plugins-kibana_utils-public-state_sync.istatestorage.md) > [change$](./kibana-plugin-plugins-kibana_utils-public-state_sync.istatestorage.change_.md) + +## IStateStorage.change$ property + +Should notify when the stored state has changed + +Signature: + +```typescript +change$?: (key: string) => Observable; +``` diff --git a/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.istatestorage.get.md b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.istatestorage.get.md new file mode 100644 index 0000000000000..2c0b2ee970cc6 --- /dev/null +++ b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.istatestorage.get.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-public-state\_sync](./kibana-plugin-plugins-kibana_utils-public-state_sync.md) > [IStateStorage](./kibana-plugin-plugins-kibana_utils-public-state_sync.istatestorage.md) > [get](./kibana-plugin-plugins-kibana_utils-public-state_sync.istatestorage.get.md) + +## IStateStorage.get property + +Should retrieve state from the storage and deserialize it + +Signature: + +```typescript +get: (key: string) => State | null; +``` diff --git a/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.istatestorage.md b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.istatestorage.md new file mode 100644 index 0000000000000..2c34a185fb7b1 --- /dev/null +++ b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.istatestorage.md @@ -0,0 +1,25 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-public-state\_sync](./kibana-plugin-plugins-kibana_utils-public-state_sync.md) > [IStateStorage](./kibana-plugin-plugins-kibana_utils-public-state_sync.istatestorage.md) + +## IStateStorage interface + +Any StateStorage have to implement IStateStorage interface StateStorage is responsible for: \* state serialisation / deserialization \* persisting to and retrieving from storage + +For an example take a look at already implemented [IKbnUrlStateStorage](./kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.md) and [ISessionStorageStateStorage](./kibana-plugin-plugins-kibana_utils-public-state_sync.isessionstoragestatestorage.md) state storages + +Signature: + +```typescript +export interface IStateStorage +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [cancel](./kibana-plugin-plugins-kibana_utils-public-state_sync.istatestorage.cancel.md) | () => void | Optional method to cancel any pending activity syncState() will call it, if it is provided by IStateStorage | +| [change$](./kibana-plugin-plugins-kibana_utils-public-state_sync.istatestorage.change_.md) | <State = unknown>(key: string) => Observable<State | null> | Should notify when the stored state has changed | +| [get](./kibana-plugin-plugins-kibana_utils-public-state_sync.istatestorage.get.md) | <State = unknown>(key: string) => State | null | Should retrieve state from the storage and deserialize it | +| [set](./kibana-plugin-plugins-kibana_utils-public-state_sync.istatestorage.set.md) | <State>(key: string, state: State) => any | Take in a state object, should serialise and persist | + diff --git a/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.istatestorage.set.md b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.istatestorage.set.md new file mode 100644 index 0000000000000..3f286994ed4af --- /dev/null +++ b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.istatestorage.set.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-public-state\_sync](./kibana-plugin-plugins-kibana_utils-public-state_sync.md) > [IStateStorage](./kibana-plugin-plugins-kibana_utils-public-state_sync.istatestorage.md) > [set](./kibana-plugin-plugins-kibana_utils-public-state_sync.istatestorage.set.md) + +## IStateStorage.set property + +Take in a state object, should serialise and persist + +Signature: + +```typescript +set: (key: string, state: State) => any; +``` diff --git a/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.istatesyncconfig.md b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.istatesyncconfig.md new file mode 100644 index 0000000000000..f9368de4240ac --- /dev/null +++ b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.istatesyncconfig.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-public-state\_sync](./kibana-plugin-plugins-kibana_utils-public-state_sync.md) > [IStateSyncConfig](./kibana-plugin-plugins-kibana_utils-public-state_sync.istatesyncconfig.md) + +## IStateSyncConfig interface + +Config for setting up state syncing with + +Signature: + +```typescript +export interface IStateSyncConfig +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [stateContainer](./kibana-plugin-plugins-kibana_utils-public-state_sync.istatesyncconfig.statecontainer.md) | INullableBaseStateContainer<State> | State container to keep in sync with storage, have to implement [INullableBaseStateContainer](./kibana-plugin-plugins-kibana_utils-public-state_sync.inullablebasestatecontainer.md) interface We encourage to use as a state container, but it is also possible to implement own custom container for advanced use cases | +| [stateStorage](./kibana-plugin-plugins-kibana_utils-public-state_sync.istatesyncconfig.statestorage.md) | StateStorage | State storage to use, State storage is responsible for serialising / deserialising and persisting / retrieving stored stateThere are common strategies already implemented: see [IKbnUrlStateStorage](./kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.md) which replicate what State (AppState, GlobalState) in legacy world did | +| [storageKey](./kibana-plugin-plugins-kibana_utils-public-state_sync.istatesyncconfig.storagekey.md) | string | Storage key to use for syncing, e.g. storageKey '\_a' should sync state to ?\_a query param | + diff --git a/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.istatesyncconfig.statecontainer.md b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.istatesyncconfig.statecontainer.md new file mode 100644 index 0000000000000..0098dd5c99aeb --- /dev/null +++ b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.istatesyncconfig.statecontainer.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-public-state\_sync](./kibana-plugin-plugins-kibana_utils-public-state_sync.md) > [IStateSyncConfig](./kibana-plugin-plugins-kibana_utils-public-state_sync.istatesyncconfig.md) > [stateContainer](./kibana-plugin-plugins-kibana_utils-public-state_sync.istatesyncconfig.statecontainer.md) + +## IStateSyncConfig.stateContainer property + +State container to keep in sync with storage, have to implement [INullableBaseStateContainer](./kibana-plugin-plugins-kibana_utils-public-state_sync.inullablebasestatecontainer.md) interface We encourage to use as a state container, but it is also possible to implement own custom container for advanced use cases + +Signature: + +```typescript +stateContainer: INullableBaseStateContainer; +``` diff --git a/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.istatesyncconfig.statestorage.md b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.istatesyncconfig.statestorage.md new file mode 100644 index 0000000000000..ef872ba0ba9b5 --- /dev/null +++ b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.istatesyncconfig.statestorage.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-public-state\_sync](./kibana-plugin-plugins-kibana_utils-public-state_sync.md) > [IStateSyncConfig](./kibana-plugin-plugins-kibana_utils-public-state_sync.istatesyncconfig.md) > [stateStorage](./kibana-plugin-plugins-kibana_utils-public-state_sync.istatesyncconfig.statestorage.md) + +## IStateSyncConfig.stateStorage property + +State storage to use, State storage is responsible for serialising / deserialising and persisting / retrieving stored state + +There are common strategies already implemented: see [IKbnUrlStateStorage](./kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.md) which replicate what State (AppState, GlobalState) in legacy world did + +Signature: + +```typescript +stateStorage: StateStorage; +``` diff --git a/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.istatesyncconfig.storagekey.md b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.istatesyncconfig.storagekey.md new file mode 100644 index 0000000000000..d3887c23df1e0 --- /dev/null +++ b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.istatesyncconfig.storagekey.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-public-state\_sync](./kibana-plugin-plugins-kibana_utils-public-state_sync.md) > [IStateSyncConfig](./kibana-plugin-plugins-kibana_utils-public-state_sync.istatesyncconfig.md) > [storageKey](./kibana-plugin-plugins-kibana_utils-public-state_sync.istatesyncconfig.storagekey.md) + +## IStateSyncConfig.storageKey property + +Storage key to use for syncing, e.g. storageKey '\_a' should sync state to ?\_a query param + +Signature: + +```typescript +storageKey: string; +``` diff --git a/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.isyncstateref.md b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.isyncstateref.md new file mode 100644 index 0000000000000..137db68cd6b48 --- /dev/null +++ b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.isyncstateref.md @@ -0,0 +1,20 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-public-state\_sync](./kibana-plugin-plugins-kibana_utils-public-state_sync.md) > [ISyncStateRef](./kibana-plugin-plugins-kibana_utils-public-state_sync.isyncstateref.md) + +## ISyncStateRef interface + + +Signature: + +```typescript +export interface ISyncStateRef +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [start](./kibana-plugin-plugins-kibana_utils-public-state_sync.isyncstateref.start.md) | StartSyncStateFnType | start state syncing | +| [stop](./kibana-plugin-plugins-kibana_utils-public-state_sync.isyncstateref.stop.md) | StopSyncStateFnType | stop state syncing | + diff --git a/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.isyncstateref.start.md b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.isyncstateref.start.md new file mode 100644 index 0000000000000..d8df808ba215f --- /dev/null +++ b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.isyncstateref.start.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-public-state\_sync](./kibana-plugin-plugins-kibana_utils-public-state_sync.md) > [ISyncStateRef](./kibana-plugin-plugins-kibana_utils-public-state_sync.isyncstateref.md) > [start](./kibana-plugin-plugins-kibana_utils-public-state_sync.isyncstateref.start.md) + +## ISyncStateRef.start property + +start state syncing + +Signature: + +```typescript +start: StartSyncStateFnType; +``` diff --git a/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.isyncstateref.stop.md b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.isyncstateref.stop.md new file mode 100644 index 0000000000000..70356dd9d6c79 --- /dev/null +++ b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.isyncstateref.stop.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-public-state\_sync](./kibana-plugin-plugins-kibana_utils-public-state_sync.md) > [ISyncStateRef](./kibana-plugin-plugins-kibana_utils-public-state_sync.isyncstateref.md) > [stop](./kibana-plugin-plugins-kibana_utils-public-state_sync.isyncstateref.stop.md) + +## ISyncStateRef.stop property + +stop state syncing + +Signature: + +```typescript +stop: StopSyncStateFnType; +``` diff --git a/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.md b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.md new file mode 100644 index 0000000000000..2b02c98e0d605 --- /dev/null +++ b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.md @@ -0,0 +1,48 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-public-state\_sync](./kibana-plugin-plugins-kibana_utils-public-state_sync.md) + +## kibana-plugin-plugins-kibana\_utils-public-state\_sync package + +State syncing utilities are a set of helpers for syncing your application state with URL or browser storage. + +They are designed to work together with state containers (). But state containers are not required. + +State syncing utilities include: + +- [syncState()](./kibana-plugin-plugins-kibana_utils-public-state_sync.syncstate.md) util which: - Subscribes to state changes and pushes them to state storage. - Optionally subscribes to state storage changes and pushes them to state. - Two types of storage compatible with `syncState`: - [IKbnUrlStateStorage](./kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.md) - Serializes state and persists it to URL's query param in rison or hashed format. Listens for state updates in the URL and pushes them back to state. - [ISessionStorageStateStorage](./kibana-plugin-plugins-kibana_utils-public-state_sync.isessionstoragestatestorage.md) - Serializes state and persists it to browser storage. + +Refer [here](https://github.com/elastic/kibana/tree/master/src/plugins/kibana_utils/docs/state_sync) for a complete guide and examples + +## Functions + +| Function | Description | +| --- | --- | +| [syncState({ storageKey, stateStorage, stateContainer, })](./kibana-plugin-plugins-kibana_utils-public-state_sync.syncstate.md) | Utility for syncing application state wrapped in state container with some kind of storage (e.g. URL) Refer [here](https://github.com/elastic/kibana/tree/master/src/plugins/kibana_utils/docs/state_sync) for a complete guide and examples | +| [syncStates(stateSyncConfigs)](./kibana-plugin-plugins-kibana_utils-public-state_sync.syncstates.md) | | + +## Interfaces + +| Interface | Description | +| --- | --- | +| [IKbnUrlStateStorage](./kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.md) | KbnUrlStateStorage is a state storage for [syncState()](./kibana-plugin-plugins-kibana_utils-public-state_sync.syncstate.md) utility which: 1. Keeps state in sync with the URL. 2. Serializes data and stores it in the URL in one of the supported formats: \* Rison encoded. \* Hashed URL: In URL we store only the hash from the serialized state, but the state itself is stored in sessionStorage. See kibana's advanced option for more context state:storeInSessionStorage 3. Takes care of listening to the URL updates and notifies state about the updates. 4. Takes care of batching URL updates to prevent redundant browser history records. [GUIDE](https://github.com/elastic/kibana/blob/master/src/plugins/kibana_utils/docs/state_sync/storages/kbn_url_storage.md) | +| [INullableBaseStateContainer](./kibana-plugin-plugins-kibana_utils-public-state_sync.inullablebasestatecontainer.md) | Extension of with one constraint: set state should handle null as incoming state | +| [ISessionStorageStateStorage](./kibana-plugin-plugins-kibana_utils-public-state_sync.isessionstoragestatestorage.md) | [IStateStorage](./kibana-plugin-plugins-kibana_utils-public-state_sync.istatestorage.md) for storing state in browser [guide](https://github.com/elastic/kibana/blob/master/src/plugins/kibana_utils/docs/state_sync/storages/session_storage.md) | +| [IStateStorage](./kibana-plugin-plugins-kibana_utils-public-state_sync.istatestorage.md) | Any StateStorage have to implement IStateStorage interface StateStorage is responsible for: \* state serialisation / deserialization \* persisting to and retrieving from storageFor an example take a look at already implemented [IKbnUrlStateStorage](./kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.md) and [ISessionStorageStateStorage](./kibana-plugin-plugins-kibana_utils-public-state_sync.isessionstoragestatestorage.md) state storages | +| [IStateSyncConfig](./kibana-plugin-plugins-kibana_utils-public-state_sync.istatesyncconfig.md) | Config for setting up state syncing with | +| [ISyncStateRef](./kibana-plugin-plugins-kibana_utils-public-state_sync.isyncstateref.md) | | + +## Variables + +| Variable | Description | +| --- | --- | +| [createKbnUrlStateStorage](./kibana-plugin-plugins-kibana_utils-public-state_sync.createkbnurlstatestorage.md) | Creates [IKbnUrlStateStorage](./kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.md) state storage | +| [createSessionStorageStateStorage](./kibana-plugin-plugins-kibana_utils-public-state_sync.createsessionstoragestatestorage.md) | Creates [ISessionStorageStateStorage](./kibana-plugin-plugins-kibana_utils-public-state_sync.isessionstoragestatestorage.md) [guide](https://github.com/elastic/kibana/blob/master/src/plugins/kibana_utils/docs/state_sync/storages/session_storage.md) | + +## Type Aliases + +| Type Alias | Description | +| --- | --- | +| [StartSyncStateFnType](./kibana-plugin-plugins-kibana_utils-public-state_sync.startsyncstatefntype.md) | | +| [StopSyncStateFnType](./kibana-plugin-plugins-kibana_utils-public-state_sync.stopsyncstatefntype.md) | | + diff --git a/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.startsyncstatefntype.md b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.startsyncstatefntype.md new file mode 100644 index 0000000000000..23f71ba330d4b --- /dev/null +++ b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.startsyncstatefntype.md @@ -0,0 +1,12 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-public-state\_sync](./kibana-plugin-plugins-kibana_utils-public-state_sync.md) > [StartSyncStateFnType](./kibana-plugin-plugins-kibana_utils-public-state_sync.startsyncstatefntype.md) + +## StartSyncStateFnType type + + +Signature: + +```typescript +export declare type StartSyncStateFnType = () => void; +``` diff --git a/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.stopsyncstatefntype.md b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.stopsyncstatefntype.md new file mode 100644 index 0000000000000..69ff6e899e860 --- /dev/null +++ b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.stopsyncstatefntype.md @@ -0,0 +1,12 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-public-state\_sync](./kibana-plugin-plugins-kibana_utils-public-state_sync.md) > [StopSyncStateFnType](./kibana-plugin-plugins-kibana_utils-public-state_sync.stopsyncstatefntype.md) + +## StopSyncStateFnType type + + +Signature: + +```typescript +export declare type StopSyncStateFnType = () => void; +``` diff --git a/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.syncstate.md b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.syncstate.md new file mode 100644 index 0000000000000..d095c3fffc512 --- /dev/null +++ b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.syncstate.md @@ -0,0 +1,93 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-public-state\_sync](./kibana-plugin-plugins-kibana_utils-public-state_sync.md) > [syncState](./kibana-plugin-plugins-kibana_utils-public-state_sync.syncstate.md) + +## syncState() function + +Utility for syncing application state wrapped in state container with some kind of storage (e.g. URL) Refer [here](https://github.com/elastic/kibana/tree/master/src/plugins/kibana_utils/docs/state_sync) for a complete guide and examples + +Signature: + +```typescript +export declare function syncState({ storageKey, stateStorage, stateContainer, }: IStateSyncConfig): ISyncStateRef; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| { storageKey, stateStorage, stateContainer, } | IStateSyncConfig<State, IStateStorage> | | + +Returns: + +`ISyncStateRef` + +- [ISyncStateRef](./kibana-plugin-plugins-kibana_utils-public-state_sync.isyncstateref.md) + +## Remarks + +1. It is responsibility of consumer to make sure that initial app state and storage are in sync before starting syncing No initial sync happens when syncState() is called + +## Example 1 + +1. the simplest use case + +```ts +const stateStorage = createKbnUrlStateStorage(); +syncState({ + storageKey: '_s', + stateContainer, + stateStorage +}); + +``` + +## Example 2 + +2. conditionally configuring sync strategy + +```ts +const stateStorage = createKbnUrlStateStorage({useHash: config.get('state:stateContainerInSessionStorage')}) +syncState({ + storageKey: '_s', + stateContainer, + stateStorage +}); + +``` + +## Example 3 + +3. implementing custom sync strategy + +```ts +const localStorageStateStorage = { + set: (storageKey, state) => localStorage.setItem(storageKey, JSON.stringify(state)), + get: (storageKey) => localStorage.getItem(storageKey) ? JSON.parse(localStorage.getItem(storageKey)) : null +}; +syncState({ + storageKey: '_s', + stateContainer, + stateStorage: localStorageStateStorage +}); + +``` + +## Example 4 + +4. Transform state before serialising Useful for: \* Migration / backward compatibility \* Syncing part of state \* Providing default values + +```ts +const stateToStorage = (s) => ({ tab: s.tab }); +syncState({ + storageKey: '_s', + stateContainer: { + get: () => stateToStorage(stateContainer.get()), + set: stateContainer.set(({ tab }) => ({ ...stateContainer.get(), tab }), + state$: stateContainer.state$.pipe(map(stateToStorage)) + }, + stateStorage +}); + +``` + diff --git a/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.syncstates.md b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.syncstates.md new file mode 100644 index 0000000000000..87a2449a384df --- /dev/null +++ b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.syncstates.md @@ -0,0 +1,42 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-public-state\_sync](./kibana-plugin-plugins-kibana_utils-public-state_sync.md) > [syncStates](./kibana-plugin-plugins-kibana_utils-public-state_sync.syncstates.md) + +## syncStates() function + +Signature: + +```typescript +export declare function syncStates(stateSyncConfigs: Array>): ISyncStateRef; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| stateSyncConfigs | Array<IStateSyncConfig<any>> | Array of [IStateSyncConfig](./kibana-plugin-plugins-kibana_utils-public-state_sync.istatesyncconfig.md) to sync | + +Returns: + +`ISyncStateRef` + +## Example + +sync multiple different sync configs + +```ts +syncStates([ + { + storageKey: '_s1', + stateStorage: stateStorage1, + stateContainer: stateContainer1, + }, + { + storageKey: '_s2', + stateStorage: stateStorage2, + stateContainer: stateContainer2, + }, +]); + +``` + diff --git a/src/dev/run_check_published_api_changes.ts b/src/dev/run_check_published_api_changes.ts index 45dafe1b415e3..0aa450c8b002a 100644 --- a/src/dev/run_check_published_api_changes.ts +++ b/src/dev/run_check_published_api_changes.ts @@ -43,7 +43,18 @@ import getopts from 'getopts'; */ const getReportFileName = (folder: string) => { - return folder.indexOf('public') > -1 ? 'public' : 'server'; + switch (true) { + case folder.includes('public'): + return 'public'; + case folder.includes('server'): + return 'server'; + case folder.includes('common'): + return 'common'; + default: + throw new Error( + `folder "${folder}" expected to include one of ["public", "server", "common"]` + ); + } }; const apiExtractorConfig = (folder: string): ExtractorConfig => { @@ -131,7 +142,7 @@ const runApiExtractor = ( messageCallback: (message: ExtractorMessage) => { if (message.messageId === 'console-api-report-not-copied') { // ConsoleMessageId.ApiReportNotCopied - log.warning(`You have changed the signature of the ${folder} Core API`); + log.warning(`You have changed the signature of the ${folder} public API`); log.warning( 'To accept these changes run `node scripts/check_published_api_changes.js --accept` and then:\n' + "\t 1. Commit the updated documentation and API review file '" + @@ -142,7 +153,7 @@ const runApiExtractor = ( message.handled = true; } else if (message.messageId === 'console-api-report-copied') { // ConsoleMessageId.ApiReportCopied - log.warning(`You have changed the signature of the ${folder} Core API`); + log.warning(`You have changed the signature of the ${folder} public API`); log.warning( "Please commit the updated API documentation and the API review file: '" + config.reportFilePath @@ -150,7 +161,7 @@ const runApiExtractor = ( message.handled = true; } else if (message.messageId === 'console-api-report-unchanged') { // ConsoleMessageId.ApiReportUnchanged - log.info(`Core ${folder} API: no changes detected ✔`); + log.info(`${folder} API: no changes detected ✔`); message.handled = true; } }, @@ -170,7 +181,7 @@ async function run( folder: string, { log, opts }: { log: ToolingLog; opts: Options } ): Promise { - log.info(`Core ${folder} API: checking for changes in API signature...`); + log.info(`${folder} API: checking for changes in API signature...`); const { apiReportChanged, succeeded } = runApiExtractor(log, folder, opts.accept); @@ -188,7 +199,7 @@ async function run( log.error(e); return false; } - log.info(`Core ${folder} API: updated documentation ✔`); + log.info(`${folder} API: updated documentation ✔`); } // If the api signature changed or any errors or warnings occured, exit with an error @@ -224,24 +235,31 @@ async function run( opts.help = true; } - const folders = ['core/public', 'core/server', 'plugins/data/server', 'plugins/data/public']; + const core = ['core/public', 'core/server']; + const plugins = [ + 'plugins/data/server', + 'plugins/data/public', + 'plugins/kibana_utils/common/state_containers', + 'plugins/kibana_utils/public/state_sync', + ]; + const folders = [...core, ...plugins]; if (opts.help) { process.stdout.write( dedent(chalk` {dim usage:} node scripts/check_published_api_changes [...options] - Checks for any changes to the Kibana Core API + Checks for any changes to the Kibana shared API Examples: - {dim # Checks for any changes to the Kibana Core API} + {dim # Checks for any changes to the Kibana shared API} {dim $} node scripts/check_published_api_changes - {dim # Checks for any changes to the Kibana Core API and updates the documentation} + {dim # Checks for any changes to the Kibana shared API and updates the documentation} {dim $} node scripts/check_published_api_changes --docs - {dim # Checks for and automatically accepts and updates documentation for any changes to the Kibana Core API} + {dim # Checks for and automatically accepts and updates documentation for any changes to the Kibana shared API} {dim $} node scripts/check_published_api_changes --accept {dim # Only checks the core/public directory} @@ -249,7 +267,7 @@ async function run( Options: --accept {dim Accepts all changes by updating the API Review files and documentation} - --docs {dim Updates the Core API documentation} + --docs {dim Updates the API documentation} --filter {dim RegExp that folder names must match, folders: [${folders.join(', ')}]} --help {dim Show this message} `) @@ -259,20 +277,22 @@ async function run( } try { - log.info(`Core: Building types...`); + log.info(`Building types for api extractor...`); await runBuildTypes(); } catch (e) { log.error(e); return false; } - const results = await Promise.all( - folders - .filter((folder) => (opts.filter.length ? folder.match(opts.filter) : true)) - .map((folder) => run(folder, { log, opts })) + const filteredFolders = folders.filter((folder) => + opts.filter.length ? folder.match(opts.filter) : true ); + const results = []; + for (const folder of filteredFolders) { + results.push(await run(folder, { log, opts })); + } - if (results.find((r) => r === false) !== undefined) { + if (results.includes(false)) { process.exitCode = 1; } })().catch((e) => { diff --git a/src/plugins/kibana_utils/common/state_containers/common.api.md b/src/plugins/kibana_utils/common/state_containers/common.api.md new file mode 100644 index 0000000000000..f85458499b719 --- /dev/null +++ b/src/plugins/kibana_utils/common/state_containers/common.api.md @@ -0,0 +1,156 @@ +## API Report File for "kibana" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import { ComponentType } from 'react'; +import { Ensure } from '@kbn/utility-types'; +import { FC } from 'react'; +import { Observable } from 'rxjs'; +import React from 'react'; + +// @public +export type BaseState = object; + +// @public +export interface BaseStateContainer { + get: () => State; + set: (state: State) => void; + // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "kibana" does not have an export "Observable" + state$: Observable; +} + +// @public +export type Comparator = (previous: Result, current: Result) => boolean; + +// @public +export type Connect = (mapStateToProp: MapStateToProps>) => (component: ComponentType) => FC>; + +// @public +export function createStateContainer(defaultState: State): ReduxLikeStateContainer; + +// @public +export function createStateContainer(defaultState: State, pureTransitions: PureTransitions): ReduxLikeStateContainer; + +// Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "kibana" does not have an export "PureSelectors" +// +// @public +export function createStateContainer(defaultState: State, pureTransitions: PureTransitions, pureSelectors: PureSelectors, options?: CreateStateContainerOptions): ReduxLikeStateContainer; + +// @public +export interface CreateStateContainerOptions { + freeze?: (state: T) => T; +} + +// @public +export const createStateContainerReactHelpers: >() => { + Provider: React.Provider; + Consumer: React.Consumer; + context: React.Context; + useContainer: () => Container; + useState: () => UnboxState; + useTransitions: () => Container["transitions"]; + useSelector: (selector: (state: UnboxState) => Result, comparator?: Comparator) => Result; + connect: Connect>; +}; + +// @public +export type Dispatch = (action: T) => void; + +// @public (undocumented) +export type EnsurePureSelector = Ensure>; + +// Warning: (ae-incompatible-release-tags) The symbol "EnsurePureTransition" is marked as @public, but its signature references "PureTransition" which is marked as @internal +// +// @public (undocumented) +export type EnsurePureTransition = Ensure>; + +// @public +export type MapStateToProps = (state: State) => StateProps; + +// Warning: (ae-incompatible-release-tags) The symbol "Middleware" is marked as @public, but its signature references "TransitionDescription" which is marked as @internal +// +// @public +export type Middleware = (store: Pick, 'getState' | 'dispatch'>) => (next: (action: TransitionDescription) => TransitionDescription | any) => Dispatch; + +// @public (undocumented) +export type PureSelector = (state: State) => Selector; + +// @public (undocumented) +export type PureSelectorsToSelectors = { + [K in keyof T]: PureSelectorToSelector>; +}; + +// @public (undocumented) +export type PureSelectorToSelector> = ReturnType>; + +// @internal (undocumented) +export type PureTransition = (state: State) => Transition; + +// @internal (undocumented) +export type PureTransitionsToTransitions = { + [K in keyof T]: PureTransitionToTransition>; +}; + +// @internal (undocumented) +export type PureTransitionToTransition> = ReturnType; + +// Warning: (ae-incompatible-release-tags) The symbol "Reducer" is marked as @public, but its signature references "TransitionDescription" which is marked as @internal +// +// @public +export type Reducer = (state: State, action: TransitionDescription) => State; + +// @public +export interface ReduxLikeStateContainer extends StateContainer { + // (undocumented) + addMiddleware: (middleware: Middleware) => void; + // Warning: (ae-incompatible-release-tags) The symbol "dispatch" is marked as @public, but its signature references "TransitionDescription" which is marked as @internal + // + // (undocumented) + dispatch: (action: TransitionDescription) => void; + // (undocumented) + getState: () => State; + // (undocumented) + reducer: Reducer; + // (undocumented) + replaceReducer: (nextReducer: Reducer) => void; + // (undocumented) + subscribe: (listener: (state: State) => void) => () => void; +} + +// @public (undocumented) +export type Selector = (...args: Args) => Result; + +// @public +export interface StateContainer extends BaseStateContainer { + // (undocumented) + selectors: Readonly>; + // Warning: (ae-incompatible-release-tags) The symbol "transitions" is marked as @public, but its signature references "PureTransitionsToTransitions" which is marked as @internal + // + // (undocumented) + transitions: Readonly>; +} + +// @internal (undocumented) +export type Transition = (...args: Args) => State; + +// @internal (undocumented) +export interface TransitionDescription { + // (undocumented) + args: Args; + // (undocumented) + type: Type; +} + +// @public +export type UnboxState> = Container extends StateContainer ? T : never; + +// @public +export const useContainerSelector: , Result>(container: Container, selector: (state: UnboxState) => Result, comparator?: Comparator) => Result; + +// @public +export const useContainerState: >(container: Container) => UnboxState; + + +``` diff --git a/src/plugins/kibana_utils/common/state_containers/create_state_container.ts b/src/plugins/kibana_utils/common/state_containers/create_state_container.ts index 69e204a642f93..6bb6e66616c91 100644 --- a/src/plugins/kibana_utils/common/state_containers/create_state_container.ts +++ b/src/plugins/kibana_utils/common/state_containers/create_state_container.ts @@ -44,29 +44,57 @@ const defaultFreeze: (value: T) => T = isProduction return value as T; }; +/** + * State container options + * @public + */ export interface CreateStateContainerOptions { /** - * Function to use when freezing state. Supply identity function + * Function to use when freezing state. Supply identity function. + * If not provided, default `deepFreeze` is used. * + * @example + * If you expect that your state will be mutated externally an you cannot + * prevent that * ```ts * { * freeze: state => state, * } * ``` - * - * if you expect that your state will be mutated externally an you cannot - * prevent that. */ freeze?: (state: T) => T; } +/** + * Creates a state container without transitions and without selectors. + * @param defaultState - initial state + * @typeParam State - shape of state + * @public + */ export function createStateContainer( defaultState: State ): ReduxLikeStateContainer; +/** + * Creates a state container with transitions, but without selectors. + * @param defaultState - initial state + * @param pureTransitions - state transitions configuration object. Map of {@link PureTransition}. + * @typeParam State - shape of state + * @public + */ export function createStateContainer( defaultState: State, pureTransitions: PureTransitions ): ReduxLikeStateContainer; + +/** + * Creates a state container with transitions and selectors. + * @param defaultState - initial state + * @param pureTransitions - state transitions configuration object. Map of {@link PureTransition}. + * @param pureSelectors - state selectors configuration object. Map of {@link PureSelectors}. + * @param options - state container options {@link CreateStateContainerOptions} + * @typeParam State - shape of state + * @public + */ export function createStateContainer< State extends BaseState, PureTransitions extends object, @@ -77,14 +105,17 @@ export function createStateContainer< pureSelectors: PureSelectors, options?: CreateStateContainerOptions ): ReduxLikeStateContainer; +/** + * @internal + */ export function createStateContainer< State extends BaseState, PureTransitions extends object, PureSelectors extends object >( defaultState: State, - pureTransitions: PureTransitions = {} as PureTransitions, - pureSelectors: PureSelectors = {} as PureSelectors, + pureTransitions: PureTransitions = {} as PureTransitions, // TODO: https://github.com/elastic/kibana/issues/54439 + pureSelectors: PureSelectors = {} as PureSelectors, // TODO: https://github.com/elastic/kibana/issues/54439 options: CreateStateContainerOptions = {} ): ReduxLikeStateContainer { const { freeze = defaultFreeze } = options; diff --git a/src/plugins/kibana_utils/common/state_containers/create_state_container_react_helpers.ts b/src/plugins/kibana_utils/common/state_containers/create_state_container_react_helpers.ts index 8536f97e00ed0..4712c2fc233f8 100644 --- a/src/plugins/kibana_utils/common/state_containers/create_state_container_react_helpers.ts +++ b/src/plugins/kibana_utils/common/state_containers/create_state_container_react_helpers.ts @@ -17,7 +17,7 @@ * under the License. */ -import * as React from 'react'; +import React from 'react'; import useObservable from 'react-use/lib/useObservable'; import defaultComparator from 'fast-deep-equal'; import { Comparator, Connect, StateContainer, UnboxState } from './types'; @@ -25,23 +25,27 @@ import { Comparator, Connect, StateContainer, UnboxState } from './types'; const { useContext, useLayoutEffect, useRef, createElement: h } = React; /** - * Returns the latest state of a state container. + * React hooks that returns the latest state of a {@link StateContainer}. * - * @param container State container which state to track. + * @param container - {@link StateContainer} which state to track. + * @returns - latest {@link StateContainer} state + * @public */ export const useContainerState = >( container: Container ): UnboxState => useObservable(container.state$, container.get()); /** - * Apply selector to state container to extract only needed information. Will + * React hook to apply selector to state container to extract only needed information. Will * re-render your component only when the section changes. * - * @param container State container which state to track. - * @param selector Function used to pick parts of state. - * @param comparator Comparator function used to memoize previous result, to not + * @param container - {@link StateContainer} which state to track. + * @param selector - Function used to pick parts of state. + * @param comparator - {@link Comparator} function used to memoize previous result, to not * re-render React component if state did not change. By default uses * `fast-deep-equal` package. + * @returns - result of a selector(state) + * @public */ export const useContainerSelector = , Result>( container: Container, @@ -68,6 +72,11 @@ export const useContainerSelector = , return value; }; +/** + * Creates helpers for using {@link StateContainer | State Containers} with react + * Refer to {@link https://github.com/elastic/kibana/blob/master/src/plugins/kibana_utils/docs/state_containers/react.md | guide} for details + * @public + */ export const createStateContainerReactHelpers = >() => { const context = React.createContext(null as any); diff --git a/src/plugins/kibana_utils/common/state_containers/index.ts b/src/plugins/kibana_utils/common/state_containers/index.ts index 43e204ecb79f7..e2e056bd67da2 100644 --- a/src/plugins/kibana_utils/common/state_containers/index.ts +++ b/src/plugins/kibana_utils/common/state_containers/index.ts @@ -17,6 +17,40 @@ * under the License. */ -export * from './types'; -export * from './create_state_container'; -export * from './create_state_container_react_helpers'; +/** + * State containers are Redux-store-like objects meant to help you manage state in your services or apps. + * Refer to {@link https://github.com/elastic/kibana/tree/master/src/plugins/kibana_utils/docs/state_containers | guides and examples} for more info + * + * @packageDocumentation + */ + +export { + BaseState, + BaseStateContainer, + TransitionDescription, + StateContainer, + ReduxLikeStateContainer, + Dispatch, + Middleware, + Selector, + Comparator, + MapStateToProps, + Connect, + Reducer, + UnboxState, + PureSelectorToSelector, + PureSelectorsToSelectors, + EnsurePureSelector, + PureTransitionsToTransitions, + PureTransitionToTransition, + EnsurePureTransition, + PureSelector, + PureTransition, + Transition, +} from './types'; +export { createStateContainer, CreateStateContainerOptions } from './create_state_container'; +export { + createStateContainerReactHelpers, + useContainerSelector, + useContainerState, +} from './create_state_container_react_helpers'; diff --git a/src/plugins/kibana_utils/common/state_containers/types.ts b/src/plugins/kibana_utils/common/state_containers/types.ts index 29ffa4cd486b5..b6adb89d9be7b 100644 --- a/src/plugins/kibana_utils/common/state_containers/types.ts +++ b/src/plugins/kibana_utils/common/state_containers/types.ts @@ -19,28 +19,76 @@ import { Observable } from 'rxjs'; import { Ensure } from '@kbn/utility-types'; +import { FC, ComponentType } from 'react'; +/** + * Base {@link StateContainer} state shape + * @public + */ export type BaseState = object; + +/** + * @internal + */ export interface TransitionDescription { type: Type; args: Args; } +/** + * @internal + */ export type Transition = (...args: Args) => State; +/** + * @internal + */ export type PureTransition = ( state: State ) => Transition; +/** + * @public + */ export type EnsurePureTransition = Ensure>; +/** + * @internal + */ export type PureTransitionToTransition> = ReturnType; +/** + * @internal + */ export type PureTransitionsToTransitions = { [K in keyof T]: PureTransitionToTransition>; }; +/** + * Base state container shape without transitions or selectors + * @typeParam State - Shape of state in the container. Have to match {@link BaseState} constraint + * @public + */ export interface BaseStateContainer { + /** + * Retrieves current state from the container + * @returns current state + * @public + */ get: () => State; + /** + * Sets state into container + * @param state - new state to set + */ set: (state: State) => void; + /** + * {@link Observable} of state + */ state$: Observable; } +/** + * Fully featured state container with {@link Selector | Selectors} and {@link Transition | Transitions}. Extends {@link BaseStateContainer}. + * @typeParam State - Shape of state in the container. Has to match {@link BaseState} constraint + * @typeParam PureTransitions - map of {@link PureTransition | transitions} to provide on state container + * @typeParam PureSelectors - map of {@link PureSelector | selectors} to provide on state container + * @public + */ export interface StateContainer< State extends BaseState, PureTransitions extends object = object, @@ -50,6 +98,11 @@ export interface StateContainer< selectors: Readonly>; } +/** + * Fully featured state container which matches Redux store interface. Extends {@link StateContainer}. + * Allows to use state container with redux libraries. + * @public + */ export interface ReduxLikeStateContainer< State extends BaseState, PureTransitions extends object = {}, @@ -63,45 +116,92 @@ export interface ReduxLikeStateContainer< subscribe: (listener: (state: State) => void) => () => void; } +/** + * Redux like dispatch + * @public + */ export type Dispatch = (action: T) => void; +/** + * Redux like Middleware + * @public + */ export type Middleware = ( store: Pick, 'getState' | 'dispatch'> ) => ( next: (action: TransitionDescription) => TransitionDescription | any ) => Dispatch; - +/** + * Redux like Reducer + * @public + */ export type Reducer = ( state: State, action: TransitionDescription ) => State; +/** + * Utility type for inferring state shape from {@link StateContainer} + * @public + */ export type UnboxState< Container extends StateContainer > = Container extends StateContainer ? T : never; +/** + * Utility type for inferring transitions type from {@link StateContainer} + * @public + */ export type UnboxTransitions< Container extends StateContainer > = Container extends StateContainer ? T : never; +/** + * @public + */ export type Selector = (...args: Args) => Result; +/** + * @public + */ export type PureSelector = ( state: State ) => Selector; +/** + * @public + */ export type EnsurePureSelector = Ensure>; +/** + * @public + */ export type PureSelectorToSelector> = ReturnType< EnsurePureSelector >; +/** + * @public + */ export type PureSelectorsToSelectors = { [K in keyof T]: PureSelectorToSelector>; }; +/** + * Used to compare state, see {@link useContainerSelector}. + * @public + */ export type Comparator = (previous: Result, current: Result) => boolean; - +/** + * State container state to component props mapper. + * See {@link Connect} + * @public + */ export type MapStateToProps = ( state: State ) => StateProps; +/** + * Similar to `connect` from react-redux, + * allows to map state from state container to component's props. + * @public + */ export type Connect = < Props extends object, StatePropKeys extends keyof Props >( mapStateToProp: MapStateToProps> -) => (component: React.ComponentType) => React.FC>; +) => (component: ComponentType) => FC>; diff --git a/src/plugins/kibana_utils/public/state_sync/index.ts b/src/plugins/kibana_utils/public/state_sync/index.ts index 1dfa998c5bb9d..da60b36255367 100644 --- a/src/plugins/kibana_utils/public/state_sync/index.ts +++ b/src/plugins/kibana_utils/public/state_sync/index.ts @@ -17,11 +17,32 @@ * under the License. */ +/** + * State syncing utilities are a set of helpers for syncing your application state + * with browser URL or browser storage. + * + * They are designed to work together with {@link https://github.com/elastic/kibana/tree/master/src/plugins/kibana_utils/docs/state_containers | state containers}. But state containers are not required. + * + * State syncing utilities include: + * + * *{@link syncState} util which: + * * Subscribes to state changes and pushes them to state storage. + * * Optionally subscribes to state storage changes and pushes them to state. + * * Two types of storages compatible with `syncState`: + * * {@link IKbnUrlStateStorage} - Serializes state and persists it to URL's query param in rison or hashed format. + * Listens for state updates in the URL and pushes them back to state. + * * {@link ISessionStorageStateStorage} - Serializes state and persists it to browser storage. + * + * Refer {@link https://github.com/elastic/kibana/tree/master/src/plugins/kibana_utils/docs/state_sync | here} for a complete guide and examples. + * @packageDocumentation + */ + export { createSessionStorageStateStorage, createKbnUrlStateStorage, IKbnUrlStateStorage, ISessionStorageStateStorage, + IStateStorage, } from './state_sync_state_storage'; export { IStateSyncConfig, INullableBaseStateContainer } from './types'; export { diff --git a/src/plugins/kibana_utils/public/state_sync/public.api.md b/src/plugins/kibana_utils/public/state_sync/public.api.md new file mode 100644 index 0000000000000..c174ba798d01a --- /dev/null +++ b/src/plugins/kibana_utils/public/state_sync/public.api.md @@ -0,0 +1,97 @@ +## API Report File for "kibana" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import { History } from 'history'; +import { Observable } from 'rxjs'; + +// @public +export const createKbnUrlStateStorage: ({ useHash, history }?: { + useHash: boolean; + history?: History | undefined; +}) => IKbnUrlStateStorage; + +// Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "kibana" does not have an export "Storage" +// +// @public +export const createSessionStorageStateStorage: (storage?: Storage) => ISessionStorageStateStorage; + +// @public +export interface IKbnUrlStateStorage extends IStateStorage { + cancel: () => void; + // (undocumented) + change$: (key: string) => Observable; + flush: (opts?: { + replace?: boolean; + }) => boolean; + // (undocumented) + get: (key: string) => State | null; + // (undocumented) + set: (key: string, state: State, opts?: { + replace: boolean; + }) => Promise; +} + +// Warning: (ae-forgotten-export) The symbol "BaseState" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "BaseStateContainer" needs to be exported by the entry point index.d.ts +// Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "kibana" does not have an export "BaseStateContainer" +// +// @public +export interface INullableBaseStateContainer extends BaseStateContainer { + // (undocumented) + set: (state: State | null) => void; +} + +// Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "kibana" does not have an export "Storage" +// +// @public +export interface ISessionStorageStateStorage extends IStateStorage { + // (undocumented) + get: (key: string) => State | null; + // (undocumented) + set: (key: string, state: State) => void; +} + +// @public +export interface IStateStorage { + cancel?: () => void; + change$?: (key: string) => Observable; + get: (key: string) => State | null; + set: (key: string, state: State) => any; +} + +// Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "kibana" does not have an export "stateSync" +// Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "kibana" does not have an export "BaseState" +// +// @public +export interface IStateSyncConfig { + // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "kibana" does not have an export "BaseStateContainer" + stateContainer: INullableBaseStateContainer; + stateStorage: StateStorage; + storageKey: string; +} + +// @public (undocumented) +export interface ISyncStateRef { + start: StartSyncStateFnType; + stop: StopSyncStateFnType; +} + +// @public (undocumented) +export type StartSyncStateFnType = () => void; + +// @public (undocumented) +export type StopSyncStateFnType = () => void; + +// @public +export function syncState({ storageKey, stateStorage, stateContainer, }: IStateSyncConfig): ISyncStateRef; + +// Warning: (ae-missing-release-tag) "syncStates" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export function syncStates(stateSyncConfigs: Array>): ISyncStateRef; + + +``` diff --git a/src/plugins/kibana_utils/public/state_sync/state_sync.ts b/src/plugins/kibana_utils/public/state_sync/state_sync.ts index 4c400d47b8e78..bbcaaedd0d8bf 100644 --- a/src/plugins/kibana_utils/public/state_sync/state_sync.ts +++ b/src/plugins/kibana_utils/public/state_sync/state_sync.ts @@ -26,29 +26,61 @@ import { distinctUntilChangedWithInitialValue } from '../../common'; import { BaseState } from '../../common/state_containers'; import { applyDiff } from '../state_management/utils/diff_object'; +/** + * @public + */ +export type StopSyncStateFnType = () => void; +/** + * @public + */ +export type StartSyncStateFnType = () => void; + +/** + * @public + */ +export interface ISyncStateRef { + /** + * stop state syncing + */ + stop: StopSyncStateFnType; + /** + * start state syncing + */ + start: StartSyncStateFnType; +} + /** * Utility for syncing application state wrapped in state container * with some kind of storage (e.g. URL) * - * Examples: + * Go {@link https://github.com/elastic/kibana/tree/master/src/plugins/kibana_utils/docs/state_sync | here} for a complete guide and examples. * - * 1. the simplest use case + * @example + * + * the simplest use case + * ```ts * const stateStorage = createKbnUrlStateStorage(); * syncState({ * storageKey: '_s', * stateContainer, * stateStorage * }); + * ``` * - * 2. conditionally configuring sync strategy + * @example + * conditionally configuring sync strategy + * ```ts * const stateStorage = createKbnUrlStateStorage({useHash: config.get('state:stateContainerInSessionStorage')}) * syncState({ * storageKey: '_s', * stateContainer, * stateStorage * }); + * ``` * - * 3. implementing custom sync strategy + * @example + * implementing custom sync strategy + * ```ts * const localStorageStateStorage = { * set: (storageKey, state) => localStorage.setItem(storageKey, JSON.stringify(state)), * get: (storageKey) => localStorage.getItem(storageKey) ? JSON.parse(localStorage.getItem(storageKey)) : null @@ -58,12 +90,15 @@ import { applyDiff } from '../state_management/utils/diff_object'; * stateContainer, * stateStorage: localStorageStateStorage * }); + * ``` * - * 4. Transform state before serialising + * @example + * transforming state before serialising * Useful for: * * Migration / backward compatibility * * Syncing part of state * * Providing default values + * ```ts * const stateToStorage = (s) => ({ tab: s.tab }); * syncState({ * storageKey: '_s', @@ -74,20 +109,12 @@ import { applyDiff } from '../state_management/utils/diff_object'; * }, * stateStorage * }); + * ``` * - * Caveats: - * - * 1. It is responsibility of consumer to make sure that initial app state and storage are in sync before starting syncing - * No initial sync happens when syncState() is called + * @param - syncing config {@link IStateSyncConfig} + * @returns - {@link ISyncStateRef} + * @public */ -export type StopSyncStateFnType = () => void; -export type StartSyncStateFnType = () => void; -export interface ISyncStateRef { - // stop syncing state with storage - stop: StopSyncStateFnType; - // start syncing state with storage - start: StartSyncStateFnType; -} export function syncState< State extends BaseState, StateStorage extends IStateStorage = IStateStorage @@ -159,7 +186,9 @@ export function syncState< } /** - * multiple different sync configs + * @example + * sync multiple different sync configs + * ```ts * syncStates([ * { * storageKey: '_s1', @@ -172,7 +201,8 @@ export function syncState< * stateContainer: stateContainer2, * }, * ]); - * @param stateSyncConfigs - Array of IStateSyncConfig to sync + * ``` + * @param stateSyncConfigs - Array of {@link IStateSyncConfig} to sync */ export function syncStates(stateSyncConfigs: Array>): ISyncStateRef { const syncRefs = stateSyncConfigs.map((config) => syncState(config)); diff --git a/src/plugins/kibana_utils/public/state_sync/state_sync_state_storage/create_kbn_url_state_storage.ts b/src/plugins/kibana_utils/public/state_sync/state_sync_state_storage/create_kbn_url_state_storage.ts index 67c1bf26aa251..0c74e1eb9f421 100644 --- a/src/plugins/kibana_utils/public/state_sync/state_sync_state_storage/create_kbn_url_state_storage.ts +++ b/src/plugins/kibana_utils/public/state_sync/state_sync_state_storage/create_kbn_url_state_storage.ts @@ -27,6 +27,19 @@ import { setStateToKbnUrl, } from '../../state_management/url'; +/** + * KbnUrlStateStorage is a state storage for {@link syncState} utility which: + * + * 1. Keeps state in sync with the URL. + * 2. Serializes data and stores it in the URL in one of the supported formats: + * * Rison encoded. + * * Hashed URL: In URL we store only the hash from the serialized state, but the state itself is stored in sessionStorage. See Kibana's `state:storeInSessionStorage` advanced option for more context. + * 3. Takes care of listening to the URL updates and notifies state about the updates. + * 4. Takes care of batching URL updates to prevent redundant browser history records. + * + * {@link https://github.com/elastic/kibana/blob/master/src/plugins/kibana_utils/docs/state_sync/storages/kbn_url_storage.md | Refer to this guide for more info} + * @public + */ export interface IKbnUrlStateStorage extends IStateStorage { set: ( key: string, @@ -36,18 +49,23 @@ export interface IKbnUrlStateStorage extends IStateStorage { get: (key: string) => State | null; change$: (key: string) => Observable; - // cancels any pending url updates + /** + * cancels any pending url updates + */ cancel: () => void; - // synchronously runs any pending url updates - // returned boolean indicates if change occurred + /** + * Synchronously runs any pending url updates, returned boolean indicates if change occurred. + * @param opts: {replace? boolean} - allows to specify if push or replace should be used for flushing update + * @returns boolean - indicates if there was an update to flush + */ flush: (opts?: { replace?: boolean }) => boolean; } /** - * Implements syncing to/from url strategies. - * Replicates what was implemented in state (AppState, GlobalState) - * Both expanded and hashed use cases + * Creates {@link IKbnUrlStateStorage} state storage + * @returns - {@link IKbnUrlStateStorage} + * @public */ export const createKbnUrlStateStorage = ( { useHash = false, history }: { useHash: boolean; history?: History } = { useHash: false } diff --git a/src/plugins/kibana_utils/public/state_sync/state_sync_state_storage/create_session_storage_state_storage.ts b/src/plugins/kibana_utils/public/state_sync/state_sync_state_storage/create_session_storage_state_storage.ts index 00edfdfd1ed61..60ff211cd590a 100644 --- a/src/plugins/kibana_utils/public/state_sync/state_sync_state_storage/create_session_storage_state_storage.ts +++ b/src/plugins/kibana_utils/public/state_sync/state_sync_state_storage/create_session_storage_state_storage.ts @@ -19,11 +19,23 @@ import { IStateStorage } from './types'; +/** + * {@link IStateStorage} for storing state in browser {@link Storage} + * {@link https://github.com/elastic/kibana/blob/master/src/plugins/kibana_utils/docs/state_sync/storages/session_storage.md | guide} + * @public + */ export interface ISessionStorageStateStorage extends IStateStorage { set: (key: string, state: State) => void; get: (key: string) => State | null; } +/** + * Creates {@link ISessionStorageStateStorage} + * {@link https://github.com/elastic/kibana/blob/master/src/plugins/kibana_utils/docs/state_sync/storages/session_storage.md | guide} + * @param storage - Option {@link Storage} to use for storing state. By default window.sessionStorage. + * @returns - {@link ISessionStorageStateStorage} + * @public + */ export const createSessionStorageStateStorage = ( storage: Storage = window.sessionStorage ): ISessionStorageStateStorage => { diff --git a/src/plugins/kibana_utils/public/state_sync/state_sync_state_storage/types.ts b/src/plugins/kibana_utils/public/state_sync/state_sync_state_storage/types.ts index add1dc259be45..bae5dc2067183 100644 --- a/src/plugins/kibana_utils/public/state_sync/state_sync_state_storage/types.ts +++ b/src/plugins/kibana_utils/public/state_sync/state_sync_state_storage/types.ts @@ -25,7 +25,8 @@ import { Observable } from 'rxjs'; * * state serialisation / deserialization * * persisting to and retrieving from storage * - * For an example take a look at already implemented KbnUrl state storage + * For an example take a look at already implemented {@link IKbnUrlStateStorage} and {@link ISessionStorageStateStorage} state storages + * @public */ export interface IStateStorage { /** @@ -45,7 +46,7 @@ export interface IStateStorage { /** * Optional method to cancel any pending activity - * syncState() will call it, if it is provided by IStateStorage + * {@link syncState} will call it during destroy, if it is provided by IStateStorage */ cancel?: () => void; } diff --git a/src/plugins/kibana_utils/public/state_sync/types.ts b/src/plugins/kibana_utils/public/state_sync/types.ts index 2acb466d92e92..e879ab7c55b2f 100644 --- a/src/plugins/kibana_utils/public/state_sync/types.ts +++ b/src/plugins/kibana_utils/public/state_sync/types.ts @@ -20,15 +20,26 @@ import { BaseState, BaseStateContainer } from '../../common/state_containers/types'; import { IStateStorage } from './state_sync_state_storage'; +/** + * Extension of {@link BaseStateContainer} with one constraint: set state should handle `null` as incoming state + * @remarks + * State container for `stateSync()` have to accept `null` + * for example, `set()` implementation could handle null and fallback to some default state + * this is required to handle edge case, when state in storage becomes empty and syncing is in progress. + * State container will be notified about about storage becoming empty with null passed in. + * @public + */ export interface INullableBaseStateContainer extends BaseStateContainer { - // State container for stateSync() have to accept "null" - // for example, set() implementation could handle null and fallback to some default state - // this is required to handle edge case, when state in storage becomes empty and syncing is in progress. - // state container will be notified about about storage becoming empty with null passed in set: (state: State | null) => void; } +/** + * Config for setting up state syncing with {@link stateSync} + * @typeParam State - State shape to sync to storage, has to extend {@link BaseState} + * @typeParam StateStorage - used state storage to sync state with + * @public + */ export interface IStateSyncConfig< State extends BaseState, StateStorage extends IStateStorage = IStateStorage @@ -39,8 +50,8 @@ export interface IStateSyncConfig< */ storageKey: string; /** - * State container to keep in sync with storage, have to implement INullableBaseStateContainer interface - * The idea is that ./state_containers/ should be used as a state container, + * State container to keep in sync with storage, have to implement {@link INullableBaseStateContainer} interface + * We encourage to use {@link BaseStateContainer} as a state container, * but it is also possible to implement own custom container for advanced use cases */ stateContainer: INullableBaseStateContainer; @@ -49,7 +60,7 @@ export interface IStateSyncConfig< * State storage is responsible for serialising / deserialising and persisting / retrieving stored state * * There are common strategies already implemented: - * './state_sync_state_storage/' + * see {@link IKbnUrlStateStorage} * which replicate what State (AppState, GlobalState) in legacy world did * */ From 684aa68f17ce9722c8441166b025c4f96dd2a4ad Mon Sep 17 00:00:00 2001 From: Vadim Dalecky Date: Fri, 26 Jun 2020 14:26:35 +0200 Subject: [PATCH 06/13] "Explore underlying data" in-chart action (#69494) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: 💡 rename folder to "explore_data" * style: 💄 check for "share" plugin in more semantic way "explore data" actions use Discover URL generator, which is registered in "share" plugin, which is optional plugin, so we check for its existance, because otherwise URL generator is not available. * refactor: 💡 move KibanaURL to a separate file * feat: 🎸 add "Explore underlying data" in-chart action * fix: 🐛 fix imports after refactor * feat: 🎸 add start.filtersFromContext to embeddable plugin * feat: 🎸 add type checkers to data plugin * feat: 🎸 better handle empty filters in Discover URL generator * feat: 🎸 implement .getUrl() method of explore data in-chart act * feat: 🎸 add embeddable.filtersAndTimeRangeFromContext() * feat: 🎸 improve getUrl() method of explore data action * test: 💍 update test mock * fix possible stale hashHistory.location in discover * style: 💄 ensureHashHistoryLocation -> syncHistoryLocations * docs: ✏️ update autogenerated docs * test: 💍 add in-chart "Explore underlying data" unit tests * test: 💍 add in-chart "Explore underlying data" functional tests * test: 💍 clean-up custom time range after panel action tests * chore: 🤖 fix embeddable plugin mocks * chore: 🤖 fix another mock * test: 💍 add support for new action to pie chart service Co-authored-by: Anton Dosov --- ...ana-plugin-plugins-data-public.isfilter.md | 11 + ...na-plugin-plugins-data-public.isfilters.md | 11 + ...bana-plugin-plugins-data-public.isquery.md | 11 + ...-plugin-plugins-data-public.istimerange.md | 11 + .../kibana-plugin-plugins-data-public.md | 4 + .../common/es_query/filters/meta_filter.ts | 10 + src/plugins/data/common/index.ts | 3 +- src/plugins/data/common/query/index.ts | 1 + src/plugins/data/common/query/is_query.ts | 27 ++ src/plugins/data/common/timefilter/index.ts | 20 ++ .../data/common/timefilter/is_time_range.ts | 26 ++ src/plugins/data/public/index.ts | 2 + src/plugins/data/public/public.api.md | 20 ++ src/plugins/discover/public/index.ts | 2 +- .../discover/public/kibana_services.ts | 15 +- src/plugins/discover/public/plugin.ts | 2 + src/plugins/discover/public/url_generator.ts | 6 +- src/plugins/embeddable/kibana.json | 1 + src/plugins/embeddable/public/index.ts | 1 + .../public/lib/triggers/triggers.ts | 14 +- src/plugins/embeddable/public/mocks.tsx | 5 + src/plugins/embeddable/public/plugin.tsx | 64 +++- .../embeddable/public/tests/test_plugin.ts | 9 +- .../services/dashboard/panel_actions.ts | 14 + .../services/visualizations/pie_chart.ts | 17 +- .../abstract_explore_data_action.ts | 74 +++++ .../explore_data_chart_action.test.ts | 274 ++++++++++++++++++ .../explore_data/explore_data_chart_action.ts | 65 +++++ .../explore_data_context_menu_action.test.ts | 28 +- .../explore_data_context_menu_action.ts | 54 ++++ .../index.ts | 1 + .../public/actions/explore_data/kibana_url.ts | 31 ++ .../public/actions/explore_data/shared.ts | 37 +++ .../discover_enhanced/public/actions/index.ts | 2 +- .../explore_data_context_menu_action.ts | 156 ---------- .../discover_enhanced/public/plugin.ts | 27 +- .../drilldowns/dashboard_drilldowns.ts | 2 +- .../drilldowns/explore_data_chart_action.ts | 98 +++++++ .../drilldowns/explore_data_panel_action.ts | 31 +- .../apps/dashboard/drilldowns/index.ts | 1 + .../services/dashboard/panel_time_range.ts | 6 + 41 files changed, 997 insertions(+), 197 deletions(-) create mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isfilter.md create mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isfilters.md create mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isquery.md create mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.istimerange.md create mode 100644 src/plugins/data/common/query/is_query.ts create mode 100644 src/plugins/data/common/timefilter/index.ts create mode 100644 src/plugins/data/common/timefilter/is_time_range.ts create mode 100644 x-pack/plugins/discover_enhanced/public/actions/explore_data/abstract_explore_data_action.ts create mode 100644 x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_chart_action.test.ts create mode 100644 x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_chart_action.ts rename x-pack/plugins/discover_enhanced/public/actions/{view_in_discover => explore_data}/explore_data_context_menu_action.test.ts (88%) create mode 100644 x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_context_menu_action.ts rename x-pack/plugins/discover_enhanced/public/actions/{view_in_discover => explore_data}/index.ts (86%) create mode 100644 x-pack/plugins/discover_enhanced/public/actions/explore_data/kibana_url.ts create mode 100644 x-pack/plugins/discover_enhanced/public/actions/explore_data/shared.ts delete mode 100644 x-pack/plugins/discover_enhanced/public/actions/view_in_discover/explore_data_context_menu_action.ts create mode 100644 x-pack/test/functional/apps/dashboard/drilldowns/explore_data_chart_action.ts diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isfilter.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isfilter.md new file mode 100644 index 0000000000000..f1916e89c2c98 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isfilter.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [isFilter](./kibana-plugin-plugins-data-public.isfilter.md) + +## isFilter variable + +Signature: + +```typescript +isFilter: (x: unknown) => x is Filter +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isfilters.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isfilters.md new file mode 100644 index 0000000000000..558da72cc26bb --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isfilters.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [isFilters](./kibana-plugin-plugins-data-public.isfilters.md) + +## isFilters variable + +Signature: + +```typescript +isFilters: (x: unknown) => x is Filter[] +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isquery.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isquery.md new file mode 100644 index 0000000000000..0884566333aa8 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isquery.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [isQuery](./kibana-plugin-plugins-data-public.isquery.md) + +## isQuery variable + +Signature: + +```typescript +isQuery: (x: unknown) => x is Query +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.istimerange.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.istimerange.md new file mode 100644 index 0000000000000..e9420493c82fb --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.istimerange.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [isTimeRange](./kibana-plugin-plugins-data-public.istimerange.md) + +## isTimeRange variable + +Signature: + +```typescript +isTimeRange: (x: unknown) => x is TimeRange +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md index f62479f02926e..feeb686a1f5ed 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md @@ -110,6 +110,10 @@ | [getKbnTypeNames](./kibana-plugin-plugins-data-public.getkbntypenames.md) | Get the esTypes known by all kbnFieldTypes {Array} | | [indexPatterns](./kibana-plugin-plugins-data-public.indexpatterns.md) | | | [injectSearchSourceReferences](./kibana-plugin-plugins-data-public.injectsearchsourcereferences.md) | | +| [isFilter](./kibana-plugin-plugins-data-public.isfilter.md) | | +| [isFilters](./kibana-plugin-plugins-data-public.isfilters.md) | | +| [isQuery](./kibana-plugin-plugins-data-public.isquery.md) | | +| [isTimeRange](./kibana-plugin-plugins-data-public.istimerange.md) | | | [parseSearchSourceJSON](./kibana-plugin-plugins-data-public.parsesearchsourcejson.md) | | | [QueryStringInput](./kibana-plugin-plugins-data-public.querystringinput.md) | | | [search](./kibana-plugin-plugins-data-public.search.md) | | diff --git a/src/plugins/data/common/es_query/filters/meta_filter.ts b/src/plugins/data/common/es_query/filters/meta_filter.ts index ff6dff9d8b749..e3099ae6a4026 100644 --- a/src/plugins/data/common/es_query/filters/meta_filter.ts +++ b/src/plugins/data/common/es_query/filters/meta_filter.ts @@ -107,3 +107,13 @@ export const pinFilter = (filter: Filter) => export const unpinFilter = (filter: Filter) => !isFilterPinned(filter) ? filter : toggleFilterPinned(filter); + +export const isFilter = (x: unknown): x is Filter => + !!x && + typeof x === 'object' && + !!(x as Filter).meta && + typeof (x as Filter).meta === 'object' && + typeof (x as Filter).meta.disabled === 'boolean'; + +export const isFilters = (x: unknown): x is Filter[] => + Array.isArray(x) && !x.find((y) => !isFilter(y)); diff --git a/src/plugins/data/common/index.ts b/src/plugins/data/common/index.ts index adbd93d518fc7..b40e02b709d30 100644 --- a/src/plugins/data/common/index.ts +++ b/src/plugins/data/common/index.ts @@ -20,11 +20,12 @@ export * from './constants'; export * from './es_query'; export * from './field_formats'; +export * from './field_mapping'; export * from './index_patterns'; export * from './kbn_field_types'; export * from './query'; export * from './search'; export * from './search/aggs'; +export * from './timefilter'; export * from './types'; export * from './utils'; -export * from './field_mapping'; diff --git a/src/plugins/data/common/query/index.ts b/src/plugins/data/common/query/index.ts index 421cc4f63e4ef..4e90f6f8bb83e 100644 --- a/src/plugins/data/common/query/index.ts +++ b/src/plugins/data/common/query/index.ts @@ -19,3 +19,4 @@ export * from './filter_manager'; export * from './types'; +export * from './is_query'; diff --git a/src/plugins/data/common/query/is_query.ts b/src/plugins/data/common/query/is_query.ts new file mode 100644 index 0000000000000..08a99a39b1ac1 --- /dev/null +++ b/src/plugins/data/common/query/is_query.ts @@ -0,0 +1,27 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Query } from './types'; + +export const isQuery = (x: unknown): x is Query => + !!x && + typeof x === 'object' && + typeof (x as Query).language === 'string' && + (typeof (x as Query).query === 'string' || + (typeof (x as Query).query === 'object' && !!(x as Query).query)); diff --git a/src/plugins/data/common/timefilter/index.ts b/src/plugins/data/common/timefilter/index.ts new file mode 100644 index 0000000000000..e0c509e119fda --- /dev/null +++ b/src/plugins/data/common/timefilter/index.ts @@ -0,0 +1,20 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export { isTimeRange } from './is_time_range'; diff --git a/src/plugins/data/common/timefilter/is_time_range.ts b/src/plugins/data/common/timefilter/is_time_range.ts new file mode 100644 index 0000000000000..f206cd04dde31 --- /dev/null +++ b/src/plugins/data/common/timefilter/is_time_range.ts @@ -0,0 +1,26 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { TimeRange } from './types'; + +export const isTimeRange = (x: unknown): x is TimeRange => + !!x && + typeof x === 'object' && + typeof (x as TimeRange).from === 'string' && + typeof (x as TimeRange).to === 'string'; diff --git a/src/plugins/data/public/index.ts b/src/plugins/data/public/index.ts index 3665d9dc2b46e..efce8d2c021c9 100644 --- a/src/plugins/data/public/index.ts +++ b/src/plugins/data/public/index.ts @@ -440,6 +440,8 @@ export { getKbnTypeNames, } from '../common'; +export { isTimeRange, isQuery, isFilter, isFilters } from '../common'; + export * from '../common/field_mapping'; /* diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md index 25c9b0718050a..b12ad94017fbb 100644 --- a/src/plugins/data/public/public.api.md +++ b/src/plugins/data/public/public.api.md @@ -1291,6 +1291,26 @@ export interface ISearchStrategy { search: ISearch; } +// Warning: (ae-missing-release-tag) "isFilter" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export const isFilter: (x: unknown) => x is Filter; + +// Warning: (ae-missing-release-tag) "isFilters" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export const isFilters: (x: unknown) => x is Filter[]; + +// Warning: (ae-missing-release-tag) "isQuery" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export const isQuery: (x: unknown) => x is Query; + +// Warning: (ae-missing-release-tag) "isTimeRange" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export const isTimeRange: (x: unknown) => x is TimeRange; + // Warning: (ae-missing-release-tag) "ISyncSearchRequest" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) diff --git a/src/plugins/discover/public/index.ts b/src/plugins/discover/public/index.ts index 4154fdfeb3ff4..6ac8f674b6153 100644 --- a/src/plugins/discover/public/index.ts +++ b/src/plugins/discover/public/index.ts @@ -27,4 +27,4 @@ export function plugin(initializerContext: PluginInitializerContext) { export { SavedSearch, SavedSearchLoader, createSavedSearchesLoader } from './saved_searches'; export { ISearchEmbeddable, SEARCH_EMBEDDABLE_TYPE, SearchInput } from './application/embeddable'; -export { DISCOVER_APP_URL_GENERATOR } from './url_generator'; +export { DISCOVER_APP_URL_GENERATOR, DiscoverUrlGeneratorState } from './url_generator'; diff --git a/src/plugins/discover/public/kibana_services.ts b/src/plugins/discover/public/kibana_services.ts index cca63cd880b60..2c6bbcc3ecce1 100644 --- a/src/plugins/discover/public/kibana_services.ts +++ b/src/plugins/discover/public/kibana_services.ts @@ -60,10 +60,23 @@ export const [getDocViewsRegistry, setDocViewsRegistry] = createGetterSetter createHashHistory()); +/** + * Discover currently uses two `history` instances: one from Kibana Platform and + * another from `history` package. Below function is used every time Discover + * app is loaded to synchronize both instances. + * + * This helper is temporary until https://github.com/elastic/kibana/issues/65161 is resolved. + */ +export const syncHistoryLocations = () => { + const h = getHistory(); + Object.assign(h.location, createHashHistory().location); + return h; +}; + export const [getScopedHistory, setScopedHistory] = createGetterSetter( 'scopedHistory' ); diff --git a/src/plugins/discover/public/plugin.ts b/src/plugins/discover/public/plugin.ts index ba97efa55068d..e97ac783c616f 100644 --- a/src/plugins/discover/public/plugin.ts +++ b/src/plugins/discover/public/plugin.ts @@ -55,6 +55,7 @@ import { setServices, setScopedHistory, getScopedHistory, + syncHistoryLocations, getServices, } from './kibana_services'; import { createSavedSearchesLoader } from './saved_searches'; @@ -245,6 +246,7 @@ export class DiscoverPlugin throw Error('Discover plugin method initializeInnerAngular is undefined'); } setScopedHistory(params.history); + syncHistoryLocations(); appMounted(); const { plugins: { data: dataStart }, diff --git a/src/plugins/discover/public/url_generator.ts b/src/plugins/discover/public/url_generator.ts index 42d689050d5ad..c7f2e2147e819 100644 --- a/src/plugins/discover/public/url_generator.ts +++ b/src/plugins/discover/public/url_generator.ts @@ -98,11 +98,13 @@ export class DiscoverUrlGenerator const queryState: QueryState = {}; if (query) appState.query = query; - if (filters) appState.filters = filters?.filter((f) => !esFilters.isFilterPinned(f)); + if (filters && filters.length) + appState.filters = filters?.filter((f) => !esFilters.isFilterPinned(f)); if (indexPatternId) appState.index = indexPatternId; if (timeRange) queryState.time = timeRange; - if (filters) queryState.filters = filters?.filter((f) => esFilters.isFilterPinned(f)); + if (filters && filters.length) + queryState.filters = filters?.filter((f) => esFilters.isFilterPinned(f)); if (refreshInterval) queryState.refreshInterval = refreshInterval; let url = `${this.params.appBasePath}#/${savedSearchPath}`; diff --git a/src/plugins/embeddable/kibana.json b/src/plugins/embeddable/kibana.json index 06b0e88da334f..332237d19e218 100644 --- a/src/plugins/embeddable/kibana.json +++ b/src/plugins/embeddable/kibana.json @@ -4,6 +4,7 @@ "server": false, "ui": true, "requiredPlugins": [ + "data", "inspector", "uiActions" ], diff --git a/src/plugins/embeddable/public/index.ts b/src/plugins/embeddable/public/index.ts index 35fbfe2e0aa38..f19974942c43d 100644 --- a/src/plugins/embeddable/public/index.ts +++ b/src/plugins/embeddable/public/index.ts @@ -28,6 +28,7 @@ export { ACTION_EDIT_PANEL, Adapters, AddPanelAction, + ChartActionContext, Container, ContainerInput, ContainerOutput, diff --git a/src/plugins/embeddable/public/lib/triggers/triggers.ts b/src/plugins/embeddable/public/lib/triggers/triggers.ts index 2b447c89e2850..5bb96a708b7ac 100644 --- a/src/plugins/embeddable/public/lib/triggers/triggers.ts +++ b/src/plugins/embeddable/public/lib/triggers/triggers.ts @@ -39,10 +39,6 @@ export interface ValueClickTriggerContext { }; } -export const isValueClickTriggerContext = ( - context: ValueClickTriggerContext | RangeSelectTriggerContext -): context is ValueClickTriggerContext => context.data && 'data' in context.data; - export interface RangeSelectTriggerContext { embeddable?: T; data: { @@ -53,8 +49,16 @@ export interface RangeSelectTriggerContext }; } +export type ChartActionContext = + | ValueClickTriggerContext + | RangeSelectTriggerContext; + +export const isValueClickTriggerContext = ( + context: ChartActionContext +): context is ValueClickTriggerContext => context.data && 'data' in context.data; + export const isRangeSelectTriggerContext = ( - context: ValueClickTriggerContext | RangeSelectTriggerContext + context: ChartActionContext ): context is RangeSelectTriggerContext => context.data && 'range' in context.data; export const CONTEXT_MENU_TRIGGER = 'CONTEXT_MENU_TRIGGER'; diff --git a/src/plugins/embeddable/public/mocks.tsx b/src/plugins/embeddable/public/mocks.tsx index 6d94af1f22829..efd0ccdc4553d 100644 --- a/src/plugins/embeddable/public/mocks.tsx +++ b/src/plugins/embeddable/public/mocks.tsx @@ -31,6 +31,7 @@ import { coreMock } from '../../../core/public/mocks'; import { UiActionsService } from './lib/ui_actions'; import { CoreStart } from '../../../core/public'; import { Start as InspectorStart } from '../../inspector/public'; +import { dataPluginMock } from '../../data/public/mocks'; // eslint-disable-next-line import { inspectorPluginMock } from '../../inspector/public/mocks'; @@ -100,6 +101,8 @@ const createStartContract = (): Start => { EmbeddablePanel: jest.fn(), getEmbeddablePanel: jest.fn(), getStateTransfer: jest.fn(() => createEmbeddableStateTransferMock() as EmbeddableStateTransfer), + filtersAndTimeRangeFromContext: jest.fn(), + filtersFromContext: jest.fn(), }; return startContract; }; @@ -108,11 +111,13 @@ const createInstance = (setupPlugins: Partial = {}) const plugin = new EmbeddablePublicPlugin({} as any); const setup = plugin.setup(coreMock.createSetup(), { uiActions: setupPlugins.uiActions || uiActionsPluginMock.createSetupContract(), + data: dataPluginMock.createSetupContract(), }); const doStart = (startPlugins: Partial = {}) => plugin.start(coreMock.createStart(), { uiActions: startPlugins.uiActions || uiActionsPluginMock.createStartContract(), inspector: inspectorPluginMock.createStartContract(), + data: dataPluginMock.createStartContract(), }); return { plugin, diff --git a/src/plugins/embeddable/public/plugin.tsx b/src/plugins/embeddable/public/plugin.tsx index c4e0ca44a4e7e..03bb4a4779267 100644 --- a/src/plugins/embeddable/public/plugin.tsx +++ b/src/plugins/embeddable/public/plugin.tsx @@ -17,6 +17,13 @@ * under the License. */ import React from 'react'; +import { + DataPublicPluginSetup, + DataPublicPluginStart, + Filter, + TimeRange, + esFilters, +} from '../../data/public'; import { getSavedObjectFinder } from '../../saved_objects/public'; import { UiActionsSetup, UiActionsStart } from '../../ui_actions/public'; import { Start as InspectorStart } from '../../inspector/public'; @@ -36,15 +43,20 @@ import { defaultEmbeddableFactoryProvider, IEmbeddable, EmbeddablePanel, + ChartActionContext, + isRangeSelectTriggerContext, + isValueClickTriggerContext, } from './lib'; import { EmbeddableFactoryDefinition } from './lib/embeddables/embeddable_factory_definition'; import { EmbeddableStateTransfer } from './lib/state_transfer'; export interface EmbeddableSetupDependencies { + data: DataPublicPluginSetup; uiActions: UiActionsSetup; } export interface EmbeddableStartDependencies { + data: DataPublicPluginStart; uiActions: UiActionsStart; inspector: InspectorStart; } @@ -70,6 +82,19 @@ export interface EmbeddableStart { embeddableFactoryId: string ) => EmbeddableFactory | undefined; getEmbeddableFactories: () => IterableIterator; + + /** + * Given {@link ChartActionContext} returns a list of `data` plugin {@link Filter} entries. + */ + filtersFromContext: (context: ChartActionContext) => Promise; + + /** + * Returns possible time range and filters that can be constructed from {@link ChartActionContext} object. + */ + filtersAndTimeRangeFromContext: ( + context: ChartActionContext + ) => Promise<{ filters: Filter[]; timeRange?: TimeRange }>; + EmbeddablePanel: EmbeddablePanelHOC; getEmbeddablePanel: (stateTransfer?: EmbeddableStateTransfer) => EmbeddablePanelHOC; getStateTransfer: (history?: ScopedHistory) => EmbeddableStateTransfer; @@ -107,7 +132,7 @@ export class EmbeddablePublicPlugin implements Plugin { this.embeddableFactories.set( @@ -121,6 +146,41 @@ export class EmbeddablePublicPlugin implements Plugin { + try { + if (isRangeSelectTriggerContext(context)) + return await data.actions.createFiltersFromRangeSelectAction(context.data); + if (isValueClickTriggerContext(context)) + return await data.actions.createFiltersFromValueClickAction(context.data); + // eslint-disable-next-line no-console + console.warn("Can't extract filters from action.", context); + } catch (error) { + // eslint-disable-next-line no-console + console.warn('Error extracting filters from action. Returning empty filter list.', error); + } + return []; + }; + + const filtersAndTimeRangeFromContext: EmbeddableStart['filtersAndTimeRangeFromContext'] = async ( + context + ) => { + const filters = await filtersFromContext(context); + + if (!context.data.timeFieldName) return { filters }; + + const { timeRangeFilter, restOfFilters } = esFilters.extractTimeFilter( + context.data.timeFieldName, + filters + ); + + return { + filters: restOfFilters, + timeRange: timeRangeFilter + ? esFilters.convertRangeFilterToTimeRangeString(timeRangeFilter) + : undefined, + }; + }; + const getEmbeddablePanelHoc = (stateTransfer?: EmbeddableStateTransfer) => ({ embeddable, hideHeader, @@ -146,6 +206,8 @@ export class EmbeddablePublicPlugin implements Plugin { return history ? new EmbeddableStateTransfer(core.application.navigateToApp, history) diff --git a/src/plugins/embeddable/public/tests/test_plugin.ts b/src/plugins/embeddable/public/tests/test_plugin.ts index e13a906e30338..bb12e3d7b9011 100644 --- a/src/plugins/embeddable/public/tests/test_plugin.ts +++ b/src/plugins/embeddable/public/tests/test_plugin.ts @@ -23,6 +23,7 @@ import { UiActionsStart } from '../../../ui_actions/public'; import { uiActionsPluginMock } from '../../../ui_actions/public/mocks'; // eslint-disable-next-line import { inspectorPluginMock } from '../../../inspector/public/mocks'; +import { dataPluginMock } from '../../../data/public/mocks'; import { coreMock } from '../../../../core/public/mocks'; import { EmbeddablePublicPlugin, EmbeddableSetup, EmbeddableStart } from '../plugin'; @@ -42,7 +43,10 @@ export const testPlugin = ( const uiActions = uiActionsPluginMock.createPlugin(coreSetup, coreStart); const initializerContext = {} as any; const plugin = new EmbeddablePublicPlugin(initializerContext); - const setup = plugin.setup(coreSetup, { uiActions: uiActions.setup }); + const setup = plugin.setup(coreSetup, { + data: dataPluginMock.createSetupContract(), + uiActions: uiActions.setup, + }); return { plugin, @@ -51,8 +55,9 @@ export const testPlugin = ( setup, doStart: (anotherCoreStart: CoreStart = coreStart) => { const start = plugin.start(anotherCoreStart, { - uiActions: uiActionsPluginMock.createStartContract(), + data: dataPluginMock.createStartContract(), inspector: inspectorPluginMock.createStartContract(), + uiActions: uiActionsPluginMock.createStartContract(), }); return start; }, diff --git a/test/functional/services/dashboard/panel_actions.ts b/test/functional/services/dashboard/panel_actions.ts index c9a5dcfba32b1..0f5d6ea74a6b6 100644 --- a/test/functional/services/dashboard/panel_actions.ts +++ b/test/functional/services/dashboard/panel_actions.ts @@ -213,5 +213,19 @@ export function DashboardPanelActionsProvider({ getService, getPageObjects }: Ft await testSubjects.click('saveNewTitleButton'); await this.toggleContextMenu(panel); } + + async getActionWebElementByText(text: string): Promise { + log.debug(`getActionWebElement: "${text}"`); + const menu = await testSubjects.find('multipleActionsContextMenu'); + const items = await menu.findAllByCssSelector('[data-test-subj*="embeddablePanelAction-"]'); + for (const item of items) { + const currentText = await item.getVisibleText(); + if (currentText === text) { + return item; + } + } + + throw new Error(`No action matching text "${text}"`); + } })(); } diff --git a/test/functional/services/visualizations/pie_chart.ts b/test/functional/services/visualizations/pie_chart.ts index 66f32d246b31f..a25695a5bfcb7 100644 --- a/test/functional/services/visualizations/pie_chart.ts +++ b/test/functional/services/visualizations/pie_chart.ts @@ -28,10 +28,13 @@ export function PieChartProvider({ getService }: FtrProviderContext) { const testSubjects = getService('testSubjects'); const find = getService('find'); const defaultFindTimeout = config.get('timeouts.find'); + const panelActions = getService('dashboardPanelActions'); return new (class PieChart { - async filterOnPieSlice(name?: string) { - log.debug(`PieChart.filterOnPieSlice(${name})`); + private readonly filterActionText = 'Apply filter to current view'; + + async clickOnPieSlice(name?: string) { + log.debug(`PieChart.clickOnPieSlice(${name})`); if (name) { await testSubjects.click(`pieSlice-${name.split(' ').join('-')}`); } else { @@ -44,6 +47,16 @@ export function PieChartProvider({ getService }: FtrProviderContext) { } } + async filterOnPieSlice(name?: string) { + log.debug(`PieChart.filterOnPieSlice(${name})`); + await this.clickOnPieSlice(name); + const hasUiActionsPopup = await testSubjects.exists('multipleActionsContextMenu'); + if (hasUiActionsPopup) { + const actionElement = await panelActions.getActionWebElementByText(this.filterActionText); + await actionElement.click(); + } + } + async filterByLegendItem(label: string) { log.debug(`PieChart.filterByLegendItem(${label})`); await testSubjects.click(`legend-${label}`); diff --git a/x-pack/plugins/discover_enhanced/public/actions/explore_data/abstract_explore_data_action.ts b/x-pack/plugins/discover_enhanced/public/actions/explore_data/abstract_explore_data_action.ts new file mode 100644 index 0000000000000..620cabe652778 --- /dev/null +++ b/x-pack/plugins/discover_enhanced/public/actions/explore_data/abstract_explore_data_action.ts @@ -0,0 +1,74 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; +import { DiscoverStart } from '../../../../../../src/plugins/discover/public'; +import { EmbeddableStart } from '../../../../../../src/plugins/embeddable/public'; +import { ViewMode, IEmbeddable } from '../../../../../../src/plugins/embeddable/public'; +import { StartServicesGetter } from '../../../../../../src/plugins/kibana_utils/public'; +import { CoreStart } from '../../../../../../src/core/public'; +import { KibanaURL } from './kibana_url'; +import * as shared from './shared'; + +export const ACTION_EXPLORE_DATA = 'ACTION_EXPLORE_DATA'; + +export interface PluginDeps { + discover: Pick; + embeddable: Pick; +} + +export interface CoreDeps { + application: Pick; +} + +export interface Params { + start: StartServicesGetter; +} + +export abstract class AbstractExploreDataAction { + public readonly getIconType = (context: Context): string => 'discoverApp'; + + public readonly getDisplayName = (context: Context): string => + i18n.translate('xpack.discover.FlyoutCreateDrilldownAction.displayName', { + defaultMessage: 'Explore underlying data', + }); + + constructor(protected readonly params: Params) {} + + protected abstract async getUrl(context: Context): Promise; + + public async isCompatible({ embeddable }: Context): Promise { + if (!embeddable) return false; + if (!this.params.start().plugins.discover.urlGenerator) return false; + if (!shared.isVisualizeEmbeddable(embeddable)) return false; + if (!shared.getIndexPattern(embeddable)) return false; + if (embeddable.getInput().viewMode !== ViewMode.VIEW) return false; + return true; + } + + public async execute(context: Context): Promise { + if (!shared.isVisualizeEmbeddable(context.embeddable)) return; + + const { core } = this.params.start(); + const { appName, appPath } = await this.getUrl(context); + + await core.application.navigateToApp(appName, { + path: appPath, + }); + } + + public async getHref(context: Context): Promise { + const { embeddable } = context; + + if (!shared.isVisualizeEmbeddable(embeddable)) { + throw new Error(`Embeddable not supported for "${this.getDisplayName(context)}" action.`); + } + + const { path } = await this.getUrl(context); + + return path; + } +} diff --git a/x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_chart_action.test.ts b/x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_chart_action.test.ts new file mode 100644 index 0000000000000..a273f0d50e45e --- /dev/null +++ b/x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_chart_action.test.ts @@ -0,0 +1,274 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ExploreDataChartAction } from './explore_data_chart_action'; +import { Params, PluginDeps } from './abstract_explore_data_action'; +import { coreMock } from '../../../../../../src/core/public/mocks'; +import { UrlGeneratorContract } from '../../../../../../src/plugins/share/public'; +import { + EmbeddableStart, + RangeSelectTriggerContext, + ValueClickTriggerContext, + ChartActionContext, +} from '../../../../../../src/plugins/embeddable/public'; +import { i18n } from '@kbn/i18n'; +import { + VisualizeEmbeddableContract, + VISUALIZE_EMBEDDABLE_TYPE, +} from '../../../../../../src/plugins/visualizations/public'; +import { ViewMode } from '../../../../../../src/plugins/embeddable/public'; +import { Filter, TimeRange } from '../../../../../../src/plugins/data/public'; + +const i18nTranslateSpy = (i18n.translate as unknown) as jest.SpyInstance; + +jest.mock('@kbn/i18n', () => ({ + i18n: { + translate: jest.fn((key, options) => options.defaultMessage), + }, +})); + +afterEach(() => { + i18nTranslateSpy.mockClear(); +}); + +const setup = ({ useRangeEvent = false }: { useRangeEvent?: boolean } = {}) => { + type UrlGenerator = UrlGeneratorContract<'DISCOVER_APP_URL_GENERATOR'>; + + const core = coreMock.createStart(); + + const urlGenerator: UrlGenerator = ({ + createUrl: jest.fn(() => Promise.resolve('/xyz/app/discover/foo#bar')), + } as unknown) as UrlGenerator; + + const filtersAndTimeRangeFromContext = jest.fn((async () => ({ + filters: [], + })) as EmbeddableStart['filtersAndTimeRangeFromContext']); + + const plugins: PluginDeps = { + discover: { + urlGenerator, + }, + embeddable: { + filtersAndTimeRangeFromContext, + }, + }; + + const params: Params = { + start: () => ({ + plugins, + self: {}, + core, + }), + }; + const action = new ExploreDataChartAction(params); + + const input = { + viewMode: ViewMode.VIEW, + }; + + const output = { + indexPatterns: [ + { + id: 'index-ptr-foo', + }, + ], + }; + + const embeddable: VisualizeEmbeddableContract = ({ + type: VISUALIZE_EMBEDDABLE_TYPE, + getInput: () => input, + getOutput: () => output, + } as unknown) as VisualizeEmbeddableContract; + + const data: ChartActionContext['data'] = { + ...(useRangeEvent + ? ({ range: {} } as RangeSelectTriggerContext['data']) + : ({ data: [] } as ValueClickTriggerContext['data'])), + timeFieldName: 'order_date', + }; + + const context = { + embeddable, + data, + } as ChartActionContext; + + return { core, plugins, urlGenerator, params, action, input, output, embeddable, data, context }; +}; + +describe('"Explore underlying data" panel action', () => { + test('action has Discover icon', () => { + const { action, context } = setup(); + expect(action.getIconType(context)).toBe('discoverApp'); + }); + + test('title is "Explore underlying data"', () => { + const { action, context } = setup(); + expect(action.getDisplayName(context)).toBe('Explore underlying data'); + }); + + test('translates title', () => { + expect(i18nTranslateSpy).toHaveBeenCalledTimes(0); + + const { action, context } = setup(); + action.getDisplayName(context); + + expect(i18nTranslateSpy).toHaveBeenCalledTimes(1); + expect(i18nTranslateSpy.mock.calls[0][0]).toBe( + 'xpack.discover.FlyoutCreateDrilldownAction.displayName' + ); + }); + + describe('isCompatible()', () => { + test('returns true when all conditions are met', async () => { + const { action, context } = setup(); + + const isCompatible = await action.isCompatible(context); + + expect(isCompatible).toBe(true); + }); + + test('returns false when URL generator is not present', async () => { + const { action, plugins, context } = setup(); + (plugins.discover as any).urlGenerator = undefined; + + const isCompatible = await action.isCompatible(context); + + expect(isCompatible).toBe(false); + }); + + test('returns false if embeddable is not Visualize embeddable', async () => { + const { action, embeddable, context } = setup(); + (embeddable as any).type = 'NOT_VISUALIZE_EMBEDDABLE'; + + const isCompatible = await action.isCompatible(context); + + expect(isCompatible).toBe(false); + }); + + test('returns false if embeddable does not have index patterns', async () => { + const { action, output, context } = setup(); + delete output.indexPatterns; + + const isCompatible = await action.isCompatible(context); + + expect(isCompatible).toBe(false); + }); + + test('returns false if embeddable index patterns are empty', async () => { + const { action, output, context } = setup(); + output.indexPatterns = []; + + const isCompatible = await action.isCompatible(context); + + expect(isCompatible).toBe(false); + }); + + test('returns false if dashboard is in edit mode', async () => { + const { action, input, context } = setup(); + input.viewMode = ViewMode.EDIT; + + const isCompatible = await action.isCompatible(context); + + expect(isCompatible).toBe(false); + }); + }); + + describe('getHref()', () => { + test('returns URL path generated by URL generator', async () => { + const { action, context } = setup(); + + const href = await action.getHref(context); + + expect(href).toBe('/xyz/app/discover/foo#bar'); + }); + + test('calls URL generator with right arguments', async () => { + const { action, urlGenerator, context } = setup(); + + expect(urlGenerator.createUrl).toHaveBeenCalledTimes(0); + + await action.getHref(context); + + expect(urlGenerator.createUrl).toHaveBeenCalledTimes(1); + expect(urlGenerator.createUrl).toHaveBeenCalledWith({ + filters: [], + indexPatternId: 'index-ptr-foo', + timeRange: undefined, + }); + }); + + test('applies chart event filters', async () => { + const { action, context, urlGenerator, plugins } = setup(); + + ((plugins.embeddable + .filtersAndTimeRangeFromContext as unknown) as jest.SpyInstance).mockImplementation(() => { + const filters: Filter[] = [ + { + meta: { + alias: 'alias', + disabled: false, + negate: false, + }, + }, + ]; + const timeRange: TimeRange = { + from: 'from', + to: 'to', + }; + return { filters, timeRange }; + }); + + expect(plugins.embeddable.filtersAndTimeRangeFromContext).toHaveBeenCalledTimes(0); + + await action.getHref(context); + + expect(plugins.embeddable.filtersAndTimeRangeFromContext).toHaveBeenCalledTimes(1); + expect(plugins.embeddable.filtersAndTimeRangeFromContext).toHaveBeenCalledWith(context); + expect(urlGenerator.createUrl).toHaveBeenCalledWith({ + filters: [ + { + meta: { + alias: 'alias', + disabled: false, + negate: false, + }, + }, + ], + indexPatternId: 'index-ptr-foo', + timeRange: { + from: 'from', + to: 'to', + }, + }); + }); + }); + + describe('execute()', () => { + test('calls platform SPA navigation method', async () => { + const { action, context, core } = setup(); + + expect(core.application.navigateToApp).toHaveBeenCalledTimes(0); + + await action.execute(context); + + expect(core.application.navigateToApp).toHaveBeenCalledTimes(1); + }); + + test('calls platform SPA navigation method with right arguments', async () => { + const { action, context, core } = setup(); + + await action.execute(context); + + expect(core.application.navigateToApp).toHaveBeenCalledTimes(1); + expect(core.application.navigateToApp.mock.calls[0]).toEqual([ + 'discover', + { + path: '/foo#bar', + }, + ]); + }); + }); +}); diff --git a/x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_chart_action.ts b/x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_chart_action.ts new file mode 100644 index 0000000000000..359f14959c6a6 --- /dev/null +++ b/x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_chart_action.ts @@ -0,0 +1,65 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Action } from '../../../../../../src/plugins/ui_actions/public'; +import { + ValueClickTriggerContext, + RangeSelectTriggerContext, +} from '../../../../../../src/plugins/embeddable/public'; +import { DiscoverUrlGeneratorState } from '../../../../../../src/plugins/discover/public'; +import { isTimeRange, isQuery, isFilters } from '../../../../../../src/plugins/data/public'; +import { KibanaURL } from './kibana_url'; +import * as shared from './shared'; +import { AbstractExploreDataAction } from './abstract_explore_data_action'; + +export type ExploreDataChartActionContext = ValueClickTriggerContext | RangeSelectTriggerContext; + +export const ACTION_EXPLORE_DATA_CHART = 'ACTION_EXPLORE_DATA_CHART'; + +/** + * This is "Explore underlying data" action which appears in popup context + * menu when user clicks a value in visualization or brushes a time range. + */ +export class ExploreDataChartAction extends AbstractExploreDataAction + implements Action { + public readonly id = ACTION_EXPLORE_DATA_CHART; + + public readonly type = ACTION_EXPLORE_DATA_CHART; + + public readonly order = 200; + + protected readonly getUrl = async ( + context: ExploreDataChartActionContext + ): Promise => { + const { plugins } = this.params.start(); + const { urlGenerator } = plugins.discover; + + if (!urlGenerator) { + throw new Error('Discover URL generator not available.'); + } + + const { embeddable } = context; + const { filters, timeRange } = await plugins.embeddable.filtersAndTimeRangeFromContext(context); + const state: DiscoverUrlGeneratorState = { + filters, + timeRange, + }; + + if (embeddable) { + state.indexPatternId = shared.getIndexPattern(embeddable) || undefined; + + const input = embeddable.getInput(); + + if (isTimeRange(input.timeRange) && !state.timeRange) state.timeRange = input.timeRange; + if (isQuery(input.query)) state.query = input.query; + if (isFilters(input.filters)) state.filters = [...input.filters, ...(state.filters || [])]; + } + + const path = await urlGenerator.createUrl(state); + + return new KibanaURL(path); + }; +} diff --git a/x-pack/plugins/discover_enhanced/public/actions/view_in_discover/explore_data_context_menu_action.test.ts b/x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_context_menu_action.test.ts similarity index 88% rename from x-pack/plugins/discover_enhanced/public/actions/view_in_discover/explore_data_context_menu_action.test.ts rename to x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_context_menu_action.test.ts index a7167d2e2e691..e742b69380973 100644 --- a/x-pack/plugins/discover_enhanced/public/actions/view_in_discover/explore_data_context_menu_action.test.ts +++ b/x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_context_menu_action.test.ts @@ -4,14 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - ExploreDataContextMenuAction, - ACTION_EXPLORE_DATA, - Params, - PluginDeps, -} from './explore_data_context_menu_action'; +import { ExploreDataContextMenuAction } from './explore_data_context_menu_action'; +import { Params, PluginDeps } from './abstract_explore_data_action'; import { coreMock } from '../../../../../../src/core/public/mocks'; import { UrlGeneratorContract } from '../../../../../../src/plugins/share/public'; +import { EmbeddableStart } from '../../../../../../src/plugins/embeddable/public'; import { i18n } from '@kbn/i18n'; import { VisualizeEmbeddableContract, @@ -37,14 +34,20 @@ const setup = () => { const core = coreMock.createStart(); const urlGenerator: UrlGenerator = ({ - id: ACTION_EXPLORE_DATA, createUrl: jest.fn(() => Promise.resolve('/xyz/app/discover/foo#bar')), } as unknown) as UrlGenerator; + const filtersAndTimeRangeFromContext = jest.fn((async () => ({ + filters: [], + })) as EmbeddableStart['filtersAndTimeRangeFromContext']); + const plugins: PluginDeps = { discover: { urlGenerator, }, + embeddable: { + filtersAndTimeRangeFromContext, + }, }; const params: Params = { @@ -83,19 +86,20 @@ const setup = () => { describe('"Explore underlying data" panel action', () => { test('action has Discover icon', () => { - const { action } = setup(); - expect(action.getIconType()).toBe('discoverApp'); + const { action, context } = setup(); + expect(action.getIconType(context)).toBe('discoverApp'); }); test('title is "Explore underlying data"', () => { - const { action } = setup(); - expect(action.getDisplayName()).toBe('Explore underlying data'); + const { action, context } = setup(); + expect(action.getDisplayName(context)).toBe('Explore underlying data'); }); test('translates title', () => { expect(i18nTranslateSpy).toHaveBeenCalledTimes(0); - setup().action.getDisplayName(); + const { action, context } = setup(); + action.getDisplayName(context); expect(i18nTranslateSpy).toHaveBeenCalledTimes(1); expect(i18nTranslateSpy.mock.calls[0][0]).toBe( diff --git a/x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_context_menu_action.ts b/x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_context_menu_action.ts new file mode 100644 index 0000000000000..6691089f875d8 --- /dev/null +++ b/x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_context_menu_action.ts @@ -0,0 +1,54 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Action } from '../../../../../../src/plugins/ui_actions/public'; +import { EmbeddableContext } from '../../../../../../src/plugins/embeddable/public'; +import { DiscoverUrlGeneratorState } from '../../../../../../src/plugins/discover/public'; +import { isTimeRange, isQuery, isFilters } from '../../../../../../src/plugins/data/public'; +import { KibanaURL } from './kibana_url'; +import * as shared from './shared'; +import { AbstractExploreDataAction } from './abstract_explore_data_action'; + +export const ACTION_EXPLORE_DATA = 'ACTION_EXPLORE_DATA'; + +/** + * This is "Explore underlying data" action which appears in the context + * menu of a dashboard panel. + */ +export class ExploreDataContextMenuAction extends AbstractExploreDataAction + implements Action { + public readonly id = ACTION_EXPLORE_DATA; + + public readonly type = ACTION_EXPLORE_DATA; + + public readonly order = 200; + + protected readonly getUrl = async (context: EmbeddableContext): Promise => { + const { plugins } = this.params.start(); + const { urlGenerator } = plugins.discover; + + if (!urlGenerator) { + throw new Error('Discover URL generator not available.'); + } + + const { embeddable } = context; + const state: DiscoverUrlGeneratorState = {}; + + if (embeddable) { + state.indexPatternId = shared.getIndexPattern(embeddable) || undefined; + + const input = embeddable.getInput(); + + if (isTimeRange(input.timeRange) && !state.timeRange) state.timeRange = input.timeRange; + if (isQuery(input.query)) state.query = input.query; + if (isFilters(input.filters)) state.filters = [...input.filters, ...(state.filters || [])]; + } + + const path = await urlGenerator.createUrl(state); + + return new KibanaURL(path); + }; +} diff --git a/x-pack/plugins/discover_enhanced/public/actions/view_in_discover/index.ts b/x-pack/plugins/discover_enhanced/public/actions/explore_data/index.ts similarity index 86% rename from x-pack/plugins/discover_enhanced/public/actions/view_in_discover/index.ts rename to x-pack/plugins/discover_enhanced/public/actions/explore_data/index.ts index 8788621365385..e6d7d4b59149e 100644 --- a/x-pack/plugins/discover_enhanced/public/actions/view_in_discover/index.ts +++ b/x-pack/plugins/discover_enhanced/public/actions/explore_data/index.ts @@ -5,3 +5,4 @@ */ export * from './explore_data_context_menu_action'; +export * from './explore_data_chart_action'; diff --git a/x-pack/plugins/discover_enhanced/public/actions/explore_data/kibana_url.ts b/x-pack/plugins/discover_enhanced/public/actions/explore_data/kibana_url.ts new file mode 100644 index 0000000000000..3c25fc2b3c3d1 --- /dev/null +++ b/x-pack/plugins/discover_enhanced/public/actions/explore_data/kibana_url.ts @@ -0,0 +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; + * you may not use this file except in compliance with the Elastic License. + */ + +// TODO: Replace this logic with KibanaURL once it is available. +// https://github.com/elastic/kibana/issues/64497 +export class KibanaURL { + public readonly path: string; + public readonly appName: string; + public readonly appPath: string; + + constructor(path: string) { + const match = path.match(/^.*\/app\/([^\/#]+)(.+)$/); + + if (!match) { + throw new Error('Unexpected Discover URL path.'); + } + + const [, appName, appPath] = match; + + if (!appName || !appPath) { + throw new Error('Could not parse Discover URL path.'); + } + + this.path = path; + this.appName = appName; + this.appPath = appPath; + } +} diff --git a/x-pack/plugins/discover_enhanced/public/actions/explore_data/shared.ts b/x-pack/plugins/discover_enhanced/public/actions/explore_data/shared.ts new file mode 100644 index 0000000000000..fa2168df944b0 --- /dev/null +++ b/x-pack/plugins/discover_enhanced/public/actions/explore_data/shared.ts @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { IEmbeddable } from '../../../../../../src/plugins/embeddable/public'; +import { + VISUALIZE_EMBEDDABLE_TYPE, + VisualizeEmbeddableContract, +} from '../../../../../../src/plugins/visualizations/public'; + +export const isOutputWithIndexPatterns = ( + output: unknown +): output is { indexPatterns: Array<{ id: string }> } => { + if (!output || typeof output !== 'object') return false; + return Array.isArray((output as any).indexPatterns); +}; + +export const isVisualizeEmbeddable = ( + embeddable?: IEmbeddable +): embeddable is VisualizeEmbeddableContract => + embeddable && embeddable?.type === VISUALIZE_EMBEDDABLE_TYPE ? true : false; + +/** + * @returns Returns empty string if no index pattern ID found. + */ +export const getIndexPattern = (embeddable?: IEmbeddable): string => { + if (!embeddable) return ''; + const output = embeddable.getOutput(); + + if (isOutputWithIndexPatterns(output) && output.indexPatterns.length > 0) { + return output.indexPatterns[0].id; + } + + return ''; +}; diff --git a/x-pack/plugins/discover_enhanced/public/actions/index.ts b/x-pack/plugins/discover_enhanced/public/actions/index.ts index cbb955fa46340..209ae6bee09b5 100644 --- a/x-pack/plugins/discover_enhanced/public/actions/index.ts +++ b/x-pack/plugins/discover_enhanced/public/actions/index.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export * from './view_in_discover'; +export * from './explore_data'; diff --git a/x-pack/plugins/discover_enhanced/public/actions/view_in_discover/explore_data_context_menu_action.ts b/x-pack/plugins/discover_enhanced/public/actions/view_in_discover/explore_data_context_menu_action.ts deleted file mode 100644 index d66ca129934a8..0000000000000 --- a/x-pack/plugins/discover_enhanced/public/actions/view_in_discover/explore_data_context_menu_action.ts +++ /dev/null @@ -1,156 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -/* eslint-disable max-classes-per-file */ - -import { i18n } from '@kbn/i18n'; -import { Action } from '../../../../../../src/plugins/ui_actions/public'; -import { DiscoverStart } from '../../../../../../src/plugins/discover/public'; -import { - EmbeddableContext, - IEmbeddable, - ViewMode, -} from '../../../../../../src/plugins/embeddable/public'; -import { StartServicesGetter } from '../../../../../../src/plugins/kibana_utils/public'; -import { CoreStart } from '../../../../../../src/core/public'; -import { - VisualizeEmbeddableContract, - VISUALIZE_EMBEDDABLE_TYPE, -} from '../../../../../../src/plugins/visualizations/public'; - -// TODO: Replace this logic with KibanaURL once it is available. -// https://github.com/elastic/kibana/issues/64497 -class KibanaURL { - public readonly path: string; - public readonly appName: string; - public readonly appPath: string; - - constructor(path: string) { - const match = path.match(/^.*\/app\/([^\/#]+)(.+)$/); - - if (!match) { - throw new Error('Unexpected Discover URL path.'); - } - - const [, appName, appPath] = match; - - if (!appName || !appPath) { - throw new Error('Could not parse Discover URL path.'); - } - - this.path = path; - this.appName = appName; - this.appPath = appPath; - } -} - -export const ACTION_EXPLORE_DATA = 'ACTION_EXPLORE_DATA'; - -const isOutputWithIndexPatterns = ( - output: unknown -): output is { indexPatterns: Array<{ id: string }> } => { - if (!output || typeof output !== 'object') return false; - return Array.isArray((output as any).indexPatterns); -}; - -const isVisualizeEmbeddable = ( - embeddable: IEmbeddable -): embeddable is VisualizeEmbeddableContract => embeddable?.type === VISUALIZE_EMBEDDABLE_TYPE; - -export interface PluginDeps { - discover: Pick; -} - -export interface CoreDeps { - application: Pick; -} - -export interface Params { - start: StartServicesGetter; -} - -export class ExploreDataContextMenuAction implements Action { - public readonly id = ACTION_EXPLORE_DATA; - - public readonly type = ACTION_EXPLORE_DATA; - - public readonly order = 200; - - constructor(private readonly params: Params) {} - - public getDisplayName() { - return i18n.translate('xpack.discover.FlyoutCreateDrilldownAction.displayName', { - defaultMessage: 'Explore underlying data', - }); - } - - public getIconType() { - return 'discoverApp'; - } - - public async isCompatible({ embeddable }: EmbeddableContext) { - if (!this.params.start().plugins.discover.urlGenerator) return false; - if (!isVisualizeEmbeddable(embeddable)) return false; - if (!this.getIndexPattern(embeddable)) return false; - if (embeddable.getInput().viewMode !== ViewMode.VIEW) return false; - return true; - } - - public async execute({ embeddable }: EmbeddableContext) { - if (!isVisualizeEmbeddable(embeddable)) return; - - const { core } = this.params.start(); - const { appName, appPath } = await this.getUrl(embeddable); - - await core.application.navigateToApp(appName, { - path: appPath, - }); - } - - public async getHref({ embeddable }: EmbeddableContext): Promise { - if (!isVisualizeEmbeddable(embeddable)) { - throw new Error(`Embeddable not supported for "${this.getDisplayName()}" action.`); - } - - const { path } = await this.getUrl(embeddable); - - return path; - } - - private async getUrl(embeddable: VisualizeEmbeddableContract): Promise { - const { plugins } = this.params.start(); - const { urlGenerator } = plugins.discover; - - if (!urlGenerator) { - throw new Error('Discover URL generator not available.'); - } - - const { timeRange, query, filters } = embeddable.getInput(); - const indexPatternId = this.getIndexPattern(embeddable); - - const path = await urlGenerator.createUrl({ - indexPatternId, - filters, - query, - timeRange, - }); - - return new KibanaURL(path); - } - - /** - * @returns Returns empty string if no index pattern ID found. - */ - private getIndexPattern(embeddable: VisualizeEmbeddableContract): string { - const output = embeddable!.getOutput(); - - if (isOutputWithIndexPatterns(output) && output.indexPatterns.length > 0) { - return output.indexPatterns[0].id; - } - - return ''; - } -} diff --git a/x-pack/plugins/discover_enhanced/public/plugin.ts b/x-pack/plugins/discover_enhanced/public/plugin.ts index f55c5dab3449b..ea3c1222eb369 100644 --- a/x-pack/plugins/discover_enhanced/public/plugin.ts +++ b/x-pack/plugins/discover_enhanced/public/plugin.ts @@ -6,7 +6,12 @@ import { CoreSetup, CoreStart, Plugin } from 'kibana/public'; import { PluginInitializerContext } from 'kibana/public'; -import { UiActionsSetup, UiActionsStart } from '../../../../src/plugins/ui_actions/public'; +import { + UiActionsSetup, + UiActionsStart, + SELECT_RANGE_TRIGGER, + VALUE_CLICK_TRIGGER, +} from '../../../../src/plugins/ui_actions/public'; import { createStartServicesGetter } from '../../../../src/plugins/kibana_utils/public'; import { DiscoverSetup, DiscoverStart } from '../../../../src/plugins/discover/public'; import { SharePluginSetup, SharePluginStart } from '../../../../src/plugins/share/public'; @@ -16,11 +21,18 @@ import { EmbeddableContext, CONTEXT_MENU_TRIGGER, } from '../../../../src/plugins/embeddable/public'; -import { ExploreDataContextMenuAction, ACTION_EXPLORE_DATA } from './actions'; +import { + ExploreDataContextMenuAction, + ExploreDataChartAction, + ACTION_EXPLORE_DATA, + ACTION_EXPLORE_DATA_CHART, + ExploreDataChartActionContext, +} from './actions'; declare module '../../../../src/plugins/ui_actions/public' { export interface ActionContextMapping { [ACTION_EXPLORE_DATA]: EmbeddableContext; + [ACTION_EXPLORE_DATA_CHART]: ExploreDataChartActionContext; } } @@ -48,10 +60,17 @@ export class DiscoverEnhancedPlugin { uiActions, share }: DiscoverEnhancedSetupDependencies ) { const start = createStartServicesGetter(core.getStartServices); + const isSharePluginInstalled = !!share; - if (!!share) { - const exploreDataAction = new ExploreDataContextMenuAction({ start }); + if (isSharePluginInstalled) { + const params = { start }; + + const exploreDataAction = new ExploreDataContextMenuAction(params); uiActions.addTriggerAction(CONTEXT_MENU_TRIGGER, exploreDataAction); + + const exploreDataChartAction = new ExploreDataChartAction(params); + uiActions.addTriggerAction(SELECT_RANGE_TRIGGER, exploreDataChartAction); + uiActions.addTriggerAction(VALUE_CLICK_TRIGGER, exploreDataChartAction); } } diff --git a/x-pack/test/functional/apps/dashboard/drilldowns/dashboard_drilldowns.ts b/x-pack/test/functional/apps/dashboard/drilldowns/dashboard_drilldowns.ts index bcdd3d1f82e7d..29ead0db1c634 100644 --- a/x-pack/test/functional/apps/dashboard/drilldowns/dashboard_drilldowns.ts +++ b/x-pack/test/functional/apps/dashboard/drilldowns/dashboard_drilldowns.ts @@ -59,7 +59,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { ); // trigger drilldown action by clicking on a pie and picking drilldown action by it's name - await pieChart.filterOnPieSlice('40,000'); + await pieChart.clickOnPieSlice('40,000'); await dashboardDrilldownPanelActions.expectMultipleActionsMenuOpened(); const href = await dashboardDrilldownPanelActions.getActionHrefByText( diff --git a/x-pack/test/functional/apps/dashboard/drilldowns/explore_data_chart_action.ts b/x-pack/test/functional/apps/dashboard/drilldowns/explore_data_chart_action.ts new file mode 100644 index 0000000000000..12363f8800c28 --- /dev/null +++ b/x-pack/test/functional/apps/dashboard/drilldowns/explore_data_chart_action.ts @@ -0,0 +1,98 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../ftr_provider_context'; + +const ACTION_ID = 'ACTION_EXPLORE_DATA_CHART'; +const ACTION_TEST_SUBJ = `embeddablePanelAction-${ACTION_ID}`; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const drilldowns = getService('dashboardDrilldownsManage'); + const { dashboard, discover, common, timePicker } = getPageObjects([ + 'dashboard', + 'discover', + 'common', + 'timePicker', + ]); + const testSubjects = getService('testSubjects'); + const pieChart = getService('pieChart'); + const dashboardDrilldownPanelActions = getService('dashboardDrilldownPanelActions'); + const filterBar = getService('filterBar'); + const browser = getService('browser'); + + describe('Explore underlying data - chart action', () => { + describe('value click action', () => { + it('action exists in chart click popup menu', async () => { + await common.navigateToApp('dashboard'); + await dashboard.preserveCrossAppState(); + await dashboard.loadSavedDashboard(drilldowns.DASHBOARD_WITH_PIE_CHART_NAME); + await pieChart.clickOnPieSlice('160,000'); + await dashboardDrilldownPanelActions.expectMultipleActionsMenuOpened(); + await testSubjects.existOrFail(ACTION_TEST_SUBJ); + }); + + it('action is a link element', async () => { + const actionElement = await testSubjects.find(ACTION_TEST_SUBJ); + const tag = await actionElement.getTagName(); + const href = await actionElement.getAttribute('href'); + + expect(tag.toLowerCase()).to.be('a'); + expect(typeof href).to.be('string'); + expect(href.length > 5).to.be(true); + }); + + it('navigates to Discover app on action click carrying over pie slice filter', async () => { + await testSubjects.clickWhenNotDisabled(ACTION_TEST_SUBJ); + await discover.waitForDiscoverAppOnScreen(); + await filterBar.hasFilter('memory', '160,000 to 200,000'); + const filterCount = await filterBar.getFilterCount(); + + expect(filterCount).to.be(1); + }); + }); + + describe('brush action', () => { + let originalTimeRangeDurationHours: number | undefined; + + it('action exists in chart brush popup menu', async () => { + await common.navigateToApp('dashboard'); + await dashboard.preserveCrossAppState(); + await dashboard.loadSavedDashboard(drilldowns.DASHBOARD_WITH_AREA_CHART_NAME); + + originalTimeRangeDurationHours = await timePicker.getTimeDurationInHours(); + const areaChart = await testSubjects.find('visualizationLoader'); + await browser.dragAndDrop( + { + location: areaChart, + offset: { + x: -100, + y: 0, + }, + }, + { + location: areaChart, + offset: { + x: 100, + y: 0, + }, + } + ); + + await dashboardDrilldownPanelActions.expectMultipleActionsMenuOpened(); + await testSubjects.existOrFail(ACTION_TEST_SUBJ); + }); + + it('navigates to Discover on click carrying over brushed time range', async () => { + await testSubjects.clickWhenNotDisabled(ACTION_TEST_SUBJ); + await discover.waitForDiscoverAppOnScreen(); + const newTimeRangeDurationHours = await timePicker.getTimeDurationInHours(); + + expect(newTimeRangeDurationHours).to.be.lessThan(originalTimeRangeDurationHours as number); + }); + }); + }); +} diff --git a/x-pack/test/functional/apps/dashboard/drilldowns/explore_data_panel_action.ts b/x-pack/test/functional/apps/dashboard/drilldowns/explore_data_panel_action.ts index 24d6e820ac0eb..fedc83a2f81c7 100644 --- a/x-pack/test/functional/apps/dashboard/drilldowns/explore_data_panel_action.ts +++ b/x-pack/test/functional/apps/dashboard/drilldowns/explore_data_panel_action.ts @@ -8,7 +8,7 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../../ftr_provider_context'; const ACTION_ID = 'ACTION_EXPLORE_DATA'; -const EXPLORE_RAW_DATA_ACTION_TEST_SUBJ = `embeddablePanelAction-${ACTION_ID}`; +const ACTION_TEST_SUBJ = `embeddablePanelAction-${ACTION_ID}`; export default function ({ getService, getPageObjects }: FtrProviderContext) { const drilldowns = getService('dashboardDrilldownsManage'); @@ -24,31 +24,46 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const kibanaServer = getService('kibanaServer'); describe('Explore underlying data - panel action', function () { - before(async () => { - await kibanaServer.uiSettings.replace({ defaultIndex: 'logstash*' }); + before( + 'change default index pattern to verify action navigates to correct index pattern', + async () => { + await kibanaServer.uiSettings.replace({ defaultIndex: 'logstash*' }); + } + ); + + before('start on Dashboard landing page', async () => { await common.navigateToApp('dashboard'); await dashboard.preserveCrossAppState(); }); - after(async () => { + after('set back default index pattern', async () => { await kibanaServer.uiSettings.replace({ defaultIndex: 'logstash-*' }); }); + after('clean-up custom time range on panel', async () => { + await common.navigateToApp('dashboard'); + await dashboard.gotoDashboardEditMode(drilldowns.DASHBOARD_WITH_PIE_CHART_NAME); + await panelActions.openContextMenu(); + await panelActionsTimeRange.clickTimeRangeActionInContextMenu(); + await panelActionsTimeRange.clickRemovePerPanelTimeRangeButton(); + await dashboard.saveDashboard('Dashboard with Pie Chart'); + }); + it('action exists in panel context menu', async () => { await dashboard.loadSavedDashboard(drilldowns.DASHBOARD_WITH_PIE_CHART_NAME); await panelActions.openContextMenu(); - await testSubjects.existOrFail(EXPLORE_RAW_DATA_ACTION_TEST_SUBJ); + await testSubjects.existOrFail(ACTION_TEST_SUBJ); }); it('is a link element', async () => { - const actionElement = await testSubjects.find(EXPLORE_RAW_DATA_ACTION_TEST_SUBJ); + const actionElement = await testSubjects.find(ACTION_TEST_SUBJ); const tag = await actionElement.getTagName(); expect(tag.toLowerCase()).to.be('a'); }); it('navigates to Discover app to index pattern of the panel on action click', async () => { - await testSubjects.clickWhenNotDisabled(EXPLORE_RAW_DATA_ACTION_TEST_SUBJ); + await testSubjects.clickWhenNotDisabled(ACTION_TEST_SUBJ); await discover.waitForDiscoverAppOnScreen(); const el = await testSubjects.find('indexPattern-switch-link'); @@ -71,7 +86,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await dashboard.saveDashboard('Dashboard with Pie Chart'); await panelActions.openContextMenu(); - await testSubjects.clickWhenNotDisabled(EXPLORE_RAW_DATA_ACTION_TEST_SUBJ); + await testSubjects.clickWhenNotDisabled(ACTION_TEST_SUBJ); await discover.waitForDiscoverAppOnScreen(); const text = await timePicker.getShowDatesButtonText(); diff --git a/x-pack/test/functional/apps/dashboard/drilldowns/index.ts b/x-pack/test/functional/apps/dashboard/drilldowns/index.ts index 19d85ad0e448f..4cdb33c06947f 100644 --- a/x-pack/test/functional/apps/dashboard/drilldowns/index.ts +++ b/x-pack/test/functional/apps/dashboard/drilldowns/index.ts @@ -24,5 +24,6 @@ export default function ({ loadTestFile, getService }: FtrProviderContext) { loadTestFile(require.resolve('./dashboard_drilldowns')); loadTestFile(require.resolve('./explore_data_panel_action')); + loadTestFile(require.resolve('./explore_data_chart_action')); }); } diff --git a/x-pack/test/functional/services/dashboard/panel_time_range.ts b/x-pack/test/functional/services/dashboard/panel_time_range.ts index 6a91a6ff0584b..f71e8284c30d9 100644 --- a/x-pack/test/functional/services/dashboard/panel_time_range.ts +++ b/x-pack/test/functional/services/dashboard/panel_time_range.ts @@ -52,5 +52,11 @@ export function DashboardPanelTimeRangeProvider({ getService }: FtrProviderConte const button = await this.findModalTestSubject('addPerPanelTimeRangeButton'); await button.click(); } + + public async clickRemovePerPanelTimeRangeButton() { + log.debug('clickRemovePerPanelTimeRangeButton'); + const button = await this.findModalTestSubject('removePerPanelTimeRangeButton'); + await button.click(); + } })(); } From 09e3f75bc3951b317973dd1f67f7b4e6c9a6ba42 Mon Sep 17 00:00:00 2001 From: Xavier Mouligneau <189600+XavierM@users.noreply.github.com> Date: Fri, 26 Jun 2020 08:56:09 -0400 Subject: [PATCH 07/13] [SECURITY] Redirect app/security to app/security/overview (#70005) * redirect app/security to app/security/overview * missing re-naming initialization * add unit test for intialization value of indicesExists Co-authored-by: Elastic Machine --- .../common/containers/source/index.test.tsx | 18 +++++++++++++ .../public/common/containers/source/index.tsx | 2 +- .../public/common/translations.ts | 4 +-- .../security_solution/public/plugin.tsx | 26 ++++++++++--------- 4 files changed, 35 insertions(+), 15 deletions(-) diff --git a/x-pack/plugins/security_solution/public/common/containers/source/index.test.tsx b/x-pack/plugins/security_solution/public/common/containers/source/index.test.tsx index c30c3668638a3..69e4ac615ebf2 100644 --- a/x-pack/plugins/security_solution/public/common/containers/source/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/containers/source/index.test.tsx @@ -17,6 +17,24 @@ jest.mock('../../utils/apollo_context', () => ({ })); describe('Index Fields & Browser Fields', () => { + test('At initialization the value of indicesExists should be true', async () => { + const { result, waitForNextUpdate } = renderHook(() => useWithSource()); + const initialResult = result.current; + + await waitForNextUpdate(); + + return expect(initialResult).toEqual({ + browserFields: {}, + errorMessage: null, + indexPattern: { + fields: [], + title: 'apm-*-transaction*,auditbeat-*,endgame-*,filebeat-*,packetbeat-*,winlogbeat-*', + }, + indicesExist: true, + loading: true, + }); + }); + test('returns memoized value', async () => { const { result, waitForNextUpdate, rerender } = renderHook(() => useWithSource()); await waitForNextUpdate(); diff --git a/x-pack/plugins/security_solution/public/common/containers/source/index.tsx b/x-pack/plugins/security_solution/public/common/containers/source/index.tsx index 34ac5f8f5d94f..5e80953914c97 100644 --- a/x-pack/plugins/security_solution/public/common/containers/source/index.tsx +++ b/x-pack/plugins/security_solution/public/common/containers/source/index.tsx @@ -102,7 +102,7 @@ export const useWithSource = (sourceId = 'default', indexToAdd?: string[] | null browserFields: EMPTY_BROWSER_FIELDS, errorMessage: null, indexPattern: getIndexFields(defaultIndex.join(), []), - indicesExist: undefined, + indicesExist: indicesExistOrDataTemporarilyUnavailable(undefined), loading: false, }); diff --git a/x-pack/plugins/security_solution/public/common/translations.ts b/x-pack/plugins/security_solution/public/common/translations.ts index b5a400d187f82..677543ec0dba6 100644 --- a/x-pack/plugins/security_solution/public/common/translations.ts +++ b/x-pack/plugins/security_solution/public/common/translations.ts @@ -7,12 +7,12 @@ import { i18n } from '@kbn/i18n'; export const EMPTY_TITLE = i18n.translate('xpack.securitySolution.pages.common.emptyTitle', { - defaultMessage: 'Welcome to SIEM. Let’s get you started.', + defaultMessage: 'Welcome to Security Solution. Let’s get you started.', }); export const EMPTY_MESSAGE = i18n.translate('xpack.securitySolution.pages.common.emptyMessage', { defaultMessage: - 'To begin using security information and event management (SIEM), you’ll need to add SIEM-related data, in Elastic Common Schema (ECS) format, to the Elastic Stack. An easy way to get started is by installing and configuring our data shippers, called Beats. Let’s do that now!', + 'To begin using security information and event management (Security Solution), you’ll need to add security solution related data, in Elastic Common Schema (ECS) format, to the Elastic Stack. An easy way to get started is by installing and configuring our data shippers, called Beats. Let’s do that now!', }); export const EMPTY_ACTION_PRIMARY = i18n.translate( diff --git a/x-pack/plugins/security_solution/public/plugin.tsx b/x-pack/plugins/security_solution/public/plugin.tsx index 360c81abadc81..b247170a4a5db 100644 --- a/x-pack/plugins/security_solution/public/plugin.tsx +++ b/x-pack/plugins/security_solution/public/plugin.tsx @@ -16,6 +16,7 @@ import { PluginInitializerContext, Plugin as IPlugin, DEFAULT_APP_CATEGORIES, + AppNavLinkStatus, } from '../../../../src/core/public'; import { Storage } from '../../../../src/plugins/kibana_utils/public'; import { FeatureCatalogueCategory } from '../../../../src/plugins/home/public'; @@ -35,6 +36,7 @@ import { APP_CASES_PATH, SHOW_ENDPOINT_ALERTS_NAV, APP_ENDPOINT_ALERTS_PATH, + APP_PATH, } from '../common/constants'; import { ConfigureEndpointDatasource } from './management/pages/policy/view/ingest_manager_integration/configure_datasource'; @@ -86,18 +88,18 @@ export class Plugin implements IPlugin { - // const [{ application }] = await core.getStartServices(); - // application.navigateToApp(`${APP_ID}:${SecurityPageName.overview}`, { replace: true }); - // return () => true; - // }, - // }); + core.application.register({ + exactRoute: true, + id: APP_ID, + title: 'Security', + appRoute: APP_PATH, + navLinkStatus: AppNavLinkStatus.hidden, + mount: async (params: AppMountParameters) => { + const [{ application }] = await core.getStartServices(); + application.navigateToApp(`${APP_ID}:${SecurityPageName.overview}`, { replace: true }); + return () => true; + }, + }); core.application.register({ id: `${APP_ID}:${SecurityPageName.overview}`, From ae7e9d9ad5748f73786f660d36f081e400b3a777 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Fern=C3=A1ndez=20Haro?= Date: Fri, 26 Jun 2020 13:57:17 +0100 Subject: [PATCH 08/13] [License Management] Do not break when `telemetry.enabled:false` (#69711) Co-authored-by: Elastic Machine --- .../public/application/app_context.tsx | 4 +- .../opt_in_example_flyout.tsx | 11 ++++++ .../telemetry_opt_in/telemetry_opt_in.tsx | 37 +++++++++++++------ .../public/application/lib/telemetry.ts | 11 +++--- .../start_trial/start_trial.tsx | 4 +- .../license_management/public/plugin.ts | 18 ++++++--- 6 files changed, 57 insertions(+), 28 deletions(-) create mode 100644 x-pack/plugins/license_management/public/application/components/telemetry_opt_in/opt_in_example_flyout.tsx diff --git a/x-pack/plugins/license_management/public/application/app_context.tsx b/x-pack/plugins/license_management/public/application/app_context.tsx index 39e7ef5f16e79..62f019682fba9 100644 --- a/x-pack/plugins/license_management/public/application/app_context.tsx +++ b/x-pack/plugins/license_management/public/application/app_context.tsx @@ -9,7 +9,7 @@ import { ScopedHistory } from 'kibana/public'; import { CoreStart } from '../../../../../src/core/public'; import { LicensingPluginSetup, ILicense } from '../../../licensing/public'; -import { TelemetryPluginSetup } from '../../../../../src/plugins/telemetry/public'; +import { TelemetryPluginStart } from '../../../../../src/plugins/telemetry/public'; import { ClientConfigType } from '../types'; import { BreadcrumbService } from './breadcrumbs'; @@ -23,7 +23,7 @@ export interface AppDependencies { }; plugins: { licensing: LicensingPluginSetup; - telemetry?: TelemetryPluginSetup; + telemetry?: TelemetryPluginStart; }; docLinks: { security: string; diff --git a/x-pack/plugins/license_management/public/application/components/telemetry_opt_in/opt_in_example_flyout.tsx b/x-pack/plugins/license_management/public/application/components/telemetry_opt_in/opt_in_example_flyout.tsx new file mode 100644 index 0000000000000..a13443ad8a0d7 --- /dev/null +++ b/x-pack/plugins/license_management/public/application/components/telemetry_opt_in/opt_in_example_flyout.tsx @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { OptInExampleFlyout } from '../../../../../../../src/plugins/telemetry_management_section/public'; + +// required for lazy loading +// eslint-disable-next-line import/no-default-export +export default OptInExampleFlyout; diff --git a/x-pack/plugins/license_management/public/application/components/telemetry_opt_in/telemetry_opt_in.tsx b/x-pack/plugins/license_management/public/application/components/telemetry_opt_in/telemetry_opt_in.tsx index eff5c6cc21c43..92e241a375cea 100644 --- a/x-pack/plugins/license_management/public/application/components/telemetry_opt_in/telemetry_opt_in.tsx +++ b/x-pack/plugins/license_management/public/application/components/telemetry_opt_in/telemetry_opt_in.tsx @@ -5,13 +5,19 @@ */ import React, { Fragment } from 'react'; -import { EuiLink, EuiCheckbox, EuiSpacer, EuiText, EuiTitle, EuiPopover } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; import { - OptInExampleFlyout, - PRIVACY_STATEMENT_URL, - TelemetryPluginSetup, -} from '../../lib/telemetry'; + EuiLink, + EuiCheckbox, + EuiSpacer, + EuiText, + EuiTitle, + EuiPopover, + EuiLoadingSpinner, +} from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { TelemetryPluginStart } from '../../lib/telemetry'; + +const OptInExampleFlyout = React.lazy(() => import('./opt_in_example_flyout')); interface State { showMoreTelemetryInfo: boolean; @@ -22,7 +28,7 @@ interface Props { onOptInChange: (isOptingInToTelemetry: boolean) => void; isOptingInToTelemetry: boolean; isStartTrial: boolean; - telemetry: TelemetryPluginSetup; + telemetry: TelemetryPluginStart; } export class TelemetryOptIn extends React.Component { @@ -54,11 +60,15 @@ export class TelemetryOptIn extends React.Component { let example = null; if (showExample) { + // Using React.Suspense and lazy loading here to avoid crashing the plugin when importing + // OptInExampleFlyout but telemetryManagementSection is disabled example = ( - this.setState({ showExample: false })} - fetchExample={telemetry.telemetryService.fetchExample} - /> + }> + this.setState({ showExample: false })} + fetchExample={telemetry.telemetryService.fetchExample} + /> + ); } @@ -116,7 +126,10 @@ export class TelemetryOptIn extends React.Component { ), telemetryPrivacyStatementLink: ( - + void; startLicenseTrial: () => void; - telemetry?: TelemetryPluginSetup; + telemetry?: TelemetryPluginStart; shouldShowStartTrial: boolean; } diff --git a/x-pack/plugins/license_management/public/plugin.ts b/x-pack/plugins/license_management/public/plugin.ts index e2e6437d12d2a..2511337793fea 100644 --- a/x-pack/plugins/license_management/public/plugin.ts +++ b/x-pack/plugins/license_management/public/plugin.ts @@ -6,7 +6,7 @@ import { first } from 'rxjs/operators'; import { CoreSetup, Plugin, PluginInitializerContext } from 'src/core/public'; -import { TelemetryPluginSetup } from '../../../../src/plugins/telemetry/public'; +import { TelemetryPluginStart } from '../../../../src/plugins/telemetry/public'; import { ManagementSetup, ManagementSectionId } from '../../../../src/plugins/management/public'; import { LicensingPluginSetup } from '../../../plugins/licensing/public'; import { PLUGIN } from '../common/constants'; @@ -14,10 +14,13 @@ import { ClientConfigType } from './types'; import { AppDependencies } from './application'; import { BreadcrumbService } from './application/breadcrumbs'; -interface PluginsDependencies { +interface PluginsDependenciesSetup { management: ManagementSetup; licensing: LicensingPluginSetup; - telemetry?: TelemetryPluginSetup; +} + +interface PluginsDependenciesStart { + telemetry?: TelemetryPluginStart; } export interface LicenseManagementUIPluginSetup { @@ -31,7 +34,10 @@ export class LicenseManagementUIPlugin constructor(private readonly initializerContext: PluginInitializerContext) {} - setup(coreSetup: CoreSetup, plugins: PluginsDependencies): LicenseManagementUIPluginSetup { + setup( + coreSetup: CoreSetup, + plugins: PluginsDependenciesSetup + ): LicenseManagementUIPluginSetup { const config = this.initializerContext.config.get(); if (!config.ui.enabled) { @@ -42,14 +48,14 @@ export class LicenseManagementUIPlugin } const { getStartServices } = coreSetup; - const { management, telemetry, licensing } = plugins; + const { management, licensing } = plugins; management.sections.getSection(ManagementSectionId.Stack).registerApp({ id: PLUGIN.id, title: PLUGIN.title, order: 0, mount: async ({ element, setBreadcrumbs, history }) => { - const [core] = await getStartServices(); + const [core, { telemetry }] = await getStartServices(); const initialLicense = await plugins.licensing.license$.pipe(first()).toPromise(); // Setup documentation links From f1a1178328617c7b687ae30da9cc1a76e4805326 Mon Sep 17 00:00:00 2001 From: Aleh Zasypkin Date: Fri, 26 Jun 2020 15:15:02 +0200 Subject: [PATCH 09/13] =?UTF-8?q?Upgrade=20`elliptic`=20dependency=20(`6.5?= =?UTF-8?q?.2`=20=E2=86=92=20`6.5.3`).=20(#70054)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 93db6de88775c..60122f8b8cde7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12958,9 +12958,9 @@ element-resize-detector@^1.1.15: batch-processor "^1.0.0" elliptic@^6.0.0: - version "6.5.2" - resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.2.tgz#05c5678d7173c049d8ca433552224a495d0e3762" - integrity sha512-f4x70okzZbIQl/NSRLkI/+tteV/9WqL98zx+SQ69KbXxmVrmjwsNUPn/gYJJ0sHvEak24cZgHIPegRePAtA/xw== + version "6.5.3" + resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.3.tgz#cb59eb2efdaf73a0bd78ccd7015a62ad6e0f93d6" + integrity sha512-IMqzv5wNQf+E6aHeIqATs0tOLeOTwj1QKbRcS3jBbYkl5oLAserA8yJTT7/VyHUYG91PRmPyeQDObKLPpeS4dw== dependencies: bn.js "^4.4.0" brorand "^1.0.1" From 9ebf41c77c9e84205df987751864d31c5a4f53df Mon Sep 17 00:00:00 2001 From: Kevin Qualters <56408403+kqualters-elastic@users.noreply.github.com> Date: Fri, 26 Jun 2020 09:42:10 -0400 Subject: [PATCH 10/13] [Endpoint] use rbush to only render to DOM resolver nodes that are in view (#68957) * [Endpoint] use rbush to only render resolver nodes that are in view in the DOM * Add related events code back * Change processNodePositionsAndEdgeLineSegments selector to return a function that takes optional bounding box * Refactor selectors to not break original, and not run as often * Memoize rtree search selector, fix tests * Update node styles to use style hook, update jest tests * Fix type change issue in jest test --- x-pack/plugins/security_solution/package.json | 6 +- .../public/resolver/lib/aabb.ts | 14 ++ .../public/resolver/lib/vector2.ts | 7 + .../public/resolver/models/process_event.ts | 4 + .../data/__snapshots__/graphing.test.ts.snap | 50 +++++- .../public/resolver/store/data/selectors.ts | 135 +++++++++++++- .../store/data/visible_entities.test.ts | 165 ++++++++++++++++++ .../public/resolver/store/middleware.ts | 1 - .../public/resolver/store/selectors.ts | 30 ++++ .../public/resolver/types.ts | 36 +++- .../public/resolver/view/assets.tsx | 32 +--- .../public/resolver/view/index.tsx | 25 ++- .../public/resolver/view/panel.tsx | 22 ++- .../panels/panel_content_process_detail.tsx | 15 +- .../panels/panel_content_process_list.tsx | 13 +- .../panels/panel_content_related_counts.tsx | 2 +- .../panels/panel_content_related_detail.tsx | 2 +- .../view/panels/process_cube_icon.tsx | 9 +- .../resolver/view/process_event_dot.tsx | 19 +- yarn.lock | 12 ++ 20 files changed, 527 insertions(+), 72 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/resolver/lib/aabb.ts create mode 100644 x-pack/plugins/security_solution/public/resolver/store/data/visible_entities.test.ts diff --git a/x-pack/plugins/security_solution/package.json b/x-pack/plugins/security_solution/package.json index 73347e00e6b34..108ed66958856 100644 --- a/x-pack/plugins/security_solution/package.json +++ b/x-pack/plugins/security_solution/package.json @@ -16,9 +16,11 @@ "@types/lodash": "^4.14.110" }, "dependencies": { + "@types/rbush": "^3.0.0", + "@types/seedrandom": ">=2.0.0 <4.0.0", "lodash": "^4.17.15", "querystring": "^0.2.0", - "redux-devtools-extension": "^2.13.8", - "@types/seedrandom": ">=2.0.0 <4.0.0" + "rbush": "^3.0.1", + "redux-devtools-extension": "^2.13.8" } } diff --git a/x-pack/plugins/security_solution/public/resolver/lib/aabb.ts b/x-pack/plugins/security_solution/public/resolver/lib/aabb.ts new file mode 100644 index 0000000000000..0937d10c24d8e --- /dev/null +++ b/x-pack/plugins/security_solution/public/resolver/lib/aabb.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import * as vector2 from './vector2'; +import { AABB } from '../types'; + +/** + * Return a boolean indicating if 2 vector objects are equal. + */ +export function isEqual(a: AABB, b: AABB): boolean { + return vector2.isEqual(a.minimum, b.minimum) && vector2.isEqual(a.maximum, b.maximum); +} diff --git a/x-pack/plugins/security_solution/public/resolver/lib/vector2.ts b/x-pack/plugins/security_solution/public/resolver/lib/vector2.ts index 898ce6f6bacd2..35f17c9460f86 100644 --- a/x-pack/plugins/security_solution/public/resolver/lib/vector2.ts +++ b/x-pack/plugins/security_solution/public/resolver/lib/vector2.ts @@ -40,6 +40,13 @@ export function applyMatrix3([x, y]: Vector2, [m11, m12, m13, m21, m22, m23]: Ma return [x * m11 + y * m12 + m13, x * m21 + y * m22 + m23]; } +/** + * Returns a boolean indicating equality of two vectors. + */ +export function isEqual([x1, y1]: Vector2, [x2, y2]: Vector2): boolean { + return x1 === x2 && y1 === y2; +} + /** * Returns the distance between two vectors */ diff --git a/x-pack/plugins/security_solution/public/resolver/models/process_event.ts b/x-pack/plugins/security_solution/public/resolver/models/process_event.ts index 1094fee6da249..0286cca93b43f 100644 --- a/x-pack/plugins/security_solution/public/resolver/models/process_event.ts +++ b/x-pack/plugins/security_solution/public/resolver/models/process_event.ts @@ -24,6 +24,10 @@ function isValue(field: string | string[], value: string) { } } +export function isTerminatedProcess(passedEvent: ResolverEvent) { + return eventType(passedEvent) === 'processTerminated'; +} + /** * Returns a custom event type for a process event based on the event's metadata. */ diff --git a/x-pack/plugins/security_solution/public/resolver/store/data/__snapshots__/graphing.test.ts.snap b/x-pack/plugins/security_solution/public/resolver/store/data/__snapshots__/graphing.test.ts.snap index f21d3b2106812..8525ccd7b1548 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/data/__snapshots__/graphing.test.ts.snap +++ b/x-pack/plugins/security_solution/public/resolver/store/data/__snapshots__/graphing.test.ts.snap @@ -36,6 +36,9 @@ exports[`resolver graph layout when rendering two forks, and one fork has an ext Object { "edgeLineSegments": Array [ Object { + "metadata": Object { + "uniqueId": "parentToMid", + }, "points": Array [ Array [ 0, @@ -48,6 +51,9 @@ Object { ], }, Object { + "metadata": Object { + "uniqueId": "midway", + }, "points": Array [ Array [ 0, @@ -60,7 +66,9 @@ Object { ], }, Object { - "metadata": Object {}, + "metadata": Object { + "uniqueId": "", + }, "points": Array [ Array [ 0, @@ -73,7 +81,9 @@ Object { ], }, Object { - "metadata": Object {}, + "metadata": Object { + "uniqueId": "", + }, "points": Array [ Array [ 395.9797974644666, @@ -86,6 +96,9 @@ Object { ], }, Object { + "metadata": Object { + "uniqueId": "parentToMid13", + }, "points": Array [ Array [ 197.9898987322333, @@ -98,6 +111,9 @@ Object { ], }, Object { + "metadata": Object { + "uniqueId": "midway13", + }, "points": Array [ Array [ 296.98484809834997, @@ -110,7 +126,9 @@ Object { ], }, Object { - "metadata": Object {}, + "metadata": Object { + "uniqueId": "13", + }, "points": Array [ Array [ 296.98484809834997, @@ -123,7 +141,9 @@ Object { ], }, Object { - "metadata": Object {}, + "metadata": Object { + "uniqueId": "14", + }, "points": Array [ Array [ 494.9747468305833, @@ -136,6 +156,9 @@ Object { ], }, Object { + "metadata": Object { + "uniqueId": "parentToMid25", + }, "points": Array [ Array [ 593.9696961966999, @@ -148,6 +171,9 @@ Object { ], }, Object { + "metadata": Object { + "uniqueId": "midway25", + }, "points": Array [ Array [ 692.9646455628166, @@ -160,7 +186,9 @@ Object { ], }, Object { - "metadata": Object {}, + "metadata": Object { + "uniqueId": "25", + }, "points": Array [ Array [ 692.9646455628166, @@ -173,7 +201,9 @@ Object { ], }, Object { - "metadata": Object {}, + "metadata": Object { + "uniqueId": "26", + }, "points": Array [ Array [ 890.9545442950499, @@ -186,7 +216,9 @@ Object { ], }, Object { - "metadata": Object {}, + "metadata": Object { + "uniqueId": "67", + }, "points": Array [ Array [ 1088.9444430272833, @@ -344,7 +376,9 @@ exports[`resolver graph layout when rendering two nodes, one being the parent of Object { "edgeLineSegments": Array [ Object { - "metadata": Object {}, + "metadata": Object { + "uniqueId": "", + }, "points": Array [ Array [ 0, diff --git a/x-pack/plugins/security_solution/public/resolver/store/data/selectors.ts b/x-pack/plugins/security_solution/public/resolver/store/data/selectors.ts index ba415e6d83c8d..5654f1ca423f3 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/data/selectors.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/data/selectors.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import rbush from 'rbush'; import { createSelector } from 'reselect'; import { DataState, @@ -16,11 +17,20 @@ import { AdjacentProcessMap, Vector2, EdgeLineMetadata, + IndexedEntity, + IndexedEdgeLineSegment, + IndexedProcessNode, + AABB, + VisibleEntites, } from '../../types'; import { ResolverEvent } from '../../../../common/endpoint/types'; -import { eventTimestamp } from '../../../../common/endpoint/models/event'; +import * as event from '../../../../common/endpoint/models/event'; import { add as vector2Add, applyMatrix3 } from '../../lib/vector2'; -import { isGraphableProcess, uniquePidForProcess } from '../../models/process_event'; +import { + isGraphableProcess, + isTerminatedProcess, + uniquePidForProcess, +} from '../../models/process_event'; import { factory as indexedProcessTreeFactory, children as indexedProcessTreeChildren, @@ -29,6 +39,7 @@ import { levelOrder, } from '../../models/indexed_process_tree'; import { getFriendlyElapsedTime } from '../../lib/date'; +import { isEqual } from '../../lib/aabb'; const unit = 140; const distanceBetweenNodesInUnits = 2; @@ -81,6 +92,20 @@ export const graphableProcesses = createSelector( } ); +/** + * Process events that will be displayed as terminated. + */ +export const terminatedProcesses = createSelector( + ({ results }: DataState) => results, + function (results: DataState['results']) { + return new Set( + results.filter(isTerminatedProcess).map((terminatedEvent) => { + return uniquePidForProcess(terminatedEvent); + }) + ); + } +); + /** * In laying out the graph, we precalculate the 'width' of each subtree. The 'width' of the subtree is determined by its * descedants and the rule that each process node must be at least 1 unit apart. Enforcing that all nodes are at least @@ -157,7 +182,7 @@ function processEdgeLineSegments( ): EdgeLineSegment[] { const edgeLineSegments: EdgeLineSegment[] = []; for (const metadata of levelOrderWithWidths(indexedProcessTree, widths)) { - const edgeLineMetadata: EdgeLineMetadata = {}; + const edgeLineMetadata: EdgeLineMetadata = { uniqueId: '' }; /** * We only handle children, drawing lines back to their parents. The root has no parent, so we skip it */ @@ -168,6 +193,9 @@ function processEdgeLineSegments( const { process, parent, parentWidth } = metadata; const position = positions.get(process); const parentPosition = positions.get(parent); + const parentId = event.entityId(parent); + const processEntityId = event.entityId(process); + const edgeLineId = parentId ? parentId + processEntityId : parentId; if (position === undefined || parentPosition === undefined) { /** @@ -176,12 +204,13 @@ function processEdgeLineSegments( throw new Error(); } - const parentTime = eventTimestamp(parent); - const processTime = eventTimestamp(process); + const parentTime = event.eventTimestamp(parent); + const processTime = event.eventTimestamp(process); if (parentTime && processTime) { const elapsedTime = getFriendlyElapsedTime(parentTime, processTime); if (elapsedTime) edgeLineMetadata.elapsedTime = elapsedTime; } + edgeLineMetadata.uniqueId = edgeLineId; /** * The point halfway between the parent and child on the y axis, we sometimes have a hard angle here in the edge line @@ -226,6 +255,7 @@ function processEdgeLineSegments( const lineFromParentToMidwayLine: EdgeLineSegment = { points: [parentPosition, [parentPosition[0], midwayY]], + metadata: { uniqueId: `parentToMid${edgeLineId}` }, }; const widthOfMidline = parentWidth - firstChildWidth / 2 - lastChildWidth / 2; @@ -246,6 +276,7 @@ function processEdgeLineSegments( midwayY, ], ], + metadata: { uniqueId: `midway${edgeLineId}` }, }; edgeLineSegments.push( @@ -508,18 +539,16 @@ export const processNodePositionsAndEdgeLineSegments = createSelector( for (const edgeLineSegment of edgeLineSegments) { const { points: [startPoint, endPoint], - metadata, } = edgeLineSegment; const transformedSegment: EdgeLineSegment = { + ...edgeLineSegment, points: [ applyMatrix3(startPoint, isometricTransformMatrix), applyMatrix3(endPoint, isometricTransformMatrix), ], }; - if (metadata) transformedSegment.metadata = metadata; - transformedEdgeLineSegments.push(transformedSegment); } @@ -530,6 +559,96 @@ export const processNodePositionsAndEdgeLineSegments = createSelector( } ); +const indexedProcessNodePositionsAndEdgeLineSegments = createSelector( + processNodePositionsAndEdgeLineSegments, + function visibleProcessNodePositionsAndEdgeLineSegments({ + /* eslint-disable no-shadow */ + processNodePositions, + edgeLineSegments, + /* eslint-enable no-shadow */ + }) { + const tree: rbush = new rbush(); + const processesToIndex: IndexedProcessNode[] = []; + const edgeLineSegmentsToIndex: IndexedEdgeLineSegment[] = []; + + // Make sure these numbers are big enough to cover the process nodes at all zoom levels. + // The process nodes don't extend equally in all directions from their center point. + const processNodeViewWidth = 720; + const processNodeViewHeight = 240; + const lineSegmentPadding = 30; + for (const [processEvent, position] of processNodePositions) { + const [nodeX, nodeY] = position; + const indexedEvent: IndexedProcessNode = { + minX: nodeX - 0.5 * processNodeViewWidth, + minY: nodeY - 0.5 * processNodeViewHeight, + maxX: nodeX + 0.5 * processNodeViewWidth, + maxY: nodeY + 0.5 * processNodeViewHeight, + position, + entity: processEvent, + type: 'processNode', + }; + processesToIndex.push(indexedEvent); + } + for (const edgeLineSegment of edgeLineSegments) { + const { + points: [[x1, y1], [x2, y2]], + } = edgeLineSegment; + const indexedLineSegment: IndexedEdgeLineSegment = { + minX: Math.min(x1, x2) - lineSegmentPadding, + minY: Math.min(y1, y2) - lineSegmentPadding, + maxX: Math.max(x1, x2) + lineSegmentPadding, + maxY: Math.max(y1, y2) + lineSegmentPadding, + entity: edgeLineSegment, + type: 'edgeLine', + }; + edgeLineSegmentsToIndex.push(indexedLineSegment); + } + tree.load([...processesToIndex, ...edgeLineSegmentsToIndex]); + return tree; + } +); + +export const visibleProcessNodePositionsAndEdgeLineSegments = createSelector( + indexedProcessNodePositionsAndEdgeLineSegments, + function visibleProcessNodePositionsAndEdgeLineSegments(tree) { + // memoize the results of this call to avoid unnecessarily rerunning + let lastBoundingBox: AABB | null = null; + let currentlyVisible: VisibleEntites = { + processNodePositions: new Map(), + connectingEdgeLineSegments: [], + }; + return (boundingBox: AABB) => { + if (lastBoundingBox !== null && isEqual(lastBoundingBox, boundingBox)) { + return currentlyVisible; + } else { + const { + minimum: [minX, minY], + maximum: [maxX, maxY], + } = boundingBox; + const entities = tree.search({ + minX, + minY, + maxX, + maxY, + }); + const visibleProcessNodePositions = new Map( + entities + .filter((entity): entity is IndexedProcessNode => entity.type === 'processNode') + .map((node) => [node.entity, node.position]) + ); + const connectingEdgeLineSegments = entities + .filter((entity): entity is IndexedEdgeLineSegment => entity.type === 'edgeLine') + .map((node) => node.entity); + currentlyVisible = { + processNodePositions: visibleProcessNodePositions, + connectingEdgeLineSegments, + }; + lastBoundingBox = boundingBox; + return currentlyVisible; + } + }; + } +); /** * Returns the `children` and `ancestors` limits for the current graph, if any. * diff --git a/x-pack/plugins/security_solution/public/resolver/store/data/visible_entities.test.ts b/x-pack/plugins/security_solution/public/resolver/store/data/visible_entities.test.ts new file mode 100644 index 0000000000000..f10cfe0ba466a --- /dev/null +++ b/x-pack/plugins/security_solution/public/resolver/store/data/visible_entities.test.ts @@ -0,0 +1,165 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Store, createStore } from 'redux'; +import { ResolverAction } from '../actions'; +import { resolverReducer } from '../reducer'; +import { ResolverState } from '../../types'; +import { LegacyEndpointEvent, ResolverEvent } from '../../../../common/endpoint/types'; +import { visibleProcessNodePositionsAndEdgeLineSegments } from '../selectors'; +import { mockProcessEvent } from '../../models/process_event_test_helpers'; + +describe('resolver visible entities', () => { + let processA: LegacyEndpointEvent; + let processB: LegacyEndpointEvent; + let processC: LegacyEndpointEvent; + let processD: LegacyEndpointEvent; + let processE: LegacyEndpointEvent; + let processF: LegacyEndpointEvent; + let processG: LegacyEndpointEvent; + let store: Store; + + beforeEach(() => { + /* + * A + * | + * B + * | + * C + * | + * D etc + */ + processA = mockProcessEvent({ + endgame: { + process_name: '', + event_type_full: 'process_event', + event_subtype_full: 'creation_event', + unique_pid: 0, + }, + }); + processB = mockProcessEvent({ + endgame: { + event_type_full: 'process_event', + event_subtype_full: 'already_running', + unique_pid: 1, + unique_ppid: 0, + }, + }); + processC = mockProcessEvent({ + endgame: { + event_type_full: 'process_event', + event_subtype_full: 'creation_event', + unique_pid: 2, + unique_ppid: 1, + }, + }); + processD = mockProcessEvent({ + endgame: { + event_type_full: 'process_event', + event_subtype_full: 'creation_event', + unique_pid: 3, + unique_ppid: 2, + }, + }); + processE = mockProcessEvent({ + endgame: { + event_type_full: 'process_event', + event_subtype_full: 'creation_event', + unique_pid: 4, + unique_ppid: 3, + }, + }); + processF = mockProcessEvent({ + endgame: { + event_type_full: 'process_event', + event_subtype_full: 'creation_event', + unique_pid: 5, + unique_ppid: 4, + }, + }); + processF = mockProcessEvent({ + endgame: { + event_type_full: 'process_event', + event_subtype_full: 'creation_event', + unique_pid: 6, + unique_ppid: 5, + }, + }); + processG = mockProcessEvent({ + endgame: { + event_type_full: 'process_event', + event_subtype_full: 'creation_event', + unique_pid: 7, + unique_ppid: 6, + }, + }); + store = createStore(resolverReducer, undefined); + }); + describe('when rendering a large tree with a small viewport', () => { + beforeEach(() => { + const events: ResolverEvent[] = [ + processA, + processB, + processC, + processD, + processE, + processF, + processG, + ]; + const action: ResolverAction = { + type: 'serverReturnedResolverData', + payload: { events, stats: new Map(), lineageLimits: { children: '', ancestors: '' } }, + }; + const cameraAction: ResolverAction = { type: 'userSetRasterSize', payload: [300, 200] }; + store.dispatch(action); + store.dispatch(cameraAction); + }); + it('the visibleProcessNodePositions list should only include 2 nodes', () => { + const { processNodePositions } = visibleProcessNodePositionsAndEdgeLineSegments( + store.getState() + )(0); + expect([...processNodePositions.keys()].length).toEqual(2); + }); + it('the visibleEdgeLineSegments list should only include one edge line', () => { + const { connectingEdgeLineSegments } = visibleProcessNodePositionsAndEdgeLineSegments( + store.getState() + )(0); + expect(connectingEdgeLineSegments.length).toEqual(1); + }); + }); + describe('when rendering a large tree with a large viewport', () => { + beforeEach(() => { + const events: ResolverEvent[] = [ + processA, + processB, + processC, + processD, + processE, + processF, + processG, + ]; + const action: ResolverAction = { + type: 'serverReturnedResolverData', + payload: { events, stats: new Map(), lineageLimits: { children: '', ancestors: '' } }, + }; + const cameraAction: ResolverAction = { type: 'userSetRasterSize', payload: [2000, 2000] }; + store.dispatch(action); + store.dispatch(cameraAction); + }); + it('the visibleProcessNodePositions list should include all process nodes', () => { + const { processNodePositions } = visibleProcessNodePositionsAndEdgeLineSegments( + store.getState() + )(0); + expect([...processNodePositions.keys()].length).toEqual(5); + }); + it('the visibleEdgeLineSegments list include all lines', () => { + const { connectingEdgeLineSegments } = visibleProcessNodePositionsAndEdgeLineSegments( + store.getState() + )(0); + expect(connectingEdgeLineSegments.length).toEqual(4); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/resolver/store/middleware.ts b/x-pack/plugins/security_solution/public/resolver/store/middleware.ts index 343b4e1a14478..a1807255b5eaf 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/middleware.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/middleware.ts @@ -112,7 +112,6 @@ export const resolverMiddlewareFactory: MiddlewareFactory = (context) => { query: { events: 100 }, } ); - api.dispatch({ type: 'serverReturnedRelatedEventData', payload: result, diff --git a/x-pack/plugins/security_solution/public/resolver/store/selectors.ts b/x-pack/plugins/security_solution/public/resolver/store/selectors.ts index 3a5c48009e5bb..5599b7e8ab613 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/selectors.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/selectors.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { createSelector } from 'reselect'; import * as cameraSelectors from './camera/selectors'; import * as dataSelectors from './data/selectors'; import * as uiSelectors from './ui/selectors'; @@ -60,6 +61,11 @@ export const processAdjacencies = composeSelectors( dataSelectors.processAdjacencies ); +export const terminatedProcesses = composeSelectors( + dataStateSelector, + dataSelectors.terminatedProcesses +); + /** * Returns a map of `ResolverEvent` entity_id to their related event and alert statistics */ @@ -171,3 +177,27 @@ function composeSelectors( ): (state: OuterState) => ReturnValue { return (state) => secondSelector(selector(state)); } + +const boundingBox = composeSelectors(cameraStateSelector, cameraSelectors.viewableBoundingBox); +const indexedProcessNodesAndEdgeLineSegments = composeSelectors( + dataStateSelector, + dataSelectors.visibleProcessNodePositionsAndEdgeLineSegments +); + +/** + * Return the visible edge lines and process nodes based on the camera position at `time`. + * The bounding box represents what the camera can see. The camera position is a function of time because it can be + * animated. So in order to get the currently visible entities, we need to pass in time. + */ +export const visibleProcessNodePositionsAndEdgeLineSegments = createSelector( + indexedProcessNodesAndEdgeLineSegments, + boundingBox, + function ( + /* eslint-disable no-shadow */ + indexedProcessNodesAndEdgeLineSegments, + boundingBox + /* eslint-enable no-shadow */ + ) { + return (time: number) => indexedProcessNodesAndEdgeLineSegments(boundingBox(time)); + } +); diff --git a/x-pack/plugins/security_solution/public/resolver/types.ts b/x-pack/plugins/security_solution/public/resolver/types.ts index f0e401dd2e893..0742fa2e30560 100644 --- a/x-pack/plugins/security_solution/public/resolver/types.ts +++ b/x-pack/plugins/security_solution/public/resolver/types.ts @@ -5,7 +5,7 @@ */ import { Store } from 'redux'; - +import { BBox } from 'rbush'; import { ResolverAction } from './store/actions'; export { ResolverAction } from './store/actions'; import { @@ -142,6 +142,36 @@ export type CameraState = { } ); +/** + * Wrappers around our internal types that make them compatible with `rbush`. + */ +export type IndexedEntity = IndexedEdgeLineSegment | IndexedProcessNode; + +/** + * The entity stored in rbush for resolver edge lines. + */ +export interface IndexedEdgeLineSegment extends BBox { + type: 'edgeLine'; + entity: EdgeLineSegment; +} + +/** + * The entity store in rbush for resolver process nodes. + */ +export interface IndexedProcessNode extends BBox { + type: 'processNode'; + entity: ResolverEvent; + position: Vector2; +} + +/** + * A type containing all things to actually be rendered to the DOM. + */ +export interface VisibleEntites { + processNodePositions: ProcessPositions; + connectingEdgeLineSegments: EdgeLineSegment[]; +} + /** * State for `data` reducer which handles receiving Resolver data from the backend. */ @@ -287,6 +317,8 @@ export interface DurationDetails { */ export interface EdgeLineMetadata { elapsedTime?: DurationDetails; + // A string of the two joined process nodes concatted together. + uniqueId: string; } /** * A tuple of 2 vector2 points forming a polyline. Used to connect process nodes in the graph. @@ -298,7 +330,7 @@ export type EdgeLinePoints = Vector2[]; */ export interface EdgeLineSegment { points: EdgeLinePoints; - metadata?: EdgeLineMetadata; + metadata: EdgeLineMetadata; } /** diff --git a/x-pack/plugins/security_solution/public/resolver/view/assets.tsx b/x-pack/plugins/security_solution/public/resolver/view/assets.tsx index 82f969b755b2f..442a90f0a5753 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/assets.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/assets.tsx @@ -12,8 +12,6 @@ import styled from 'styled-components'; import { i18n } from '@kbn/i18n'; import { useUiSetting } from '../../common/lib/kibana'; import { DEFAULT_DARK_MODE } from '../../../common/constants'; -import { ResolverEvent } from '../../../common/endpoint/types'; -import * as processModel from '../models/process_event'; import { ResolverProcessType } from '../types'; type ResolverColorNames = @@ -417,27 +415,13 @@ const processTypeToCube: Record = { unknownEvent: 'runningProcessCube', }; -/** - * This will return which type the ResolverEvent will display as in the Node component - * it will be something like 'runningProcessCube' or 'terminatedProcessCube' - * - * @param processEvent {ResolverEvent} the event to get the Resolver Component Node type of - */ -export function nodeType(processEvent: ResolverEvent): keyof NodeStyleMap { - const processType = processModel.eventType(processEvent); - if (processType in processTypeToCube) { - return processTypeToCube[processType]; - } - return 'runningProcessCube'; -} - /** * A hook to bring Resolver theming information into components. */ export const useResolverTheme = (): { colorMap: ColorMap; nodeAssets: NodeStyleMap; - cubeAssetsForNode: (arg0: ResolverEvent) => NodeStyleConfig; + cubeAssetsForNode: (isProcessTerimnated: boolean, isProcessOrigin: boolean) => NodeStyleConfig; } => { const isDarkMode = useUiSetting(DEFAULT_DARK_MODE); const theme = isDarkMode ? euiThemeAmsterdamDark : euiThemeAmsterdamLight; @@ -511,12 +495,14 @@ export const useResolverTheme = (): { }, }; - /** - * Export assets to reuse symbols/icons in other places in the app (e.g. tables, etc.) - * @param processEvent : The process event to fetch node assets for - */ - function cubeAssetsForNode(processEvent: ResolverEvent) { - return nodeAssets[nodeType(processEvent)]; + function cubeAssetsForNode(isProcessTerminated: boolean, isProcessOrigin: boolean) { + if (isProcessTerminated) { + return nodeAssets[processTypeToCube.processTerminated]; + } else if (isProcessOrigin) { + return nodeAssets[processTypeToCube.processCausedAlert]; + } else { + return nodeAssets[processTypeToCube.processRan]; + } } return { colorMap, nodeAssets, cubeAssetsForNode }; diff --git a/x-pack/plugins/security_solution/public/resolver/view/index.tsx b/x-pack/plugins/security_solution/public/resolver/view/index.tsx index 9dfc9a45fafeb..9b7114b56495c 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/index.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/index.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useLayoutEffect } from 'react'; +import React, { useLayoutEffect, useContext } from 'react'; import { useSelector, useDispatch } from 'react-redux'; import styled from 'styled-components'; import { EuiLoadingSpinner } from '@elastic/eui'; @@ -16,9 +16,10 @@ import { GraphControls } from './graph_controls'; import { ProcessEventDot } from './process_event_dot'; import { useCamera } from './use_camera'; import { SymbolDefinitions, useResolverTheme } from './assets'; +import { entityId } from '../../../common/endpoint/models/event'; import { ResolverAction } from '../types'; import { ResolverEvent } from '../../../common/endpoint/types'; -import * as eventModel from '../../../common/endpoint/models/event'; +import { SideEffectContext } from './side_effect_context'; interface StyledResolver { backgroundColor: string; @@ -74,17 +75,20 @@ export const Resolver = React.memo(function Resolver({ className?: string; selectedEvent?: ResolverEvent; }) { - const { processNodePositions, edgeLineSegments } = useSelector( - selectors.processNodePositionsAndEdgeLineSegments - ); + const { timestamp } = useContext(SideEffectContext); + + const { processNodePositions, connectingEdgeLineSegments } = useSelector( + selectors.visibleProcessNodePositionsAndEdgeLineSegments + )(timestamp()); const dispatch: (action: ResolverAction) => unknown = useDispatch(); const { processToAdjacencyMap } = useSelector(selectors.processAdjacencies); - const relatedEventsStats = useSelector(selectors.relatedEventsStats); const { projectionMatrix, ref, onMouseDown } = useCamera(); const isLoading = useSelector(selectors.isLoading); const hasError = useSelector(selectors.hasError); + const relatedEventsStats = useSelector(selectors.relatedEventsStats); const activeDescendantId = useSelector(selectors.uiActiveDescendantId); + const terminatedProcesses = useSelector(selectors.terminatedProcesses); const { colorMap } = useResolverTheme(); useLayoutEffect(() => { @@ -123,10 +127,10 @@ export const Resolver = React.memo(function Resolver({ tabIndex={0} aria-activedescendant={activeDescendantId || undefined} > - {edgeLineSegments.map(({ points: [startPosition, endPosition], metadata }, index) => ( + {connectingEdgeLineSegments.map(({ points: [startPosition, endPosition], metadata }) => ( { const adjacentNodeMap = processToAdjacencyMap.get(processEvent); + const processEntityId = entityId(processEvent); if (!adjacentNodeMap) { // This should never happen throw new Error('Issue calculating adjacency node map.'); @@ -145,7 +150,9 @@ export const Resolver = React.memo(function Resolver({ projectionMatrix={projectionMatrix} event={processEvent} adjacentNodeMap={adjacentNodeMap} - relatedEventsStats={relatedEventsStats.get(eventModel.entityId(processEvent))} + relatedEventsStats={relatedEventsStats.get(entityId(processEvent))} + isProcessTerminated={terminatedProcesses.has(processEntityId)} + isProcessOrigin={false} /> ); })} diff --git a/x-pack/plugins/security_solution/public/resolver/view/panel.tsx b/x-pack/plugins/security_solution/public/resolver/view/panel.tsx index 4bef2f4d2a10e..c8f6512077a6f 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/panel.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/panel.tsx @@ -51,6 +51,7 @@ const PanelContent = memo(function PanelContent() { const urlSearch = history.location.search; const dispatch = useResolverDispatch(); + const { timestamp } = useContext(SideEffectContext); const queryParams: CrumbInfo = useMemo(() => { return { crumbId: '', crumbEvent: '', ...querystring.parse(urlSearch.slice(1)) }; }, [urlSearch]); @@ -84,7 +85,7 @@ const PanelContent = memo(function PanelContent() { const paramsSelectedEvent = useMemo(() => { return graphableProcesses.find((evt) => event.entityId(evt) === idFromParams); }, [graphableProcesses, idFromParams]); - const { timestamp } = useContext(SideEffectContext); + const [lastUpdatedProcess, setLastUpdatedProcess] = useState(null); /** @@ -218,11 +219,19 @@ const PanelContent = memo(function PanelContent() { }, [panelToShow, dispatch]); const currentPanelView = useSelector(selectors.currentPanelView); + const terminatedProcesses = useSelector(selectors.terminatedProcesses); + const processEntityId = uiSelectedEvent ? event.entityId(uiSelectedEvent) : undefined; + const isProcessTerminated = processEntityId ? terminatedProcesses.has(processEntityId) : false; const panelInstance = useMemo(() => { if (currentPanelView === 'processDetails') { return ( - + ); } @@ -261,7 +270,13 @@ const PanelContent = memo(function PanelContent() { ); } // The default 'Event List' / 'List of all processes' view - return ; + return ( + + ); }, [ uiSelectedEvent, crumbEvent, @@ -269,6 +284,7 @@ const PanelContent = memo(function PanelContent() { pushToQueryParams, relatedStatsForIdFromParams, currentPanelView, + isProcessTerminated, ]); return <>{panelInstance}; diff --git a/x-pack/plugins/security_solution/public/resolver/view/panels/panel_content_process_detail.tsx b/x-pack/plugins/security_solution/public/resolver/view/panels/panel_content_process_detail.tsx index fcb7bf1d12e1b..3127c7132df3d 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/panels/panel_content_process_detail.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/panels/panel_content_process_detail.tsx @@ -41,10 +41,14 @@ const StyledDescriptionList = styled(EuiDescriptionList)` */ export const ProcessDetails = memo(function ProcessDetails({ processEvent, + isProcessTerminated, + isProcessOrigin, pushToQueryParams, }: { processEvent: ResolverEvent; - pushToQueryParams: (arg0: CrumbInfo) => unknown; + isProcessTerminated: boolean; + isProcessOrigin: boolean; + pushToQueryParams: (queryStringKeyValuePair: CrumbInfo) => unknown; }) { const processName = event.eventName(processEvent); const processInfoEntry = useMemo(() => { @@ -178,8 +182,8 @@ export const ProcessDetails = memo(function ProcessDetails({ if (!processEvent) { return { descriptionText: '' }; } - return cubeAssetsForNode(processEvent); - }, [processEvent, cubeAssetsForNode]); + return cubeAssetsForNode(isProcessTerminated, isProcessOrigin); + }, [processEvent, cubeAssetsForNode, isProcessTerminated, isProcessOrigin]); const titleId = useMemo(() => htmlIdGenerator('resolverTable')(), []); return ( @@ -188,7 +192,10 @@ export const ProcessDetails = memo(function ProcessDetails({

- + {processName}

diff --git a/x-pack/plugins/security_solution/public/resolver/view/panels/panel_content_process_list.tsx b/x-pack/plugins/security_solution/public/resolver/view/panels/panel_content_process_list.tsx index 86ae10b3b38c8..9152649c07abf 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/panels/panel_content_process_list.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/panels/panel_content_process_list.tsx @@ -28,8 +28,12 @@ import { ResolverEvent } from '../../../../common/endpoint/types'; */ export const ProcessListWithCounts = memo(function ProcessListWithCounts({ pushToQueryParams, + isProcessTerminated, + isProcessOrigin, }: { - pushToQueryParams: (arg0: CrumbInfo) => unknown; + pushToQueryParams: (queryStringKeyValuePair: CrumbInfo) => unknown; + isProcessTerminated: boolean; + isProcessOrigin: boolean; }) { interface ProcessTableView { name: string; @@ -82,7 +86,10 @@ export const ProcessListWithCounts = memo(function ProcessListWithCounts({ pushToQueryParams({ crumbId: event.entityId(item.event), crumbEvent: '' }); }} > - + {name} ); @@ -114,7 +121,7 @@ export const ProcessListWithCounts = memo(function ProcessListWithCounts({ }, }, ], - [pushToQueryParams, handleBringIntoViewClick] + [pushToQueryParams, handleBringIntoViewClick, isProcessOrigin, isProcessTerminated] ); const { processNodePositions } = useSelector(selectors.processNodePositionsAndEdgeLineSegments); diff --git a/x-pack/plugins/security_solution/public/resolver/view/panels/panel_content_related_counts.tsx b/x-pack/plugins/security_solution/public/resolver/view/panels/panel_content_related_counts.tsx index 2e4211f568ffe..880ee1dc7a10a 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/panels/panel_content_related_counts.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/panels/panel_content_related_counts.tsx @@ -30,7 +30,7 @@ export const EventCountsForProcess = memo(function EventCountsForProcess({ relatedStats, }: { processEvent: ResolverEvent; - pushToQueryParams: (arg0: CrumbInfo) => unknown; + pushToQueryParams: (queryStringKeyValuePair: CrumbInfo) => unknown; relatedStats: ResolverNodeStats; }) { interface EventCountsTableView { diff --git a/x-pack/plugins/security_solution/public/resolver/view/panels/panel_content_related_detail.tsx b/x-pack/plugins/security_solution/public/resolver/view/panels/panel_content_related_detail.tsx index 1fe6599e0829a..f27ec56fef697 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/panels/panel_content_related_detail.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/panels/panel_content_related_detail.tsx @@ -96,7 +96,7 @@ export const RelatedEventDetail = memo(function RelatedEventDetail({ }: { relatedEventId: string; parentEvent: ResolverEvent; - pushToQueryParams: (arg0: CrumbInfo) => unknown; + pushToQueryParams: (queryStringKeyValuePair: CrumbInfo) => unknown; countForParent: number | undefined; }) { const processName = (parentEvent && event.eventName(parentEvent)) || '*'; diff --git a/x-pack/plugins/security_solution/public/resolver/view/panels/process_cube_icon.tsx b/x-pack/plugins/security_solution/public/resolver/view/panels/process_cube_icon.tsx index 29ffe154d5719..98eea51a011b6 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/panels/process_cube_icon.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/panels/process_cube_icon.tsx @@ -5,7 +5,6 @@ */ import React, { memo } from 'react'; -import { ResolverEvent } from '../../../../common/endpoint/types'; import { useResolverTheme } from '../assets'; /** @@ -13,12 +12,14 @@ import { useResolverTheme } from '../assets'; * Nodes on the graph and what's in the table. Using the same symbol in both places (as below) could help with that. */ export const CubeForProcess = memo(function CubeForProcess({ - processEvent, + isProcessTerminated, + isProcessOrigin, }: { - processEvent: ResolverEvent; + isProcessTerminated: boolean; + isProcessOrigin: boolean; }) { const { cubeAssetsForNode } = useResolverTheme(); - const { cubeSymbol, descriptionText } = cubeAssetsForNode(processEvent); + const { cubeSymbol, descriptionText } = cubeAssetsForNode(isProcessTerminated, isProcessOrigin); return ( <> diff --git a/x-pack/plugins/security_solution/public/resolver/view/process_event_dot.tsx b/x-pack/plugins/security_solution/public/resolver/view/process_event_dot.tsx index 78b70611a6972..e7c9960f78052 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/process_event_dot.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/process_event_dot.tsx @@ -15,7 +15,7 @@ import querystring from 'querystring'; import { NodeSubMenu, subMenuAssets } from './submenu'; import { applyMatrix3 } from '../lib/vector2'; import { Vector2, Matrix3, AdjacentProcessMap } from '../types'; -import { SymbolIds, useResolverTheme, calculateResolverFontSize, nodeType } from './assets'; +import { SymbolIds, useResolverTheme, calculateResolverFontSize } from './assets'; import { ResolverEvent, ResolverNodeStats } from '../../../common/endpoint/types'; import { useResolverDispatch } from './use_resolver_dispatch'; import * as eventModel from '../../../common/endpoint/models/event'; @@ -239,6 +239,8 @@ const ProcessEventDotComponents = React.memo( event, projectionMatrix, adjacentNodeMap, + isProcessTerminated, + isProcessOrigin, relatedEventsStats, }: { /** @@ -262,6 +264,16 @@ const ProcessEventDotComponents = React.memo( */ adjacentNodeMap: AdjacentProcessMap; /** + * Whether or not to show the process as terminated. + */ + isProcessTerminated: boolean; + /** + * Whether or not to show the process as the originating event. + */ + isProcessOrigin: boolean; + /** + * A collection of events related to the current node and statistics (e.g. counts indexed by event type) + * to provide the user some visibility regarding the contents thereof. * Statistics for the number of related events and alerts for this process node */ relatedEventsStats?: ResolverNodeStats; @@ -363,7 +375,7 @@ const ProcessEventDotComponents = React.memo( }) | null; } = React.createRef(); - const { colorMap, nodeAssets } = useResolverTheme(); + const { colorMap, cubeAssetsForNode } = useResolverTheme(); const { backingFill, cubeSymbol, @@ -371,7 +383,8 @@ const ProcessEventDotComponents = React.memo( isLabelFilled, labelButtonFill, strokeColor, - } = nodeAssets[nodeType(event)]; + } = cubeAssetsForNode(isProcessTerminated, isProcessOrigin); + const resolverNodeIdGenerator = useMemo(() => htmlIdGenerator('resolverNode'), []); const nodeId = useMemo(() => resolverNodeIdGenerator(selfId), [ diff --git a/yarn.lock b/yarn.lock index 60122f8b8cde7..53fef40b44c93 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5616,6 +5616,11 @@ resolved "https://registry.yarnpkg.com/@types/q/-/q-1.5.2.tgz#690a1475b84f2a884fd07cd797c00f5f31356ea8" integrity sha512-ce5d3q03Ex0sy4R14722Rmt6MT07Ua+k4FwDfdcToYJcMKNtRVQvJ6JCAPdAmAnbRb6CsX6aYb9m96NGod9uTw== +"@types/rbush@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@types/rbush/-/rbush-3.0.0.tgz#b6887d99b159e87ae23cd14eceff34f139842aa6" + integrity sha512-W3ue/GYWXBOpkRm0VSoifrP3HV0Ni47aVJWvXyWMcbtpBy/l/K/smBRiJ+fI8f7shXRjZBiux+iJzYbh7VmcZg== + "@types/reach__router@^1.2.3", "@types/reach__router@^1.2.6": version "1.2.6" resolved "https://registry.yarnpkg.com/@types/reach__router/-/reach__router-1.2.6.tgz#b14cf1adbd1a365d204bbf6605cd9dd7b8816c87" @@ -25291,6 +25296,13 @@ raw-loader@~0.5.1: resolved "https://registry.yarnpkg.com/raw-loader/-/raw-loader-0.5.1.tgz#0c3d0beaed8a01c966d9787bf778281252a979aa" integrity sha1-DD0L6u2KAclm2Xh793goElKpeao= +rbush@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/rbush/-/rbush-3.0.1.tgz#5fafa8a79b3b9afdfe5008403a720cc1de882ecf" + integrity sha512-XRaVO0YecOpEuIvbhbpTrZgoiI6xBlz6hnlr6EHhd+0x9ase6EmeN+hdwwUaJvLcsFFQ8iWVF1GAK1yB0BWi0w== + dependencies: + quickselect "^2.0.0" + rc@^1.0.1, rc@^1.1.6, rc@^1.2.7, rc@^1.2.8: version "1.2.8" resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed" From c8089a5aa2ff21c5c56dbefc2f24ced67a78992f Mon Sep 17 00:00:00 2001 From: Jean-Louis Leysens Date: Fri, 26 Jun 2020 16:25:50 +0200 Subject: [PATCH 11/13] [Ingest Pipelines Editor] First round of UX improvements (#69381) * First round of UX tweaks - Fixed potential text overflow issue on descriptions - Removed border around text input when editing description * Updated the on-failure pipeline description copy * Properly encode URI component pipeline names * use xjson editor in flyout * also hide the test flyout if we are editing a component * add much stronger dimming effect when in edit mode * also added dimming effect to moving state * remove box shadow if dimmed * add tooltips to dropzones * fix CITs after master merge * fix nested rendering of processors tree * only show the tooltip when the dropzone is unavaiable and visible * keep white background on dim * hide controls when moving * fix on blur bug * Rename variables and prefix booleans with "is" * Remove box shadow on all nested tree items * use classNames as it is intended to be used * Refactor SCSS values to variables * Added cancel move button - also hide the description in move mode when it is empty - update and refactor some shared sass variables - some number of sass changes to make labels play nice in move mode - changed the logic to not render the buttons when in move mode instead of display: none on them. The issue is with the tooltip not hiding when when we change to move mode and the mouse event "leave" does get through the tooltip element causing tooltips to hang even though the mouse has left them. * Fixes for monaco XJSON grammar parser and update form copy - Monaco XJSON worker was not handling trailing whitespace - Update copy in the processor configuration form Co-authored-by: Elastic Machine --- packages/kbn-monaco/src/xjson/grammar.ts | 3 +- packages/kbn-monaco/src/xjson/language.ts | 5 +- .../pipeline_form/pipeline_form.tsx | 1 + .../pipeline_processors_editor.helpers.tsx | 28 +- .../components/_shared.scss | 2 + .../on_failure_processors_title.tsx | 2 +- .../context_menu.tsx | 46 +-- .../inline_text_input.tsx | 60 ++-- .../messages.ts | 11 +- .../pipeline_processors_editor_item.scss | 50 +++- .../pipeline_processors_editor_item.tsx | 265 +++++++++++------- .../field_components/index.ts | 7 + .../field_components/xjson_editor.tsx | 66 +++++ .../processor_settings_form.tsx | 21 +- .../processors/custom.tsx | 15 +- .../components/drop_zone_button.tsx | 52 +++- .../components/private_tree.tsx | 60 ++-- .../processors_tree/components/tree_node.tsx | 50 +--- .../processors_tree/processors_tree.scss | 64 ++--- .../components/processors_tree/utils.ts | 4 +- .../pipeline_processors_editor.scss | 2 +- .../pipeline_processors_editor.tsx | 2 +- .../public/application/index.tsx | 3 +- .../application/mount_management_section.ts | 1 + .../ingest_pipelines/public/shared_imports.ts | 7 +- 25 files changed, 511 insertions(+), 316 deletions(-) create mode 100644 x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/_shared.scss create mode 100644 x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_settings_form/field_components/index.ts create mode 100644 x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_settings_form/field_components/xjson_editor.tsx diff --git a/packages/kbn-monaco/src/xjson/grammar.ts b/packages/kbn-monaco/src/xjson/grammar.ts index e95059f9ece2d..fbd7b3d319c1d 100644 --- a/packages/kbn-monaco/src/xjson/grammar.ts +++ b/packages/kbn-monaco/src/xjson/grammar.ts @@ -200,12 +200,13 @@ export const createParser = () => { try { value(); + white(); } catch (e) { errored = true; annos.push({ type: AnnoTypes.error, at: e.at - 1, text: e.message }); } if (!errored && ch) { - error('Syntax error'); + annos.push({ type: AnnoTypes.error, at: at, text: 'Syntax Error' }); } return { annotations: annos }; } diff --git a/packages/kbn-monaco/src/xjson/language.ts b/packages/kbn-monaco/src/xjson/language.ts index fe505818d3c9a..54b7004fecd8e 100644 --- a/packages/kbn-monaco/src/xjson/language.ts +++ b/packages/kbn-monaco/src/xjson/language.ts @@ -52,7 +52,10 @@ export const registerGrammarChecker = (editor: monaco.editor.IEditor) => { const updateAnnos = async () => { const { annotations } = await wps.getAnnos(); - const model = editor.getModel() as monaco.editor.ITextModel; + const model = editor.getModel() as monaco.editor.ITextModel | null; + if (!model) { + return; + } monaco.editor.setModelMarkers( model, OWNER, diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form.tsx index ec065a74abca0..05c9f0a08b0c7 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form.tsx @@ -81,6 +81,7 @@ export const PipelineForm: React.FunctionComponent = ({ }); const onEditorFlyoutOpen = useCallback(() => { + setIsTestingPipeline(false); setIsRequestVisible(false); }, [setIsRequestVisible]); diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/__jest__/pipeline_processors_editor.helpers.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/__jest__/pipeline_processors_editor.helpers.tsx index 320ccd0cbe8c0..7ad9aed3c44a4 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/__jest__/pipeline_processors_editor.helpers.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/__jest__/pipeline_processors_editor.helpers.tsx @@ -24,8 +24,15 @@ jest.mock('@elastic/eui', () => { }} /> ), - // Mocking EuiCodeEditor, which uses React Ace under the hood - EuiCodeEditor: (props: any) => ( + }; +}); + +jest.mock('../../../../../../../../src/plugins/kibana_react/public', () => { + const original = jest.requireActual('../../../../../../../../src/plugins/kibana_react/public'); + return { + ...original, + // Mocking CodeEditor, which uses React Monaco under the hood + CodeEditor: (props: any) => ( ) => { act(() => { find(`${processorSelector}.moveItemButton`).simulate('click'); }); + component.update(); act(() => { - find(dropZoneSelector).last().simulate('click'); + find(dropZoneSelector).simulate('click'); }); component.update(); }, @@ -122,13 +130,6 @@ const createActions = (testBed: TestBed) => { }); }, - duplicateProcessor(processorSelector: string) { - find(`${processorSelector}.moreMenu.button`).simulate('click'); - act(() => { - find(`${processorSelector}.moreMenu.duplicateButton`).simulate('click'); - }); - }, - startAndCancelMove(processorSelector: string) { act(() => { find(`${processorSelector}.moveItemButton`).simulate('click'); @@ -139,6 +140,13 @@ const createActions = (testBed: TestBed) => { }); }, + duplicateProcessor(processorSelector: string) { + find(`${processorSelector}.moreMenu.button`).simulate('click'); + act(() => { + find(`${processorSelector}.moreMenu.duplicateButton`).simulate('click'); + }); + }, + toggleOnFailure() { find('pipelineEditorOnFailureToggle').simulate('click'); }, diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/_shared.scss b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/_shared.scss new file mode 100644 index 0000000000000..8d17a3970d94f --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/_shared.scss @@ -0,0 +1,2 @@ +$dropZoneZIndex: 1; /* Prevent the next item down from obscuring the button */ +$cancelButtonZIndex: 2; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/on_failure_processors_title.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/on_failure_processors_title.tsx index 6451096c897d7..251a2ffe95212 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/on_failure_processors_title.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/on_failure_processors_title.tsx @@ -31,7 +31,7 @@ export const OnFailureProcessorsTitle: FunctionComponent = () => { void; onDelete: () => void; @@ -20,9 +22,13 @@ interface Props { } export const ContextMenu: FunctionComponent = (props) => { - const { showAddOnFailure, onDuplicate, onAddOnFailure, onDelete, disabled } = props; + const { showAddOnFailure, onDuplicate, onAddOnFailure, onDelete, disabled, hidden } = props; const [isOpen, setIsOpen] = useState(false); + const containerClasses = classNames({ + 'pipelineProcessorsEditor__item--displayNone': hidden, + }); + const contextMenuItems = [ = (props) => { ].filter(Boolean) as JSX.Element[]; return ( - setIsOpen(false)} - button={ - setIsOpen((v) => !v)} - iconType="boxesHorizontal" - aria-label={editorItemMessages.moreButtonAriaLabel} - /> - } - > - - +
+ setIsOpen(false)} + button={ + setIsOpen((v) => !v)} + iconType="boxesHorizontal" + aria-label={editorItemMessages.moreButtonAriaLabel} + /> + } + > + + +
); }; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/pipeline_processors_editor_item/inline_text_input.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/pipeline_processors_editor_item/inline_text_input.tsx index e0b67bc907ca9..00ac8d4f6d729 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/pipeline_processors_editor_item/inline_text_input.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/pipeline_processors_editor_item/inline_text_input.tsx @@ -3,7 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ - +import classNames from 'classnames'; import React, { FunctionComponent, useState, useEffect, useCallback } from 'react'; import { EuiFieldText, EuiText, keyCodes } from '@elastic/eui'; @@ -11,10 +11,12 @@ export interface Props { placeholder: string; ariaLabel: string; onChange: (value: string) => void; + disabled: boolean; text?: string; } export const InlineTextInput: FunctionComponent = ({ + disabled, placeholder, text, ariaLabel, @@ -23,26 +25,17 @@ export const InlineTextInput: FunctionComponent = ({ const [isShowingTextInput, setIsShowingTextInput] = useState(false); const [textValue, setTextValue] = useState(text ?? ''); - const content = isShowingTextInput ? ( - el?.focus()} - onChange={(event) => setTextValue(event.target.value)} - /> - ) : ( - - {text || {placeholder}} - - ); + const containerClasses = classNames('pipelineProcessorsEditor__item__textContainer', { + 'pipelineProcessorsEditor__item__textContainer--notEditing': !isShowingTextInput && !disabled, + }); const submitChange = useCallback(() => { - setIsShowingTextInput(false); - onChange(textValue); + // Give any on blur handlers the chance to complete if the user is + // tabbing over this component. + setTimeout(() => { + setIsShowingTextInput(false); + onChange(textValue); + }); }, [setIsShowingTextInput, onChange, textValue]); useEffect(() => { @@ -62,14 +55,27 @@ export const InlineTextInput: FunctionComponent = ({ }; }, [isShowingTextInput, submitChange, setIsShowingTextInput]); - return ( -
setIsShowingTextInput(true)} - onBlur={submitChange} - > - {content} + return isShowingTextInput && !disabled ? ( +
+ el?.focus()} + onChange={(event) => setTextValue(event.target.value)} + /> +
+ ) : ( +
setIsShowingTextInput(true)}> + +
+ {text || {placeholder}} +
+
); }; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/pipeline_processors_editor_item/messages.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/pipeline_processors_editor_item/messages.ts index 67dbf2708d665..913902d295503 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/pipeline_processors_editor_item/messages.ts +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/pipeline_processors_editor_item/messages.ts @@ -10,12 +10,9 @@ export const editorItemMessages = { moveButtonLabel: i18n.translate('xpack.ingestPipelines.pipelineEditor.item.moveButtonLabel', { defaultMessage: 'Move this processor', }), - editorButtonLabel: i18n.translate( - 'xpack.ingestPipelines.pipelineEditor.item.editButtonAriaLabel', - { - defaultMessage: 'Edit this processor', - } - ), + editButtonLabel: i18n.translate('xpack.ingestPipelines.pipelineEditor.item.editButtonAriaLabel', { + defaultMessage: 'Edit this processor', + }), duplicateButtonLabel: i18n.translate( 'xpack.ingestPipelines.pipelineEditor.item.moreMenu.duplicateButtonLabel', { @@ -31,7 +28,7 @@ export const editorItemMessages = { cancelMoveButtonLabel: i18n.translate( 'xpack.ingestPipelines.pipelineEditor.item.cancelMoveButtonAriaLabel', { - defaultMessage: 'Cancel moving this processor', + defaultMessage: 'Cancel move', } ), deleteButtonLabel: i18n.translate( diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/pipeline_processors_editor_item/pipeline_processors_editor_item.scss b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/pipeline_processors_editor_item/pipeline_processors_editor_item.scss index a17e644853847..6b5e118084606 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/pipeline_processors_editor_item/pipeline_processors_editor_item.scss +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/pipeline_processors_editor_item/pipeline_processors_editor_item.scss @@ -1,17 +1,57 @@ +@import '../shared'; + .pipelineProcessorsEditor__item { + transition: border-color 1s; + min-height: 50px; + &--selected { + border: 1px solid $euiColorPrimary; + } + + &--displayNone { + display: none; + } + + &--dimmed { + box-shadow: none; + } + + // Remove the box-shadow on all nested items + .pipelineProcessorsEditor__item { + box-shadow: none !important; + } + + &__processorTypeLabel { + line-height: $euiButtonHeightSmall; + } + &__textContainer { padding: 4px; border-radius: 2px; - transition: border-color .3s; - border: 2px solid #FFF; + transition: border-color 0.3s; + border: 2px solid transparent; - &:hover { - border: 2px solid $euiColorLightShade; + &--notEditing { + &:hover { + border: 2px solid $euiColorLightShade; + } } } + + &__description { + overflow-x: hidden; + white-space: nowrap; + text-overflow: ellipsis; + max-width: 600px; + } + &__textInput { height: 21px; - min-width: 100px; + min-width: 150px; + } + + &__cancelMoveButton { + // Ensure that the cancel button is above the drop zones + z-index: $cancelButtonZIndex; } } diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/pipeline_processors_editor_item/pipeline_processors_editor_item.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/pipeline_processors_editor_item/pipeline_processors_editor_item.tsx index 0eb259db75f47..0fe804adaeb48 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/pipeline_processors_editor_item/pipeline_processors_editor_item.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/pipeline_processors_editor_item/pipeline_processors_editor_item.tsx @@ -4,8 +4,17 @@ * you may not use this file except in compliance with the Elastic License. */ +import classNames from 'classnames'; import React, { FunctionComponent, memo } from 'react'; -import { EuiButtonIcon, EuiFlexGroup, EuiFlexItem, EuiText, EuiToolTip } from '@elastic/eui'; +import { + EuiButtonIcon, + EuiButton, + EuiFlexGroup, + EuiFlexItem, + EuiPanel, + EuiText, + EuiToolTip, +} from '@elastic/eui'; import { ProcessorInternal, ProcessorSelector } from '../../types'; import { selectorToDataTestSubject } from '../../utils'; @@ -17,6 +26,7 @@ import './pipeline_processors_editor_item.scss'; import { InlineTextInput } from './inline_text_input'; import { ContextMenu } from './context_menu'; import { editorItemMessages } from './messages'; +import { ProcessorInfo } from '../processors_tree'; export interface Handlers { onMove: () => void; @@ -25,127 +35,166 @@ export interface Handlers { export interface Props { processor: ProcessorInternal; - selected: boolean; handlers: Handlers; selector: ProcessorSelector; description?: string; + movingProcessor?: ProcessorInfo; + renderOnFailureHandlers?: () => React.ReactNode; } export const PipelineProcessorsEditorItem: FunctionComponent = memo( - ({ processor, description, handlers: { onCancelMove, onMove }, selector, selected }) => { + ({ + processor, + description, + handlers: { onCancelMove, onMove }, + selector, + movingProcessor, + renderOnFailureHandlers, + }) => { const { state: { editor, processorsDispatch }, } = usePipelineProcessorsContext(); - const disabled = editor.mode.id !== 'idle'; - const isDarkBold = - editor.mode.id !== 'editingProcessor' || processor.id === editor.mode.arg.processor.id; + const isDisabled = editor.mode.id !== 'idle'; + const isInMoveMode = Boolean(movingProcessor); + const isMovingThisProcessor = processor.id === movingProcessor?.id; + const isEditingThisProcessor = + editor.mode.id === 'editingProcessor' && processor.id === editor.mode.arg.processor.id; + const isEditingOtherProcessor = + editor.mode.id === 'editingProcessor' && !isEditingThisProcessor; + const isMovingOtherProcessor = editor.mode.id === 'movingProcessor' && !isMovingThisProcessor; + const isDimmed = isEditingOtherProcessor || isMovingOtherProcessor; + + const panelClasses = classNames('pipelineProcessorsEditor__item', { + 'pipelineProcessorsEditor__item--selected': isMovingThisProcessor || isEditingThisProcessor, + 'pipelineProcessorsEditor__item--dimmed': isDimmed, + }); + + const actionElementClasses = classNames({ + 'pipelineProcessorsEditor__item--displayNone': isInMoveMode, + }); + + const inlineTextInputContainerClasses = classNames({ + 'pipelineProcessorsEditor__item--displayNone': isInMoveMode && !processor.options.description, + }); + + const cancelMoveButtonClasses = classNames('pipelineProcessorsEditor__item__cancelMoveButton', { + 'pipelineProcessorsEditor__item--displayNone': !isMovingThisProcessor, + }); return ( - - - - - - {processor.type} - - - - { - let nextOptions: Record; - if (!nextDescription) { - const { description: __, ...restOptions } = processor.options; - nextOptions = restOptions; - } else { - nextOptions = { - ...processor.options, - description: nextDescription, - }; - } - processorsDispatch({ - type: 'updateProcessor', - payload: { - processor: { - ...processor, - options: nextOptions, + + + + + + + {processor.type} + + + + { + let nextOptions: Record; + if (!nextDescription) { + const { description: __, ...restOptions } = processor.options; + nextOptions = restOptions; + } else { + nextOptions = { + ...processor.options, + description: nextDescription, + }; + } + processorsDispatch({ + type: 'updateProcessor', + payload: { + processor: { + ...processor, + options: nextOptions, + }, + selector, }, - selector, - }, - }); - }} - ariaLabel={editorItemMessages.processorTypeLabel({ type: processor.type })} - text={description} - placeholder={editorItemMessages.descriptionPlaceholder} - /> - - - { - editor.setMode({ - id: 'editingProcessor', - arg: { processor, selector }, - }); - }} - /> - - - {selected ? ( - - ) : ( - - - - )} - - - - - { - editor.setMode({ id: 'creatingProcessor', arg: { selector } }); - }} - onDelete={() => { - editor.setMode({ id: 'removingProcessor', arg: { selector } }); - }} - onDuplicate={() => { - processorsDispatch({ - type: 'duplicateProcessor', - payload: { - source: selector, - }, - }); - }} - /> - - + + + {!isInMoveMode && ( + + { + editor.setMode({ + id: 'editingProcessor', + arg: { processor, selector }, + }); + }} + /> + + )} + + + {!isInMoveMode && ( + + + + )} + + + + {editorItemMessages.cancelMoveButtonLabel} + + + + + + + + {renderOnFailureHandlers && renderOnFailureHandlers()} + ); } ); diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_settings_form/field_components/index.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_settings_form/field_components/index.ts new file mode 100644 index 0000000000000..6f7b55a3ea4b0 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_settings_form/field_components/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { OnXJsonEditorUpdateHandler, XJsonEditor } from './xjson_editor'; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_settings_form/field_components/xjson_editor.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_settings_form/field_components/xjson_editor.tsx new file mode 100644 index 0000000000000..a8456ad0ffd72 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_settings_form/field_components/xjson_editor.tsx @@ -0,0 +1,66 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiPanel } from '@elastic/eui'; +import { XJsonLang } from '@kbn/monaco'; +import React, { FunctionComponent, useCallback } from 'react'; +import { EuiFormRow } from '@elastic/eui'; +import { + CodeEditor, + FieldHook, + getFieldValidityAndErrorMessage, + Monaco, +} from '../../../../../../shared_imports'; + +export type OnXJsonEditorUpdateHandler = (arg: { + data: { + raw: string; + format(): T; + }; + validate(): boolean; + isValid: boolean | undefined; +}) => void; + +interface Props { + field: FieldHook; + editorProps: { [key: string]: any }; +} + +export const XJsonEditor: FunctionComponent = ({ field, editorProps }) => { + const { value, helpText, setValue, label } = field; + const { xJson, setXJson, convertToJson } = Monaco.useXJsonMode(value); + const { errorMessage } = getFieldValidityAndErrorMessage(field); + + const onChange = useCallback( + (s) => { + setXJson(s); + setValue(convertToJson(s)); + }, + [setValue, setXJson, convertToJson] + ); + return ( + + + { + XJsonLang.registerGrammarChecker(m); + }} + options={{ minimap: { enabled: false } }} + onChange={onChange} + {...(editorProps as any)} + /> + + + ); +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_settings_form/processor_settings_form.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_settings_form/processor_settings_form.tsx index 84dfce64f602b..9d284748a3d15 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_settings_form/processor_settings_form.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_settings_form/processor_settings_form.tsx @@ -18,26 +18,32 @@ import { EuiFlexItem, } from '@elastic/eui'; -import { Form, useForm, FormDataProvider } from '../../../../../shared_imports'; +import { Form, FormDataProvider, FormHook } from '../../../../../shared_imports'; import { usePipelineProcessorsContext } from '../../context'; import { ProcessorInternal } from '../../types'; import { DocumentationButton } from './documentation_button'; -import { ProcessorSettingsFromOnSubmitArg } from './processor_settings_form.container'; import { getProcessorFormDescriptor } from './map_processor_type_to_form'; import { CommonProcessorFields, ProcessorTypeField } from './processors/common_fields'; import { Custom } from './processors/custom'; -export type OnSubmitHandler = (processor: ProcessorSettingsFromOnSubmitArg) => void; - export interface Props { isOnFailure: boolean; processor?: ProcessorInternal; - form: ReturnType['form']; + form: FormHook; onClose: () => void; onOpen: () => void; } +const updateButtonLabel = i18n.translate( + 'xpack.ingestPipelines.settingsFormOnFailureFlyout.updateButtonLabel', + { defaultMessage: 'Update' } +); +const addButtonLabel = i18n.translate( + 'xpack.ingestPipelines.settingsFormOnFailureFlyout.addButtonLabel', + { defaultMessage: 'Add' } +); + export const ProcessorSettingsForm: FunctionComponent = memo( ({ processor, form, isOnFailure, onClose, onOpen }) => { const { @@ -123,10 +129,7 @@ export const ProcessorSettingsForm: FunctionComponent = memo( <> {formContent} - {i18n.translate( - 'xpack.ingestPipelines.pipelineEditor.settingsForm.submitButtonLabel', - { defaultMessage: 'Submit' } - )} + {processor ? updateButtonLabel : addButtonLabel} ); diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_settings_form/processors/custom.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_settings_form/processors/custom.tsx index 4d8634e6f2855..82fdc81e0a843 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_settings_form/processors/custom.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_settings_form/processors/custom.tsx @@ -12,15 +12,16 @@ import { FIELD_TYPES, fieldValidators, UseField, - JsonEditorField, } from '../../../../../../shared_imports'; const { emptyField, isJsonField } = fieldValidators; +import { XJsonEditor } from '../field_components'; + const customConfig: FieldConfig = { type: FIELD_TYPES.TEXT, label: i18n.translate('xpack.ingestPipelines.pipelineEditor.customForm.optionsFieldLabel', { - defaultMessage: 'Configuration options', + defaultMessage: 'Configuration', }), serializer: (value: string) => { try { @@ -42,7 +43,7 @@ const customConfig: FieldConfig = { i18n.translate( 'xpack.ingestPipelines.pipelineEditor.customForm.configurationRequiredError', { - defaultMessage: 'Configuration options are required.', + defaultMessage: 'Configuration is required.', } ) ), @@ -71,17 +72,17 @@ export const Custom: FunctionComponent = ({ defaultOptions }) => { return ( ) => void; 'data-test-subj'?: string; } -const MOVE_HERE_LABEL = i18n.translate('xpack.ingestPipelines.pipelineEditor.moveTargetLabel', { - defaultMessage: 'Move here', -}); +const moveHereLabel = i18n.translate( + 'xpack.ingestPipelines.pipelineEditor.dropZoneButton.moveHereToolTip', + { + defaultMessage: 'Move here', + } +); + +const cannotMoveHereLabel = i18n.translate( + 'xpack.ingestPipelines.pipelineEditor.dropZoneButton.unavailableToolTip', + { defaultMessage: 'Cannot move here' } +); export const DropZoneButton: FunctionComponent = (props) => { - const { onClick, isDisabled } = props; + const { onClick, isDisabled, isVisible } = props; + const isUnavailable = isVisible && isDisabled; const containerClasses = classNames({ - 'pipelineProcessorsEditor__tree__dropZoneContainer--active': !isDisabled, + 'pipelineProcessorsEditor__tree__dropZoneContainer--visible': isVisible, + 'pipelineProcessorsEditor__tree__dropZoneContainer--unavailable': isUnavailable, }); const buttonClasses = classNames({ - 'pipelineProcessorsEditor__tree__dropZoneButton--active': !isDisabled, + 'pipelineProcessorsEditor__tree__dropZoneButton--visible': isVisible, + 'pipelineProcessorsEditor__tree__dropZoneButton--unavailable': isUnavailable, }); - return ( - + const content = ( +
{} : onClick} iconType="empty" /> - +
+ ); + + return isUnavailable ? ( + + {content} + + ) : ( + content ); }; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processors_tree/components/private_tree.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processors_tree/components/private_tree.tsx index 661bde1aa8b35..89407fd4366d8 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processors_tree/components/private_tree.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processors_tree/components/private_tree.tsx @@ -78,46 +78,50 @@ export const PrivateTree: FunctionComponent = ({ return ( <> {idx === 0 ? ( + + { + event.preventDefault(); + onAction({ + type: 'move', + payload: { + destination: selector.concat(DropSpecialLocations.top), + source: movingProcessor!.selector, + }, + }); + }} + isVisible={Boolean(movingProcessor)} + isDisabled={!movingProcessor || isDropZoneAboveDisabled(info, movingProcessor)} + /> + + ) : undefined} + + + + { event.preventDefault(); onAction({ type: 'move', payload: { - destination: selector.concat(DropSpecialLocations.top), + destination: selector.concat(String(idx + 1)), source: movingProcessor!.selector, }, }); }} - isDisabled={Boolean( - !movingProcessor || isDropZoneAboveDisabled(info, movingProcessor!) - )} - /> - ) : undefined} - - - { - event.preventDefault(); - onAction({ - type: 'move', - payload: { - destination: selector.concat(String(idx + 1)), - source: movingProcessor!.selector, - }, - }); - }} - /> ); }; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processors_tree/components/tree_node.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processors_tree/components/tree_node.tsx index a396a7f4d5ecd..2e3f39ef1d3ac 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processors_tree/components/tree_node.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processors_tree/components/tree_node.tsx @@ -5,9 +5,8 @@ */ import React, { FunctionComponent, useMemo } from 'react'; -import classNames from 'classnames'; import { i18n } from '@kbn/i18n'; -import { EuiPanel, EuiText } from '@elastic/eui'; +import { EuiText } from '@elastic/eui'; import { ProcessorInternal } from '../../../types'; @@ -47,40 +46,21 @@ export const TreeNode: FunctionComponent = ({ }; }, [onAction, stringSelector, processor]); // eslint-disable-line react-hooks/exhaustive-deps - const selected = movingProcessor?.id === processor.id; - - const panelClasses = classNames({ - 'pipelineProcessorsEditor__tree__item--selected': selected, - }); - const renderOnFailureHandlersTree = () => { if (!processor.onFailure?.length) { return; } - const onFailureHandlerLabelClasses = classNames({ - 'pipelineProcessorsEditor__tree__onFailureHandlerLabel--withDropZone': - movingProcessor != null && - movingProcessor.id !== processor.onFailure[0].id && - movingProcessor.id !== processor.id, - }); - return (
-
- - {i18n.translate('xpack.ingestPipelines.pipelineEditor.onFailureProcessorsLabel', { - defaultMessage: 'Failure handlers', - })} - -
+ + {i18n.translate('xpack.ingestPipelines.pipelineEditor.onFailureProcessorsLabel', { + defaultMessage: 'Failure handlers', + })} + = ({ }; return ( - - - {renderOnFailureHandlersTree()} - + ); }; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processors_tree/processors_tree.scss b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processors_tree/processors_tree.scss index ad9058cea5e18..2feb71f21a4f5 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processors_tree/processors_tree.scss +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processors_tree/processors_tree.scss @@ -1,61 +1,61 @@ @import '@elastic/eui/src/global_styling/variables/size'; +@import '../shared'; .pipelineProcessorsEditor__tree { - &__container { background-color: $euiColorLightestShade; padding: $euiSizeS; } &__dropZoneContainer { + position: relative; margin: 2px; visibility: hidden; - border: 2px dashed $euiColorLightShade; - height: 12px; - border-radius: 2px; - - transition: border .5s; + background-color: transparent; + height: 2px; - &--active { + &--visible { &:hover { - border: 2px dashed $euiColorPrimary; + background-color: $euiColorPrimary; } visibility: visible; } + + &--unavailable { + &:hover { + background-color: $euiColorMediumShade; + } + } + + &__toolTip { + pointer-events: none; + } } + $dropZoneButtonHeight: 60px; + $dropZoneButtonOffsetY: $dropZoneButtonHeight * -0.5; &__dropZoneButton { - height: 8px; + position: absolute; + padding: 0; + height: $dropZoneButtonHeight; + margin-top: $dropZoneButtonOffsetY; + width: 100%; opacity: 0; text-decoration: none !important; + z-index: $dropZoneZIndex; - &--active { + &--visible { + pointer-events: visible !important; &:hover { transform: none !important; } } - &:disabled { - cursor: default !important; - & > * { - cursor: default !important; - } + &--unavailable { + cursor: not-allowed !important; } } - &__onFailureHandlerLabelContainer { - position: relative; - height: 14px; - } - &__onFailureHandlerLabel { - position: absolute; - bottom: -16px; - &--withDropZone { - bottom: -4px; - } - } - - &__onFailureHandlerContainer { margin-top: $euiSizeS; margin-bottom: $euiSizeS; @@ -63,12 +63,4 @@ overflow: visible; } } - - &__item { - transition: border-color 1s; - min-height: 50px; - &--selected { - border: 1px solid $euiColorPrimary; - } - } } diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processors_tree/utils.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processors_tree/utils.ts index 457e335602b9b..6f8681b38fc7a 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processors_tree/utils.ts +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processors_tree/utils.ts @@ -13,9 +13,9 @@ import { ProcessorInternal } from '../../types'; // - ./components/drop_zone_button.tsx // - ./components/pipeline_processors_editor_item.tsx const itemHeightsPx = { - WITHOUT_NESTED_ITEMS: 67, + WITHOUT_NESTED_ITEMS: 57, WITH_NESTED_ITEMS: 137, - TOP_PADDING: 16, + TOP_PADDING: 6, }; export const calculateItemHeight = ({ diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/pipeline_processors_editor.scss b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/pipeline_processors_editor.scss index ee7421d7dbfa8..73eb54827e04f 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/pipeline_processors_editor.scss +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/pipeline_processors_editor.scss @@ -1,3 +1,3 @@ .pipelineProcessorsEditor { - margin-bottom: $euiSize; + margin-bottom: $euiSizeXL; } diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/pipeline_processors_editor.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/pipeline_processors_editor.tsx index b64f77f582b3a..09e77c5107754 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/pipeline_processors_editor.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/pipeline_processors_editor.tsx @@ -175,7 +175,7 @@ export const PipelineProcessorsEditor: FunctionComponent = memo( /> - + diff --git a/x-pack/plugins/ingest_pipelines/public/application/index.tsx b/x-pack/plugins/ingest_pipelines/public/application/index.tsx index a8e6febeb2e59..6ffebd1854b78 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/index.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/index.tsx @@ -7,7 +7,7 @@ import { HttpSetup } from 'kibana/public'; import React, { ReactNode } from 'react'; import { render, unmountComponentAtNode } from 'react-dom'; -import { NotificationsSetup } from 'kibana/public'; +import { NotificationsSetup, IUiSettingsClient } from 'kibana/public'; import { ManagementAppMountParams } from 'src/plugins/management/public'; import { KibanaContextProvider } from '../../../../../src/plugins/kibana_react/public'; @@ -25,6 +25,7 @@ export interface AppServices { api: ApiService; notifications: NotificationsSetup; history: ManagementAppMountParams['history']; + uiSettings: IUiSettingsClient; } export interface CoreServices { diff --git a/x-pack/plugins/ingest_pipelines/public/application/mount_management_section.ts b/x-pack/plugins/ingest_pipelines/public/application/mount_management_section.ts index 49c8f5a7b2e1e..16ba9f9cd7a12 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/mount_management_section.ts +++ b/x-pack/plugins/ingest_pipelines/public/application/mount_management_section.ts @@ -30,6 +30,7 @@ export async function mountManagementSection( api: apiService, notifications, history, + uiSettings: coreStart.uiSettings, }; return renderApp(element, I18nContext, services, { http }); diff --git a/x-pack/plugins/ingest_pipelines/public/shared_imports.ts b/x-pack/plugins/ingest_pipelines/public/shared_imports.ts index 9ddb953c71978..05e7d1e41c5fa 100644 --- a/x-pack/plugins/ingest_pipelines/public/shared_imports.ts +++ b/x-pack/plugins/ingest_pipelines/public/shared_imports.ts @@ -3,9 +3,11 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { useKibana as _useKibana } from '../../../../src/plugins/kibana_react/public'; +import { useKibana as _useKibana, CodeEditor } from '../../../../src/plugins/kibana_react/public'; import { AppServices } from './application'; +export { CodeEditor }; + export { AuthorizationProvider, Error, @@ -19,6 +21,7 @@ export { useRequest, UseRequestConfig, WithPrivileges, + Monaco, } from '../../../../src/plugins/es_ui_shared/public/'; export { @@ -36,6 +39,8 @@ export { FormDataProvider, OnFormUpdateArg, FieldConfig, + FieldHook, + getFieldValidityAndErrorMessage, } from '../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib'; export { From 2a68dc7c6b8cb2fe8f77579241e70a204d6a26af Mon Sep 17 00:00:00 2001 From: Marta Bondyra Date: Fri, 26 Jun 2020 16:33:09 +0200 Subject: [PATCH 12/13] [Lens] Last used Index pattern is saved to and retrieved from local storage (#69511) --- .../indexpattern_datasource/indexpattern.tsx | 3 + .../indexpattern_datasource/loader.test.ts | 88 +++++++++++++++++++ .../public/indexpattern_datasource/loader.ts | 29 +++++- .../plugins/lens/public/settings_storage.tsx | 17 ++++ 4 files changed, 135 insertions(+), 2 deletions(-) create mode 100644 x-pack/plugins/lens/public/settings_storage.tsx diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx index 9c4f6c9b590ce..a98f63cf9b360 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx @@ -125,6 +125,7 @@ export function getIndexPatternDatasource({ state, savedObjectsClient: await savedObjectsClient, defaultIndexPatternId: core.uiSettings.get('defaultIndex'), + storage, }); }, @@ -207,6 +208,7 @@ export function getIndexPatternDatasource({ setState, savedObjectsClient, onError: onIndexPatternLoadError, + storage, }); }} data={data} @@ -290,6 +292,7 @@ export function getIndexPatternDatasource({ layerId: props.layerId, onError: onIndexPatternLoadError, replaceIfPossible: true, + storage, }); }} {...props} diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/loader.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/loader.test.ts index b54ad3651471d..55fd8a6d936d3 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/loader.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/loader.test.ts @@ -18,6 +18,15 @@ import { documentField } from './document_field'; jest.mock('./operations'); +const createMockStorage = (lastData?: Record) => { + return { + get: jest.fn().mockImplementation(() => lastData), + set: jest.fn(), + remove: jest.fn(), + clear: jest.fn(), + }; +}; + const sampleIndexPatterns = { a: { id: 'a', @@ -269,8 +278,10 @@ describe('loader', () => { describe('loadInitialState', () => { it('should load a default state', async () => { + const storage = createMockStorage(); const state = await loadInitialState({ savedObjectsClient: mockClient(), + storage, }); expect(state).toMatchObject({ @@ -285,12 +296,61 @@ describe('loader', () => { layers: {}, showEmptyFields: false, }); + expect(storage.set).toHaveBeenCalledWith('lens-settings', { + indexPatternId: 'a', + }); + }); + + it('should load a default state when lastUsedIndexPatternId is not found in indexPatternRefs', async () => { + const storage = createMockStorage({ indexPatternId: 'c' }); + const state = await loadInitialState({ + savedObjectsClient: mockClient(), + storage, + }); + + expect(state).toMatchObject({ + currentIndexPatternId: 'a', + indexPatternRefs: [ + { id: 'a', title: sampleIndexPatterns.a.title }, + { id: 'b', title: sampleIndexPatterns.b.title }, + ], + indexPatterns: { + a: sampleIndexPatterns.a, + }, + layers: {}, + showEmptyFields: false, + }); + expect(storage.set).toHaveBeenCalledWith('lens-settings', { + indexPatternId: 'a', + }); + }); + + it('should load lastUsedIndexPatternId if in localStorage', async () => { + const state = await loadInitialState({ + savedObjectsClient: mockClient(), + storage: createMockStorage({ indexPatternId: 'b' }), + }); + + expect(state).toMatchObject({ + currentIndexPatternId: 'b', + indexPatternRefs: [ + { id: 'a', title: sampleIndexPatterns.a.title }, + { id: 'b', title: sampleIndexPatterns.b.title }, + ], + indexPatterns: { + b: sampleIndexPatterns.b, + }, + layers: {}, + showEmptyFields: false, + }); }); it('should use the default index pattern id, if provided', async () => { + const storage = createMockStorage(); const state = await loadInitialState({ defaultIndexPatternId: 'b', savedObjectsClient: mockClient(), + storage, }); expect(state).toMatchObject({ @@ -305,6 +365,9 @@ describe('loader', () => { layers: {}, showEmptyFields: false, }); + expect(storage.set).toHaveBeenCalledWith('lens-settings', { + indexPatternId: 'b', + }); }); it('should initialize from saved state', async () => { @@ -336,9 +399,11 @@ describe('loader', () => { }, }, }; + const storage = createMockStorage({ indexPatternId: 'a' }); const state = await loadInitialState({ state: savedState, savedObjectsClient: mockClient(), + storage, }); expect(state).toMatchObject({ @@ -353,6 +418,10 @@ describe('loader', () => { layers: savedState.layers, showEmptyFields: false, }); + + expect(storage.set).toHaveBeenCalledWith('lens-settings', { + indexPatternId: 'b', + }); }); }); @@ -367,6 +436,7 @@ describe('loader', () => { layers: {}, showEmptyFields: true, }; + const storage = createMockStorage({ indexPatternId: 'b' }); await changeIndexPattern({ state, @@ -374,6 +444,7 @@ describe('loader', () => { id: 'a', savedObjectsClient: mockClient(), onError: jest.fn(), + storage, }); expect(setState).toHaveBeenCalledTimes(1); @@ -383,6 +454,9 @@ describe('loader', () => { a: sampleIndexPatterns.a, }, }); + expect(storage.set).toHaveBeenCalledWith('lens-settings', { + indexPatternId: 'a', + }); }); it('handles errors', async () => { @@ -398,6 +472,8 @@ describe('loader', () => { showEmptyFields: true, }; + const storage = createMockStorage({ indexPatternId: 'b' }); + await changeIndexPattern({ state, setState, @@ -409,9 +485,11 @@ describe('loader', () => { }), }, onError, + storage, }); expect(setState).not.toHaveBeenCalled(); + expect(storage.set).not.toHaveBeenCalled(); expect(onError).toHaveBeenCalledWith(err); }); }); @@ -452,6 +530,8 @@ describe('loader', () => { showEmptyFields: true, }; + const storage = createMockStorage({ indexPatternId: 'a' }); + await changeLayerIndexPattern({ state, setState, @@ -459,6 +539,7 @@ describe('loader', () => { layerId: 'l1', savedObjectsClient: mockClient(), onError: jest.fn(), + storage, }); expect(setState).toHaveBeenCalledTimes(1); @@ -492,6 +573,9 @@ describe('loader', () => { }, }, }); + expect(storage.set).toHaveBeenCalledWith('lens-settings', { + indexPatternId: 'b', + }); }); it('handles errors', async () => { @@ -515,6 +599,8 @@ describe('loader', () => { showEmptyFields: true, }; + const storage = createMockStorage({ indexPatternId: 'b' }); + await changeLayerIndexPattern({ state, setState, @@ -527,9 +613,11 @@ describe('loader', () => { }), }, onError, + storage, }); expect(setState).not.toHaveBeenCalled(); + expect(storage.set).not.toHaveBeenCalled(); expect(onError).toHaveBeenCalledWith(err); }); }); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/loader.ts b/x-pack/plugins/lens/public/indexpattern_datasource/loader.ts index c34f4c1d23148..ca52ffe73a871 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/loader.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/loader.ts @@ -5,6 +5,7 @@ */ import _ from 'lodash'; +import { IStorageWrapper } from 'src/plugins/kibana_utils/public'; import { SavedObjectsClientContract, SavedObjectAttributes, HttpSetup } from 'kibana/public'; import { SimpleSavedObject } from 'kibana/public'; import { StateSetter } from '../types'; @@ -24,6 +25,7 @@ import { IFieldType, IndexPatternTypeMeta, } from '../../../../../src/plugins/data/public'; +import { readFromStorage, writeToStorage } from '../settings_storage'; interface SavedIndexPatternAttributes extends SavedObjectAttributes { title: string; @@ -68,31 +70,48 @@ export async function loadIndexPatterns({ ); } +const getLastUsedIndexPatternId = ( + storage: IStorageWrapper, + indexPatternRefs: IndexPatternRef[] +) => { + const indexPattern = readFromStorage(storage, 'indexPatternId'); + return indexPattern && indexPatternRefs.find((i) => i.id === indexPattern)?.id; +}; + +const setLastUsedIndexPatternId = (storage: IStorageWrapper, value: string) => { + writeToStorage(storage, 'indexPatternId', value); +}; + export async function loadInitialState({ state, savedObjectsClient, defaultIndexPatternId, + storage, }: { state?: IndexPatternPersistedState; savedObjectsClient: SavedObjectsClient; defaultIndexPatternId?: string; + storage: IStorageWrapper; }): Promise { const indexPatternRefs = await loadIndexPatternRefs(savedObjectsClient); + const lastUsedIndexPatternId = getLastUsedIndexPatternId(storage, indexPatternRefs); + const requiredPatterns = _.unique( state ? Object.values(state.layers) .map((l) => l.indexPatternId) .concat(state.currentIndexPatternId) - : [defaultIndexPatternId || indexPatternRefs[0].id] + : [lastUsedIndexPatternId || defaultIndexPatternId || indexPatternRefs[0].id] ); const currentIndexPatternId = requiredPatterns[0]; + setLastUsedIndexPatternId(storage, currentIndexPatternId); + const indexPatterns = await loadIndexPatterns({ savedObjectsClient, cache: {}, patterns: requiredPatterns, }); - if (state) { return { ...state, @@ -120,12 +139,14 @@ export async function changeIndexPattern({ state, setState, onError, + storage, }: { id: string; savedObjectsClient: SavedObjectsClient; state: IndexPatternPrivateState; setState: SetState; onError: ErrorHandler; + storage: IStorageWrapper; }) { try { const indexPatterns = await loadIndexPatterns({ @@ -145,6 +166,7 @@ export async function changeIndexPattern({ }, currentIndexPatternId: id, })); + setLastUsedIndexPatternId(storage, id); } catch (err) { onError(err); } @@ -158,6 +180,7 @@ export async function changeLayerIndexPattern({ setState, onError, replaceIfPossible, + storage, }: { indexPatternId: string; layerId: string; @@ -166,6 +189,7 @@ export async function changeLayerIndexPattern({ setState: SetState; onError: ErrorHandler; replaceIfPossible?: boolean; + storage: IStorageWrapper; }) { try { const indexPatterns = await loadIndexPatterns({ @@ -186,6 +210,7 @@ export async function changeLayerIndexPattern({ }, currentIndexPatternId: replaceIfPossible ? indexPatternId : s.currentIndexPatternId, })); + setLastUsedIndexPatternId(storage, indexPatternId); } catch (err) { onError(err); } diff --git a/x-pack/plugins/lens/public/settings_storage.tsx b/x-pack/plugins/lens/public/settings_storage.tsx new file mode 100644 index 0000000000000..58e014512edab --- /dev/null +++ b/x-pack/plugins/lens/public/settings_storage.tsx @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { IStorageWrapper } from 'src/plugins/kibana_utils/public'; + +const STORAGE_KEY = 'lens-settings'; + +export const readFromStorage = (storage: IStorageWrapper, key: string) => { + const data = storage.get(STORAGE_KEY); + return data && data[key]; +}; +export const writeToStorage = (storage: IStorageWrapper, key: string, value: string) => { + storage.set(STORAGE_KEY, { [key]: value }); +}; From eea33a0db208ae3895228ebe60349e143e858acf Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Fri, 26 Jun 2020 17:03:00 +0200 Subject: [PATCH 13/13] [ML] Transforms: Adds functional tests for transform cloning and editing. (#69933) Adds functional tests for transform cloning and editing. --- .../edit_transform_flyout.tsx | 16 +- .../edit_transform_flyout_form.tsx | 3 + .../edit_transform_flyout_form_text_input.tsx | 3 + .../__snapshots__/action_delete.test.tsx.snap | 1 + .../__snapshots__/action_start.test.tsx.snap | 1 + .../__snapshots__/action_stop.test.tsx.snap | 1 + .../transform_list/action_clone.tsx | 1 + .../transform_list/action_delete.tsx | 1 + .../components/transform_list/action_edit.tsx | 1 + .../transform_list/action_start.tsx | 1 + .../components/transform_list/action_stop.tsx | 1 + .../test/functional/apps/transform/cloning.ts | 171 +++++++++++++++++- .../apps/transform/creation_index_pattern.ts | 2 +- .../apps/transform/creation_saved_search.ts | 2 +- .../test/functional/apps/transform/editing.ts | 149 +++++++++++++++ .../test/functional/apps/transform/index.ts | 1 + .../services/transform/edit_flyout.ts | 52 ++++++ .../functional/services/transform/index.ts | 3 + .../services/transform/transform_table.ts | 42 ++++- 19 files changed, 442 insertions(+), 10 deletions(-) create mode 100644 x-pack/test/functional/apps/transform/editing.ts create mode 100644 x-pack/test/functional/services/transform/edit_flyout.ts diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_flyout.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_flyout.tsx index d3dae0a8c8b63..77a7ae25ce887 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_flyout.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_flyout.tsx @@ -77,10 +77,15 @@ export const EditTransformFlyout: FC = ({ closeFlyout, return ( - + -

+

{i18n.translate('xpack.transform.transformList.editFlyoutTitle', { defaultMessage: 'Edit {transformId}', values: { @@ -121,7 +126,12 @@ export const EditTransformFlyout: FC = ({ closeFlyout, - + {i18n.translate('xpack.transform.transformList.editFlyoutUpdateButtonText', { defaultMessage: 'Update', })} diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_flyout_form.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_flyout_form.tsx index a9c230870bfca..5836898755224 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_flyout_form.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_flyout_form.tsx @@ -25,6 +25,7 @@ export const EditTransformFlyoutForm: FC = ({ return ( = ({ value={formFields.description.value} /> = ({ value={formFields.docsPerSecond.value} /> = ({ + dataTestSubj, errorMessages, helpText, label, @@ -33,6 +35,7 @@ export const EditTransformFlyoutFormTextInput: FC 0} value={value} diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/__snapshots__/action_delete.test.tsx.snap b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/__snapshots__/action_delete.test.tsx.snap index 5695b8a847496..da5ad27c9d6b1 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/__snapshots__/action_delete.test.tsx.snap +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/__snapshots__/action_delete.test.tsx.snap @@ -10,6 +10,7 @@ exports[`Transform: Transform List Actions Minimal initializati Minimal initializatio Minimal initialization = ({ itemId }) => { const cloneButton = ( = ({ items, forceDisable }) => let deleteButton = ( = ({ config }) => { const editButton = ( = ({ items, forceDisable }) => { let startButton = ( = ({ items, forceDisable }) => { const stopButton = ( { - // await transform.api.deleteIndices(); + await transform.api.deleteIndices(testData.destinationIndex); + await transform.testResources.deleteIndexPatternByTitle(testData.destinationIndex); }); - it('loads the home page', async () => { + it('should load the home page', async () => { await transform.navigation.navigateTo(); await transform.management.assertTransformListPageExists(); }); + + it('should display the transforms table', async () => { + await transform.management.assertTransformsTableExists(); + }); + + it('should display the original transform in the transform list', async () => { + await transform.table.refreshTransformList(); + await transform.table.filterWithSearchString(transformConfig.id); + const rows = await transform.table.parseTransformTable(); + expect(rows.filter((row) => row.id === transformConfig.id)).to.have.length(1); + }); + + it('should show the actions popover', async () => { + await transform.table.assertTransformRowActions(false); + }); + + it('should display the define pivot step', async () => { + await transform.table.clickTransformRowAction('Clone'); + await transform.wizard.assertDefineStepActive(); + }); + + it('should load the index preview', async () => { + await transform.wizard.assertIndexPreviewLoaded(); + }); + + it('should show the index preview', async () => { + await transform.wizard.assertIndexPreview( + testData.expected.indexPreview.columns, + testData.expected.indexPreview.rows + ); + }); + + it('should display the query input', async () => { + await transform.wizard.assertQueryInputExists(); + await transform.wizard.assertQueryValue(''); + }); + + it('should show the pre-filled group-by configuration', async () => { + await transform.wizard.assertGroupByEntryExists( + testData.expected.groupBy.index, + testData.expected.groupBy.label + ); + }); + + it('should show the pre-filled aggs configuration', async () => { + await transform.wizard.assertAggregationEntryExists( + testData.expected.aggs.index, + testData.expected.aggs.label + ); + }); + + it('should show the pivot preview', async () => { + await transform.wizard.assertPivotPreviewChartHistogramButtonMissing(); + await transform.wizard.assertPivotPreviewColumnValues( + testData.expected.pivotPreview.column, + testData.expected.pivotPreview.values + ); + }); + + it('should load the details step', async () => { + await transform.wizard.advanceToDetailsStep(); + }); + + it('should input the transform id', async () => { + await transform.wizard.assertTransformIdInputExists(); + await transform.wizard.assertTransformIdValue(''); + await transform.wizard.setTransformId(testData.transformId); + }); + + it('should input the transform description', async () => { + await transform.wizard.assertTransformDescriptionInputExists(); + await transform.wizard.assertTransformDescriptionValue(''); + await transform.wizard.setTransformDescription(testData.transformDescription); + }); + + it('should input the destination index', async () => { + await transform.wizard.assertDestinationIndexInputExists(); + await transform.wizard.assertDestinationIndexValue(''); + await transform.wizard.setDestinationIndex(testData.destinationIndex); + }); + + it('should display the create index pattern switch', async () => { + await transform.wizard.assertCreateIndexPatternSwitchExists(); + await transform.wizard.assertCreateIndexPatternSwitchCheckState(true); + }); + + it('should display the continuous mode switch', async () => { + await transform.wizard.assertContinuousModeSwitchExists(); + await transform.wizard.assertContinuousModeSwitchCheckState(false); + }); + + it('should load the create step', async () => { + await transform.wizard.advanceToCreateStep(); + }); + + it('should display the create and start button', async () => { + await transform.wizard.assertCreateAndStartButtonExists(); + await transform.wizard.assertCreateAndStartButtonEnabled(true); + }); + + it('should display the create button', async () => { + await transform.wizard.assertCreateButtonExists(); + await transform.wizard.assertCreateButtonEnabled(true); + }); + + it('should display the copy to clipboard button', async () => { + await transform.wizard.assertCopyToClipboardButtonExists(); + await transform.wizard.assertCopyToClipboardButtonEnabled(true); + }); + + it('should create the transform', async () => { + await transform.wizard.createTransform(); + }); + + it('should start the transform and finish processing', async () => { + await transform.wizard.startTransform(); + await transform.wizard.waitForProgressBarComplete(); + }); + + it('should return to the management page', async () => { + await transform.wizard.returnToManagement(); + }); + + it('should display the transforms table', async () => { + await transform.management.assertTransformsTableExists(); + }); + + it('should display the created transform in the transform list', async () => { + await transform.table.refreshTransformList(); + await transform.table.filterWithSearchString(testData.transformId); + const rows = await transform.table.parseTransformTable(); + expect(rows.filter((row) => row.id === testData.transformId)).to.have.length(1); + }); }); } }); diff --git a/x-pack/test/functional/apps/transform/creation_index_pattern.ts b/x-pack/test/functional/apps/transform/creation_index_pattern.ts index bf267c80cdcce..7c9983101f607 100644 --- a/x-pack/test/functional/apps/transform/creation_index_pattern.ts +++ b/x-pack/test/functional/apps/transform/creation_index_pattern.ts @@ -432,7 +432,7 @@ export default function ({ getService }: FtrProviderContext) { expect(rows.filter((row) => row.id === testData.transformId)).to.have.length(1); }); - it('job creation displays details for the created job in the job list', async () => { + it('transform creation displays details for the created transform in the transform list', async () => { await transform.table.assertTransformRowFields(testData.transformId, { id: testData.transformId, description: testData.transformDescription, diff --git a/x-pack/test/functional/apps/transform/creation_saved_search.ts b/x-pack/test/functional/apps/transform/creation_saved_search.ts index bc4ded49660f4..54cc5b3f62933 100644 --- a/x-pack/test/functional/apps/transform/creation_saved_search.ts +++ b/x-pack/test/functional/apps/transform/creation_saved_search.ts @@ -235,7 +235,7 @@ export default function ({ getService }: FtrProviderContext) { expect(rows.filter((row) => row.id === testData.transformId)).to.have.length(1); }); - it('job creation displays details for the created job in the job list', async () => { + it('transform creation displays details for the created transform in the transform list', async () => { await transform.table.assertTransformRowFields(testData.transformId, { id: testData.transformId, description: testData.transformDescription, diff --git a/x-pack/test/functional/apps/transform/editing.ts b/x-pack/test/functional/apps/transform/editing.ts new file mode 100644 index 0000000000000..44ecca17328a7 --- /dev/null +++ b/x-pack/test/functional/apps/transform/editing.ts @@ -0,0 +1,149 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import expect from '@kbn/expect'; + +import { FtrProviderContext } from '../../ftr_provider_context'; +import { TransformPivotConfig } from '../../../../plugins/transform/public/app/common'; + +function getTransformConfig(): TransformPivotConfig { + const date = Date.now(); + return { + id: `ec_2_${date}`, + source: { index: ['ft_ecommerce'] }, + pivot: { + group_by: { category: { terms: { field: 'category.keyword' } } }, + aggregations: { 'products.base_price.avg': { avg: { field: 'products.base_price' } } }, + }, + description: + 'ecommerce batch transform with avg(products.base_price) grouped by terms(category.keyword)', + dest: { index: `user-ec_2_${date}` }, + }; +} + +export default function ({ getService }: FtrProviderContext) { + const esArchiver = getService('esArchiver'); + const transform = getService('transform'); + + describe('editing', function () { + const transformConfig = getTransformConfig(); + + before(async () => { + await esArchiver.loadIfNeeded('ml/ecommerce'); + await transform.testResources.createIndexPatternIfNeeded('ft_ecommerce', 'order_date'); + await transform.api.createAndRunTransform(transformConfig); + await transform.testResources.setKibanaTimeZoneToUTC(); + + await transform.securityUI.loginAsTransformPowerUser(); + }); + + after(async () => { + await transform.testResources.deleteIndexPatternByTitle(transformConfig.dest.index); + await transform.api.deleteIndices(transformConfig.dest.index); + await transform.api.cleanTransformIndices(); + }); + + const testData = { + suiteTitle: 'edit transform', + transformDescription: 'updated description', + transformDocsPerSecond: '1000', + transformFrequency: '10m', + expected: { + messageText: 'updated transform.', + row: { + status: 'stopped', + mode: 'batch', + progress: '100', + }, + }, + }; + + describe(`${testData.suiteTitle}`, function () { + it('should load the home page', async () => { + await transform.navigation.navigateTo(); + await transform.management.assertTransformListPageExists(); + }); + + it('should display the transforms table', async () => { + await transform.management.assertTransformsTableExists(); + }); + + it('should display the original transform in the transform list', async () => { + await transform.table.refreshTransformList(); + await transform.table.filterWithSearchString(transformConfig.id); + const rows = await transform.table.parseTransformTable(); + expect(rows.filter((row) => row.id === transformConfig.id)).to.have.length(1); + }); + + it('should show the actions popover', async () => { + await transform.table.assertTransformRowActions(false); + }); + + it('should show the edit flyout', async () => { + await transform.table.clickTransformRowAction('Edit'); + await transform.editFlyout.assertTransformEditFlyoutExists(); + }); + + it('should update the transform description', async () => { + await transform.editFlyout.assertTransformEditFlyoutInputExists('Description'); + await transform.editFlyout.assertTransformEditFlyoutInputValue( + 'Description', + transformConfig?.description ?? '' + ); + await transform.editFlyout.setTransformEditFlyoutInputValue( + 'Description', + testData.transformDescription + ); + }); + + it('should update the transform documents per second', async () => { + await transform.editFlyout.assertTransformEditFlyoutInputExists('DocsPerSecond'); + await transform.editFlyout.assertTransformEditFlyoutInputValue('DocsPerSecond', ''); + await transform.editFlyout.setTransformEditFlyoutInputValue( + 'DocsPerSecond', + testData.transformDocsPerSecond + ); + }); + + it('should update the transform frequency', async () => { + await transform.editFlyout.assertTransformEditFlyoutInputExists('Frequency'); + await transform.editFlyout.assertTransformEditFlyoutInputValue('Frequency', ''); + await transform.editFlyout.setTransformEditFlyoutInputValue( + 'Frequency', + testData.transformFrequency + ); + }); + + it('should update the transform', async () => { + await transform.editFlyout.updateTransform(); + }); + + it('should display the transforms table', async () => { + await transform.management.assertTransformsTableExists(); + }); + + it('should display the updated transform in the transform list', async () => { + await transform.table.refreshTransformList(); + await transform.table.filterWithSearchString(transformConfig.id); + const rows = await transform.table.parseTransformTable(); + expect(rows.filter((row) => row.id === transformConfig.id)).to.have.length(1); + }); + + it('should display the updated transform in the transform list row cells', async () => { + await transform.table.assertTransformRowFields(transformConfig.id, { + id: transformConfig.id, + description: testData.transformDescription, + status: testData.expected.row.status, + mode: testData.expected.row.mode, + progress: testData.expected.row.progress, + }); + }); + + it('should display the messages tab and include an update message', async () => { + await transform.table.assertTransformExpandedRowMessages(testData.expected.messageText); + }); + }); + }); +} diff --git a/x-pack/test/functional/apps/transform/index.ts b/x-pack/test/functional/apps/transform/index.ts index a7859be6923d5..04a751279bf3c 100644 --- a/x-pack/test/functional/apps/transform/index.ts +++ b/x-pack/test/functional/apps/transform/index.ts @@ -35,5 +35,6 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./creation_index_pattern')); loadTestFile(require.resolve('./creation_saved_search')); loadTestFile(require.resolve('./cloning')); + loadTestFile(require.resolve('./editing')); }); } diff --git a/x-pack/test/functional/services/transform/edit_flyout.ts b/x-pack/test/functional/services/transform/edit_flyout.ts new file mode 100644 index 0000000000000..f9504deb39f6a --- /dev/null +++ b/x-pack/test/functional/services/transform/edit_flyout.ts @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ +import expect from '@kbn/expect'; + +import { FtrProviderContext } from '../../ftr_provider_context'; + +export function TransformEditFlyoutProvider({ getService }: FtrProviderContext) { + const retry = getService('retry'); + const testSubjects = getService('testSubjects'); + + return { + async assertTransformEditFlyoutExists() { + await testSubjects.existOrFail('transformEditFlyout'); + }, + + async assertTransformEditFlyoutMissing() { + await testSubjects.missingOrFail('transformEditFlyout'); + }, + + async assertTransformEditFlyoutInputExists(input: string) { + await testSubjects.existOrFail(`transformEditFlyout${input}Input`); + }, + + async assertTransformEditFlyoutInputValue(input: string, expectedValue: string) { + const actualValue = await testSubjects.getAttribute( + `transformEditFlyout${input}Input`, + 'value' + ); + expect(actualValue).to.eql( + expectedValue, + `Transform edit flyout '${input}' input text should be '${expectedValue}' (got '${actualValue}')` + ); + }, + + async setTransformEditFlyoutInputValue(input: string, value: string) { + await testSubjects.setValue(`transformEditFlyout${input}Input`, value, { + clearWithKeyboard: true, + }); + await this.assertTransformEditFlyoutInputValue(input, value); + }, + + async updateTransform() { + await testSubjects.click('transformEditFlyoutUpdateButton'); + await retry.tryForTime(5000, async () => { + await this.assertTransformEditFlyoutMissing(); + }); + }, + }; +} diff --git a/x-pack/test/functional/services/transform/index.ts b/x-pack/test/functional/services/transform/index.ts index 070bc48b432e1..24091ba773218 100644 --- a/x-pack/test/functional/services/transform/index.ts +++ b/x-pack/test/functional/services/transform/index.ts @@ -7,6 +7,7 @@ import { FtrProviderContext } from '../../ftr_provider_context'; import { TransformAPIProvider } from './api'; +import { TransformEditFlyoutProvider } from './edit_flyout'; import { TransformManagementProvider } from './management'; import { TransformNavigationProvider } from './navigation'; import { TransformSecurityCommonProvider } from './security_common'; @@ -19,6 +20,7 @@ import { MachineLearningTestResourcesProvider } from '../ml/test_resources'; export function TransformProvider(context: FtrProviderContext) { const api = TransformAPIProvider(context); + const editFlyout = TransformEditFlyoutProvider(context); const management = TransformManagementProvider(context); const navigation = TransformNavigationProvider(context); const securityCommon = TransformSecurityCommonProvider(context); @@ -30,6 +32,7 @@ export function TransformProvider(context: FtrProviderContext) { return { api, + editFlyout, management, navigation, securityCommon, diff --git a/x-pack/test/functional/services/transform/transform_table.ts b/x-pack/test/functional/services/transform/transform_table.ts index 0c9a5414bdd2b..453dca904b605 100644 --- a/x-pack/test/functional/services/transform/transform_table.ts +++ b/x-pack/test/functional/services/transform/transform_table.ts @@ -145,12 +145,52 @@ export function TransformTableProvider({ getService }: FtrProviderContext) { await testSubjects.existOrFail('~transformPivotPreview'); } + public async assertTransformExpandedRowMessages(expectedText: string) { + await testSubjects.click('transformListRowDetailsToggle'); + + // The expanded row should show the details tab content by default + await testSubjects.existOrFail('transformDetailsTab'); + await testSubjects.existOrFail('~transformDetailsTabContent'); + + // Click on the messages tab and assert the messages + await testSubjects.existOrFail('transformMessagesTab'); + await testSubjects.click('transformMessagesTab'); + await testSubjects.existOrFail('~transformMessagesTabContent'); + await retry.tryForTime(5000, async () => { + const actualText = await testSubjects.getVisibleText('~transformMessagesTabContent'); + expect(actualText.includes(expectedText)).to.eql( + true, + `Expected transform messages text to include '${expectedText}'` + ); + }); + } + + public async assertTransformRowActions(isTransformRunning = false) { + await testSubjects.click('euiCollapsedItemActionsButton'); + + await testSubjects.existOrFail('transformActionClone'); + await testSubjects.existOrFail('transformActionDelete'); + await testSubjects.existOrFail('transformActionEdit'); + + if (isTransformRunning) { + await testSubjects.missingOrFail('transformActionStart'); + await testSubjects.existOrFail('transformActionStop'); + } else { + await testSubjects.existOrFail('transformActionStart'); + await testSubjects.missingOrFail('transformActionStop'); + } + } + + public async clickTransformRowAction(action: string) { + await testSubjects.click(`transformAction${action}`); + } + public async waitForTransformsExpandedRowPreviewTabToLoad() { await testSubjects.existOrFail('~transformPivotPreview', { timeout: 60 * 1000 }); await testSubjects.existOrFail('transformPivotPreview loaded', { timeout: 30 * 1000 }); } - async assertTransformsExpandedRowPreviewColumnValues(column: number, values: string[]) { + public async assertTransformsExpandedRowPreviewColumnValues(column: number, values: string[]) { await this.waitForTransformsExpandedRowPreviewTabToLoad(); await this.assertEuiDataGridColumnValues('transformPivotPreview', column, values); }