diff --git a/src/plugins/ui_actions/public/actions/action_internal.ts b/src/plugins/ui_actions/public/actions/action_internal.ts index aba1e22fe09ee2..10eb760b130898 100644 --- a/src/plugins/ui_actions/public/actions/action_internal.ts +++ b/src/plugins/ui_actions/public/actions/action_internal.ts @@ -24,6 +24,9 @@ import { Presentable } from '../util/presentable'; import { uiToReactComponent } from '../../../kibana_react/public'; import { ActionType } from '../types'; +/** + * @internal + */ export class ActionInternal implements Action>, Presentable> { constructor(public readonly definition: A) {} diff --git a/src/plugins/ui_actions/public/service/ui_actions_service.test.ts b/src/plugins/ui_actions/public/service/ui_actions_service.test.ts index 45a1bdffa52adf..39502c3dd17fcd 100644 --- a/src/plugins/ui_actions/public/service/ui_actions_service.test.ts +++ b/src/plugins/ui_actions/public/service/ui_actions_service.test.ts @@ -20,7 +20,7 @@ import { UiActionsService } from './ui_actions_service'; import { Action, ActionInternal, createAction } from '../actions'; import { createHelloWorldAction } from '../tests/test_samples'; -import { ActionRegistry, TriggerRegistry, TriggerId, ActionType } from '../types'; +import { TriggerRegistry, TriggerId, ActionType, ActionRegistry } from '../types'; import { Trigger } from '../triggers'; // Casting to ActionType or TriggerId is a hack - in a real situation use diff --git a/src/plugins/ui_actions/public/service/ui_actions_service.ts b/src/plugins/ui_actions/public/service/ui_actions_service.ts index 760897f0287d82..11f5769a946483 100644 --- a/src/plugins/ui_actions/public/service/ui_actions_service.ts +++ b/src/plugins/ui_actions/public/service/ui_actions_service.ts @@ -220,7 +220,6 @@ export class UiActionsService { for (const [key, value] of this.actions.entries()) actions.set(key, value); for (const [key, value] of this.triggerToActions.entries()) triggerToActions.set(key, [...value]); - return new UiActionsService({ triggers, actions, triggerToActions }); }; } diff --git a/test/functional/apps/dashboard/time_zones.js b/test/functional/apps/dashboard/time_zones.js index b0344a8b69064c..4e95a14efb4d60 100644 --- a/test/functional/apps/dashboard/time_zones.js +++ b/test/functional/apps/dashboard/time_zones.js @@ -24,7 +24,13 @@ export default function ({ getService, getPageObjects }) { const pieChart = getService('pieChart'); const esArchiver = getService('esArchiver'); const kibanaServer = getService('kibanaServer'); - const PageObjects = getPageObjects(['dashboard', 'timePicker', 'settings', 'common']); + const PageObjects = getPageObjects([ + 'dashboard', + 'timePicker', + 'settings', + 'common', + 'savedObjects', + ]); describe('dashboard time zones', function () { this.tags('includeFirefox'); @@ -36,10 +42,10 @@ export default function ({ getService, getPageObjects }) { }); await PageObjects.settings.navigateTo(); await PageObjects.settings.clickKibanaSavedObjects(); - await PageObjects.settings.importFile( + await PageObjects.savedObjects.importFile( path.join(__dirname, 'exports', 'timezonetest_6_2_4.json') ); - await PageObjects.settings.checkImportSucceeded(); + await PageObjects.savedObjects.checkImportSucceeded(); await PageObjects.common.navigateToApp('dashboard'); await PageObjects.dashboard.preserveCrossAppState(); await PageObjects.dashboard.loadSavedDashboard('time zone test'); diff --git a/test/functional/apps/management/_import_objects.js b/test/functional/apps/management/_import_objects.js index 6306d11eadb653..c69111be6972bb 100644 --- a/test/functional/apps/management/_import_objects.js +++ b/test/functional/apps/management/_import_objects.js @@ -24,7 +24,7 @@ import { indexBy } from 'lodash'; export default function ({ getService, getPageObjects }) { const kibanaServer = getService('kibanaServer'); const esArchiver = getService('esArchiver'); - const PageObjects = getPageObjects(['common', 'settings', 'header']); + const PageObjects = getPageObjects(['common', 'settings', 'header', 'savedObjects']); const testSubjects = getService('testSubjects'); const log = getService('log'); @@ -43,22 +43,19 @@ export default function ({ getService, getPageObjects }) { }); it('should import saved objects', async function () { - await PageObjects.settings.importFile( + await PageObjects.savedObjects.importFile( path.join(__dirname, 'exports', '_import_objects.ndjson') ); - await PageObjects.settings.checkImportSucceeded(); - await PageObjects.settings.clickImportDone(); + await PageObjects.savedObjects.checkImportSucceeded(); + await PageObjects.savedObjects.clickImportDone(); // get all the elements in the table, and index them by the 'title' visible text field - const elements = indexBy( - await PageObjects.settings.getSavedObjectElementsInTable(), - 'title' - ); + const elements = indexBy(await PageObjects.savedObjects.getElementsInTable(), 'title'); log.debug("check that 'Log Agents' is in table as a visualization"); expect(elements['Log Agents'].objectType).to.eql('visualization'); await elements['logstash-*'].relationshipsElement.click(); - const flyout = indexBy(await PageObjects.settings.getRelationshipFlyout(), 'title'); + const flyout = indexBy(await PageObjects.savedObjects.getRelationshipFlyout(), 'title'); log.debug( "check that 'Shared-Item Visualization AreaChart' shows 'logstash-*' as it's Parent" ); @@ -68,18 +65,18 @@ export default function ({ getService, getPageObjects }) { }); it('should provide dialog to allow the importing of saved objects with index pattern conflicts', async function () { - await PageObjects.settings.importFile( + await PageObjects.savedObjects.importFile( path.join(__dirname, 'exports', '_import_objects_conflicts.ndjson') ); - await PageObjects.settings.checkImportConflictsWarning(); + await PageObjects.savedObjects.checkImportConflictsWarning(); await PageObjects.settings.associateIndexPattern( 'd1e4c910-a2e6-11e7-bb30-233be9be6a15', 'logstash-*' ); - await PageObjects.settings.clickConfirmChanges(); + await PageObjects.savedObjects.clickConfirmChanges(); await PageObjects.header.waitUntilLoadingHasFinished(); - await PageObjects.settings.clickImportDone(); - const objects = await PageObjects.settings.getSavedObjectsInTable(); + await PageObjects.savedObjects.clickImportDone(); + const objects = await PageObjects.savedObjects.getRowTitles(); const isSavedObjectImported = objects.includes('saved object with index pattern conflict'); expect(isSavedObjectImported).to.be(true); }); @@ -87,14 +84,14 @@ export default function ({ getService, getPageObjects }) { it('should allow the user to override duplicate saved objects', async function () { // This data has already been loaded by the "visualize" esArchive. We'll load it again // so that we can override the existing visualization. - await PageObjects.settings.importFile( + await PageObjects.savedObjects.importFile( path.join(__dirname, 'exports', '_import_objects_exists.ndjson'), false ); - await PageObjects.settings.checkImportConflictsWarning(); + await PageObjects.savedObjects.checkImportConflictsWarning(); await PageObjects.settings.associateIndexPattern('logstash-*', 'logstash-*'); - await PageObjects.settings.clickConfirmChanges(); + await PageObjects.savedObjects.clickConfirmChanges(); // Override the visualization. await PageObjects.common.clickConfirmOnModal(); @@ -106,14 +103,14 @@ export default function ({ getService, getPageObjects }) { it('should allow the user to cancel overriding duplicate saved objects', async function () { // This data has already been loaded by the "visualize" esArchive. We'll load it again // so that we can be prompted to override the existing visualization. - await PageObjects.settings.importFile( + await PageObjects.savedObjects.importFile( path.join(__dirname, 'exports', '_import_objects_exists.ndjson'), false ); - await PageObjects.settings.checkImportConflictsWarning(); + await PageObjects.savedObjects.checkImportConflictsWarning(); await PageObjects.settings.associateIndexPattern('logstash-*', 'logstash-*'); - await PageObjects.settings.clickConfirmChanges(); + await PageObjects.savedObjects.clickConfirmChanges(); // *Don't* override the visualization. await PageObjects.common.clickCancelOnModal(); @@ -123,86 +120,80 @@ export default function ({ getService, getPageObjects }) { }); it('should import saved objects linked to saved searches', async function () { - await PageObjects.settings.importFile( + await PageObjects.savedObjects.importFile( path.join(__dirname, 'exports', '_import_objects_saved_search.ndjson') ); - await PageObjects.settings.checkImportSucceeded(); - await PageObjects.settings.clickImportDone(); + await PageObjects.savedObjects.checkImportSucceeded(); + await PageObjects.savedObjects.clickImportDone(); - await PageObjects.settings.importFile( + await PageObjects.savedObjects.importFile( path.join(__dirname, 'exports', '_import_objects_connected_to_saved_search.ndjson') ); - await PageObjects.settings.checkImportSucceeded(); - await PageObjects.settings.clickImportDone(); + await PageObjects.savedObjects.checkImportSucceeded(); + await PageObjects.savedObjects.clickImportDone(); - const objects = await PageObjects.settings.getSavedObjectsInTable(); + const objects = await PageObjects.savedObjects.getRowTitles(); const isSavedObjectImported = objects.includes('saved object connected to saved search'); expect(isSavedObjectImported).to.be(true); }); it('should not import saved objects linked to saved searches when saved search does not exist', async function () { - await PageObjects.settings.importFile( + await PageObjects.savedObjects.importFile( path.join(__dirname, 'exports', '_import_objects_connected_to_saved_search.ndjson') ); - await PageObjects.settings.checkNoneImported(); - await PageObjects.settings.clickImportDone(); + await PageObjects.savedObjects.checkNoneImported(); + await PageObjects.savedObjects.clickImportDone(); - const objects = await PageObjects.settings.getSavedObjectsInTable(); + const objects = await PageObjects.savedObjects.getRowTitles(); const isSavedObjectImported = objects.includes('saved object connected to saved search'); expect(isSavedObjectImported).to.be(false); }); it('should not import saved objects linked to saved searches when saved search index pattern does not exist', async function () { - const elements = indexBy( - await PageObjects.settings.getSavedObjectElementsInTable(), - 'title' - ); + const elements = indexBy(await PageObjects.savedObjects.getElementsInTable(), 'title'); await elements['logstash-*'].checkbox.click(); - await PageObjects.settings.clickSavedObjectsDelete(); + await PageObjects.savedObjects.clickDelete(); - await PageObjects.settings.importFile( + await PageObjects.savedObjects.importFile( path.join(__dirname, 'exports', '_import_objects_with_saved_search.ndjson') ); // Wait for all the saves to happen - await PageObjects.settings.checkImportConflictsWarning(); - await PageObjects.settings.clickConfirmChanges(); - await PageObjects.settings.clickImportDone(); + await PageObjects.savedObjects.checkImportConflictsWarning(); + await PageObjects.savedObjects.clickConfirmChanges(); + await PageObjects.savedObjects.clickImportDone(); - const objects = await PageObjects.settings.getSavedObjectsInTable(); + const objects = await PageObjects.savedObjects.getRowTitles(); const isSavedObjectImported = objects.includes('saved object connected to saved search'); expect(isSavedObjectImported).to.be(false); }); it('should import saved objects with index patterns when index patterns already exists', async () => { // First, import the objects - await PageObjects.settings.importFile( + await PageObjects.savedObjects.importFile( path.join(__dirname, 'exports', '_import_objects_with_index_patterns.ndjson') ); - await PageObjects.settings.checkImportSucceeded(); - await PageObjects.settings.clickImportDone(); + await PageObjects.savedObjects.checkImportSucceeded(); + await PageObjects.savedObjects.clickImportDone(); - const objects = await PageObjects.settings.getSavedObjectsInTable(); + const objects = await PageObjects.savedObjects.getRowTitles(); const isSavedObjectImported = objects.includes('saved object imported with index pattern'); expect(isSavedObjectImported).to.be(true); }); it('should import saved objects with index patterns when index patterns does not exists', async () => { // First, we need to delete the index pattern - const elements = indexBy( - await PageObjects.settings.getSavedObjectElementsInTable(), - 'title' - ); + const elements = indexBy(await PageObjects.savedObjects.getElementsInTable(), 'title'); await elements['logstash-*'].checkbox.click(); - await PageObjects.settings.clickSavedObjectsDelete(); + await PageObjects.savedObjects.clickDelete(); // Then, import the objects - await PageObjects.settings.importFile( + await PageObjects.savedObjects.importFile( path.join(__dirname, 'exports', '_import_objects_with_index_patterns.ndjson') ); - await PageObjects.settings.checkImportSucceeded(); - await PageObjects.settings.clickImportDone(); + await PageObjects.savedObjects.checkImportSucceeded(); + await PageObjects.savedObjects.clickImportDone(); - const objects = await PageObjects.settings.getSavedObjectsInTable(); + const objects = await PageObjects.savedObjects.getRowTitles(); const isSavedObjectImported = objects.includes('saved object imported with index pattern'); expect(isSavedObjectImported).to.be(true); }); @@ -222,30 +213,30 @@ export default function ({ getService, getPageObjects }) { }); it('should import saved objects', async function () { - await PageObjects.settings.importFile( + await PageObjects.savedObjects.importFile( path.join(__dirname, 'exports', '_import_objects.json') ); - await PageObjects.settings.checkImportSucceeded(); - await PageObjects.settings.clickImportDone(); - const objects = await PageObjects.settings.getSavedObjectsInTable(); + await PageObjects.savedObjects.checkImportSucceeded(); + await PageObjects.savedObjects.clickImportDone(); + const objects = await PageObjects.savedObjects.getRowTitles(); const isSavedObjectImported = objects.includes('Log Agents'); expect(isSavedObjectImported).to.be(true); }); it('should provide dialog to allow the importing of saved objects with index pattern conflicts', async function () { - await PageObjects.settings.importFile( + await PageObjects.savedObjects.importFile( path.join(__dirname, 'exports', '_import_objects-conflicts.json') ); - await PageObjects.settings.checkImportLegacyWarning(); - await PageObjects.settings.checkImportConflictsWarning(); + await PageObjects.savedObjects.checkImportLegacyWarning(); + await PageObjects.savedObjects.checkImportConflictsWarning(); await PageObjects.settings.associateIndexPattern( 'd1e4c910-a2e6-11e7-bb30-233be9be6a15', 'logstash-*' ); - await PageObjects.settings.clickConfirmChanges(); + await PageObjects.savedObjects.clickConfirmChanges(); await PageObjects.header.waitUntilLoadingHasFinished(); - await PageObjects.settings.clickImportDone(); - const objects = await PageObjects.settings.getSavedObjectsInTable(); + await PageObjects.savedObjects.clickImportDone(); + const objects = await PageObjects.savedObjects.getRowTitles(); const isSavedObjectImported = objects.includes('saved object with index pattern conflict'); expect(isSavedObjectImported).to.be(true); }); @@ -253,15 +244,15 @@ export default function ({ getService, getPageObjects }) { it('should allow the user to override duplicate saved objects', async function () { // This data has already been loaded by the "visualize" esArchive. We'll load it again // so that we can override the existing visualization. - await PageObjects.settings.importFile( + await PageObjects.savedObjects.importFile( path.join(__dirname, 'exports', '_import_objects_exists.json'), false ); - await PageObjects.settings.checkImportLegacyWarning(); - await PageObjects.settings.checkImportConflictsWarning(); + await PageObjects.savedObjects.checkImportLegacyWarning(); + await PageObjects.savedObjects.checkImportConflictsWarning(); await PageObjects.settings.associateIndexPattern('logstash-*', 'logstash-*'); - await PageObjects.settings.clickConfirmChanges(); + await PageObjects.savedObjects.clickConfirmChanges(); // Override the visualization. await PageObjects.common.clickConfirmOnModal(); @@ -273,15 +264,15 @@ export default function ({ getService, getPageObjects }) { it('should allow the user to cancel overriding duplicate saved objects', async function () { // This data has already been loaded by the "visualize" esArchive. We'll load it again // so that we can be prompted to override the existing visualization. - await PageObjects.settings.importFile( + await PageObjects.savedObjects.importFile( path.join(__dirname, 'exports', '_import_objects_exists.json'), false ); - await PageObjects.settings.checkImportLegacyWarning(); - await PageObjects.settings.checkImportConflictsWarning(); + await PageObjects.savedObjects.checkImportLegacyWarning(); + await PageObjects.savedObjects.checkImportConflictsWarning(); await PageObjects.settings.associateIndexPattern('logstash-*', 'logstash-*'); - await PageObjects.settings.clickConfirmChanges(); + await PageObjects.savedObjects.clickConfirmChanges(); // *Don't* override the visualization. await PageObjects.common.clickCancelOnModal(); @@ -291,95 +282,89 @@ export default function ({ getService, getPageObjects }) { }); it('should import saved objects linked to saved searches', async function () { - await PageObjects.settings.importFile( + await PageObjects.savedObjects.importFile( path.join(__dirname, 'exports', '_import_objects_saved_search.json') ); - await PageObjects.settings.checkImportSucceeded(); - await PageObjects.settings.clickImportDone(); + await PageObjects.savedObjects.checkImportSucceeded(); + await PageObjects.savedObjects.clickImportDone(); - await PageObjects.settings.importFile( + await PageObjects.savedObjects.importFile( path.join(__dirname, 'exports', '_import_objects_connected_to_saved_search.json') ); - await PageObjects.settings.checkImportSucceeded(); - await PageObjects.settings.clickImportDone(); + await PageObjects.savedObjects.checkImportSucceeded(); + await PageObjects.savedObjects.clickImportDone(); - const objects = await PageObjects.settings.getSavedObjectsInTable(); + const objects = await PageObjects.savedObjects.getRowTitles(); const isSavedObjectImported = objects.includes('saved object connected to saved search'); expect(isSavedObjectImported).to.be(true); }); it('should not import saved objects linked to saved searches when saved search does not exist', async function () { - await PageObjects.settings.importFile( + await PageObjects.savedObjects.importFile( path.join(__dirname, 'exports', '_import_objects_connected_to_saved_search.json') ); - await PageObjects.settings.checkImportFailedWarning(); - await PageObjects.settings.clickImportDone(); + await PageObjects.savedObjects.checkImportFailedWarning(); + await PageObjects.savedObjects.clickImportDone(); - const objects = await PageObjects.settings.getSavedObjectsInTable(); + const objects = await PageObjects.savedObjects.getRowTitles(); const isSavedObjectImported = objects.includes('saved object connected to saved search'); expect(isSavedObjectImported).to.be(false); }); it('should not import saved objects linked to saved searches when saved search index pattern does not exist', async function () { // First, import the saved search - await PageObjects.settings.importFile( + await PageObjects.savedObjects.importFile( path.join(__dirname, 'exports', '_import_objects_saved_search.json') ); // Wait for all the saves to happen - await PageObjects.settings.checkImportSucceeded(); - await PageObjects.settings.clickImportDone(); + await PageObjects.savedObjects.checkImportSucceeded(); + await PageObjects.savedObjects.clickImportDone(); // Second, we need to delete the index pattern - const elements = indexBy( - await PageObjects.settings.getSavedObjectElementsInTable(), - 'title' - ); + const elements = indexBy(await PageObjects.savedObjects.getElementsInTable(), 'title'); await elements['logstash-*'].checkbox.click(); - await PageObjects.settings.clickSavedObjectsDelete(); + await PageObjects.savedObjects.clickDelete(); // Last, import a saved object connected to the saved search // This should NOT show the conflicts - await PageObjects.settings.importFile( + await PageObjects.savedObjects.importFile( path.join(__dirname, 'exports', '_import_objects_connected_to_saved_search.json') ); // Wait for all the saves to happen - await PageObjects.settings.checkNoneImported(); - await PageObjects.settings.clickImportDone(); + await PageObjects.savedObjects.checkNoneImported(); + await PageObjects.savedObjects.clickImportDone(); - const objects = await PageObjects.settings.getSavedObjectsInTable(); + const objects = await PageObjects.savedObjects.getRowTitles(); const isSavedObjectImported = objects.includes('saved object connected to saved search'); expect(isSavedObjectImported).to.be(false); }); it('should import saved objects with index patterns when index patterns already exists', async () => { // First, import the objects - await PageObjects.settings.importFile( + await PageObjects.savedObjects.importFile( path.join(__dirname, 'exports', '_import_objects_with_index_patterns.json') ); - await PageObjects.settings.clickImportDone(); + await PageObjects.savedObjects.clickImportDone(); - const objects = await PageObjects.settings.getSavedObjectsInTable(); + const objects = await PageObjects.savedObjects.getRowTitles(); const isSavedObjectImported = objects.includes('saved object imported with index pattern'); expect(isSavedObjectImported).to.be(true); }); it('should import saved objects with index patterns when index patterns does not exists', async () => { // First, we need to delete the index pattern - const elements = indexBy( - await PageObjects.settings.getSavedObjectElementsInTable(), - 'title' - ); + const elements = indexBy(await PageObjects.savedObjects.getElementsInTable(), 'title'); await elements['logstash-*'].checkbox.click(); - await PageObjects.settings.clickSavedObjectsDelete(); + await PageObjects.savedObjects.clickDelete(); // Then, import the objects - await PageObjects.settings.importFile( + await PageObjects.savedObjects.importFile( path.join(__dirname, 'exports', '_import_objects_with_index_patterns.json') ); - await PageObjects.settings.checkImportSucceeded(); - await PageObjects.settings.clickImportDone(); + await PageObjects.savedObjects.checkImportSucceeded(); + await PageObjects.savedObjects.clickImportDone(); - const objects = await PageObjects.settings.getSavedObjectsInTable(); + const objects = await PageObjects.savedObjects.getRowTitles(); const isSavedObjectImported = objects.includes('saved object imported with index pattern'); expect(isSavedObjectImported).to.be(true); }); diff --git a/test/functional/apps/management/_mgmt_import_saved_objects.js b/test/functional/apps/management/_mgmt_import_saved_objects.js index a8a0a19d4962d9..3a9f8665fd33bc 100644 --- a/test/functional/apps/management/_mgmt_import_saved_objects.js +++ b/test/functional/apps/management/_mgmt_import_saved_objects.js @@ -22,7 +22,7 @@ import path from 'path'; export default function ({ getService, getPageObjects }) { const esArchiver = getService('esArchiver'); - const PageObjects = getPageObjects(['common', 'settings', 'header']); + const PageObjects = getPageObjects(['common', 'settings', 'header', 'savedObjects']); //in 6.4.0 bug the Saved Search conflict would be resolved and get imported but the visualization //that referenced the saved search was not imported.( https://github.com/elastic/kibana/issues/22238) @@ -40,19 +40,19 @@ export default function ({ getService, getPageObjects }) { it('should import saved objects mgmt', async function () { await PageObjects.settings.clickKibanaSavedObjects(); - await PageObjects.settings.importFile( + await PageObjects.savedObjects.importFile( path.join(__dirname, 'exports', 'mgmt_import_objects.json') ); await PageObjects.settings.associateIndexPattern( '4c3f3c30-ac94-11e8-a651-614b2788174a', 'logstash-*' ); - await PageObjects.settings.clickConfirmChanges(); - await PageObjects.settings.clickImportDone(); - await PageObjects.settings.waitUntilSavedObjectsTableIsNotLoading(); + await PageObjects.savedObjects.clickConfirmChanges(); + await PageObjects.savedObjects.clickImportDone(); + await PageObjects.savedObjects.waitTableIsLoaded(); //instead of asserting on count- am asserting on the titles- which is more accurate than count. - const objects = await PageObjects.settings.getSavedObjectsInTable(); + const objects = await PageObjects.savedObjects.getRowTitles(); expect(objects.includes('mysavedsearch')).to.be(true); expect(objects.includes('mysavedviz')).to.be(true); }); diff --git a/test/functional/apps/saved_objects_management/edit_saved_object.ts b/test/functional/apps/saved_objects_management/edit_saved_object.ts index 6e4b820879ed38..2c9200c2f8d936 100644 --- a/test/functional/apps/saved_objects_management/edit_saved_object.ts +++ b/test/functional/apps/saved_objects_management/edit_saved_object.ts @@ -25,7 +25,7 @@ const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); export default function ({ getPageObjects, getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const testSubjects = getService('testSubjects'); - const PageObjects = getPageObjects(['common', 'settings']); + const PageObjects = getPageObjects(['common', 'settings', 'savedObjects']); const browser = getService('browser'); const find = getService('find'); @@ -79,7 +79,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await PageObjects.settings.navigateTo(); await PageObjects.settings.clickKibanaSavedObjects(); - let objects = await PageObjects.settings.getSavedObjectsInTable(); + let objects = await PageObjects.savedObjects.getRowTitles(); expect(objects.includes('A Dashboard')).to.be(true); await PageObjects.common.navigateToUrl( @@ -99,7 +99,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await focusAndClickButton('savedObjectEditSave'); - objects = await PageObjects.settings.getSavedObjectsInTable(); + objects = await PageObjects.savedObjects.getRowTitles(); expect(objects.includes('A Dashboard')).to.be(false); expect(objects.includes('Edited Dashboard')).to.be(true); @@ -127,7 +127,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await focusAndClickButton('savedObjectEditDelete'); await PageObjects.common.clickConfirmOnModal(); - const objects = await PageObjects.settings.getSavedObjectsInTable(); + const objects = await PageObjects.savedObjects.getRowTitles(); expect(objects.includes('A Dashboard')).to.be(false); }); @@ -145,7 +145,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await PageObjects.settings.navigateTo(); await PageObjects.settings.clickKibanaSavedObjects(); - const objects = await PageObjects.settings.getSavedObjectsInTable(); + const objects = await PageObjects.savedObjects.getRowTitles(); expect(objects.includes('A Pie')).to.be(true); await PageObjects.common.navigateToUrl('management', testVisualizationUrl, { @@ -160,7 +160,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await focusAndClickButton('savedObjectEditSave'); - await PageObjects.settings.getSavedObjectsInTable(); + await PageObjects.savedObjects.getRowTitles(); await PageObjects.common.navigateToUrl('management', testVisualizationUrl, { shouldUseHashForSubUrl: false, @@ -173,7 +173,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await focusAndClickButton('savedObjectEditSave'); - await PageObjects.settings.getSavedObjectsInTable(); + await PageObjects.savedObjects.getRowTitles(); await PageObjects.common.navigateToUrl('management', testVisualizationUrl, { shouldUseHashForSubUrl: false, diff --git a/test/functional/page_objects/index.ts b/test/functional/page_objects/index.ts index 10b09c742f58e7..d3a8fb73ac3e5c 100644 --- a/test/functional/page_objects/index.ts +++ b/test/functional/page_objects/index.ts @@ -38,6 +38,7 @@ import { VisualizeChartPageProvider } from './visualize_chart_page'; import { TileMapPageProvider } from './tile_map_page'; import { TagCloudPageProvider } from './tag_cloud_page'; import { VegaChartPageProvider } from './vega_chart_page'; +import { SavedObjectsPageProvider } from './management/saved_objects_page'; export const pageObjects = { common: CommonPageProvider, @@ -61,4 +62,5 @@ export const pageObjects = { tileMap: TileMapPageProvider, tagCloud: TagCloudPageProvider, vegaChart: VegaChartPageProvider, + savedObjects: SavedObjectsPageProvider, }; diff --git a/test/functional/page_objects/management/saved_objects_page.ts b/test/functional/page_objects/management/saved_objects_page.ts new file mode 100644 index 00000000000000..d058695ea6819d --- /dev/null +++ b/test/functional/page_objects/management/saved_objects_page.ts @@ -0,0 +1,184 @@ +/* + * 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 { map as mapAsync } from 'bluebird'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +export function SavedObjectsPageProvider({ getService, getPageObjects }: FtrProviderContext) { + const log = getService('log'); + const retry = getService('retry'); + const browser = getService('browser'); + const find = getService('find'); + const testSubjects = getService('testSubjects'); + const PageObjects = getPageObjects(['header', 'common']); + + class SavedObjectsPage { + async searchForObject(objectName: string) { + const searchBox = await testSubjects.find('savedObjectSearchBar'); + await searchBox.clearValue(); + await searchBox.type(objectName); + await searchBox.pressKeys(browser.keys.ENTER); + } + + async importFile(path: string, overwriteAll = true) { + log.debug(`importFile(${path})`); + + log.debug(`Clicking importObjects`); + await testSubjects.click('importObjects'); + await PageObjects.common.setFileInputPath(path); + + if (!overwriteAll) { + log.debug(`Toggling overwriteAll`); + await testSubjects.click('importSavedObjectsOverwriteToggle'); + } else { + log.debug(`Leaving overwriteAll alone`); + } + await testSubjects.click('importSavedObjectsImportBtn'); + log.debug(`done importing the file`); + + // Wait for all the saves to happen + await PageObjects.header.waitUntilLoadingHasFinished(); + } + + async checkImportSucceeded() { + await testSubjects.existOrFail('importSavedObjectsSuccess', { timeout: 20000 }); + } + + async checkNoneImported() { + await testSubjects.existOrFail('importSavedObjectsSuccessNoneImported', { timeout: 20000 }); + } + + async checkImportConflictsWarning() { + await testSubjects.existOrFail('importSavedObjectsConflictsWarning', { timeout: 20000 }); + } + + async checkImportLegacyWarning() { + await testSubjects.existOrFail('importSavedObjectsLegacyWarning', { timeout: 20000 }); + } + + async checkImportFailedWarning() { + await testSubjects.existOrFail('importSavedObjectsFailedWarning', { timeout: 20000 }); + } + + async clickImportDone() { + await testSubjects.click('importSavedObjectsDoneBtn'); + await this.waitTableIsLoaded(); + } + + async clickConfirmChanges() { + await testSubjects.click('importSavedObjectsConfirmBtn'); + } + + async waitTableIsLoaded() { + return retry.try(async () => { + const exists = await find.existsByDisplayedByCssSelector( + '*[data-test-subj="savedObjectsTable"] .euiBasicTable-loading' + ); + if (exists) { + throw new Error('Waiting'); + } + return true; + }); + } + + async getElementsInTable() { + const rows = await testSubjects.findAll('~savedObjectsTableRow'); + return mapAsync(rows, async (row) => { + const checkbox = await row.findByCssSelector('[data-test-subj*="checkboxSelectRow"]'); + // return the object type aria-label="index patterns" + const objectType = await row.findByTestSubject('objectType'); + const titleElement = await row.findByTestSubject('savedObjectsTableRowTitle'); + // not all rows have inspect button - Advanced Settings objects don't + let inspectElement; + const innerHtml = await row.getAttribute('innerHTML'); + if (innerHtml.includes('Inspect')) { + inspectElement = await row.findByTestSubject('savedObjectsTableAction-inspect'); + } else { + inspectElement = null; + } + const relationshipsElement = await row.findByTestSubject( + 'savedObjectsTableAction-relationships' + ); + return { + checkbox, + objectType: await objectType.getAttribute('aria-label'), + titleElement, + title: await titleElement.getVisibleText(), + inspectElement, + relationshipsElement, + }; + }); + } + + async getRowTitles() { + await this.waitTableIsLoaded(); + const table = await testSubjects.find('savedObjectsTable'); + const $ = await table.parseDomContent(); + return $.findTestSubjects('savedObjectsTableRowTitle') + .toArray() + .map((cell) => $(cell).find('.euiTableCellContent').text()); + } + + async getRelationshipFlyout() { + const rows = await testSubjects.findAll('relationshipsTableRow'); + return mapAsync(rows, async (row) => { + const objectType = await row.findByTestSubject('relationshipsObjectType'); + const relationship = await row.findByTestSubject('directRelationship'); + const titleElement = await row.findByTestSubject('relationshipsTitle'); + const inspectElement = await row.findByTestSubject('relationshipsTableAction-inspect'); + return { + objectType: await objectType.getAttribute('aria-label'), + relationship: await relationship.getVisibleText(), + titleElement, + title: await titleElement.getVisibleText(), + inspectElement, + }; + }); + } + + async getTableSummary() { + const table = await testSubjects.find('savedObjectsTable'); + const $ = await table.parseDomContent(); + return $('tbody tr') + .toArray() + .map((row) => { + return { + title: $(row).find('td:nth-child(3) .euiTableCellContent').text(), + canViewInApp: Boolean($(row).find('td:nth-child(3) a').length), + }; + }); + } + + async clickTableSelectAll() { + await testSubjects.click('checkboxSelectAll'); + } + + async canBeDeleted() { + return await testSubjects.isEnabled('savedObjectsManagementDelete'); + } + + async clickDelete() { + await testSubjects.click('savedObjectsManagementDelete'); + await testSubjects.click('confirmModalConfirmButton'); + await this.waitTableIsLoaded(); + } + } + + return new SavedObjectsPage(); +} diff --git a/test/functional/page_objects/settings_page.ts b/test/functional/page_objects/settings_page.ts index f5b4eb7ad5de83..e491cd7e4fe40a 100644 --- a/test/functional/page_objects/settings_page.ts +++ b/test/functional/page_objects/settings_page.ts @@ -29,7 +29,7 @@ export function SettingsPageProvider({ getService, getPageObjects }: FtrProvider const flyout = getService('flyout'); const testSubjects = getService('testSubjects'); const comboBox = getService('comboBox'); - const PageObjects = getPageObjects(['header', 'common']); + const PageObjects = getPageObjects(['header', 'common', 'savedObjects']); class SettingsPage { async clickNavigation() { @@ -47,7 +47,7 @@ export function SettingsPageProvider({ getService, getPageObjects }: FtrProvider async clickKibanaSavedObjects() { await testSubjects.click('objects'); - await this.waitUntilSavedObjectsTableIsNotLoading(); + await PageObjects.savedObjects.waitTableIsLoaded(); } async clickKibanaIndexPatterns() { @@ -68,13 +68,13 @@ export function SettingsPageProvider({ getService, getPageObjects }: FtrProvider async getAdvancedSettings(propertyName: string) { log.debug('in getAdvancedSettings'); - const setting = await testSubjects.find(`advancedSetting-editField-${propertyName}`); - return await setting.getAttribute('value'); + return await testSubjects.getAttribute(`advancedSetting-editField-${propertyName}`, 'value'); } async expectDisabledAdvancedSetting(propertyName: string) { - const setting = await testSubjects.find(`advancedSetting-editField-${propertyName}`); - expect(setting.getAttribute('disabled')).to.eql(''); + expect( + await testSubjects.getAttribute(`advancedSetting-editField-${propertyName}`, 'disabled') + ).to.eql('true'); } async getAdvancedSettingCheckbox(propertyName: string) { @@ -274,9 +274,7 @@ export function SettingsPageProvider({ getService, getPageObjects }: FtrProvider } async increasePopularity() { - const field = await testSubjects.find('editorFieldCount'); - await field.clearValueWithKeyboard(); - await field.type('1'); + await testSubjects.setValue('editorFieldCount', '1', { clearWithKeyboard: true }); } async getPopularity() { @@ -499,9 +497,7 @@ export function SettingsPageProvider({ getService, getPageObjects }: FtrProvider async setScriptedFieldName(name: string) { log.debug('set scripted field name = ' + name); - const field = await testSubjects.find('editorFieldName'); - await field.clearValue(); - await field.type(name); + await testSubjects.setValue('editorFieldName', name); } async setScriptedFieldLanguage(language: string) { @@ -568,9 +564,7 @@ export function SettingsPageProvider({ getService, getPageObjects }: FtrProvider async setScriptedFieldPopularity(popularity: string) { log.debug('set scripted field popularity = ' + popularity); - const field = await testSubjects.find('editorFieldCount'); - await field.clearValue(); - await field.type(popularity); + await testSubjects.setValue('editorFieldCount', popularity); } async setScriptedFieldScript(script: string) { @@ -623,55 +617,6 @@ export function SettingsPageProvider({ getService, getPageObjects }: FtrProvider return scriptResults; } - async importFile(path: string, overwriteAll = true) { - log.debug(`importFile(${path})`); - - log.debug(`Clicking importObjects`); - await testSubjects.click('importObjects'); - await PageObjects.common.setFileInputPath(path); - - if (!overwriteAll) { - log.debug(`Toggling overwriteAll`); - await testSubjects.click('importSavedObjectsOverwriteToggle'); - } else { - log.debug(`Leaving overwriteAll alone`); - } - await testSubjects.click('importSavedObjectsImportBtn'); - log.debug(`done importing the file`); - - // Wait for all the saves to happen - await PageObjects.header.waitUntilLoadingHasFinished(); - } - - async checkImportSucceeded() { - await testSubjects.existOrFail('importSavedObjectsSuccess', { timeout: 20000 }); - } - - async checkNoneImported() { - await testSubjects.existOrFail('importSavedObjectsSuccessNoneImported', { timeout: 20000 }); - } - - async checkImportConflictsWarning() { - await testSubjects.existOrFail('importSavedObjectsConflictsWarning', { timeout: 20000 }); - } - - async checkImportLegacyWarning() { - await testSubjects.existOrFail('importSavedObjectsLegacyWarning', { timeout: 20000 }); - } - - async checkImportFailedWarning() { - await testSubjects.existOrFail('importSavedObjectsFailedWarning', { timeout: 20000 }); - } - - async clickImportDone() { - await testSubjects.click('importSavedObjectsDoneBtn'); - await this.waitUntilSavedObjectsTableIsNotLoading(); - } - - async clickConfirmChanges() { - await testSubjects.click('importSavedObjectsConfirmBtn'); - } - async clickEditFieldFormat() { await testSubjects.click('editFieldFormat'); } @@ -686,112 +631,6 @@ export function SettingsPageProvider({ getService, getPageObjects }: FtrProvider async clickChangeIndexConfirmButton() { await testSubjects.click('changeIndexConfirmButton'); } - - async waitUntilSavedObjectsTableIsNotLoading() { - return retry.try(async () => { - const exists = await find.existsByDisplayedByCssSelector( - '*[data-test-subj="savedObjectsTable"] .euiBasicTable-loading' - ); - if (exists) { - throw new Error('Waiting'); - } - return true; - }); - } - - async getSavedObjectElementsInTable() { - const rows = await testSubjects.findAll('~savedObjectsTableRow'); - return mapAsync(rows, async (row) => { - const checkbox = await row.findByCssSelector('[data-test-subj*="checkboxSelectRow"]'); - // return the object type aria-label="index patterns" - const objectType = await row.findByTestSubject('objectType'); - const titleElement = await row.findByTestSubject('savedObjectsTableRowTitle'); - // not all rows have inspect button - Advanced Settings objects don't - let inspectElement; - const innerHtml = await row.getAttribute('innerHTML'); - if (innerHtml.includes('Inspect')) { - inspectElement = await row.findByTestSubject('savedObjectsTableAction-inspect'); - } else { - inspectElement = null; - } - const relationshipsElement = await row.findByTestSubject( - 'savedObjectsTableAction-relationships' - ); - return { - checkbox, - objectType: await objectType.getAttribute('aria-label'), - titleElement, - title: await titleElement.getVisibleText(), - inspectElement, - relationshipsElement, - }; - }); - } - - async getSavedObjectsInTable() { - const table = await testSubjects.find('savedObjectsTable'); - const cells = await table.findAllByTestSubject('savedObjectsTableRowTitle'); - - const objects = []; - for (const cell of cells) { - objects.push(await cell.getVisibleText()); - } - - return objects; - } - - async getRelationshipFlyout() { - const rows = await testSubjects.findAll('relationshipsTableRow'); - return mapAsync(rows, async (row) => { - const objectType = await row.findByTestSubject('relationshipsObjectType'); - const relationship = await row.findByTestSubject('directRelationship'); - const titleElement = await row.findByTestSubject('relationshipsTitle'); - const inspectElement = await row.findByTestSubject('relationshipsTableAction-inspect'); - return { - objectType: await objectType.getAttribute('aria-label'), - relationship: await relationship.getVisibleText(), - titleElement, - title: await titleElement.getVisibleText(), - inspectElement, - }; - }); - } - - async getSavedObjectsTableSummary() { - const table = await testSubjects.find('savedObjectsTable'); - const rows = await table.findAllByCssSelector('tbody tr'); - - const summary = []; - for (const row of rows) { - const titleCell = await row.findByCssSelector('td:nth-child(3)'); - const title = await titleCell.getVisibleText(); - - const viewInAppButtons = await row.findAllByCssSelector('td:nth-child(3) a'); - const canViewInApp = Boolean(viewInAppButtons.length); - summary.push({ - title, - canViewInApp, - }); - } - - return summary; - } - - async clickSavedObjectsTableSelectAll() { - const checkboxSelectAll = await testSubjects.find('checkboxSelectAll'); - await checkboxSelectAll.click(); - } - - async canSavedObjectsBeDeleted() { - const deleteButton = await testSubjects.find('savedObjectsManagementDelete'); - return await deleteButton.isEnabled(); - } - - async clickSavedObjectsDelete() { - await testSubjects.click('savedObjectsManagementDelete'); - await testSubjects.click('confirmModalConfirmButton'); - await this.waitUntilSavedObjectsTableIsNotLoading(); - } } return new SettingsPage(); diff --git a/test/scripts/jenkins_xpack.sh b/test/scripts/jenkins_xpack.sh index 067ed213c49f5f..bc927b1ed7b4da 100755 --- a/test/scripts/jenkins_xpack.sh +++ b/test/scripts/jenkins_xpack.sh @@ -15,9 +15,9 @@ if [[ -z "$CODE_COVERAGE" ]] ; then echo "" echo "" - echo " -> Running SIEM cyclic dependency test" + echo " -> Running Security Solution cyclic dependency test" cd "$XPACK_DIR" - checks-reporter-with-killswitch "X-Pack SIEM cyclic dependency test" node plugins/security_solution/scripts/check_circular_deps + checks-reporter-with-killswitch "X-Pack Security Solution cyclic dependency test" node plugins/security_solution/scripts/check_circular_deps echo "" echo "" diff --git a/x-pack/examples/ui_actions_enhanced_examples/public/dashboard_to_url_drilldown/index.tsx b/x-pack/examples/ui_actions_enhanced_examples/public/dashboard_to_url_drilldown/index.tsx index 4810fb2d6ad8da..5e4ba54864461b 100644 --- a/x-pack/examples/ui_actions_enhanced_examples/public/dashboard_to_url_drilldown/index.tsx +++ b/x-pack/examples/ui_actions_enhanced_examples/public/dashboard_to_url_drilldown/index.tsx @@ -39,6 +39,8 @@ export class DashboardToUrlDrilldown implements Drilldown public readonly order = 8; + readonly minimalLicense = 'gold'; // example of minimal license support + public readonly getDisplayName = () => 'Go to URL (example)'; public readonly euiIcon = 'link'; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/__mocks__/loader.ts b/x-pack/plugins/lens/public/indexpattern_datasource/__mocks__/loader.ts index fe865edd629868..f2fedda1fa3530 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/__mocks__/loader.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/__mocks__/loader.ts @@ -19,7 +19,6 @@ export function loadInitialState() { [restricted.id]: restricted, }, layers: {}, - showEmptyFields: false, }; return result; } diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/__snapshots__/no_fields_callout.test.tsx.snap b/x-pack/plugins/lens/public/indexpattern_datasource/__snapshots__/no_fields_callout.test.tsx.snap new file mode 100644 index 00000000000000..607f968d86faa0 --- /dev/null +++ b/x-pack/plugins/lens/public/indexpattern_datasource/__snapshots__/no_fields_callout.test.tsx.snap @@ -0,0 +1,49 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`NoFieldCallout renders properly for index with no fields 1`] = ` + +`; + +exports[`NoFieldCallout renders properly when affected by field filter 1`] = ` + + + Try: + +
    +
  • + Using different field filters +
  • +
+
+`; + +exports[`NoFieldCallout renders properly when affected by field filters, global filter and timerange 1`] = ` + + + Try: + +
    +
  • + Extending the time range +
  • +
  • + Using different field filters +
  • +
  • + Changing the global filters +
  • +
+
+`; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/_index.scss b/x-pack/plugins/lens/public/indexpattern_datasource/_index.scss index a0f3e53d7ac2c7..a10dde48816911 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/_index.scss +++ b/x-pack/plugins/lens/public/indexpattern_datasource/_index.scss @@ -1,2 +1 @@ -@import 'datapanel'; @import 'field_item'; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/_datapanel.scss b/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.scss similarity index 81% rename from x-pack/plugins/lens/public/indexpattern_datasource/_datapanel.scss rename to x-pack/plugins/lens/public/indexpattern_datasource/datapanel.scss index 77d4b41a0413c1..3e767502fae3b5 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/_datapanel.scss +++ b/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.scss @@ -16,10 +16,6 @@ line-height: $euiSizeXXL; } -.lnsInnerIndexPatternDataPanel__filterWrapper { - flex-grow: 0; -} - /** * 1. Don't cut off the shadow of the field items */ @@ -41,11 +37,9 @@ right: $euiSizeXS; /* 1 */ } -.lnsInnerIndexPatternDataPanel__filterButton { - width: 100%; - color: $euiColorPrimary; - padding-left: $euiSizeS; - padding-right: $euiSizeS; +.lnsInnerIndexPatternDataPanel__fieldItems { + // Quick fix for making sure the shadow and focus rings are visible outside the accordion bounds + padding: $euiSizeXS $euiSizeXS 0; } .lnsInnerIndexPatternDataPanel__textField { @@ -54,7 +48,9 @@ } .lnsInnerIndexPatternDataPanel__filterType { + font-size: $euiFontSizeS; padding: $euiSizeS; + border-bottom: 1px solid $euiColorLightestShade; } .lnsInnerIndexPatternDataPanel__filterTypeInner { diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.test.tsx index 187ccb8c47563c..7653dab2c9b846 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.test.tsx @@ -9,19 +9,19 @@ import { createMockedDragDropContext } from './mocks'; import { dataPluginMock } from '../../../../../src/plugins/data/public/mocks'; import { InnerIndexPatternDataPanel, IndexPatternDataPanel, MemoizedDataPanel } from './datapanel'; import { FieldItem } from './field_item'; +import { NoFieldsCallout } from './no_fields_callout'; import { act } from 'react-dom/test-utils'; import { coreMock } from 'src/core/public/mocks'; import { IndexPatternPrivateState } from './types'; import { mountWithIntl, shallowWithIntl } from 'test_utils/enzyme_helpers'; import { ChangeIndexPattern } from './change_indexpattern'; -import { EuiProgress } from '@elastic/eui'; +import { EuiProgress, EuiLoadingSpinner } from '@elastic/eui'; import { documentField } from './document_field'; const initialState: IndexPatternPrivateState = { indexPatternRefs: [], existingFields: {}, currentIndexPatternId: '1', - showEmptyFields: false, layers: { first: { indexPatternId: '1', @@ -229,8 +229,6 @@ describe('IndexPattern Data Panel', () => { }, query: { query: '', language: 'lucene' }, filters: [], - showEmptyFields: false, - onToggleEmptyFields: jest.fn(), }; }); @@ -303,7 +301,6 @@ describe('IndexPattern Data Panel', () => { state: { indexPatternRefs: [], existingFields: {}, - showEmptyFields: false, currentIndexPatternId: 'a', indexPatterns: { a: { id: 'a', title: 'aaa', timeFieldName: 'atime', fields: [] }, @@ -534,42 +531,97 @@ describe('IndexPattern Data Panel', () => { }); }); - describe('while showing empty fields', () => { - it('should list all supported fields in the pattern sorted alphabetically', async () => { - const wrapper = shallowWithIntl( - + describe('displaying field list', () => { + let props: Parameters[0]; + beforeEach(() => { + props = { + ...defaultProps, + existingFields: { + idx1: { + bytes: true, + memory: true, + }, + }, + }; + }); + it('should list all supported fields in the pattern sorted alphabetically in groups', async () => { + const wrapper = mountWithIntl(); + expect(wrapper.find(FieldItem).first().prop('field').name).toEqual('Records'); + expect( + wrapper + .find('[data-test-subj="lnsIndexPatternAvailableFields"]') + .find(FieldItem) + .map((fieldItem) => fieldItem.prop('field').name) + ).toEqual(['bytes', 'memory']); + wrapper + .find('[data-test-subj="lnsIndexPatternEmptyFields"]') + .find('button') + .first() + .simulate('click'); + expect( + wrapper + .find('[data-test-subj="lnsIndexPatternEmptyFields"]') + .find(FieldItem) + .map((fieldItem) => fieldItem.prop('field').name) + ).toEqual(['client', 'source', 'timestamp']); + }); + + it('should display NoFieldsCallout when all fields are empty', async () => { + const wrapper = mountWithIntl( + ); + expect(wrapper.find(NoFieldsCallout).length).toEqual(1); + expect( + wrapper + .find('[data-test-subj="lnsIndexPatternAvailableFields"]') + .find(FieldItem) + .map((fieldItem) => fieldItem.prop('field').name) + ).toEqual([]); + wrapper + .find('[data-test-subj="lnsIndexPatternEmptyFields"]') + .find('button') + .first() + .simulate('click'); + expect( + wrapper + .find('[data-test-subj="lnsIndexPatternEmptyFields"]') + .find(FieldItem) + .map((fieldItem) => fieldItem.prop('field').name) + ).toEqual(['bytes', 'client', 'memory', 'source', 'timestamp']); + }); - expect(wrapper.find(FieldItem).map((fieldItem) => fieldItem.prop('field').name)).toEqual([ - 'Records', - 'bytes', - 'client', - 'memory', - 'source', - 'timestamp', - ]); + it('should display spinner for available fields accordion if existing fields are not loaded yet', async () => { + const wrapper = mountWithIntl(); + expect( + wrapper.find('[data-test-subj="lnsIndexPatternAvailableFields"]').find(EuiLoadingSpinner) + .length + ).toEqual(1); + wrapper.setProps({ existingFields: { idx1: {} } }); + expect(wrapper.find(NoFieldsCallout).length).toEqual(1); }); it('should filter down by name', () => { - const wrapper = shallowWithIntl( - - ); - + const wrapper = mountWithIntl(); act(() => { wrapper.find('[data-test-subj="lnsIndexPatternFieldSearch"]').prop('onChange')!({ - target: { value: 'mem' }, + target: { value: 'me' }, } as ChangeEvent); }); + wrapper + .find('[data-test-subj="lnsIndexPatternEmptyFields"]') + .find('button') + .first() + .simulate('click'); + expect(wrapper.find(FieldItem).map((fieldItem) => fieldItem.prop('field').name)).toEqual([ 'memory', + 'timestamp', ]); }); it('should filter down by type', () => { - const wrapper = mountWithIntl( - - ); + const wrapper = mountWithIntl(); wrapper.find('[data-test-subj="lnsIndexPatternFiltersToggle"]').first().simulate('click'); @@ -581,112 +633,55 @@ describe('IndexPattern Data Panel', () => { ]); }); - it('should toggle type if clicked again', () => { - const wrapper = mountWithIntl( - - ); + it('should display no fields in groups when filtered by type Record', () => { + const wrapper = mountWithIntl(); wrapper.find('[data-test-subj="lnsIndexPatternFiltersToggle"]').first().simulate('click'); - wrapper.find('[data-test-subj="typeFilter-number"]').first().simulate('click'); - wrapper.find('[data-test-subj="typeFilter-number"]').first().simulate('click'); + wrapper.find('[data-test-subj="typeFilter-document"]').first().simulate('click'); expect(wrapper.find(FieldItem).map((fieldItem) => fieldItem.prop('field').name)).toEqual([ 'Records', - 'bytes', - 'client', - 'memory', - 'source', - 'timestamp', ]); + expect(wrapper.find(NoFieldsCallout).length).toEqual(2); }); - it('should filter down by type and by name', () => { - const wrapper = mountWithIntl( - - ); - - act(() => { - wrapper.find('[data-test-subj="lnsIndexPatternFieldSearch"]').prop('onChange')!({ - target: { value: 'mem' }, - } as ChangeEvent); - }); - + it('should toggle type if clicked again', () => { + const wrapper = mountWithIntl(); wrapper.find('[data-test-subj="lnsIndexPatternFiltersToggle"]').first().simulate('click'); wrapper.find('[data-test-subj="typeFilter-number"]').first().simulate('click'); - - expect(wrapper.find(FieldItem).map((fieldItem) => fieldItem.prop('field').name)).toEqual([ - 'memory', - ]); - }); - }); - - describe('filtering out empty fields', () => { - let emptyFieldsTestProps: typeof defaultProps; - - beforeEach(() => { - emptyFieldsTestProps = { - ...defaultProps, - indexPatterns: { - ...defaultProps.indexPatterns, - '1': { - ...defaultProps.indexPatterns['1'], - fields: defaultProps.indexPatterns['1'].fields.map((field) => ({ - ...field, - exists: field.type === 'number', - })), - }, - }, - onToggleEmptyFields: jest.fn(), - }; - }); - - it('should list all supported fields in the pattern sorted alphabetically', async () => { - const props = { - ...emptyFieldsTestProps, - existingFields: { - idx1: { - bytes: true, - memory: true, - }, - }, - }; - const wrapper = shallowWithIntl(); - + wrapper.find('[data-test-subj="typeFilter-number"]').first().simulate('click'); + wrapper + .find('[data-test-subj="lnsIndexPatternEmptyFields"]') + .find('button') + .first() + .simulate('click'); expect(wrapper.find(FieldItem).map((fieldItem) => fieldItem.prop('field').name)).toEqual([ 'Records', 'bytes', 'memory', + 'client', + 'source', + 'timestamp', ]); }); - it('should filter down by name', () => { - const wrapper = shallowWithIntl( - - ); - + it('should filter down by type and by name', () => { + const wrapper = mountWithIntl(); act(() => { wrapper.find('[data-test-subj="lnsIndexPatternFieldSearch"]').prop('onChange')!({ - target: { value: 'mem' }, + target: { value: 'me' }, } as ChangeEvent); }); - expect(wrapper.find(FieldItem).map((fieldItem) => fieldItem.prop('field').name)).toEqual([ - 'memory', - ]); - }); - - it('should allow removing the filter for data', () => { - const wrapper = mountWithIntl(); - wrapper.find('[data-test-subj="lnsIndexPatternFiltersToggle"]').first().simulate('click'); - wrapper.find('[data-test-subj="lnsEmptyFilter"]').first().prop('onChange')!( - {} as ChangeEvent - ); + wrapper.find('[data-test-subj="typeFilter-number"]').first().simulate('click'); - expect(emptyFieldsTestProps.onToggleEmptyFields).toHaveBeenCalled(); + expect(wrapper.find(FieldItem).map((fieldItem) => fieldItem.prop('field').name)).toEqual([ + 'memory', + ]); }); }); }); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx index ae5632ddae84e2..b72f87e243dcd2 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx @@ -4,26 +4,21 @@ * you may not use this file except in compliance with the Elastic License. */ -import { uniq, indexBy } from 'lodash'; -import React, { useState, useEffect, memo, useCallback } from 'react'; +import './datapanel.scss'; +import { uniq, indexBy, groupBy, throttle } from 'lodash'; +import React, { useState, useEffect, memo, useCallback, useMemo } from 'react'; import { - // @ts-ignore - EuiHighlight, EuiFlexGroup, EuiFlexItem, EuiContextMenuPanel, EuiContextMenuItem, EuiContextMenuPanelProps, EuiPopover, - EuiPopoverTitle, - EuiPopoverFooter, EuiCallOut, EuiFormControlLayout, - EuiSwitch, - EuiFacetButton, - EuiIcon, EuiSpacer, - EuiFormLabel, + EuiFilterGroup, + EuiFilterButton, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; @@ -31,6 +26,7 @@ import { DataPublicPluginStart } from 'src/plugins/data/public'; import { DatasourceDataPanelProps, DataType, StateSetter } from '../types'; import { ChildDragDropProvider, DragContextState } from '../drag_drop'; import { FieldItem } from './field_item'; +import { NoFieldsCallout } from './no_fields_callout'; import { IndexPattern, IndexPatternPrivateState, @@ -41,6 +37,7 @@ import { trackUiEvent } from '../lens_ui_telemetry'; import { syncExistingFields } from './loader'; import { fieldExists } from './pure_helpers'; import { Loader } from '../loader'; +import { FieldsAccordion } from './fields_accordion'; import { esQuery, IIndexPattern } from '../../../../../src/plugins/data/public'; export type Props = DatasourceDataPanelProps & { @@ -87,21 +84,9 @@ export function IndexPatternDataPanel({ changeIndexPattern, }: Props) { const { indexPatternRefs, indexPatterns, currentIndexPatternId } = state; - const onChangeIndexPattern = useCallback( (id: string) => changeIndexPattern(id, state, setState), - [state, setState] - ); - - const onToggleEmptyFields = useCallback( - (showEmptyFields?: boolean) => { - setState((prevState) => ({ - ...prevState, - showEmptyFields: - showEmptyFields === undefined ? !prevState.showEmptyFields : showEmptyFields, - })); - }, - [setState] + [state, setState, changeIndexPattern] ); const indexPatternList = uniq( @@ -179,8 +164,6 @@ export function IndexPatternDataPanel({ dateRange={dateRange} filters={filters} dragDropContext={dragDropContext} - showEmptyFields={state.showEmptyFields} - onToggleEmptyFields={onToggleEmptyFields} core={core} data={data} onChangeIndexPattern={onChangeIndexPattern} @@ -195,8 +178,26 @@ interface DataPanelState { nameFilter: string; typeFilter: DataType[]; isTypeFilterOpen: boolean; + isAvailableAccordionOpen: boolean; + isEmptyAccordionOpen: boolean; +} + +export interface FieldsGroup { + specialFields: IndexPatternField[]; + availableFields: IndexPatternField[]; + emptyFields: IndexPatternField[]; } +const defaultFieldGroups = { + specialFields: [], + availableFields: [], + emptyFields: [], +}; + +const fieldFiltersLabel = i18n.translate('xpack.lens.indexPatterns.fieldFiltersLabel', { + defaultMessage: 'Field filters', +}); + export const InnerIndexPatternDataPanel = function InnerIndexPatternDataPanel({ currentIndexPatternId, indexPatternRefs, @@ -206,8 +207,6 @@ export const InnerIndexPatternDataPanel = function InnerIndexPatternDataPanel({ filters, dragDropContext, onChangeIndexPattern, - showEmptyFields, - onToggleEmptyFields, core, data, existingFields, @@ -217,8 +216,6 @@ export const InnerIndexPatternDataPanel = function InnerIndexPatternDataPanel({ indexPatternRefs: IndexPatternRef[]; indexPatterns: Record; dragDropContext: DragContextState; - showEmptyFields: boolean; - onToggleEmptyFields: (showEmptyFields?: boolean) => void; onChangeIndexPattern: (newId: string) => void; existingFields: IndexPatternPrivateState['existingFields']; }) { @@ -226,79 +223,158 @@ export const InnerIndexPatternDataPanel = function InnerIndexPatternDataPanel({ nameFilter: '', typeFilter: [], isTypeFilterOpen: false, + isAvailableAccordionOpen: true, + isEmptyAccordionOpen: false, }); const [pageSize, setPageSize] = useState(PAGINATION_SIZE); const [scrollContainer, setScrollContainer] = useState(undefined); const currentIndexPattern = indexPatterns[currentIndexPatternId]; const allFields = currentIndexPattern.fields; - const fieldByName = indexBy(allFields, 'name'); const clearLocalState = () => setLocalState((s) => ({ ...s, nameFilter: '', typeFilter: [] })); - - const lazyScroll = () => { - if (scrollContainer) { - const nearBottom = - scrollContainer.scrollTop + scrollContainer.clientHeight > - scrollContainer.scrollHeight * 0.9; - if (nearBottom) { - setPageSize(Math.max(PAGINATION_SIZE, Math.min(pageSize * 1.5, allFields.length))); - } - } - }; + const hasSyncedExistingFields = existingFields[currentIndexPattern.title]; + const availableFieldTypes = uniq(allFields.map(({ type }) => type)).filter( + (type) => type in fieldTypeNames + ); useEffect(() => { // Reset the scroll if we have made material changes to the field list if (scrollContainer) { scrollContainer.scrollTop = 0; setPageSize(PAGINATION_SIZE); - lazyScroll(); } - }, [localState.nameFilter, localState.typeFilter, currentIndexPatternId, showEmptyFields]); + }, [localState.nameFilter, localState.typeFilter, currentIndexPatternId, scrollContainer]); - const availableFieldTypes = uniq(allFields.map(({ type }) => type)).filter( - (type) => type in fieldTypeNames - ); + const fieldGroups: FieldsGroup = useMemo(() => { + const containsData = (field: IndexPatternField) => { + const fieldByName = indexBy(allFields, 'name'); + const overallField = fieldByName[field.name]; - const displayedFields = allFields.filter((field) => { - if (!supportedFieldTypes.has(field.type)) { - return false; - } + return ( + overallField && fieldExists(existingFields, currentIndexPattern.title, overallField.name) + ); + }; - if ( - localState.nameFilter.length && - !field.name.toLowerCase().includes(localState.nameFilter.toLowerCase()) - ) { - return false; + const allSupportedTypesFields = allFields.filter((field) => + supportedFieldTypes.has(field.type) + ); + const sorted = allSupportedTypesFields.sort(sortFields); + // optimization before existingFields are synced + if (!hasSyncedExistingFields) { + return { + ...defaultFieldGroups, + ...groupBy(sorted, (field) => { + if (field.type === 'document') { + return 'specialFields'; + } else { + return 'emptyFields'; + } + }), + }; } + return { + ...defaultFieldGroups, + ...groupBy(sorted, (field) => { + if (field.type === 'document') { + return 'specialFields'; + } else if (containsData(field)) { + return 'availableFields'; + } else return 'emptyFields'; + }), + }; + }, [allFields, existingFields, currentIndexPattern, hasSyncedExistingFields]); - if (!showEmptyFields) { - const indexField = currentIndexPattern && fieldByName[field.name]; - const exists = - field.type === 'document' || - (indexField && fieldExists(existingFields, currentIndexPattern.title, indexField.name)); - if (localState.typeFilter.length > 0) { - return exists && localState.typeFilter.includes(field.type as DataType); - } + const filteredFieldGroups: FieldsGroup = useMemo(() => { + const filterFieldGroup = (fieldGroup: IndexPatternField[]) => + fieldGroup.filter((field) => { + if ( + localState.nameFilter.length && + !field.name.toLowerCase().includes(localState.nameFilter.toLowerCase()) + ) { + return false; + } - return exists; - } + if (localState.typeFilter.length > 0) { + return localState.typeFilter.includes(field.type as DataType); + } + return true; + }); - if (localState.typeFilter.length > 0) { - return localState.typeFilter.includes(field.type as DataType); + return Object.entries(fieldGroups).reduce((acc, [name, fields]) => { + return { + ...acc, + [name]: filterFieldGroup(fields), + }; + }, defaultFieldGroups); + }, [fieldGroups, localState.nameFilter, localState.typeFilter]); + + const lazyScroll = useCallback(() => { + if (scrollContainer) { + const nearBottom = + scrollContainer.scrollTop + scrollContainer.clientHeight > + scrollContainer.scrollHeight * 0.9; + if (nearBottom) { + const displayedFieldsLength = + (localState.isAvailableAccordionOpen ? filteredFieldGroups.availableFields.length : 0) + + (localState.isEmptyAccordionOpen ? filteredFieldGroups.emptyFields.length : 0); + setPageSize( + Math.max( + PAGINATION_SIZE, + Math.min(pageSize + PAGINATION_SIZE * 0.5, displayedFieldsLength) + ) + ); + } } + }, [ + scrollContainer, + localState.isAvailableAccordionOpen, + localState.isEmptyAccordionOpen, + filteredFieldGroups, + pageSize, + setPageSize, + ]); - return true; - }); + const [paginatedAvailableFields, paginatedEmptyFields]: [ + IndexPatternField[], + IndexPatternField[] + ] = useMemo(() => { + const { availableFields, emptyFields } = filteredFieldGroups; + const isAvailableAccordionOpen = localState.isAvailableAccordionOpen; + const isEmptyAccordionOpen = localState.isEmptyAccordionOpen; + + if (isAvailableAccordionOpen && isEmptyAccordionOpen) { + if (availableFields.length > pageSize) { + return [availableFields.slice(0, pageSize), []]; + } else { + return [availableFields, emptyFields.slice(0, pageSize - availableFields.length)]; + } + } + if (isAvailableAccordionOpen && !isEmptyAccordionOpen) { + return [availableFields.slice(0, pageSize), []]; + } - const specialFields = displayedFields.filter((f) => f.type === 'document'); - const paginatedFields = displayedFields - .filter((f) => f.type !== 'document') - .sort(sortFields) - .slice(0, pageSize); - const hilight = localState.nameFilter.toLowerCase(); + if (!isAvailableAccordionOpen && isEmptyAccordionOpen) { + return [[], emptyFields.slice(0, pageSize)]; + } + return [[], []]; + }, [ + localState.isAvailableAccordionOpen, + localState.isEmptyAccordionOpen, + filteredFieldGroups, + pageSize, + ]); - const filterByTypeLabel = i18n.translate('xpack.lens.indexPatterns.filterByTypeLabel', { - defaultMessage: 'Filter by type', - }); + const fieldProps = useMemo( + () => ({ + core, + data, + indexPattern: currentIndexPattern, + highlight: localState.nameFilter.toLowerCase(), + dateRange, + query, + filters, + }), + [core, data, currentIndexPattern, dateRange, query, filters, localState.nameFilter] + ); return ( @@ -308,7 +384,7 @@ export const InnerIndexPatternDataPanel = function InnerIndexPatternDataPanel({ direction="column" responsive={false} > - +
- -
- { - trackUiEvent('indexpattern_filters_cleared'); - clearLocalState(); - }, + + { + trackUiEvent('indexpattern_filters_cleared'); + clearLocalState(); + }, + }} + > + { + setLocalState({ ...localState, nameFilter: e.target.value }); }} - > - { - setLocalState({ ...localState, nameFilter: e.target.value }); - }} - aria-label={i18n.translate('xpack.lens.indexPatterns.filterByNameAriaLabel', { - defaultMessage: 'Search fields', - })} - /> - -
-
+ aria-label={i18n.translate('xpack.lens.indexPatterns.filterByNameAriaLabel', { + defaultMessage: 'Search fields', + })} + /> + + + + + setLocalState(() => ({ ...localState, isTypeFilterOpen: false }))} button={ - } - isSelected={localState.typeFilter.length ? true : false} onClick={() => { setLocalState((s) => ({ ...s, @@ -386,11 +463,10 @@ export const InnerIndexPatternDataPanel = function InnerIndexPatternDataPanel({ })); }} > - {filterByTypeLabel} - + {fieldFiltersLabel} + } > - {filterByTypeLabel} ))} /> - - { - trackUiEvent('indexpattern_existence_toggled'); - onToggleEmptyFields(); - }} - label={i18n.translate('xpack.lens.indexPatterns.toggleEmptyFieldsSwitch', { - defaultMessage: 'Only show fields with data', - })} - data-test-subj="lnsEmptyFilter" - /> - -
+ +
+
{ @@ -440,101 +504,95 @@ export const InnerIndexPatternDataPanel = function InnerIndexPatternDataPanel({ setScrollContainer(el); } }} - onScroll={lazyScroll} + onScroll={throttle(lazyScroll, 100)} >
- {specialFields.map((field) => ( + {filteredFieldGroups.specialFields.map((field: IndexPatternField) => ( 0} - dateRange={dateRange} - query={query} - filters={filters} hideDetails={true} + key={field.name} /> ))} - {specialFields.length > 0 && ( - <> - - - {i18n.translate('xpack.lens.indexPattern.individualFieldsLabel', { - defaultMessage: 'Individual fields', - })} - - - - )} - {paginatedFields.map((field) => { - const overallField = fieldByName[field.name]; - return ( - + { + setLocalState((s) => ({ + ...s, + isAvailableAccordionOpen: open, + })); + const displayedFieldLength = + (open ? filteredFieldGroups.availableFields.length : 0) + + (localState.isEmptyAccordionOpen ? filteredFieldGroups.emptyFields.length : 0); + setPageSize( + Math.max(PAGINATION_SIZE, Math.min(pageSize * 1.5, displayedFieldLength)) + ); + }} + renderCallout={ + - ); - })} - - {paginatedFields.length === 0 && ( - - {(!showEmptyFields || - localState.typeFilter.length || - localState.nameFilter.length) && ( - <> - - {i18n.translate('xpack.lens.indexPatterns.noFields.tryText', { - defaultMessage: 'Try:', - })} - -
    -
  • - {i18n.translate('xpack.lens.indexPatterns.noFields.extendTimeBullet', { - defaultMessage: 'Extending the time range', - })} -
  • -
  • - {i18n.translate('xpack.lens.indexPatterns.noFields.fieldFilterBullet', { - defaultMessage: - 'Using {filterByTypeLabel} {arrow} to show fields without data', - values: { filterByTypeLabel, arrow: '↑' }, - })} -
  • -
- - )} -
- )} + } + /> + + { + setLocalState((s) => ({ + ...s, + isEmptyAccordionOpen: open, + })); + const displayedFieldLength = + (localState.isAvailableAccordionOpen + ? filteredFieldGroups.availableFields.length + : 0) + (open ? filteredFieldGroups.emptyFields.length : 0); + setPageSize( + Math.max(PAGINATION_SIZE, Math.min(pageSize * 1.5, displayedFieldLength)) + ); + }} + renderCallout={ + + } + /> +
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx index ebf5abd4fbfe98..ee9b6778650efd 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx @@ -79,7 +79,6 @@ describe('IndexPatternDimensionEditorPanel', () => { indexPatternRefs: [], indexPatterns: expectedIndexPatterns, currentIndexPatternId: '1', - showEmptyFields: false, existingFields: { 'my-fake-index-pattern': { timestamp: true, @@ -1258,7 +1257,6 @@ describe('IndexPatternDimensionEditorPanel', () => { }, }, currentIndexPatternId: '1', - showEmptyFields: false, layers: { myLayer: { indexPatternId: 'foo', diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/field_select.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/field_select.tsx index ee566951d2b768..35c510521b35b7 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/field_select.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/field_select.tsx @@ -27,7 +27,6 @@ export interface FieldChoice { export interface FieldSelectProps { currentIndexPattern: IndexPattern; - showEmptyFields: boolean; fieldMap: Record; incompatibleSelectedOperationType: OperationType | null; selectedColumnOperationType?: OperationType; @@ -40,7 +39,6 @@ export interface FieldSelectProps { export function FieldSelect({ currentIndexPattern, - showEmptyFields, fieldMap, incompatibleSelectedOperationType, selectedColumnOperationType, @@ -69,6 +67,10 @@ export function FieldSelect({ (field) => fieldMap[field].type === 'document' ); + const containsData = (field: string) => + fieldMap[field].type === 'document' || + fieldExists(existingFields, currentIndexPattern.title, field); + function fieldNamesToOptions(items: string[]) { return items .map((field) => ({ @@ -82,12 +84,9 @@ export function FieldSelect({ ? selectedColumnOperationType : undefined, }, - exists: - fieldMap[field].type === 'document' || - fieldExists(existingFields, currentIndexPattern.title, field), + exists: containsData(field), compatible: isCompatibleWithCurrentOperation(field), })) - .filter((field) => showEmptyFields || field.exists) .sort((a, b) => { if (a.compatible && !b.compatible) { return -1; @@ -108,18 +107,33 @@ export function FieldSelect({ })); } - const fieldOptions: unknown[] = fieldNamesToOptions(specialFields); + const [availableFields, emptyFields] = _.partition(normalFields, containsData); - if (fields.length > 0) { - fieldOptions.push({ - label: i18n.translate('xpack.lens.indexPattern.individualFieldsLabel', { - defaultMessage: 'Individual fields', - }), - options: fieldNamesToOptions(normalFields), - }); - } + const constructFieldsOptions = (fieldsArr: string[], label: string) => + fieldsArr.length > 0 && { + label, + options: fieldNamesToOptions(fieldsArr), + }; + + const availableFieldsOptions = constructFieldsOptions( + availableFields, + i18n.translate('xpack.lens.indexPattern.availableFieldsLabel', { + defaultMessage: 'Available fields', + }) + ); + + const emptyFieldsOptions = constructFieldsOptions( + emptyFields, + i18n.translate('xpack.lens.indexPattern.emptyFieldsLabel', { + defaultMessage: 'Empty fields', + }) + ); - return fieldOptions; + return [ + ...fieldNamesToOptions(specialFields), + availableFieldsOptions, + emptyFieldsOptions, + ].filter(Boolean); }, [ incompatibleSelectedOperationType, selectedColumnOperationType, @@ -127,7 +141,6 @@ export function FieldSelect({ operationFieldSupportMatrix, currentIndexPattern, fieldMap, - showEmptyFields, ]); return ( diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/popover_editor.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/popover_editor.tsx index 4468686aa41eaa..eb2475756417e7 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/popover_editor.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/popover_editor.tsx @@ -200,7 +200,6 @@ export function PopoverEditor(props: PopoverEditorProps) { { core.http.post.mockImplementationOnce(() => { return Promise.resolve({}); }); - const wrapper = mountWithIntl(); + const wrapper = mountWithIntl(); await act(async () => { wrapper.find('[data-test-subj="lnsFieldListPanelField-bytes"]').simulate('click'); @@ -119,7 +119,7 @@ describe('IndexPattern Field Item', () => { }); }); - const wrapper = mountWithIntl(); + const wrapper = mountWithIntl(); wrapper.find('[data-test-subj="lnsFieldListPanelField-bytes"]').simulate('click'); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx index 6c00706cc86091..1a1a34d30f8a8e 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx @@ -49,6 +49,8 @@ import { IndexPattern, IndexPatternField } from './types'; import { LensFieldIcon } from './lens_field_icon'; import { trackUiEvent } from '../lens_ui_telemetry'; +import { debouncedComponent } from '../debounced_component'; + export interface FieldItemProps { core: DatasourceDataPanelProps['core']; data: DataPublicPluginStart; @@ -78,7 +80,7 @@ function wrapOnDot(str?: string) { return str ? str.replace(/\./g, '.\u200B') : ''; } -export const FieldItem = React.memo(function FieldItem(props: FieldItemProps) { +export const InnerFieldItem = function InnerFieldItem(props: FieldItemProps) { const { core, field, @@ -239,7 +241,9 @@ export const FieldItem = React.memo(function FieldItem(props: FieldItemProps) { ); -}); +}; + +export const FieldItem = debouncedComponent(InnerFieldItem); function FieldItemPopoverContents(props: State & FieldItemProps) { const { diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/fields_accordion.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/fields_accordion.test.tsx new file mode 100644 index 00000000000000..41d90a4f8870ff --- /dev/null +++ b/x-pack/plugins/lens/public/indexpattern_datasource/fields_accordion.test.tsx @@ -0,0 +1,97 @@ +/* + * 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 React from 'react'; +import { EuiLoadingSpinner, EuiNotificationBadge } from '@elastic/eui'; +import { coreMock } from 'src/core/public/mocks'; +import { mountWithIntl, shallowWithIntl } from 'test_utils/enzyme_helpers'; +import { DataPublicPluginStart } from '../../../../../src/plugins/data/public'; +import { dataPluginMock } from '../../../../../src/plugins/data/public/mocks'; +import { IndexPattern } from './types'; +import { FieldItem } from './field_item'; +import { FieldsAccordion, FieldsAccordionProps, FieldItemSharedProps } from './fields_accordion'; + +describe('Fields Accordion', () => { + let defaultProps: FieldsAccordionProps; + let indexPattern: IndexPattern; + let core: ReturnType; + let data: DataPublicPluginStart; + let fieldProps: FieldItemSharedProps; + + beforeEach(() => { + indexPattern = { + id: '1', + title: 'my-fake-index-pattern', + timeFieldName: 'timestamp', + fields: [ + { + name: 'timestamp', + type: 'date', + aggregatable: true, + searchable: true, + }, + { + name: 'bytes', + type: 'number', + aggregatable: true, + searchable: true, + }, + ], + } as IndexPattern; + core = coreMock.createSetup(); + data = dataPluginMock.createStartContract(); + core.http.post.mockClear(); + + fieldProps = { + indexPattern, + data, + core, + highlight: '', + dateRange: { + fromDate: 'now-7d', + toDate: 'now', + }, + query: { query: '', language: 'lucene' }, + filters: [], + }; + + defaultProps = { + initialIsOpen: true, + onToggle: jest.fn(), + id: 'id', + label: 'label', + hasLoaded: true, + fieldsCount: 2, + isFiltered: false, + paginatedFields: indexPattern.fields, + fieldProps, + renderCallout:
Callout
, + exists: true, + }; + }); + + it('renders correct number of Field Items', () => { + const wrapper = mountWithIntl(); + expect(wrapper.find(FieldItem).length).toEqual(2); + }); + + it('renders callout if no fields', () => { + const wrapper = shallowWithIntl( + + ); + expect(wrapper.find('#lens-test-callout').length).toEqual(1); + }); + + it('renders accented notificationBadge state if isFiltered', () => { + const wrapper = mountWithIntl(); + expect(wrapper.find(EuiNotificationBadge).prop('color')).toEqual('accent'); + }); + + it('renders spinner if has not loaded', () => { + const wrapper = mountWithIntl(); + expect(wrapper.find(EuiLoadingSpinner).length).toEqual(1); + }); +}); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/fields_accordion.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/fields_accordion.tsx new file mode 100644 index 00000000000000..b756cf81a90739 --- /dev/null +++ b/x-pack/plugins/lens/public/indexpattern_datasource/fields_accordion.tsx @@ -0,0 +1,101 @@ +/* + * 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 './datapanel.scss'; +import React, { memo, useCallback } from 'react'; +import { + EuiText, + EuiNotificationBadge, + EuiSpacer, + EuiAccordion, + EuiLoadingSpinner, +} from '@elastic/eui'; +import { DataPublicPluginStart } from 'src/plugins/data/public'; +import { IndexPatternField } from './types'; +import { FieldItem } from './field_item'; +import { Query, Filter } from '../../../../../src/plugins/data/public'; +import { DatasourceDataPanelProps } from '../types'; +import { IndexPattern } from './types'; + +export interface FieldItemSharedProps { + core: DatasourceDataPanelProps['core']; + data: DataPublicPluginStart; + indexPattern: IndexPattern; + highlight?: string; + query: Query; + dateRange: DatasourceDataPanelProps['dateRange']; + filters: Filter[]; +} + +export interface FieldsAccordionProps { + initialIsOpen: boolean; + onToggle: (open: boolean) => void; + id: string; + label: string; + hasLoaded: boolean; + fieldsCount: number; + isFiltered: boolean; + paginatedFields: IndexPatternField[]; + fieldProps: FieldItemSharedProps; + renderCallout: JSX.Element; + exists: boolean; +} + +export const InnerFieldsAccordion = function InnerFieldsAccordion({ + initialIsOpen, + onToggle, + id, + label, + hasLoaded, + fieldsCount, + isFiltered, + paginatedFields, + fieldProps, + renderCallout, + exists, +}: FieldsAccordionProps) { + const renderField = useCallback( + (field: IndexPatternField) => { + return ; + }, + [fieldProps, exists] + ); + + return ( + + {label} + + } + extraAction={ + hasLoaded ? ( + + {fieldsCount} + + ) : ( + + ) + } + > + + {hasLoaded && + (!!fieldsCount ? ( +
+ {paginatedFields && paginatedFields.map(renderField)} +
+ ) : ( + renderCallout + ))} +
+ ); +}; + +export const FieldsAccordion = memo(InnerFieldsAccordion); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts index d8449143b569fe..a69d7c055eaa7a 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts @@ -127,7 +127,6 @@ function stateFromPersistedState( indexPatterns: expectedIndexPatterns, indexPatternRefs: [], existingFields: {}, - showEmptyFields: true, }; } @@ -402,7 +401,6 @@ describe('IndexPattern Data Source', () => { }, }, currentIndexPatternId: '1', - showEmptyFields: false, }; expect(indexPatternDatasource.insertLayer(state, 'newLayer')).toEqual({ ...state, @@ -423,7 +421,6 @@ describe('IndexPattern Data Source', () => { const state = { indexPatternRefs: [], existingFields: {}, - showEmptyFields: false, indexPatterns: expectedIndexPatterns, layers: { first: { @@ -458,7 +455,6 @@ describe('IndexPattern Data Source', () => { indexPatternDatasource.getLayers({ indexPatternRefs: [], existingFields: {}, - showEmptyFields: false, indexPatterns: expectedIndexPatterns, layers: { first: { @@ -484,7 +480,6 @@ describe('IndexPattern Data Source', () => { indexPatternDatasource.getMetaData({ indexPatternRefs: [], existingFields: {}, - showEmptyFields: false, indexPatterns: expectedIndexPatterns, layers: { first: { diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.test.tsx index 5eca55cbfcbdab..87d91b56d2a5cf 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.test.tsx @@ -146,7 +146,6 @@ function testInitialState(): IndexPatternPrivateState { }, }, }, - showEmptyFields: false, }; } @@ -305,7 +304,6 @@ describe('IndexPattern Data Source suggestions', () => { indexPatternRefs: [], existingFields: {}, currentIndexPatternId: '1', - showEmptyFields: false, indexPatterns: { 1: { id: '1', @@ -510,7 +508,6 @@ describe('IndexPattern Data Source suggestions', () => { indexPatternRefs: [], existingFields: {}, currentIndexPatternId: '1', - showEmptyFields: false, indexPatterns: { 1: { id: '1', @@ -1049,7 +1046,6 @@ describe('IndexPattern Data Source suggestions', () => { it('returns no suggestions if there are no columns', () => { expect( getDatasourceSuggestionsFromCurrentState({ - showEmptyFields: false, indexPatternRefs: [], existingFields: {}, indexPatterns: expectedIndexPatterns, @@ -1355,7 +1351,6 @@ describe('IndexPattern Data Source suggestions', () => { ], }, }, - showEmptyFields: true, layers: { first: { ...initialState.layers.first, @@ -1475,7 +1470,6 @@ describe('IndexPattern Data Source suggestions', () => { ], }, }, - showEmptyFields: true, layers: { first: { ...initialState.layers.first, @@ -1529,7 +1523,6 @@ describe('IndexPattern Data Source suggestions', () => { ], }, }, - showEmptyFields: true, layers: { first: { ...initialState.layers.first, @@ -1560,7 +1553,6 @@ describe('IndexPattern Data Source suggestions', () => { existingFields: {}, currentIndexPatternId: '1', indexPatterns: expectedIndexPatterns, - showEmptyFields: true, layers: { first: { ...initialState.layers.first, diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/layerpanel.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/layerpanel.test.tsx index 0d16e2d054a775..9cbd624b42d3e2 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/layerpanel.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/layerpanel.test.tsx @@ -22,7 +22,6 @@ const initialState: IndexPatternPrivateState = { ], existingFields: {}, currentIndexPatternId: '1', - showEmptyFields: false, layers: { first: { indexPatternId: '1', 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 55fd8a6d936d35..5e59627d8c3353 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/loader.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/loader.test.ts @@ -294,7 +294,6 @@ describe('loader', () => { a: sampleIndexPatterns.a, }, layers: {}, - showEmptyFields: false, }); expect(storage.set).toHaveBeenCalledWith('lens-settings', { indexPatternId: 'a', @@ -363,7 +362,6 @@ describe('loader', () => { b: sampleIndexPatterns.b, }, layers: {}, - showEmptyFields: false, }); expect(storage.set).toHaveBeenCalledWith('lens-settings', { indexPatternId: 'b', @@ -416,7 +414,6 @@ describe('loader', () => { b: sampleIndexPatterns.b, }, layers: savedState.layers, - showEmptyFields: false, }); expect(storage.set).toHaveBeenCalledWith('lens-settings', { @@ -434,7 +431,6 @@ describe('loader', () => { indexPatterns: {}, existingFields: {}, layers: {}, - showEmptyFields: true, }; const storage = createMockStorage({ indexPatternId: 'b' }); @@ -469,7 +465,6 @@ describe('loader', () => { existingFields: {}, indexPatterns: {}, layers: {}, - showEmptyFields: true, }; const storage = createMockStorage({ indexPatternId: 'b' }); @@ -527,7 +522,6 @@ describe('loader', () => { indexPatternId: 'a', }, }, - showEmptyFields: true, }; const storage = createMockStorage({ indexPatternId: 'a' }); @@ -596,7 +590,6 @@ describe('loader', () => { indexPatternId: 'a', }, }, - showEmptyFields: true, }; const storage = createMockStorage({ indexPatternId: 'b' }); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/loader.ts b/x-pack/plugins/lens/public/indexpattern_datasource/loader.ts index ca52ffe73a871f..6c57988dfc7b65 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/loader.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/loader.ts @@ -118,7 +118,6 @@ export async function loadInitialState({ currentIndexPatternId, indexPatternRefs, indexPatterns, - showEmptyFields: false, existingFields: {}, }; } @@ -128,7 +127,6 @@ export async function loadInitialState({ indexPatternRefs, indexPatterns, layers: {}, - showEmptyFields: false, existingFields: {}, }; } diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/no_fields_callout.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/no_fields_callout.test.tsx new file mode 100644 index 00000000000000..f32bf52339e1c0 --- /dev/null +++ b/x-pack/plugins/lens/public/indexpattern_datasource/no_fields_callout.test.tsx @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React from 'react'; +import { shallow } from 'enzyme'; +import { NoFieldsCallout } from './no_fields_callout'; + +describe('NoFieldCallout', () => { + it('renders properly for index with no fields', () => { + const component = shallow( + + ); + expect(component).toMatchSnapshot(); + }); + + it('renders properly when affected by field filters, global filter and timerange', () => { + const component = shallow( + + ); + expect(component).toMatchSnapshot(); + }); + + it('renders properly when affected by field filter', () => { + const component = shallow( + + ); + expect(component).toMatchSnapshot(); + }); +}); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/no_fields_callout.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/no_fields_callout.tsx new file mode 100644 index 00000000000000..066d60f0062073 --- /dev/null +++ b/x-pack/plugins/lens/public/indexpattern_datasource/no_fields_callout.tsx @@ -0,0 +1,75 @@ +/* + * 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 React from 'react'; +import { EuiCallOut } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +export const NoFieldsCallout = ({ + isAffectedByFieldFilter, + existFieldsInIndex, + isAffectedByTimerange = false, + isAffectedByGlobalFilter = false, +}: { + isAffectedByFieldFilter: boolean; + existFieldsInIndex: boolean; + isAffectedByTimerange?: boolean; + isAffectedByGlobalFilter?: boolean; +}) => { + return ( + + {existFieldsInIndex && ( + <> + + {i18n.translate('xpack.lens.indexPatterns.noFields.tryText', { + defaultMessage: 'Try:', + })} + +
    + {isAffectedByTimerange && ( + <> +
  • + {i18n.translate('xpack.lens.indexPatterns.noFields.extendTimeBullet', { + defaultMessage: 'Extending the time range', + })} +
  • + + )} + {isAffectedByFieldFilter ? ( +
  • + {i18n.translate('xpack.lens.indexPatterns.noFields.fieldTypeFilterBullet', { + defaultMessage: 'Using different field filters', + })} +
  • + ) : null} + {isAffectedByGlobalFilter ? ( +
  • + {i18n.translate('xpack.lens.indexPatterns.noFields.globalFiltersBullet', { + defaultMessage: 'Changing the global filters', + })} +
  • + ) : null} +
+ + )} +
+ ); +}; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.test.tsx index defc142d4976ea..d0c7af42114e35 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.test.tsx @@ -51,7 +51,6 @@ describe('date_histogram', () => { indexPatternRefs: [], existingFields: {}, currentIndexPatternId: '1', - showEmptyFields: false, indexPatterns: { 1: { id: '1', diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms.test.tsx index 89d02708a900c7..1e1d83a0a5c4c2 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms.test.tsx @@ -34,7 +34,6 @@ describe('terms', () => { indexPatterns: {}, existingFields: {}, currentIndexPatternId: '1', - showEmptyFields: false, layers: { first: { indexPatternId: '1', diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/operations.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/operations.test.ts index e5d20839aae3d1..a73f6e13d94c59 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/operations.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/operations.test.ts @@ -147,7 +147,6 @@ describe('getOperationTypesForField', () => { indexPatternRefs: [], existingFields: {}, currentIndexPatternId: '1', - showEmptyFields: false, indexPatterns: expectedIndexPatterns, layers: { first: { diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/state_helpers.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/state_helpers.test.ts index 074cb8f5bde17a..65a2401fd689a3 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/state_helpers.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/state_helpers.test.ts @@ -42,7 +42,6 @@ describe('state_helpers', () => { existingFields: {}, indexPatterns: {}, currentIndexPatternId: '1', - showEmptyFields: false, layers: { first: { indexPatternId: '1', @@ -96,7 +95,6 @@ describe('state_helpers', () => { existingFields: {}, indexPatterns: {}, currentIndexPatternId: '1', - showEmptyFields: false, layers: { first: { indexPatternId: '1', @@ -147,7 +145,6 @@ describe('state_helpers', () => { existingFields: {}, indexPatterns: {}, currentIndexPatternId: '1', - showEmptyFields: false, layers: { first: { indexPatternId: '1', @@ -188,7 +185,6 @@ describe('state_helpers', () => { existingFields: {}, indexPatterns: {}, currentIndexPatternId: '1', - showEmptyFields: false, layers: { first: { indexPatternId: '1', @@ -222,7 +218,6 @@ describe('state_helpers', () => { existingFields: {}, indexPatterns: {}, currentIndexPatternId: '1', - showEmptyFields: false, layers: { first: { indexPatternId: '1', @@ -284,7 +279,6 @@ describe('state_helpers', () => { existingFields: {}, indexPatterns: {}, currentIndexPatternId: '1', - showEmptyFields: false, layers: { first: { indexPatternId: '1', @@ -337,7 +331,6 @@ describe('state_helpers', () => { existingFields: {}, indexPatterns: {}, currentIndexPatternId: '1', - showEmptyFields: false, layers: { first: { indexPatternId: '1', @@ -417,7 +410,6 @@ describe('state_helpers', () => { existingFields: {}, indexPatterns: {}, currentIndexPatternId: '1', - showEmptyFields: false, layers: { first: { indexPatternId: '1', diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/types.ts b/x-pack/plugins/lens/public/indexpattern_datasource/types.ts index 563af40ed2720d..35a82d8774130e 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/types.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/types.ts @@ -51,7 +51,6 @@ export type IndexPatternPrivateState = IndexPatternPersistedState & { * indexPatternId -> fieldName -> boolean */ existingFields: Record>; - showEmptyFields: boolean; }; export interface IndexPatternRef { diff --git a/x-pack/plugins/licensing/public/mocks.ts b/x-pack/plugins/licensing/public/mocks.ts index 68b280c5341f26..8421a343d91ca8 100644 --- a/x-pack/plugins/licensing/public/mocks.ts +++ b/x-pack/plugins/licensing/public/mocks.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import { BehaviorSubject } from 'rxjs'; -import { LicensingPluginSetup } from './types'; +import { LicensingPluginSetup, LicensingPluginStart } from './types'; import { licenseMock } from '../common/licensing.mock'; const createSetupMock = () => { @@ -18,7 +18,19 @@ const createSetupMock = () => { return mock; }; +const createStartMock = () => { + const license = licenseMock.createLicense(); + const mock: jest.Mocked = { + license$: new BehaviorSubject(license), + refresh: jest.fn(), + }; + mock.refresh.mockResolvedValue(license); + + return mock; +}; + export const licensingMock = { createSetup: createSetupMock, + createStart: createStartMock, ...licenseMock, }; diff --git a/x-pack/plugins/security_solution/public/timelines/containers/api.ts b/x-pack/plugins/security_solution/public/timelines/containers/api.ts index 10893feccfed4d..a2277897e99bff 100644 --- a/x-pack/plugins/security_solution/public/timelines/containers/api.ts +++ b/x-pack/plugins/security_solution/public/timelines/containers/api.ts @@ -103,8 +103,6 @@ export const persistTimeline = async ({ export const importTimelines = async ({ fileToImport, - overwrite = false, - signal, }: ImportDataProps): Promise => { const formData = new FormData(); formData.append('file', fileToImport); @@ -112,31 +110,25 @@ export const importTimelines = async ({ return KibanaServices.get().http.fetch(`${TIMELINE_IMPORT_URL}`, { method: 'POST', headers: { 'Content-Type': undefined }, - query: { overwrite }, body: formData, - signal, }); }; export const exportSelectedTimeline: ExportSelectedData = async ({ - excludeExportDetails = false, filename = `timelines_export.ndjson`, ids = [], signal, }): Promise => { const body = ids.length > 0 ? JSON.stringify({ ids }) : undefined; - const response = await KibanaServices.get().http.fetch(`${TIMELINE_EXPORT_URL}`, { + const response = await KibanaServices.get().http.fetch<{ body: Blob }>(`${TIMELINE_EXPORT_URL}`, { method: 'POST', body, query: { - exclude_export_details: excludeExportDetails, file_name: filename, }, - signal, - asResponse: true, }); - return response.body!; + return response.body; }; export const getDraftTimeline = async ({ diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/README.md b/x-pack/plugins/security_solution/server/lib/timeline/routes/README.md index 2c5547e39fc4e5..ee57d5bb3d0314 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/README.md +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/README.md @@ -323,4 +323,301 @@ kbn-version: 8.0.0 "timelineId":"f5a4bd10-83cd-11ea-bf78-0547a65f1281", // This is a must as well "version":"Wzg2LDFd" // Please provide the existing timeline version } -``` \ No newline at end of file +``` + +## Export timeline api + +#### POST /api/timeline/_export + +##### Authorization + +Type: Basic Auth + +username: Your Kibana username + +password: Your Kibana password + + + + +##### Request header + +``` + +Content-Type: application/json + +kbn-version: 8.0.0 + +``` + +##### Request param + +``` +file_name: ${filename}.ndjson +``` + +##### Request body +```json +{ + ids: [ + ${timelineId} + ] +} +``` + +## Import timeline api + +#### POST /api/timeline/_import + +##### Authorization + +Type: Basic Auth + +username: Your Kibana username + +password: Your Kibana password + + + + +##### Request header + +``` + +Content-Type: application/json + +kbn-version: 8.0.0 + +``` + +##### Request body + +``` +{ + file: sample.ndjson +} +``` + + +(each json in the file should match this format) +example: +``` +{"savedObjectId":"a3002fd0-781b-11ea-85e4-df9002f1452c","version":"WzIzLDFd","columns":[{"columnHeaderType":"not-filtered","id":"@timestamp"},{"columnHeaderType":"not-filtered","id":"message"},{"columnHeaderType":"not-filtered","id":"event.category"},{"columnHeaderType":"not-filtered","id":"event.action"},{"columnHeaderType":"not-filtered","id":"host.name"},{"columnHeaderType":"not-filtered","id":"source.ip"},{"columnHeaderType":"not-filtered","id":"destination.ip"},{"columnHeaderType":"not-filtered","id":"user.name"}],"dataProviders":[],"description":"tes description","eventType":"all","filters":[{"meta":{"field":null,"negate":false,"alias":null,"disabled":false,"params":"{\"query\":\"MacBook-Pro-de-Gloria.local\"}","type":"phrase","key":"host.name"},"query":"{\"match_phrase\":{\"host.name\":\"MacBook-Pro-de-Gloria.local\"}}","missing":null,"exists":null,"match_all":null,"range":null,"script":null}],"kqlMode":"filter","kqlQuery":{"filterQuery":{"serializedQuery":"{\"bool\":{\"should\":[{\"exists\":{\"field\":\"host.name\"}}],\"minimum_should_match\":1}}","kuery":{"expression":"host.name: *","kind":"kuery"}}},"title":"Test","dateRange":{"start":1585227005527,"end":1585313405527},"savedQueryId":null,"sort":{"columnId":"@timestamp","sortDirection":"desc"},"created":1586187068132,"createdBy":"angela","updated":1586187068132,"updatedBy":"angela","eventNotes":[],"globalNotes":[{"noteId":"a3b4d9d0-781b-11ea-85e4-df9002f1452c","version":"WzI1LDFd","note":"this is a note","timelineId":"a3002fd0-781b-11ea-85e4-df9002f1452c","created":1586187069313,"createdBy":"angela","updated":1586187069313,"updatedBy":"angela"}],"pinnedEventIds":[]} +``` + +##### Response +``` +{"success":true,"success_count":1,"errors":[]} +``` + +## Get draft timeline api + +#### GET /api/timeline/_draft + +##### Authorization + +Type: Basic Auth + +username: Your Kibana username + +password: Your Kibana password + + +##### Request header + +``` + +Content-Type: application/json + +kbn-version: 8.0.0 + +``` + +##### Request param +``` +timelineType: `default` or `template` +``` + +##### Response +```json +{ + "data": { + "persistTimeline": { + "timeline": { + "savedObjectId": "ababbd90-99de-11ea-8446-1d7fd9f03ebf", + "version": "WzM2MiwzXQ==", + "columns": [ + { + "columnHeaderType": "not-filtered", + "id": "@timestamp" + }, + { + "columnHeaderType": "not-filtered", + "id": "message" + }, + { + "columnHeaderType": "not-filtered", + "id": "event.category" + }, + { + "columnHeaderType": "not-filtered", + "id": "event.action" + }, + { + "columnHeaderType": "not-filtered", + "id": "host.name" + }, + { + "columnHeaderType": "not-filtered", + "id": "source.ip" + }, + { + "columnHeaderType": "not-filtered", + "id": "destination.ip" + }, + { + "columnHeaderType": "not-filtered", + "id": "user.name" + } + ], + "dataProviders": [], + "description": "", + "eventType": "all", + "filters": [], + "kqlMode": "filter", + "timelineType": "default", + "kqlQuery": { + "filterQuery": null + }, + "title": "", + "sort": { + "columnId": "@timestamp", + "sortDirection": "desc" + }, + "status": "draft", + "created": 1589899222908, + "createdBy": "casetester", + "updated": 1589899222908, + "updatedBy": "casetester", + "templateTimelineId": null, + "templateTimelineVersion": null, + "favorite": [], + "eventIdToNoteIds": [], + "noteIds": [], + "notes": [], + "pinnedEventIds": [], + "pinnedEventsSaveObject": [] + } + } + } +} +``` + +## Create draft timeline api + +#### POST /api/timeline/_draft + +##### Authorization + +Type: Basic Auth + +username: Your Kibana username + +password: Your Kibana password + + +##### Request header + +``` + +Content-Type: application/json + +kbn-version: 8.0.0 + +``` + +##### Request body + +```json +{ + "timelineType": "default" or "template" +} +``` + +##### Response +```json +{ + "data": { + "persistTimeline": { + "timeline": { + "savedObjectId": "ababbd90-99de-11ea-8446-1d7fd9f03ebf", + "version": "WzQyMywzXQ==", + "columns": [ + { + "columnHeaderType": "not-filtered", + "id": "@timestamp" + }, + { + "columnHeaderType": "not-filtered", + "id": "message" + }, + { + "columnHeaderType": "not-filtered", + "id": "event.category" + }, + { + "columnHeaderType": "not-filtered", + "id": "event.action" + }, + { + "columnHeaderType": "not-filtered", + "id": "host.name" + }, + { + "columnHeaderType": "not-filtered", + "id": "source.ip" + }, + { + "columnHeaderType": "not-filtered", + "id": "destination.ip" + }, + { + "columnHeaderType": "not-filtered", + "id": "user.name" + } + ], + "dataProviders": [], + "description": "", + "eventType": "all", + "filters": [], + "kqlMode": "filter", + "timelineType": "default", + "kqlQuery": { + "filterQuery": null + }, + "title": "", + "sort": { + "columnId": "@timestamp", + "sortDirection": "desc" + }, + "status": "draft", + "created": 1589903306582, + "createdBy": "casetester", + "updated": 1589903306582, + "updatedBy": "casetester", + "templateTimelineId": null, + "templateTimelineVersion": null, + "favorite": [], + "eventIdToNoteIds": [], + "noteIds": [], + "notes": [], + "pinnedEventIds": [], + "pinnedEventsSaveObject": [] + } + } + } +} +``` + + + diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/__mocks__/request_responses.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/__mocks__/request_responses.ts index 470ba1a853b585..0b320459c76a8c 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/__mocks__/request_responses.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/__mocks__/request_responses.ts @@ -23,7 +23,6 @@ export const getExportTimelinesRequest = () => path: TIMELINE_EXPORT_URL, query: { file_name: 'mock_export_timeline.ndjson', - exclude_export_details: 'false', }, body: { ids: ['f0e58720-57b6-11ea-b88d-3f1a31716be8', '890b8ae0-57df-11ea-a7c9-3976b7f1cb37'], diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/export_timelines_route.test.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/export_timelines_route.test.ts index 2bccb7c393837b..c66bf7b192c620 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/export_timelines_route.test.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/export_timelines_route.test.ts @@ -98,7 +98,7 @@ describe('export timelines', () => { const result = server.validate(request); expect(result.badRequest.mock.calls[1][0]).toEqual( - 'Invalid value "undefined" supplied to "file_name",Invalid value "undefined" supplied to "exclude_export_details",Invalid value "undefined" supplied to "exclude_export_details"' + 'Invalid value "undefined" supplied to "file_name"' ); }); }); diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/schemas/export_timelines_schema.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/schemas/export_timelines_schema.ts index 6f8265903b2a7f..9264f1e3e50474 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/schemas/export_timelines_schema.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/schemas/export_timelines_schema.ts @@ -8,7 +8,6 @@ import * as rt from 'io-ts'; export const exportTimelinesQuerySchema = rt.type({ file_name: rt.string, - exclude_export_details: rt.union([rt.literal('true'), rt.literal('false')]), }); export const exportTimelinesRequestBodySchema = rt.type({ diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 2a7517540e7085..ab7215ef923af5 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -8645,7 +8645,6 @@ "xpack.lens.indexPattern.groupingSecondDateHistogram": "各 {target} の日付", "xpack.lens.indexPattern.groupingSecondTerms": "各 {target} のトップの値", "xpack.lens.indexPattern.indexPatternLoadError": "インデックスパターンの読み込み中にエラーが発生", - "xpack.lens.indexPattern.individualFieldsLabel": "個々のフィールド", "xpack.lens.indexPattern.invalidInterval": "無効な間隔値", "xpack.lens.indexPattern.invalidOperationLabel": "この関数を使用するには、別のフィールドを選択してください。", "xpack.lens.indexPattern.max": "最高", @@ -8676,16 +8675,11 @@ "xpack.lens.indexPattern.termsOf": "{name} のトップの値", "xpack.lens.indexPattern.uniqueLabel": "{label} [{num}]", "xpack.lens.indexPatterns.clearFiltersLabel": "名前とタイプフィルターを消去", - "xpack.lens.indexPatterns.emptyFieldsWithDataLabel": "データがないようです。", "xpack.lens.indexPatterns.filterByNameAriaLabel": "検索フィールド", "xpack.lens.indexPatterns.filterByNameLabel": "フィールドを検索", - "xpack.lens.indexPatterns.filterByTypeLabel": "タイプでフィルタリング", "xpack.lens.indexPatterns.noFields.extendTimeBullet": "時間範囲を拡張中", - "xpack.lens.indexPatterns.noFields.fieldFilterBullet": "{filterByTypeLabel} {arrow} を使用してデータなしのフィールドを表示", "xpack.lens.indexPatterns.noFields.tryText": "試行対象:", "xpack.lens.indexPatterns.noFieldsLabel": "このインデックスパターンにはフィールドがありません。", - "xpack.lens.indexPatterns.noFilteredFieldsLabel": "現在のフィルターと一致するフィールドはありません。", - "xpack.lens.indexPatterns.toggleEmptyFieldsSwitch": "データがあるフィールドだけを表示", "xpack.lens.indexPatternSuggestion.removeLayerLabel": "{indexPatternTitle}のみを表示", "xpack.lens.indexPatternSuggestion.removeLayerPositionLabel": "レイヤー{layerNumber}のみを表示", "xpack.lens.lensSavedObjectLabel": "レンズビジュアライゼーション", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 9a55fee2b88981..a72b79c3ae0c78 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -8649,7 +8649,6 @@ "xpack.lens.indexPattern.groupingSecondDateHistogram": "每个 {target} 的日期", "xpack.lens.indexPattern.groupingSecondTerms": "每个 {target} 的排名最前值", "xpack.lens.indexPattern.indexPatternLoadError": "加载索引模式时出错", - "xpack.lens.indexPattern.individualFieldsLabel": "各个字段", "xpack.lens.indexPattern.invalidInterval": "时间间隔值无效", "xpack.lens.indexPattern.invalidOperationLabel": "要使用此函数,请选择不同的字段。", "xpack.lens.indexPattern.max": "最大值", @@ -8680,16 +8679,11 @@ "xpack.lens.indexPattern.termsOf": "{name} 的排名最前值", "xpack.lens.indexPattern.uniqueLabel": "{label} [{num}]", "xpack.lens.indexPatterns.clearFiltersLabel": "清除名称和类型筛选", - "xpack.lens.indexPatterns.emptyFieldsWithDataLabel": "似乎您没有任何数据。", "xpack.lens.indexPatterns.filterByNameAriaLabel": "搜索字段", "xpack.lens.indexPatterns.filterByNameLabel": "搜索字段", - "xpack.lens.indexPatterns.filterByTypeLabel": "按类型筛选", "xpack.lens.indexPatterns.noFields.extendTimeBullet": "延伸时间范围", - "xpack.lens.indexPatterns.noFields.fieldFilterBullet": "使用 {filterByTypeLabel} {arrow} 显示没有数据的字段", "xpack.lens.indexPatterns.noFields.tryText": "尝试:", "xpack.lens.indexPatterns.noFieldsLabel": "在此索引模式中不存在任何字段。", - "xpack.lens.indexPatterns.noFilteredFieldsLabel": "没有任何字段匹配当前筛选。", - "xpack.lens.indexPatterns.toggleEmptyFieldsSwitch": "仅显示具有数据的字段", "xpack.lens.indexPatternSuggestion.removeLayerLabel": "仅显示 {indexPatternTitle}", "xpack.lens.indexPatternSuggestion.removeLayerPositionLabel": "仅显示图层 {layerNumber}", "xpack.lens.lensSavedObjectLabel": "Lens 可视化", diff --git a/x-pack/plugins/ui_actions_enhanced/kibana.json b/x-pack/plugins/ui_actions_enhanced/kibana.json index 027004f165c3b8..a813903d8b2124 100644 --- a/x-pack/plugins/ui_actions_enhanced/kibana.json +++ b/x-pack/plugins/ui_actions_enhanced/kibana.json @@ -4,7 +4,8 @@ "configPath": ["xpack", "ui_actions_enhanced"], "requiredPlugins": [ "embeddable", - "uiActions" + "uiActions", + "licensing" ], "server": false, "ui": true diff --git a/x-pack/plugins/ui_actions_enhanced/public/components/action_wizard/action_wizard.test.tsx b/x-pack/plugins/ui_actions_enhanced/public/components/action_wizard/action_wizard.test.tsx index 745b3c403afc66..78252dccd20d24 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/components/action_wizard/action_wizard.test.tsx +++ b/x-pack/plugins/ui_actions_enhanced/public/components/action_wizard/action_wizard.test.tsx @@ -7,7 +7,15 @@ import React from 'react'; import { cleanup, fireEvent, render } from '@testing-library/react/pure'; import { TEST_SUBJ_ACTION_FACTORY_ITEM, TEST_SUBJ_SELECTED_ACTION_FACTORY } from './action_wizard'; -import { dashboardFactory, dashboards, Demo, urlFactory } from './test_data'; +import { + dashboardFactory, + dashboards, + Demo, + urlFactory, + urlDrilldownActionFactory, +} from './test_data'; +import { ActionFactory } from '../../dynamic_actions'; +import { licenseMock } from '../../../../licensing/common/licensing.mock'; // TODO: afterEach is not available for it globally during setup // https://github.com/elastic/kibana/issues/59469 @@ -54,3 +62,19 @@ test('If only one actions factory is available then actionFactory selection is e // check that can't change to action factory type expect(screen.queryByTestId(/change/i)).not.toBeInTheDocument(); }); + +test('If not enough license, button is disabled', () => { + const urlWithGoldLicense = new ActionFactory( + { + ...urlDrilldownActionFactory, + minimalLicense: 'gold', + }, + () => licenseMock.createLicense() + ); + const screen = render(); + + // check that all factories are displayed to pick + expect(screen.getAllByTestId(new RegExp(TEST_SUBJ_ACTION_FACTORY_ITEM))).toHaveLength(2); + + expect(screen.getByText(/Go to URL/i)).toBeDisabled(); +}); diff --git a/x-pack/plugins/ui_actions_enhanced/public/components/action_wizard/action_wizard.tsx b/x-pack/plugins/ui_actions_enhanced/public/components/action_wizard/action_wizard.tsx index ccadf60426edf8..6769c8bab07327 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/components/action_wizard/action_wizard.tsx +++ b/x-pack/plugins/ui_actions_enhanced/public/components/action_wizard/action_wizard.tsx @@ -10,10 +10,12 @@ import { EuiFlexGroup, EuiFlexItem, EuiIcon, + EuiKeyPadMenuItem, EuiSpacer, EuiText, - EuiKeyPadMenuItem, + EuiToolTip, } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; import { txtChangeButton } from './i18n'; import './action_wizard.scss'; import { ActionFactory } from '../../dynamic_actions'; @@ -61,7 +63,11 @@ export const ActionWizard: React.FC = ({ context, }) => { // auto pick action factory if there is only 1 available - if (!currentActionFactory && actionFactories.length === 1) { + if ( + !currentActionFactory && + actionFactories.length === 1 && + actionFactories[0].isCompatibleLicence() + ) { onActionFactoryChange(actionFactories[0]); } @@ -175,24 +181,46 @@ const ActionFactorySelector: React.FC = ({ willChange: 'opacity', }; + /** + * make sure not compatible factories are in the end + */ + const ensureOrder = (factories: ActionFactory[]) => { + const compatibleLicense = factories.filter((f) => f.isCompatibleLicence()); + const notCompatibleLicense = factories.filter((f) => !f.isCompatibleLicence()); + return [ + ...compatibleLicense.sort((f1, f2) => f2.order - f1.order), + ...notCompatibleLicense.sort((f1, f2) => f2.order - f1.order), + ]; + }; + return ( - {[...actionFactories] - .sort((f1, f2) => f2.order - f1.order) - .map((actionFactory) => ( - + {ensureOrder(actionFactories).map((actionFactory) => ( + + + ) + } + > onActionFactorySelected(actionFactory)} + disabled={!actionFactory.isCompatibleLicence()} > {actionFactory.getIconType(context) && ( )} - - ))} + + + ))} ); }; diff --git a/x-pack/plugins/ui_actions_enhanced/public/components/action_wizard/test_data.tsx b/x-pack/plugins/ui_actions_enhanced/public/components/action_wizard/test_data.tsx index 0a135e60126ca9..2672a086dca735 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/components/action_wizard/test_data.tsx +++ b/x-pack/plugins/ui_actions_enhanced/public/components/action_wizard/test_data.tsx @@ -10,6 +10,7 @@ import { reactToUiComponent } from '../../../../../../src/plugins/kibana_react/p import { ActionWizard } from './action_wizard'; import { ActionFactoryDefinition, ActionFactory } from '../../dynamic_actions'; import { CollectConfigProps } from '../../../../../../src/plugins/kibana_utils/public'; +import { licenseMock } from '../../../../licensing/common/licensing.mock'; type ActionBaseConfig = object; @@ -101,10 +102,13 @@ export const dashboardDrilldownActionFactory: ActionFactoryDefinition< create: () => ({ id: 'test', execute: async () => alert('Navigate to dashboard!'), + enhancements: {}, }), }; -export const dashboardFactory = new ActionFactory(dashboardDrilldownActionFactory); +export const dashboardFactory = new ActionFactory(dashboardDrilldownActionFactory, () => + licenseMock.createLicense() +); interface UrlDrilldownConfig { url: string; @@ -159,7 +163,9 @@ export const urlDrilldownActionFactory: ActionFactoryDefinition null as any, }; -export const urlFactory = new ActionFactory(urlDrilldownActionFactory); +export const urlFactory = new ActionFactory(urlDrilldownActionFactory, () => + licenseMock.createLicense() +); export function Demo({ actionFactories }: { actionFactories: Array> }) { const [state, setState] = useState<{ diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/connected_flyout_manage_drilldowns/connected_flyout_manage_drilldowns.tsx b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/connected_flyout_manage_drilldowns/connected_flyout_manage_drilldowns.tsx index fbc72d04706351..20d15b4f4d2bd3 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/connected_flyout_manage_drilldowns/connected_flyout_manage_drilldowns.tsx +++ b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/connected_flyout_manage_drilldowns/connected_flyout_manage_drilldowns.tsx @@ -18,6 +18,8 @@ import { import { useContainerState } from '../../../../../../../src/plugins/kibana_utils/public'; import { DrilldownListItem } from '../list_manage_drilldowns'; import { + insufficientLicenseLevel, + invalidDrilldownType, toastDrilldownCreated, toastDrilldownDeleted, toastDrilldownEdited, @@ -133,6 +135,11 @@ export function createFlyoutManageDrilldowns({ drilldownName: drilldown.action.name, actionName: actionFactory?.getDisplayName(factoryContext) ?? drilldown.action.factoryId, icon: actionFactory?.getIconType(factoryContext), + error: !actionFactory + ? invalidDrilldownType(drilldown.action.factoryId) // this shouldn't happen for the end user, but useful during development + : !actionFactory.isCompatibleLicence() + ? insufficientLicenseLevel + : undefined, }; } diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/connected_flyout_manage_drilldowns/i18n.ts b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/connected_flyout_manage_drilldowns/i18n.ts index e75ee2634aa43c..4b2be5db0c5584 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/connected_flyout_manage_drilldowns/i18n.ts +++ b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/connected_flyout_manage_drilldowns/i18n.ts @@ -86,3 +86,23 @@ export const toastDrilldownsCRUDError = i18n.translate( description: 'Title for generic error toast when persisting drilldown updates failed', } ); + +export const insufficientLicenseLevel = i18n.translate( + 'xpack.uiActionsEnhanced.drilldowns.components.flyoutDrilldownWizard.insufficientLicenseLevelError', + { + defaultMessage: 'Insufficient license level', + description: + 'User created drilldown with higher license type, but then downgraded the license. This error is shown in the list near created drilldown', + } +); + +export const invalidDrilldownType = (type: string) => + i18n.translate( + 'xpack.uiActionsEnhanced.drilldowns.components.flyoutDrilldownWizard.invalidDrilldownType', + { + defaultMessage: "Drilldown type {type} doesn't exist", + values: { + type, + }, + } + ); diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/flyout_list_manage_drilldowns/flyout_list_manage_drilldowns.story.tsx b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/flyout_list_manage_drilldowns/flyout_list_manage_drilldowns.story.tsx index 0529f0451b16a0..603de39bc89081 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/flyout_list_manage_drilldowns/flyout_list_manage_drilldowns.story.tsx +++ b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/flyout_list_manage_drilldowns/flyout_list_manage_drilldowns.story.tsx @@ -15,7 +15,7 @@ storiesOf('components/FlyoutListManageDrilldowns', module).add('default', () => drilldowns={[ { id: '1', actionName: 'Dashboard', drilldownName: 'Drilldown 1' }, { id: '2', actionName: 'Dashboard', drilldownName: 'Drilldown 2' }, - { id: '3', actionName: 'Dashboard', drilldownName: 'Drilldown 3' }, + { id: '3', actionName: 'Dashboard', drilldownName: 'Drilldown 3', error: 'Some error...' }, ]} /> diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/form_drilldown_wizard/form_drilldown_wizard.tsx b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/form_drilldown_wizard/form_drilldown_wizard.tsx index 622ed58e3625d7..e7e7f72dbf58f4 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/form_drilldown_wizard/form_drilldown_wizard.tsx +++ b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/form_drilldown_wizard/form_drilldown_wizard.tsx @@ -5,11 +5,14 @@ */ import React from 'react'; -import { EuiFieldText, EuiForm, EuiFormRow, EuiSpacer } from '@elastic/eui'; +import { EuiFieldText, EuiForm, EuiFormRow, EuiLink, EuiSpacer, EuiText } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; import { txtDrilldownAction, txtNameOfDrilldown, txtUntitledDrilldown } from './i18n'; import { ActionFactory } from '../../../dynamic_actions'; import { ActionWizard } from '../../../components/action_wizard'; +const GET_MORE_ACTIONS_LINK = 'https://www.elastic.co/subscriptions'; + const noopFn = () => {}; export interface FormDrilldownWizardProps { @@ -49,10 +52,32 @@ export const FormDrilldownWizard: React.FC = ({ ); + const hasNotCompatibleLicenseFactory = () => + actionFactories?.some((f) => !f.isCompatibleLicence()); + + const renderGetMoreActionsLink = () => ( + + + + + + ); + const actionWizard = ( 1 ? txtDrilldownAction : undefined} fullWidth={true} + labelAppend={ + !currentActionFactory && hasNotCompatibleLicenseFactory() && renderGetMoreActionsLink() + } > { @@ -67,3 +67,8 @@ test('Can delete drilldowns', () => { expect(fn).toBeCalledWith([drilldowns[1].id, drilldowns[2].id]); }); + +test('Error is displayed', () => { + const screen = render(); + expect(screen.getByLabelText('an error')).toBeInTheDocument(); +}); diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/list_manage_drilldowns/list_manage_drilldowns.tsx b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/list_manage_drilldowns/list_manage_drilldowns.tsx index cd41a3d6ec23a4..b828c4d7d076da 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/list_manage_drilldowns/list_manage_drilldowns.tsx +++ b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/list_manage_drilldowns/list_manage_drilldowns.tsx @@ -14,6 +14,7 @@ import { EuiIcon, EuiSpacer, EuiTextColor, + EuiToolTip, } from '@elastic/eui'; import React, { useState } from 'react'; import { @@ -28,6 +29,7 @@ export interface DrilldownListItem { actionName: string; drilldownName: string; icon?: string; + error?: string; } export interface ListManageDrilldownsProps { @@ -52,11 +54,27 @@ export function ListManageDrilldowns({ const columns: Array> = [ { - field: 'drilldownName', name: 'Name', truncateText: true, width: '50%', 'data-test-subj': 'drilldownListItemName', + render: (drilldown: DrilldownListItem) => ( +
+ {drilldown.drilldownName}{' '} + {drilldown.error && ( + + + + )} +
+ ), }, { name: 'Action', diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_definition.ts b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_definition.ts index f01dd22c06bc54..a41ae851e185b0 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_definition.ts +++ b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_definition.ts @@ -5,6 +5,7 @@ */ import { ActionFactoryDefinition } from '../dynamic_actions'; +import { LicenseType } from '../../../licensing/public'; /** * This is a convenience interface to register a drilldown. Drilldown has @@ -28,6 +29,12 @@ export interface DrilldownDefinition< */ id: string; + /** + * Minimal licence level + * Empty means no restrictions + */ + minimalLicense?: LicenseType; + /** * Determines the display order of the drilldowns in the flyout picker. * Higher numbers are displayed first. diff --git a/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/action_factory.test.ts b/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/action_factory.test.ts new file mode 100644 index 00000000000000..918c6422546f40 --- /dev/null +++ b/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/action_factory.test.ts @@ -0,0 +1,46 @@ +/* + * 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 { ActionFactory } from './action_factory'; +import { ActionFactoryDefinition } from './action_factory_definition'; +import { licensingMock } from '../../../licensing/public/mocks'; + +const def: ActionFactoryDefinition = { + id: 'ACTION_FACTORY_1', + CollectConfig: {} as any, + createConfig: () => ({}), + isConfigValid: (() => true) as any, + create: ({ name }) => ({ + id: '', + execute: async () => {}, + getDisplayName: () => name, + enhancements: {}, + }), +}; + +describe('License & ActionFactory', () => { + test('no license requirements', async () => { + const factory = new ActionFactory(def, () => licensingMock.createLicense()); + expect(await factory.isCompatible({})).toBe(true); + expect(factory.isCompatibleLicence()).toBe(true); + }); + + test('not enough license level', async () => { + const factory = new ActionFactory({ ...def, minimalLicense: 'gold' }, () => + licensingMock.createLicense() + ); + expect(await factory.isCompatible({})).toBe(true); + expect(factory.isCompatibleLicence()).toBe(false); + }); + + test('enough license level', async () => { + const factory = new ActionFactory({ ...def, minimalLicense: 'gold' }, () => + licensingMock.createLicense({ license: { type: 'gold' } }) + ); + expect(await factory.isCompatible({})).toBe(true); + expect(factory.isCompatibleLicence()).toBe(true); + }); +}); diff --git a/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/action_factory.ts b/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/action_factory.ts index 262a5ef7d4561c..95b7941b48ed35 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/action_factory.ts +++ b/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/action_factory.ts @@ -5,13 +5,12 @@ */ import { uiToReactComponent } from '../../../../../src/plugins/kibana_react/public'; -import { - UiActionsActionDefinition as ActionDefinition, - UiActionsPresentable as Presentable, -} from '../../../../../src/plugins/ui_actions/public'; +import { UiActionsPresentable as Presentable } from '../../../../../src/plugins/ui_actions/public'; import { ActionFactoryDefinition } from './action_factory_definition'; import { Configurable } from '../../../../../src/plugins/kibana_utils/public'; import { SerializedAction } from './types'; +import { ILicense } from '../../../licensing/public'; +import { UiActionsActionDefinition as ActionDefinition } from '../../../../../src/plugins/ui_actions/public'; export class ActionFactory< Config extends object = object, @@ -19,10 +18,12 @@ export class ActionFactory< ActionContext extends object = object > implements Omit, 'getHref'>, Configurable { constructor( - protected readonly def: ActionFactoryDefinition + protected readonly def: ActionFactoryDefinition, + protected readonly getLicence: () => ILicense ) {} public readonly id = this.def.id; + public readonly minimalLicense = this.def.minimalLicense; public readonly order = this.def.order || 0; public readonly MenuItem? = this.def.MenuItem; public readonly ReactMenuItem? = this.MenuItem ? uiToReactComponent(this.MenuItem) : undefined; @@ -51,9 +52,26 @@ export class ActionFactory< return await this.def.isCompatible(context); } + /** + * Does this action factory licence requirements + * compatible with current license? + */ + public isCompatibleLicence() { + if (!this.minimalLicense) return true; + return this.getLicence().hasAtLeast(this.minimalLicense); + } + public create( serializedAction: Omit, 'factoryId'> ): ActionDefinition { - return this.def.create(serializedAction); + const action = this.def.create(serializedAction); + return { + ...action, + isCompatible: async (context: ActionContext): Promise => { + if (!this.isCompatibleLicence()) return false; + if (!action.isCompatible) return true; + return action.isCompatible(context); + }, + }; } } diff --git a/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/action_factory_definition.ts b/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/action_factory_definition.ts index d3751fe8116655..d63f69ba5ab729 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/action_factory_definition.ts +++ b/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/action_factory_definition.ts @@ -4,12 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ +import { Configurable } from '../../../../../src/plugins/kibana_utils/public'; +import { SerializedAction } from './types'; +import { LicenseType } from '../../../licensing/public'; import { UiActionsActionDefinition as ActionDefinition, UiActionsPresentable as Presentable, } from '../../../../../src/plugins/ui_actions/public'; -import { Configurable } from '../../../../../src/plugins/kibana_utils/public'; -import { SerializedAction } from './types'; /** * This is a convenience interface for registering new action factories. @@ -28,6 +29,12 @@ export interface ActionFactoryDefinition< */ id: string; + /** + * Minimal licence level + * Empty means no licence restrictions + */ + readonly minimalLicense?: LicenseType; + /** * This method should return a definition of a new action, normally used to * register it in `ui_actions` registry. diff --git a/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/dynamic_action_manager.test.ts b/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/dynamic_action_manager.test.ts index 516b1f3cd27738..930f88ff08775c 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/dynamic_action_manager.test.ts +++ b/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/dynamic_action_manager.test.ts @@ -7,11 +7,12 @@ import { DynamicActionManager } from './dynamic_action_manager'; import { ActionStorage, MemoryActionStorage } from './dynamic_action_storage'; import { UiActionsService } from '../../../../../src/plugins/ui_actions/public'; -import { ActionInternal } from '../../../../../src/plugins/ui_actions/public/actions'; +import { ActionRegistry } from '../../../../../src/plugins/ui_actions/public/types'; import { of } from '../../../../../src/plugins/kibana_utils'; import { UiActionsServiceEnhancements } from '../services'; import { ActionFactoryDefinition } from './action_factory_definition'; import { SerializedAction, SerializedEvent } from './types'; +import { licensingMock } from '../../../licensing/public/mocks'; const actionFactoryDefinition1: ActionFactoryDefinition = { id: 'ACTION_FACTORY_1', @@ -67,14 +68,21 @@ const event3: SerializedEvent = { }, }; -const setup = (events: readonly SerializedEvent[] = []) => { +const setup = ( + events: readonly SerializedEvent[] = [], + { getLicenseInfo = () => licensingMock.createLicense() } = { + getLicenseInfo: () => licensingMock.createLicense(), + } +) => { const isCompatible = async () => true; const storage: ActionStorage = new MemoryActionStorage(events); - const actions = new Map(); + const actions: ActionRegistry = new Map(); const uiActions = new UiActionsService({ actions, }); - const uiActionsEnhancements = new UiActionsServiceEnhancements(); + const uiActionsEnhancements = new UiActionsServiceEnhancements({ + getLicenseInfo, + }); const manager = new DynamicActionManager({ isCompatible, storage, @@ -95,6 +103,9 @@ const setup = (events: readonly SerializedEvent[] = []) => { }; describe('DynamicActionManager', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); test('can instantiate', () => { const { manager } = setup([event1]); expect(manager).toBeInstanceOf(DynamicActionManager); @@ -103,11 +114,11 @@ describe('DynamicActionManager', () => { describe('.start()', () => { test('instantiates stored events', async () => { const { manager, actions, uiActions } = setup([event1]); - const create1 = jest.fn(); - const create2 = jest.fn(); + const create1 = jest.spyOn(actionFactoryDefinition1, 'create'); + const create2 = jest.spyOn(actionFactoryDefinition2, 'create'); - uiActions.registerActionFactory({ ...actionFactoryDefinition1, create: create1 }); - uiActions.registerActionFactory({ ...actionFactoryDefinition2, create: create2 }); + uiActions.registerActionFactory(actionFactoryDefinition1); + uiActions.registerActionFactory(actionFactoryDefinition2); expect(create1).toHaveBeenCalledTimes(0); expect(create2).toHaveBeenCalledTimes(0); @@ -122,11 +133,11 @@ describe('DynamicActionManager', () => { test('does nothing when no events stored', async () => { const { manager, actions, uiActions } = setup(); - const create1 = jest.fn(); - const create2 = jest.fn(); + const create1 = jest.spyOn(actionFactoryDefinition1, 'create'); + const create2 = jest.spyOn(actionFactoryDefinition2, 'create'); - uiActions.registerActionFactory({ ...actionFactoryDefinition1, create: create1 }); - uiActions.registerActionFactory({ ...actionFactoryDefinition2, create: create2 }); + uiActions.registerActionFactory(actionFactoryDefinition1); + uiActions.registerActionFactory(actionFactoryDefinition2); expect(create1).toHaveBeenCalledTimes(0); expect(create2).toHaveBeenCalledTimes(0); @@ -207,11 +218,9 @@ describe('DynamicActionManager', () => { describe('.stop()', () => { test('removes events from UI actions registry', async () => { const { manager, actions, uiActions } = setup([event1, event2]); - const create1 = jest.fn(); - const create2 = jest.fn(); - uiActions.registerActionFactory({ ...actionFactoryDefinition1, create: create1 }); - uiActions.registerActionFactory({ ...actionFactoryDefinition2, create: create2 }); + uiActions.registerActionFactory(actionFactoryDefinition1); + uiActions.registerActionFactory(actionFactoryDefinition2); expect(actions.size).toBe(0); @@ -632,4 +641,42 @@ describe('DynamicActionManager', () => { }); }); }); + + test('revived actions incompatible when license is not enough', async () => { + const getLicenseInfo = jest.fn(() => + licensingMock.createLicense({ license: { type: 'basic' } }) + ); + const { manager, uiActions } = setup([event1, event3], { getLicenseInfo }); + const basicActionFactory: ActionFactoryDefinition = { + ...actionFactoryDefinition1, + minimalLicense: 'basic', + }; + + const goldActionFactory: ActionFactoryDefinition = { + ...actionFactoryDefinition2, + minimalLicense: 'gold', + }; + + uiActions.registerActionFactory(basicActionFactory); + uiActions.registerActionFactory(goldActionFactory); + + await manager.start(); + + const basicActions = await uiActions.getTriggerCompatibleActions( + 'VALUE_CLICK_TRIGGER', + {} as any + ); + expect(basicActions).toHaveLength(1); + + getLicenseInfo.mockImplementation(() => + licensingMock.createLicense({ license: { type: 'gold' } }) + ); + + const basicAndGoldActions = await uiActions.getTriggerCompatibleActions( + 'VALUE_CLICK_TRIGGER', + {} as any + ); + + expect(basicAndGoldActions).toHaveLength(2); + }); }); diff --git a/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/dynamic_action_manager.ts b/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/dynamic_action_manager.ts index 58344026079e79..4afefe3006a43c 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/dynamic_action_manager.ts +++ b/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/dynamic_action_manager.ts @@ -72,14 +72,18 @@ export class DynamicActionManager { const { uiActions, isCompatible } = this.params; const actionId = this.generateActionId(eventId); + const factory = uiActions.getActionFactory(event.action.factoryId); - const actionDefinition: ActionDefinition = { - ...factory.create(action as SerializedAction), + const actionDefinition: ActionDefinition = factory.create(action as SerializedAction); + uiActions.registerAction({ + ...actionDefinition, id: actionId, - isCompatible, - }; - - uiActions.registerAction(actionDefinition); + isCompatible: async (context) => { + if (!(await isCompatible(context))) return false; + if (!actionDefinition.isCompatible) return true; + return actionDefinition.isCompatible(context); + }, + }); for (const trigger of triggers) uiActions.attachAction(trigger as any, actionId); } diff --git a/x-pack/plugins/ui_actions_enhanced/public/mocks.ts b/x-pack/plugins/ui_actions_enhanced/public/mocks.ts index 196b8f2c1d5c79..ff07d6e74a9c0b 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/mocks.ts +++ b/x-pack/plugins/ui_actions_enhanced/public/mocks.ts @@ -10,6 +10,7 @@ import { uiActionsPluginMock } from '../../../../src/plugins/ui_actions/public/m import { embeddablePluginMock } from '../../../../src/plugins/embeddable/public/mocks'; import { AdvancedUiActionsSetup, AdvancedUiActionsStart } from '.'; import { plugin as pluginInitializer } from '.'; +import { licensingMock } from '../../licensing/public/mocks'; export type Setup = jest.Mocked; export type Start = jest.Mocked; @@ -62,6 +63,7 @@ const createPlugin = ( return plugin.start(anotherCoreStart, { uiActions: uiActionsStart, embeddable: embeddableStart, + licensing: licensingMock.createStart(), }); }, }; diff --git a/x-pack/plugins/ui_actions_enhanced/public/plugin.ts b/x-pack/plugins/ui_actions_enhanced/public/plugin.ts index 04caef92f15a22..a625ea2e2118bc 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/plugin.ts +++ b/x-pack/plugins/ui_actions_enhanced/public/plugin.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { BehaviorSubject, Subscription } from 'rxjs'; import { PluginInitializerContext, CoreSetup, @@ -31,6 +32,7 @@ import { } from './custom_time_range_badge'; import { CommonlyUsedRange } from './types'; import { UiActionsServiceEnhancements } from './services'; +import { ILicense, LicensingPluginStart } from '../../licensing/public'; import { createFlyoutManageDrilldowns } from './drilldowns'; import { Storage } from '../../../../src/plugins/kibana_utils/public'; @@ -42,6 +44,7 @@ interface SetupDependencies { interface StartDependencies { embeddable: EmbeddableStart; uiActions: UiActionsStart; + licensing: LicensingPluginStart; } export interface SetupContract @@ -63,7 +66,19 @@ declare module '../../../../src/plugins/ui_actions/public' { export class AdvancedUiActionsPublicPlugin implements Plugin { - private readonly enhancements = new UiActionsServiceEnhancements(); + readonly licenceInfo = new BehaviorSubject(undefined); + private getLicenseInfo(): ILicense { + if (!this.licenceInfo.getValue()) { + throw new Error( + 'AdvancedUiActionsPublicPlugin: Licence is not ready! Licence becomes available only after setup.' + ); + } + return this.licenceInfo.getValue()!; + } + private readonly enhancements = new UiActionsServiceEnhancements({ + getLicenseInfo: () => this.getLicenseInfo(), + }); + private subs: Subscription[] = []; constructor(initializerContext: PluginInitializerContext) {} @@ -74,7 +89,9 @@ export class AdvancedUiActionsPublicPlugin }; } - public start(core: CoreStart, { uiActions }: StartDependencies): StartContract { + public start(core: CoreStart, { uiActions, licensing }: StartDependencies): StartContract { + this.subs.push(licensing.license$.subscribe(this.licenceInfo)); + const dateFormat = core.uiSettings.get('dateFormat') as string; const commonlyUsedRanges = core.uiSettings.get( UI_SETTINGS.TIMEPICKER_QUICK_RANGES @@ -106,5 +123,7 @@ export class AdvancedUiActionsPublicPlugin }; } - public stop() {} + public stop() { + this.subs.forEach((s) => s.unsubscribe()); + } } diff --git a/x-pack/plugins/ui_actions_enhanced/public/services/ui_actions_service_enhancements.test.ts b/x-pack/plugins/ui_actions_enhanced/public/services/ui_actions_service_enhancements.test.ts index 3137e35a2fe476..4f2ddcf7e0491c 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/services/ui_actions_service_enhancements.test.ts +++ b/x-pack/plugins/ui_actions_enhanced/public/services/ui_actions_service_enhancements.test.ts @@ -6,6 +6,9 @@ import { UiActionsServiceEnhancements } from './ui_actions_service_enhancements'; import { ActionFactoryDefinition, ActionFactory } from '../dynamic_actions'; +import { licensingMock } from '../../../licensing/public/mocks'; + +const getLicenseInfo = () => licensingMock.createLicense(); describe('UiActionsService', () => { describe('action factories', () => { @@ -25,7 +28,7 @@ describe('UiActionsService', () => { }; test('.getActionFactories() returns empty array if no action factories registered', () => { - const service = new UiActionsServiceEnhancements(); + const service = new UiActionsServiceEnhancements({ getLicenseInfo }); const factories = service.getActionFactories(); @@ -33,7 +36,7 @@ describe('UiActionsService', () => { }); test('can register and retrieve an action factory', () => { - const service = new UiActionsServiceEnhancements(); + const service = new UiActionsServiceEnhancements({ getLicenseInfo }); service.registerActionFactory(factoryDefinition1); @@ -44,7 +47,7 @@ describe('UiActionsService', () => { }); test('can retrieve all action factories', () => { - const service = new UiActionsServiceEnhancements(); + const service = new UiActionsServiceEnhancements({ getLicenseInfo }); service.registerActionFactory(factoryDefinition1); service.registerActionFactory(factoryDefinition2); @@ -58,7 +61,7 @@ describe('UiActionsService', () => { }); test('throws when retrieving action factory that does not exist', () => { - const service = new UiActionsServiceEnhancements(); + const service = new UiActionsServiceEnhancements({ getLicenseInfo }); service.registerActionFactory(factoryDefinition1); diff --git a/x-pack/plugins/ui_actions_enhanced/public/services/ui_actions_service_enhancements.ts b/x-pack/plugins/ui_actions_enhanced/public/services/ui_actions_service_enhancements.ts index b7bdced228584a..bd05659d59e9d8 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/services/ui_actions_service_enhancements.ts +++ b/x-pack/plugins/ui_actions_enhanced/public/services/ui_actions_service_enhancements.ts @@ -7,16 +7,20 @@ import { ActionFactoryRegistry } from '../types'; import { ActionFactory, ActionFactoryDefinition } from '../dynamic_actions'; import { DrilldownDefinition } from '../drilldowns'; +import { ILicense } from '../../../licensing/common/types'; export interface UiActionsServiceEnhancementsParams { readonly actionFactories?: ActionFactoryRegistry; + readonly getLicenseInfo: () => ILicense; } export class UiActionsServiceEnhancements { protected readonly actionFactories: ActionFactoryRegistry; + protected readonly getLicenseInfo: () => ILicense; - constructor({ actionFactories = new Map() }: UiActionsServiceEnhancementsParams = {}) { + constructor({ actionFactories = new Map(), getLicenseInfo }: UiActionsServiceEnhancementsParams) { this.actionFactories = actionFactories; + this.getLicenseInfo = getLicenseInfo; } /** @@ -34,7 +38,10 @@ export class UiActionsServiceEnhancements { throw new Error(`ActionFactory [actionFactory.id = ${definition.id}] already registered.`); } - const actionFactory = new ActionFactory(definition); + const actionFactory = new ActionFactory( + definition, + this.getLicenseInfo + ); this.actionFactories.set(actionFactory.id, actionFactory as ActionFactory); }; @@ -72,9 +79,11 @@ export class UiActionsServiceEnhancements { euiIcon, execute, getHref, + minimalLicense, }: DrilldownDefinition): void => { const actionFactory: ActionFactoryDefinition = { id: factoryId, + minimalLicense, order, CollectConfig, createConfig, diff --git a/x-pack/test/api_integration/apis/management/index_management/data_streams.ts b/x-pack/test/api_integration/apis/management/index_management/data_streams.ts index e1756df42ca25b..74ab59f2ffdc69 100644 --- a/x-pack/test/api_integration/apis/management/index_management/data_streams.ts +++ b/x-pack/test/api_integration/apis/management/index_management/data_streams.ts @@ -19,79 +19,131 @@ export default function ({ getService }: FtrProviderContext) { const supertest = getService('supertest'); const es = getService('legacyEs'); - const createDataStream = (name: string) => { + const createDataStream = async (name: string) => { // A data stream requires an index template before it can be created. - return es.dataManagement - .saveComposableIndexTemplate({ - name, - body: { - // We need to match the names of backing indices with this template - index_patterns: [name + '*'], - template: { - mappings: { - properties: { - '@timestamp': { - type: 'date', - }, + await es.dataManagement.saveComposableIndexTemplate({ + name, + body: { + // We need to match the names of backing indices with this template. + index_patterns: [name + '*'], + template: { + mappings: { + properties: { + '@timestamp': { + type: 'date', }, }, }, - data_stream: { - timestamp_field: '@timestamp', - }, }, - }) - .then(() => - es.dataManagement.createDataStream({ - name, - }) - ); + data_stream: { + timestamp_field: '@timestamp', + }, + }, + }); + + await es.dataManagement.createDataStream({ name }); }; - const deleteDataStream = (name: string) => { - return es.dataManagement - .deleteDataStream({ - name, - }) - .then(() => - es.dataManagement.deleteComposableIndexTemplate({ - name, - }) - ); + const deleteComposableIndexTemplate = async (name: string) => { + await es.dataManagement.deleteComposableIndexTemplate({ name }); }; - // Unskip once ES snapshot has been promoted that updates the data stream response - describe.skip('Data streams', function () { - const testDataStreamName = 'test-data-stream'; + const deleteDataStream = async (name: string) => { + await es.dataManagement.deleteDataStream({ name }); + await deleteComposableIndexTemplate(name); + }; + describe('Data streams', function () { describe('Get', () => { + const testDataStreamName = 'test-data-stream'; + before(async () => await createDataStream(testDataStreamName)); after(async () => await deleteDataStream(testDataStreamName)); - describe('all data streams', () => { - it('returns an array of data streams', async () => { - const { body: dataStreams } = await supertest - .get(`${API_BASE_PATH}/data_streams`) - .set('kbn-xsrf', 'xxx') - .expect(200); + it('returns an array of all data streams', async () => { + const { body: dataStreams } = await supertest + .get(`${API_BASE_PATH}/data_streams`) + .set('kbn-xsrf', 'xxx') + .expect(200); + + // ES determines these values so we'll just echo them back. + const { name: indexName, uuid } = dataStreams[0].indices[0]; + expect(dataStreams).to.eql([ + { + name: testDataStreamName, + timeStampField: { name: '@timestamp', mapping: { type: 'date' } }, + indices: [ + { + name: indexName, + uuid, + }, + ], + generation: 1, + }, + ]); + }); + + it('returns a single data stream by ID', async () => { + const { body: dataStream } = await supertest + .get(`${API_BASE_PATH}/data_streams/${testDataStreamName}`) + .set('kbn-xsrf', 'xxx') + .expect(200); - // ES determines these values so we'll just echo them back. - const { name: indexName, uuid } = dataStreams[0].indices[0]; - expect(dataStreams).to.eql([ + // ES determines these values so we'll just echo them back. + const { name: indexName, uuid } = dataStream.indices[0]; + expect(dataStream).to.eql({ + name: testDataStreamName, + timeStampField: { name: '@timestamp', mapping: { type: 'date' } }, + indices: [ { - name: testDataStreamName, - timeStampField: { name: '@timestamp', mapping: { type: 'date' } }, - indices: [ - { - name: indexName, - uuid, - }, - ], - generation: 1, + name: indexName, + uuid, }, - ]); + ], + generation: 1, }); }); }); + + describe('Delete', () => { + const testDataStreamName1 = 'test-data-stream1'; + const testDataStreamName2 = 'test-data-stream2'; + + before(async () => { + await Promise.all([ + createDataStream(testDataStreamName1), + createDataStream(testDataStreamName2), + ]); + }); + + after(async () => { + // The Delete API only deletes the data streams, so we still need to manually delete their + // related index patterns to clean up. + await Promise.all([ + deleteComposableIndexTemplate(testDataStreamName1), + deleteComposableIndexTemplate(testDataStreamName2), + ]); + }); + + it('deletes multiple data streams', async () => { + await supertest + .post(`${API_BASE_PATH}/delete_data_streams`) + .set('kbn-xsrf', 'xxx') + .send({ + dataStreams: [testDataStreamName1, testDataStreamName2], + }) + .expect(200); + + await supertest + .get(`${API_BASE_PATH}/data_streams/${testDataStreamName1}`) + .set('kbn-xsrf', 'xxx') + .expect(404); + + await supertest + .get(`${API_BASE_PATH}/data_streams/${testDataStreamName2}`) + .set('kbn-xsrf', 'xxx') + .expect(404); + }); + }); }); } diff --git a/x-pack/test/functional/apps/rollup_job/rollup_jobs.js b/x-pack/test/functional/apps/rollup_job/rollup_jobs.js index 28f5e2ae00f09e..5b6484d7184f30 100644 --- a/x-pack/test/functional/apps/rollup_job/rollup_jobs.js +++ b/x-pack/test/functional/apps/rollup_job/rollup_jobs.js @@ -31,9 +31,9 @@ export default function ({ getService, getPageObjects }) { it('create new rollup job', async () => { const interval = '1000ms'; - pastDates.map(async (day) => { + for (const day of pastDates) { await es.index(mockIndices(day, rollupSourceDataPrepend)); - }); + } await PageObjects.common.navigateToApp('rollupJob'); await PageObjects.rollup.createNewRollUpJob( diff --git a/x-pack/test/functional/apps/saved_objects_management/feature_controls/saved_objects_management_security.ts b/x-pack/test/functional/apps/saved_objects_management/feature_controls/saved_objects_management_security.ts index 9969608bd2a453..819d03d8119468 100644 --- a/x-pack/test/functional/apps/saved_objects_management/feature_controls/saved_objects_management_security.ts +++ b/x-pack/test/functional/apps/saved_objects_management/feature_controls/saved_objects_management_security.ts @@ -10,7 +10,14 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const security = getService('security'); const testSubjects = getService('testSubjects'); - const PageObjects = getPageObjects(['common', 'settings', 'security', 'error', 'header']); + const PageObjects = getPageObjects([ + 'common', + 'settings', + 'security', + 'error', + 'header', + 'savedObjects', + ]); let version: string = ''; describe('feature controls saved objects management', () => { @@ -66,7 +73,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { }); it('shows all saved objects', async () => { - const objects = await PageObjects.settings.getSavedObjectsInTable(); + const objects = await PageObjects.savedObjects.getRowTitles(); expect(objects).to.eql([ 'Advanced Settings [6.0.0]', `Advanced Settings [${version}]`, @@ -77,7 +84,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { }); it('can view all saved objects in applications', async () => { - const bools = await PageObjects.settings.getSavedObjectsTableSummary(); + const bools = await PageObjects.savedObjects.getTableSummary(); expect(bools).to.eql([ { title: 'Advanced Settings [6.0.0]', @@ -103,8 +110,8 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { }); it('can delete all saved objects', async () => { - await PageObjects.settings.clickSavedObjectsTableSelectAll(); - const actual = await PageObjects.settings.canSavedObjectsBeDeleted(); + await PageObjects.savedObjects.clickTableSelectAll(); + const actual = await PageObjects.savedObjects.canBeDeleted(); expect(actual).to.be(true); }); }); @@ -185,7 +192,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { }); it('shows all saved objects', async () => { - const objects = await PageObjects.settings.getSavedObjectsInTable(); + const objects = await PageObjects.savedObjects.getRowTitles(); expect(objects).to.eql([ 'Advanced Settings [6.0.0]', `Advanced Settings [${version}]`, @@ -196,7 +203,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { }); it('cannot view any saved objects in applications', async () => { - const bools = await PageObjects.settings.getSavedObjectsTableSummary(); + const bools = await PageObjects.savedObjects.getTableSummary(); expect(bools).to.eql([ { title: 'Advanced Settings [6.0.0]', @@ -222,8 +229,8 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { }); it(`can't delete all saved objects`, async () => { - await PageObjects.settings.clickSavedObjectsTableSelectAll(); - const actual = await PageObjects.settings.canSavedObjectsBeDeleted(); + await PageObjects.savedObjects.clickTableSelectAll(); + const actual = await PageObjects.savedObjects.canBeDeleted(); expect(actual).to.be(false); }); }); diff --git a/x-pack/test/functional/page_objects/copy_saved_objects_to_space_page.ts b/x-pack/test/functional/page_objects/copy_saved_objects_to_space_page.ts index 69e79d63d5fd56..03596aa68dbc6a 100644 --- a/x-pack/test/functional/page_objects/copy_saved_objects_to_space_page.ts +++ b/x-pack/test/functional/page_objects/copy_saved_objects_to_space_page.ts @@ -10,21 +10,17 @@ function extractCountFromSummary(str: string) { return parseInt(str.split('\n')[1], 10); } -export function CopySavedObjectsToSpacePageProvider({ getService }: FtrProviderContext) { +export function CopySavedObjectsToSpacePageProvider({ + getService, + getPageObjects, +}: FtrProviderContext) { const testSubjects = getService('testSubjects'); - const browser = getService('browser'); const find = getService('find'); + const { savedObjects } = getPageObjects(['savedObjects']); return { - async searchForObject(objectName: string) { - const searchBox = await testSubjects.find('savedObjectSearchBar'); - await searchBox.clearValue(); - await searchBox.type(objectName); - await searchBox.pressKeys(browser.keys.ENTER); - }, - async openCopyToSpaceFlyoutForObject(objectName: string) { - await this.searchForObject(objectName); + await savedObjects.searchForObject(objectName); // Click action button to show context menu await find.clickByCssSelector( diff --git a/x-pack/test/functional/page_objects/lens_page.ts b/x-pack/test/functional/page_objects/lens_page.ts index 3f048a9ee2aaac..bae11e1ea8a900 100644 --- a/x-pack/test/functional/page_objects/lens_page.ts +++ b/x-pack/test/functional/page_objects/lens_page.ts @@ -30,15 +30,6 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont await testSubjects.click('lnsIndexPatternFiltersToggle'); }, - /** - * Toggles the field existence checkbox. - */ - async toggleExistenceFilter() { - await this.toggleIndexPatternFiltersPopover(); - await testSubjects.click('lnsEmptyFilter'); - await this.toggleIndexPatternFiltersPopover(); - }, - async findAllFields() { return await testSubjects.findAll('lnsFieldListPanelField'); },