From 3c51fb0314799365bfe641adc7423d34e81f0d3c Mon Sep 17 00:00:00 2001 From: James Gowdy Date: Tue, 18 Feb 2020 07:16:40 +0000 Subject: [PATCH 01/12] [ML] File data viz fix index pattern warning after index change (#57807) --- .../file_based/components/import_view/import_view.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/import_view/import_view.js b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/import_view/import_view.js index bb95d3e420d2a0..beb5918e277aed 100644 --- a/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/import_view/import_view.js +++ b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/import_view/import_view.js @@ -325,9 +325,17 @@ export class ImportView extends Component { onIndexChange = e => { const name = e.target.value; + const { indexNames, indexPattern, indexPatternNames } = this.state; + this.setState({ index: name, - indexNameError: isIndexNameValid(name, this.state.indexNames), + indexNameError: isIndexNameValid(name, indexNames), + // if index pattern has been altered, check that it still matches the inputted index + ...(indexPattern === '' + ? {} + : { + indexPatternNameError: isIndexPatternNameValid(indexPattern, indexPatternNames, name), + }), }); }; From 446fda62f20af07d15f9a39cceebeac9f406ffcd Mon Sep 17 00:00:00 2001 From: Victor Martinez Date: Tue, 18 Feb 2020 08:25:14 +0000 Subject: [PATCH 02/12] [jenkins] Notify GH Checks for the apm-ui e2e pipeline (#52900) --- .ci/end2end.groovy | 27 +++++++++++++++++-- .../plugins/apm/cypress/ci/kibana.dev.yml | 3 +++ 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/.ci/end2end.groovy b/.ci/end2end.groovy index 5cf6efe324ac3f..38fed4aca19dcb 100644 --- a/.ci/end2end.groovy +++ b/.ci/end2end.groovy @@ -25,7 +25,7 @@ pipeline { durabilityHint('PERFORMANCE_OPTIMIZED') } triggers { - issueCommentTrigger('(?i).*jenkins\\W+run\\W+(?:the\\W+)?e2e(?:\\W+please)?.*') + issueCommentTrigger('(?i)(retest|.*jenkins\\W+run\\W+(?:the\\W+)?e2e?.*)') } parameters { booleanParam(name: 'FORCE', defaultValue: false, description: 'Whether to force the run.') @@ -60,8 +60,14 @@ pipeline { } } steps { + notifyStatus('Starting services', 'PENDING') dir("${APM_ITS}"){ - sh './scripts/compose.py start master --no-kibana --no-xpack-secure' + sh './scripts/compose.py start master --no-kibana' + } + } + post { + unsuccessful { + notifyStatus('Environmental issue', 'FAILURE') } } } @@ -77,10 +83,16 @@ pipeline { JENKINS_NODE_COOKIE = 'dontKillMe' } steps { + notifyStatus('Preparing kibana', 'PENDING') dir("${BASE_DIR}"){ sh script: "${CYPRESS_DIR}/ci/prepare-kibana.sh" } } + post { + unsuccessful { + notifyStatus('Kibana warm up failed', 'FAILURE') + } + } } stage('Smoke Tests'){ options { skipDefaultCheckout() } @@ -91,6 +103,7 @@ pipeline { } } steps{ + notifyStatus('Running smoke tests', 'PENDING') dir("${BASE_DIR}"){ sh ''' jobs -l @@ -112,6 +125,12 @@ pipeline { archiveArtifacts(allowEmptyArchive: false, artifacts: 'apm-its.log') } } + unsuccessful { + notifyStatus('Test failures', 'FAILURE') + } + success { + notifyStatus('Tests passed', 'SUCCESS') + } } } } @@ -123,3 +142,7 @@ pipeline { } } } + +def notifyStatus(String description, String status) { + withGithubNotify.notify('end2end-for-apm-ui', description, status, getBlueoceanDisplayURL()) +} diff --git a/x-pack/legacy/plugins/apm/cypress/ci/kibana.dev.yml b/x-pack/legacy/plugins/apm/cypress/ci/kibana.dev.yml index 3082391f23a15d..db57db9a1abe96 100644 --- a/x-pack/legacy/plugins/apm/cypress/ci/kibana.dev.yml +++ b/x-pack/legacy/plugins/apm/cypress/ci/kibana.dev.yml @@ -2,3 +2,6 @@ # Disabled plugins ######################## logging.verbose: true +elasticsearch.username: "kibana_system_user" +elasticsearch.password: "changeme" +xpack.security.encryptionKey: "something_at_least_32_characters" From 66e685b4a80e671e0ce840bc7e98b1c944ecdd61 Mon Sep 17 00:00:00 2001 From: MadameSheema Date: Tue, 18 Feb 2020 10:03:43 +0100 Subject: [PATCH 03/12] refactors 'flyout-button' tests (#57572) Co-authored-by: Elastic Machine --- .../fields_browser/fields_browser.spec.ts | 8 ++--- .../smoke_tests/inspect/inspect.spec.ts | 2 +- .../timeline/data_providers.spec.ts | 3 +- .../timeline/flyout_button.spec.ts | 30 +++++++------------ .../plugins/siem/cypress/screens/siem_main.ts | 9 ++++++ .../siem/cypress/screens/timeline/main.ts | 5 ++++ .../plugins/siem/cypress/tasks/siem_main.ts | 20 +++++++++++++ .../siem/cypress/tasks/timeline/main.ts | 5 ---- 8 files changed, 50 insertions(+), 32 deletions(-) create mode 100644 x-pack/legacy/plugins/siem/cypress/screens/siem_main.ts create mode 100644 x-pack/legacy/plugins/siem/cypress/tasks/siem_main.ts diff --git a/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/fields_browser/fields_browser.spec.ts b/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/fields_browser/fields_browser.spec.ts index 2889d78891a062..6e8ef93a540160 100644 --- a/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/fields_browser/fields_browser.spec.ts +++ b/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/fields_browser/fields_browser.spec.ts @@ -22,11 +22,9 @@ import { FIELDS_BROWSER_HEADER_HOST_GEO_CONTINENT_NAME_HEADER, } from '../../../screens/timeline/fields_browser'; -import { - openTimeline, - populateTimeline, - openTimelineFieldsBrowser, -} from '../../../tasks/timeline/main'; +import { populateTimeline, openTimelineFieldsBrowser } from '../../../tasks/timeline/main'; + +import { openTimeline } from '../../../tasks/siem_main'; import { clearFieldsBrowser, diff --git a/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/inspect/inspect.spec.ts b/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/inspect/inspect.spec.ts index e7411aba11af57..1555470f5eee79 100644 --- a/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/inspect/inspect.spec.ts +++ b/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/inspect/inspect.spec.ts @@ -12,10 +12,10 @@ import { } from '../../../screens/inspect'; import { executeTimelineKQL, - openTimeline, openTimelineSettings, openTimelineInspectButton, } from '../../../tasks/timeline/main'; +import { openTimeline } from '../../../tasks/siem_main'; import { DEFAULT_TIMEOUT, loginAndWaitForPage } from '../../../tasks/login'; import { closesModal, openStatsAndTables } from '../../../tasks/inspect'; diff --git a/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/timeline/data_providers.spec.ts b/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/timeline/data_providers.spec.ts index 3d251c1c6bcacc..c3fedfb06939b6 100644 --- a/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/timeline/data_providers.spec.ts +++ b/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/timeline/data_providers.spec.ts @@ -13,7 +13,8 @@ import { } from '../../../tasks/hosts/all_hosts'; import { HOSTS_NAMES } from '../../../screens/hosts/all_hosts'; import { DEFAULT_TIMEOUT, loginAndWaitForPage } from '../../../tasks/login'; -import { openTimeline, createNewTimeline } from '../../../tasks/timeline/main'; +import { createNewTimeline } from '../../../tasks/timeline/main'; +import { openTimeline } from '../../../tasks/siem_main'; import { TIMELINE_DATA_PROVIDERS_EMPTY, TIMELINE_DATA_PROVIDERS, diff --git a/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/timeline/flyout_button.spec.ts b/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/timeline/flyout_button.spec.ts index 63fe56371a4cd8..b7faaaac1c06c4 100644 --- a/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/timeline/flyout_button.spec.ts +++ b/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/timeline/flyout_button.spec.ts @@ -4,44 +4,34 @@ * you may not use this file except in compliance with the Elastic License. */ +import { HOSTS_PAGE } from '../../../urls/navigation'; +import { waitForAllHostsToBeLoaded, dragFirstHostToTimeline } from '../../../tasks/hosts/all_hosts'; +import { loginAndWaitForPage } from '../../../tasks/login'; +import { openTimelineIfClosed, openTimeline } from '../../../tasks/siem_main'; import { TIMELINE_FLYOUT_BODY, TIMELINE_NOT_READY_TO_DROP_BUTTON, -} from '../../lib/timeline/selectors'; -import { ALL_HOSTS_WIDGET_DRAGGABLE_HOSTS } from '../../lib/hosts/selectors'; -import { HOSTS_PAGE } from '../../lib/urls'; -import { waitForAllHostsWidget } from '../../lib/hosts/helpers'; -import { loginAndWaitForPage } from '../../lib/util/helpers'; -import { drag } from '../../lib/drag_n_drop/helpers'; -import { createNewTimeline, toggleTimelineVisibility } from '../../lib/timeline/helpers'; +} from '../../../screens/timeline/main'; +import { createNewTimeline } from '../../../tasks/timeline/main'; describe('timeline flyout button', () => { before(() => { loginAndWaitForPage(HOSTS_PAGE); + waitForAllHostsToBeLoaded(); }); afterEach(() => { - cy.get('[data-test-subj="kibanaChrome"]').then($page => { - if ($page.find('[data-test-subj="flyoutOverlay"]').length === 1) { - toggleTimelineVisibility(); - } - }); - + openTimelineIfClosed(); createNewTimeline(); }); it('toggles open the timeline', () => { - toggleTimelineVisibility(); - + openTimeline(); cy.get(TIMELINE_FLYOUT_BODY).should('have.css', 'visibility', 'visible'); }); it('sets the flyout button background to euiColorSuccess with a 10% alpha channel when the user starts dragging a host, but is not hovering over the flyout button', () => { - waitForAllHostsWidget(); - - cy.get(ALL_HOSTS_WIDGET_DRAGGABLE_HOSTS) - .first() - .then(host => drag(host)); + dragFirstHostToTimeline(); cy.get(TIMELINE_NOT_READY_TO_DROP_BUTTON).should( 'have.css', diff --git a/x-pack/legacy/plugins/siem/cypress/screens/siem_main.ts b/x-pack/legacy/plugins/siem/cypress/screens/siem_main.ts new file mode 100644 index 00000000000000..d4eeeb036ee956 --- /dev/null +++ b/x-pack/legacy/plugins/siem/cypress/screens/siem_main.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export const MAIN_PAGE = '[data-test-subj="kibanaChrome"]'; + +export const TIMELINE_TOGGLE_BUTTON = '[data-test-subj="flyoutOverlay"]'; diff --git a/x-pack/legacy/plugins/siem/cypress/screens/timeline/main.ts b/x-pack/legacy/plugins/siem/cypress/screens/timeline/main.ts index 60c9c2ab44372e..4c722ffa5f215c 100644 --- a/x-pack/legacy/plugins/siem/cypress/screens/timeline/main.ts +++ b/x-pack/legacy/plugins/siem/cypress/screens/timeline/main.ts @@ -32,3 +32,8 @@ export const TIMELINE_DATA_PROVIDERS_EMPTY = export const TIMELINE_DROPPED_DATA_PROVIDERS = '[data-test-subj="dataProviders"] [data-test-subj="providerContainer"]'; + +export const TIMELINE_FLYOUT_BODY = '[data-test-subj="eui-flyout-body"]'; + +export const TIMELINE_NOT_READY_TO_DROP_BUTTON = + '[data-test-subj="flyout-button-not-ready-to-drop"]'; diff --git a/x-pack/legacy/plugins/siem/cypress/tasks/siem_main.ts b/x-pack/legacy/plugins/siem/cypress/tasks/siem_main.ts new file mode 100644 index 00000000000000..8501bb3d94e263 --- /dev/null +++ b/x-pack/legacy/plugins/siem/cypress/tasks/siem_main.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { MAIN_PAGE, TIMELINE_TOGGLE_BUTTON } from '../screens/siem_main'; +import { DEFAULT_TIMEOUT } from '../tasks/login'; + +export const openTimelineIfClosed = () => { + cy.get(MAIN_PAGE).then($page => { + if ($page.find(TIMELINE_TOGGLE_BUTTON).length === 1) { + openTimeline(); + } + }); +}; + +export const openTimeline = () => { + cy.get(TIMELINE_TOGGLE_BUTTON, { timeout: DEFAULT_TIMEOUT }).click(); +}; diff --git a/x-pack/legacy/plugins/siem/cypress/tasks/timeline/main.ts b/x-pack/legacy/plugins/siem/cypress/tasks/timeline/main.ts index 068b6dd9f8bd4d..f347c072a35840 100644 --- a/x-pack/legacy/plugins/siem/cypress/tasks/timeline/main.ts +++ b/x-pack/legacy/plugins/siem/cypress/tasks/timeline/main.ts @@ -7,7 +7,6 @@ import { DEFAULT_TIMEOUT } from '../../integration/lib/util/helpers'; import { - TIMELINE_TOGGLE_BUTTON, SEARCH_OR_FILTER_CONTAINER, TIMELINE_FIELDS_BUTTON, SERVER_SIDE_EVENT_COUNT, @@ -19,10 +18,6 @@ import { export const hostExistsQuery = 'host.name: *'; -export const openTimeline = () => { - cy.get(TIMELINE_TOGGLE_BUTTON, { timeout: DEFAULT_TIMEOUT }).click(); -}; - export const populateTimeline = () => { cy.get(`${SEARCH_OR_FILTER_CONTAINER} input`).type(`${hostExistsQuery} {enter}`); cy.get(SERVER_SIDE_EVENT_COUNT, { timeout: DEFAULT_TIMEOUT }) From 5b7734cd4d97b03b487b257c3c68ad5169b76b2f Mon Sep 17 00:00:00 2001 From: Maryia Lapata Date: Tue, 18 Feb 2020 12:54:30 +0300 Subject: [PATCH 04/12] Apply sub url tracking utils to visualize and discover (#57307) * Apply sub url tracking utils to visualize and discover * Update query karma mock * Remove unnecessary chrome legacy calls * Fix typo * Add setActiveUrl * Add unit test for setActiveUrl * Refactoring Co-authored-by: Elastic Machine --- src/legacy/core_plugins/kibana/index.js | 2 + .../kibana/public/discover/plugin.ts | 54 +++++++++++++- .../public/visualize/kibana_services.ts | 3 +- .../kibana/public/visualize/legacy.ts | 9 +-- .../kibana/public/visualize/legacy_imports.ts | 7 -- .../public/visualize/np_ready/application.ts | 3 +- .../visualize/np_ready/editor/editor.js | 7 +- .../np_ready/listing/visualize_listing.js | 17 ++--- .../kibana/public/visualize/plugin.ts | 70 +++++++++++++++---- .../new_platform/new_platform.karma_mock.js | 50 +++++++++++-- .../url/kbn_url_tracker.test.ts | 6 ++ .../state_management/url/kbn_url_tracker.ts | 26 ++++--- 12 files changed, 189 insertions(+), 65 deletions(-) diff --git a/src/legacy/core_plugins/kibana/index.js b/src/legacy/core_plugins/kibana/index.js index ea81193c1dd0ab..8e6bae0b588bc0 100644 --- a/src/legacy/core_plugins/kibana/index.js +++ b/src/legacy/core_plugins/kibana/index.js @@ -77,6 +77,7 @@ export default function(kibana) { order: -1003, url: `${kbnBaseUrl}#/discover`, euiIconType: 'discoverApp', + disableSubUrlTracking: true, category: DEFAULT_APP_CATEGORIES.analyze, }, { @@ -87,6 +88,7 @@ export default function(kibana) { order: -1002, url: `${kbnBaseUrl}#/visualize`, euiIconType: 'visualizeApp', + disableSubUrlTracking: true, category: DEFAULT_APP_CATEGORIES.analyze, }, { diff --git a/src/legacy/core_plugins/kibana/public/discover/plugin.ts b/src/legacy/core_plugins/kibana/public/discover/plugin.ts index 565382313e3692..e8ded9d99f8924 100644 --- a/src/legacy/core_plugins/kibana/public/discover/plugin.ts +++ b/src/legacy/core_plugins/kibana/public/discover/plugin.ts @@ -16,11 +16,17 @@ * specific language governing permissions and limitations * under the License. */ + +import { BehaviorSubject } from 'rxjs'; import { i18n } from '@kbn/i18n'; import { AppMountParameters, CoreSetup, CoreStart, Plugin } from 'kibana/public'; import angular, { auto } from 'angular'; import { UiActionsSetup, UiActionsStart } from 'src/plugins/ui_actions/public'; -import { DataPublicPluginStart } from 'src/plugins/data/public'; +import { + DataPublicPluginStart, + DataPublicPluginSetup, + getQueryStateContainer, +} from '../../../../../plugins/data/public'; import { registerFeature } from './np_ready/register_feature'; import './kibana_services'; import { IEmbeddableStart, IEmbeddableSetup } from '../../../../../plugins/embeddable/public'; @@ -30,7 +36,10 @@ import { NavigationPublicPluginStart as NavigationStart } from '../../../../../p import { ChartsPluginStart } from '../../../../../plugins/charts/public'; import { buildServices } from './build_services'; import { SharePluginStart } from '../../../../../plugins/share/public'; -import { KibanaLegacySetup } from '../../../../../plugins/kibana_legacy/public'; +import { + KibanaLegacySetup, + AngularRenderedAppUpdater, +} from '../../../../../plugins/kibana_legacy/public'; import { DocViewsRegistry } from './np_ready/doc_views/doc_views_registry'; import { DocViewInput, DocViewInputFn } from './np_ready/doc_views/doc_views_types'; import { DocViewTable } from './np_ready/components/table/table'; @@ -40,6 +49,7 @@ import { VisualizationsStart, VisualizationsSetup, } from '../../../visualizations/public/np_ready/public'; +import { createKbnUrlTracker } from '../../../../../plugins/kibana_utils/public'; /** * These are the interfaces with your public contracts. You should export these @@ -56,6 +66,7 @@ export interface DiscoverSetupPlugins { kibanaLegacy: KibanaLegacySetup; home: HomePublicPluginSetup; visualizations: VisualizationsSetup; + data: DataPublicPluginSetup; } export interface DiscoverStartPlugins { uiActions: UiActionsStart; @@ -81,6 +92,9 @@ export class DiscoverPlugin implements Plugin { private docViewsRegistry: DocViewsRegistry | null = null; private embeddableInjector: auto.IInjectorService | null = null; private getEmbeddableInjector: (() => Promise) | null = null; + private appStateUpdater = new BehaviorSubject(() => ({})); + private stopUrlTracking: (() => void) | undefined = undefined; + /** * why are those functions public? they are needed for some mocha tests * can be removed once all is Jest @@ -89,6 +103,27 @@ export class DiscoverPlugin implements Plugin { public initializeServices?: () => Promise<{ core: CoreStart; plugins: DiscoverStartPlugins }>; setup(core: CoreSetup, plugins: DiscoverSetupPlugins): DiscoverSetup { + const { querySyncStateContainer, stop: stopQuerySyncStateContainer } = getQueryStateContainer( + plugins.data.query + ); + const { appMounted, appUnMounted, stop: stopUrlTracker } = createKbnUrlTracker({ + baseUrl: core.http.basePath.prepend('/app/kibana'), + defaultSubUrl: '#/discover', + storageKey: 'lastUrl:discover', + navLinkUpdater$: this.appStateUpdater, + toastNotifications: core.notifications.toasts, + stateParams: [ + { + kbnUrlKey: '_g', + stateUpdate$: querySyncStateContainer.state$, + }, + ], + }); + this.stopUrlTracking = () => { + stopQuerySyncStateContainer(); + stopUrlTracker(); + }; + this.getEmbeddableInjector = this.getInjector.bind(this); this.docViewsRegistry = new DocViewsRegistry(this.getEmbeddableInjector); this.docViewsRegistry.addDocView({ @@ -108,6 +143,8 @@ export class DiscoverPlugin implements Plugin { plugins.kibanaLegacy.registerLegacyApp({ id: 'discover', title: 'Discover', + updater$: this.appStateUpdater.asObservable(), + navLinkId: 'kibana:discover', order: -1004, euiIconType: 'discoverApp', mount: async (params: AppMountParameters) => { @@ -117,11 +154,16 @@ export class DiscoverPlugin implements Plugin { if (!this.initializeInnerAngular) { throw Error('Discover plugin method initializeInnerAngular is undefined'); } + appMounted(); await this.initializeServices(); await this.initializeInnerAngular(); const { renderApp } = await import('./np_ready/application'); - return renderApp(innerAngularName, params.element); + const unmount = await renderApp(innerAngularName, params.element); + return () => { + unmount(); + appUnMounted(); + }; }, }); registerFeature(plugins.home); @@ -160,6 +202,12 @@ export class DiscoverPlugin implements Plugin { this.registerEmbeddable(core, plugins); } + stop() { + if (this.stopUrlTracking) { + this.stopUrlTracking(); + } + } + /** * register embeddable with a slimmer embeddable version of inner angular */ diff --git a/src/legacy/core_plugins/kibana/public/visualize/kibana_services.ts b/src/legacy/core_plugins/kibana/public/visualize/kibana_services.ts index 6082fb8428ac3f..096877d5824c49 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/kibana_services.ts +++ b/src/legacy/core_plugins/kibana/public/visualize/kibana_services.ts @@ -35,7 +35,6 @@ import { DataPublicPluginStart, IndexPatternsContract } from '../../../../../plu import { VisualizationsStart } from '../../../visualizations/public'; import { SavedVisualizations } from './np_ready/types'; import { UsageCollectionSetup } from '../../../../../plugins/usage_collection/public'; -import { Chrome } from './legacy_imports'; import { KibanaLegacyStart } from '../../../../../plugins/kibana_legacy/public'; export interface VisualizeKibanaServices { @@ -47,7 +46,6 @@ export interface VisualizeKibanaServices { embeddable: IEmbeddableStart; getBasePath: () => string; indexPatterns: IndexPatternsContract; - legacyChrome: Chrome; localStorage: Storage; navigation: NavigationStart; toastNotifications: ToastsStart; @@ -61,6 +59,7 @@ export interface VisualizeKibanaServices { visualizations: VisualizationsStart; usageCollection?: UsageCollectionSetup; I18nContext: I18nStart['Context']; + setActiveUrl: (newUrl: string) => void; } let services: VisualizeKibanaServices | null = null; diff --git a/src/legacy/core_plugins/kibana/public/visualize/legacy.ts b/src/legacy/core_plugins/kibana/public/visualize/legacy.ts index bc2d700f6c6a10..fbbc7ab944dafe 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/legacy.ts +++ b/src/legacy/core_plugins/kibana/public/visualize/legacy.ts @@ -18,19 +18,14 @@ */ import { PluginInitializerContext } from 'kibana/public'; -import { legacyChrome, npSetup, npStart } from './legacy_imports'; +import { npSetup, npStart } from 'ui/new_platform'; import { start as visualizations } from '../../../visualizations/public/np_ready/public/legacy'; import { plugin } from './index'; const instance = plugin({ env: npSetup.plugins.kibanaLegacy.env, } as PluginInitializerContext); -instance.setup(npSetup.core, { - ...npSetup.plugins, - __LEGACY: { - legacyChrome, - }, -}); +instance.setup(npSetup.core, npSetup.plugins); instance.start(npStart.core, { ...npStart.plugins, visualizations, diff --git a/src/legacy/core_plugins/kibana/public/visualize/legacy_imports.ts b/src/legacy/core_plugins/kibana/public/visualize/legacy_imports.ts index ac9fc227406ffd..92433799ba4204 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/legacy_imports.ts +++ b/src/legacy/core_plugins/kibana/public/visualize/legacy_imports.ts @@ -24,11 +24,6 @@ * directly where they are needed. */ -import chrome from 'ui/chrome'; - -export const legacyChrome = chrome; -export { Chrome } from 'ui/chrome'; - // @ts-ignore export { AppState, AppStateProvider } from 'ui/state_management/app_state'; export { State } from 'ui/state_management/state'; @@ -39,8 +34,6 @@ export { StateManagementConfigProvider } from 'ui/state_management/config_provid export { stateMonitorFactory } from 'ui/state_management/state_monitor_factory'; export { PersistedState } from 'ui/persisted_state'; -export { npSetup, npStart } from 'ui/new_platform'; - export { subscribeWithScope } from 'ui/utils/subscribe_with_scope'; // @ts-ignore export { EventsProvider } from 'ui/events'; diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/application.ts b/src/legacy/core_plugins/kibana/public/visualize/np_ready/application.ts index 3d5fd6605f56b1..bd7b478f827a65 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/np_ready/application.ts +++ b/src/legacy/core_plugins/kibana/public/visualize/np_ready/application.ts @@ -45,7 +45,7 @@ import { VisualizeKibanaServices } from '../kibana_services'; let angularModuleInstance: IModule | null = null; -export const renderApp = async ( +export const renderApp = ( element: HTMLElement, appBasePath: string, deps: VisualizeKibanaServices @@ -58,7 +58,6 @@ export const renderApp = async ( { core: deps.core, env: deps.pluginInitializerContext.env }, true ); - // custom routing stuff initVisualizeApp(angularModuleInstance, deps); } const $injector = mountVisualizeApp(appBasePath, element); diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/editor.js b/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/editor.js index 657104344662f7..409d4b41fbe696 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/editor.js +++ b/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/editor.js @@ -90,13 +90,13 @@ function VisualizeAppController( }, }, toastNotifications, - legacyChrome, chrome, getBasePath, core: { docLinks }, savedQueryService, uiSettings, I18nContext, + setActiveUrl, } = getServices(); const filterStateManager = new FilterStateManager(globalState, getAppState, filterManager); @@ -580,10 +580,7 @@ function VisualizeAppController( }); // Manually insert a new url so the back button will open the saved visualization. $window.history.pushState({}, '', savedVisualizationParsedUrl.getRootRelativePath()); - // Since we aren't reloading the page, only inserting a new browser history item, we need to manually update - // the last url for this app, so directly clicking on the Visualize tab will also bring the user to the saved - // url, not the unsaved one. - legacyChrome.trackSubUrlForApp('kibana:visualize', savedVisualizationParsedUrl); + setActiveUrl(savedVisualizationParsedUrl.appPath); const lastDashboardAbsoluteUrl = chrome.navLinks.get('kibana:dashboard').url; const dashboardParsedUrl = absoluteToParsedUrl( diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/listing/visualize_listing.js b/src/legacy/core_plugins/kibana/public/visualize/np_ready/listing/visualize_listing.js index cae1e40cd445a4..c0cc499b598f02 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/np_ready/listing/visualize_listing.js +++ b/src/legacy/core_plugins/kibana/public/visualize/np_ready/listing/visualize_listing.js @@ -36,7 +36,6 @@ export function VisualizeListingController($injector, $scope, createNewVis) { const { addBasePath, chrome, - legacyChrome, savedObjectsClient, savedVisualizations, data: { @@ -100,17 +99,13 @@ export function VisualizeListingController($injector, $scope, createNewVis) { selectedItems.map(item => { return savedObjectsClient.delete(item.savedObjectType, item.id); }) - ) - .then(() => { - legacyChrome.untrackNavLinksForDeletedSavedObjects(selectedItems.map(item => item.id)); - }) - .catch(error => { - toastNotifications.addError(error, { - title: i18n.translate('kbn.visualize.visualizeListingDeleteErrorTitle', { - defaultMessage: 'Error deleting visualization', - }), - }); + ).catch(error => { + toastNotifications.addError(error, { + title: i18n.translate('kbn.visualize.visualizeListingDeleteErrorTitle', { + defaultMessage: 'Error deleting visualization', + }), }); + }); }; chrome.setBreadcrumbs([ diff --git a/src/legacy/core_plugins/kibana/public/visualize/plugin.ts b/src/legacy/core_plugins/kibana/public/visualize/plugin.ts index 16715677d1e207..22804685db3ccd 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/plugin.ts +++ b/src/legacy/core_plugins/kibana/public/visualize/plugin.ts @@ -17,6 +17,7 @@ * under the License. */ +import { BehaviorSubject } from 'rxjs'; import { i18n } from '@kbn/i18n'; import { @@ -28,12 +29,19 @@ import { SavedObjectsClientContract, } from 'kibana/public'; -import { Storage } from '../../../../../plugins/kibana_utils/public'; -import { DataPublicPluginStart } from '../../../../../plugins/data/public'; +import { Storage, createKbnUrlTracker } from '../../../../../plugins/kibana_utils/public'; +import { + DataPublicPluginStart, + DataPublicPluginSetup, + getQueryStateContainer, +} from '../../../../../plugins/data/public'; import { IEmbeddableStart } from '../../../../../plugins/embeddable/public'; import { NavigationPublicPluginStart as NavigationStart } from '../../../../../plugins/navigation/public'; import { SharePluginStart } from '../../../../../plugins/share/public'; -import { KibanaLegacySetup } from '../../../../../plugins/kibana_legacy/public'; +import { + KibanaLegacySetup, + AngularRenderedAppUpdater, +} from '../../../../../plugins/kibana_legacy/public'; import { VisualizationsStart } from '../../../visualizations/public'; import { VisualizeConstants } from './np_ready/visualize_constants'; import { setServices, VisualizeKibanaServices } from './kibana_services'; @@ -42,7 +50,6 @@ import { HomePublicPluginSetup, } from '../../../../../plugins/home/public'; import { UsageCollectionSetup } from '../../../../../plugins/usage_collection/public'; -import { Chrome } from './legacy_imports'; export interface VisualizePluginStartDependencies { data: DataPublicPluginStart; @@ -53,12 +60,10 @@ export interface VisualizePluginStartDependencies { } export interface VisualizePluginSetupDependencies { - __LEGACY: { - legacyChrome: Chrome; - }; home: HomePublicPluginSetup; kibanaLegacy: KibanaLegacySetup; usageCollection?: UsageCollectionSetup; + data: DataPublicPluginSetup; } export class VisualizePlugin implements Plugin { @@ -70,46 +75,72 @@ export class VisualizePlugin implements Plugin { share: SharePluginStart; visualizations: VisualizationsStart; } | null = null; + private appStateUpdater = new BehaviorSubject(() => ({})); + private stopUrlTracking: (() => void) | undefined = undefined; constructor(private initializerContext: PluginInitializerContext) {} public async setup( core: CoreSetup, - { home, kibanaLegacy, __LEGACY, usageCollection }: VisualizePluginSetupDependencies + { home, kibanaLegacy, usageCollection, data }: VisualizePluginSetupDependencies ) { + const { querySyncStateContainer, stop: stopQuerySyncStateContainer } = getQueryStateContainer( + data.query + ); + const { appMounted, appUnMounted, stop: stopUrlTracker, setActiveUrl } = createKbnUrlTracker({ + baseUrl: core.http.basePath.prepend('/app/kibana'), + defaultSubUrl: '#/visualize', + storageKey: 'lastUrl:visualize', + navLinkUpdater$: this.appStateUpdater, + toastNotifications: core.notifications.toasts, + stateParams: [ + { + kbnUrlKey: '_g', + stateUpdate$: querySyncStateContainer.state$, + }, + ], + }); + this.stopUrlTracking = () => { + stopQuerySyncStateContainer(); + stopUrlTracker(); + }; + kibanaLegacy.registerLegacyApp({ id: 'visualize', title: 'Visualize', + updater$: this.appStateUpdater.asObservable(), + navLinkId: 'kibana:visualize', mount: async (params: AppMountParameters) => { const [coreStart] = await core.getStartServices(); + if (this.startDependencies === null) { throw new Error('not started yet'); } + appMounted(); const { savedObjectsClient, embeddable, navigation, visualizations, - data, + data: dataStart, share, } = this.startDependencies; const deps: VisualizeKibanaServices = { - ...__LEGACY, pluginInitializerContext: this.initializerContext, addBasePath: coreStart.http.basePath.prepend, core: coreStart, chrome: coreStart.chrome, - data, + data: dataStart, embeddable, getBasePath: core.http.basePath.get, - indexPatterns: data.indexPatterns, + indexPatterns: dataStart.indexPatterns, localStorage: new Storage(localStorage), navigation, savedObjectsClient, savedVisualizations: visualizations.getSavedVisualizationsLoader(), - savedQueryService: data.query.savedQueries, + savedQueryService: dataStart.query.savedQueries, share, toastNotifications: coreStart.notifications.toasts, uiSettings: coreStart.uiSettings, @@ -118,11 +149,16 @@ export class VisualizePlugin implements Plugin { visualizations, usageCollection, I18nContext: coreStart.i18n.Context, + setActiveUrl, }; setServices(deps); const { renderApp } = await import('./np_ready/application'); - return renderApp(params.element, params.appBasePath, deps); + const unmount = renderApp(params.element, params.appBasePath, deps); + return () => { + unmount(); + appUnMounted(); + }; }, }); @@ -153,4 +189,10 @@ export class VisualizePlugin implements Plugin { visualizations, }; } + + stop() { + if (this.stopUrlTracking) { + this.stopUrlTracking(); + } + } } diff --git a/src/legacy/ui/public/new_platform/new_platform.karma_mock.js b/src/legacy/ui/public/new_platform/new_platform.karma_mock.js index 4e52f6f6bafec3..38b3434ef9c48e 100644 --- a/src/legacy/ui/public/new_platform/new_platform.karma_mock.js +++ b/src/legacy/ui/public/new_platform/new_platform.karma_mock.js @@ -61,6 +61,10 @@ const mockCore = { }, }; +let refreshInterval = undefined; +let isTimeRangeSelectorEnabled = true; +let isAutoRefreshSelectorEnabled = true; + export const npSetup = { core: mockCore, plugins: { @@ -101,7 +105,14 @@ export const npSetup = { }, query: { filterManager: { + getFetches$: sinon.fake(), + getFilters: sinon.fake(), + getAppFilters: sinon.fake(), getGlobalFilters: sinon.fake(), + removeFilter: sinon.fake(), + addFilters: sinon.fake(), + setFilters: sinon.fake(), + removeAll: sinon.fake(), getUpdates$: mockObservable, }, timefilter: { @@ -110,6 +121,41 @@ export const npSetup = { getRefreshInterval: sinon.fake(), getTimeUpdate$: mockObservable, getRefreshIntervalUpdate$: mockObservable, + getFetch$: mockObservable, + getAutoRefreshFetch$: mockObservable, + getEnabledUpdated$: mockObservable, + getTimeUpdate$: mockObservable, + getRefreshIntervalUpdate$: mockObservable, + isTimeRangeSelectorEnabled: () => { + return isTimeRangeSelectorEnabled; + }, + isAutoRefreshSelectorEnabled: () => { + return isAutoRefreshSelectorEnabled; + }, + disableAutoRefreshSelector: () => { + isAutoRefreshSelectorEnabled = false; + }, + enableAutoRefreshSelector: () => { + isAutoRefreshSelectorEnabled = true; + }, + getRefreshInterval: () => { + return refreshInterval; + }, + setRefreshInterval: interval => { + refreshInterval = interval; + }, + enableTimeRangeSelector: () => { + isTimeRangeSelectorEnabled = true; + }, + disableTimeRangeSelector: () => { + isTimeRangeSelectorEnabled = false; + }, + getTime: sinon.fake(), + setTime: sinon.fake(), + getActiveBounds: sinon.fake(), + getBounds: sinon.fake(), + calculateBounds: sinon.fake(), + createFilter: sinon.fake(), }, history: sinon.fake(), }, @@ -183,10 +229,6 @@ export const npSetup = { }, }; -let refreshInterval = undefined; -let isTimeRangeSelectorEnabled = true; -let isAutoRefreshSelectorEnabled = true; - export const npStart = { core: { chrome: { diff --git a/src/plugins/kibana_utils/public/state_management/url/kbn_url_tracker.test.ts b/src/plugins/kibana_utils/public/state_management/url/kbn_url_tracker.test.ts index 4b17d8517328bf..4cf74d991ceb92 100644 --- a/src/plugins/kibana_utils/public/state_management/url/kbn_url_tracker.test.ts +++ b/src/plugins/kibana_utils/public/state_management/url/kbn_url_tracker.test.ts @@ -181,4 +181,10 @@ describe('kbnUrlTracker', () => { `"/app/test#/start?state1=(key1:abc)&state2=(key2:def)"` ); }); + + test('set url to storage when setActiveUrl was called', () => { + createTracker(); + urlTracker.setActiveUrl('/deep/path/4'); + expect(storage.getItem('storageKey')).toEqual('#/deep/path/4'); + }); }); diff --git a/src/plugins/kibana_utils/public/state_management/url/kbn_url_tracker.ts b/src/plugins/kibana_utils/public/state_management/url/kbn_url_tracker.ts index 6f3f64ea7b9419..2edd135c184ecd 100644 --- a/src/plugins/kibana_utils/public/state_management/url/kbn_url_tracker.ts +++ b/src/plugins/kibana_utils/public/state_management/url/kbn_url_tracker.ts @@ -36,6 +36,7 @@ export interface KbnUrlTracker { * Unregistering the url tracker. This won't reset the current state of the nav link */ stop: () => void; + setActiveUrl: (newUrl: string) => void; } /** @@ -130,20 +131,24 @@ export function createKbnUrlTracker({ } } + function setActiveUrl(newUrl: string) { + const urlWithHashes = baseUrl + '#' + newUrl; + let urlWithStates = ''; + try { + urlWithStates = unhashUrl(urlWithHashes); + } catch (e) { + toastNotifications.addDanger(e.message); + } + + activeUrl = getActiveSubUrl(urlWithStates || urlWithHashes); + storageInstance.setItem(storageKey, activeUrl); + } + function onMountApp() { unsubscribe(); // track current hash when within app unsubscribeURLHistory = historyInstance.listen(location => { - const urlWithHashes = baseUrl + '#' + location.pathname + location.search; - let urlWithStates = ''; - try { - urlWithStates = unhashUrl(urlWithHashes); - } catch (e) { - toastNotifications.addDanger(e.message); - } - - activeUrl = getActiveSubUrl(urlWithStates || urlWithHashes); - storageInstance.setItem(storageKey, activeUrl); + setActiveUrl(location.pathname + location.search); }); } @@ -188,5 +193,6 @@ export function createKbnUrlTracker({ stop() { unsubscribe(); }, + setActiveUrl, }; } From 8bc3fa40425778a8e056eb4d561ff28f3e870c22 Mon Sep 17 00:00:00 2001 From: Anton Dosov Date: Tue, 18 Feb 2020 13:45:25 +0100 Subject: [PATCH 05/12] Bugfix: Navigation from unsaved dashboard to recently used fails (#57795) Co-authored-by: Elastic Machine --- .../np_ready/dashboard_state_manager.ts | 20 ++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_state_manager.ts b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_state_manager.ts index fa5354a17b6d93..fe7beafcad18c2 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_state_manager.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_state_manager.ts @@ -165,7 +165,7 @@ export class DashboardStateManager { // make sure url ('_a') matches initial state this.kbnUrlStateStorage.set(this.STATE_STORAGE_KEY, initialState, { replace: true }); - // setup state syncing utils. state container will be synched with url into `this.STATE_STORAGE_KEY` query param + // setup state syncing utils. state container will be synced with url into `this.STATE_STORAGE_KEY` query param this.stateSyncRef = syncState({ storageKey: this.STATE_STORAGE_KEY, stateContainer: { @@ -173,10 +173,20 @@ export class DashboardStateManager { set: (state: DashboardAppState | null) => { // sync state required state container to be able to handle null // overriding set() so it could handle null coming from url - this.stateContainer.set({ - ...this.stateDefaults, - ...state, - }); + if (state) { + this.stateContainer.set({ + ...this.stateDefaults, + ...state, + }); + } else { + // Do nothing in case when state from url is empty, + // this fixes: https://github.com/elastic/kibana/issues/57789 + // There are not much cases when state in url could become empty: + // 1. User manually removed `_a` from the url + // 2. Browser is navigating away from the page and most likely there is no `_a` in the url. + // In this case we don't want to do any state updates + // and just allow $scope.$on('destroy') fire later and clean up everything + } }, }, stateStorage: this.kbnUrlStateStorage, From 23306d80973878ecb22f1d3cb9d0a71b8e2ae6c1 Mon Sep 17 00:00:00 2001 From: Matthias Wilhelm Date: Tue, 18 Feb 2020 14:24:22 +0100 Subject: [PATCH 06/12] [Discover] Migrate context AppState / GlobalState to use new app state helpers (#57078) * Remove globalState, migrate to the new helpers * Remove appState, migrate to the new helpers * Add tests --- .../discover/np_ready/angular/context.js | 73 +++-- .../np_ready/angular/context/api/context.ts | 2 +- .../np_ready/angular/context/query/actions.js | 4 +- .../np_ready/angular/context_state.test.ts | 193 ++++++++++++ .../np_ready/angular/context_state.ts | 275 ++++++++++++++++++ 5 files changed, 506 insertions(+), 41 deletions(-) create mode 100644 src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context_state.test.ts create mode 100644 src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context_state.ts diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context.js b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context.js index a370c66ae330b9..038f783a0daf14 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context.js +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context.js @@ -19,13 +19,11 @@ import _ from 'lodash'; import { i18n } from '@kbn/i18n'; -import { getAngularModule, getServices, subscribeWithScope } from '../../kibana_services'; - +import { getAngularModule, getServices } from '../../kibana_services'; import './context_app'; +import { getState } from './context_state'; import contextAppRouteTemplate from './context.html'; import { getRootBreadcrumbs } from '../helpers/breadcrumbs'; -import { FilterStateManager } from '../../../../../data/public'; -const { chrome } = getServices(); const k7Breadcrumbs = $route => { const { indexPattern } = $route.current.locals; @@ -68,53 +66,50 @@ getAngularModule().config($routeProvider => { }); }); -function ContextAppRouteController( - $routeParams, - $scope, - AppState, - config, - $route, - getAppState, - globalState -) { +function ContextAppRouteController($routeParams, $scope, config, $route) { const filterManager = getServices().filterManager; - const filterStateManager = new FilterStateManager(globalState, getAppState, filterManager); const indexPattern = $route.current.locals.indexPattern.ip; + const { + startSync: startStateSync, + stopSync: stopStateSync, + appState, + getFilters, + setFilters, + setAppState, + } = getState({ + defaultStepSize: config.get('context:defaultSize'), + timeFieldName: indexPattern.timeFieldName, + storeInSessionStorage: config.get('state:storeInSessionStorage'), + }); + this.state = { ...appState.getState() }; + this.anchorId = $routeParams.id; + this.indexPattern = indexPattern; + this.discoverUrl = getServices().chrome.navLinks.get('kibana:discover').url; + filterManager.setFilters(_.cloneDeep(getFilters())); + startStateSync(); - this.state = new AppState(createDefaultAppState(config, indexPattern)); - this.state.save(true); - + // take care of parameter changes in UI $scope.$watchGroup( [ 'contextAppRoute.state.columns', 'contextAppRoute.state.predecessorCount', 'contextAppRoute.state.successorCount', ], - () => this.state.save(true) + newValues => { + const [columns, predecessorCount, successorCount] = newValues; + if (Array.isArray(columns) && predecessorCount >= 0 && successorCount >= 0) { + setAppState({ columns, predecessorCount, successorCount }); + } + } ); - - const updateSubsciption = subscribeWithScope($scope, filterManager.getUpdates$(), { - next: () => { - this.filters = _.cloneDeep(filterManager.getFilters()); - }, + // take care of parameter filter changes + const filterObservable = filterManager.getUpdates$().subscribe(() => { + setFilters(filterManager); + $route.reload(); }); $scope.$on('$destroy', () => { - filterStateManager.destroy(); - updateSubsciption.unsubscribe(); + stopStateSync(); + filterObservable.unsubscribe(); }); - this.anchorId = $routeParams.id; - this.indexPattern = indexPattern; - this.discoverUrl = chrome.navLinks.get('kibana:discover').url; - this.filters = _.cloneDeep(filterManager.getFilters()); -} - -function createDefaultAppState(config, indexPattern) { - return { - columns: ['_source'], - filters: [], - predecessorCount: parseInt(config.get('context:defaultSize'), 10), - sort: [indexPattern.timeFieldName, 'desc'], - successorCount: parseInt(config.get('context:defaultSize'), 10), - }; } diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context/api/context.ts b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context/api/context.ts index a9c6918adbfdea..b91ef5a6b79fbb 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context/api/context.ts +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context/api/context.ts @@ -67,7 +67,7 @@ function fetchContextProvider(indexPatterns: IndexPatternsContract) { size: number, filters: Filter[] ) { - if (typeof anchor !== 'object' || anchor === null) { + if (typeof anchor !== 'object' || anchor === null || !size) { return []; } const indexPattern = await indexPatterns.get(indexPatternId); diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context/query/actions.js b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context/query/actions.js index 966ecffda7755d..1cebb88cbda5a0 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context/query/actions.js +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context/query/actions.js @@ -88,9 +88,11 @@ export function QueryActionsProvider(Promise) { const fetchSurroundingRows = (type, state) => { const { - queryParameters: { indexPatternId, filters, sort, tieBreakerField }, + queryParameters: { indexPatternId, sort, tieBreakerField }, rows: { anchor }, } = state; + const filters = getServices().filterManager.getFilters(); + const count = type === 'successors' ? state.queryParameters.successorCount diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context_state.test.ts b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context_state.test.ts new file mode 100644 index 00000000000000..1fa71ed11643a6 --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context_state.test.ts @@ -0,0 +1,193 @@ +/* + * 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 { getState } from './context_state'; +import { createBrowserHistory, History } from 'history'; +import { FilterManager, Filter } from '../../../../../../../plugins/data/public'; +import { coreMock } from '../../../../../../../core/public/mocks'; +const setupMock = coreMock.createSetup(); + +describe('Test Discover Context State', () => { + let history: History; + let state: any; + const getCurrentUrl = () => history.createHref(history.location); + beforeEach(async () => { + history = createBrowserHistory(); + history.push('/'); + state = await getState({ + defaultStepSize: '4', + timeFieldName: 'time', + history, + }); + state.startSync(); + }); + afterEach(() => { + state.stopSync(); + }); + test('getState function default return', () => { + expect(state.appState.getState()).toMatchInlineSnapshot(` + Object { + "columns": Array [ + "_source", + ], + "filters": Array [], + "predecessorCount": 4, + "sort": Array [ + "time", + "desc", + ], + "successorCount": 4, + } + `); + expect(state.globalState.getState()).toMatchInlineSnapshot(`null`); + expect(state.startSync).toBeDefined(); + expect(state.stopSync).toBeDefined(); + expect(state.getFilters()).toStrictEqual([]); + }); + test('getState -> setAppState syncing to url', async () => { + state.setAppState({ predecessorCount: 10 }); + state.flushToUrl(); + expect(getCurrentUrl()).toMatchInlineSnapshot( + `"/#?_a=(columns:!(_source),filters:!(),predecessorCount:10,sort:!(time,desc),successorCount:4)"` + ); + }); + test('getState -> url to appState syncing', async () => { + history.push( + '/#?_a=(columns:!(_source),predecessorCount:1,sort:!(time,desc),successorCount:1)' + ); + expect(state.appState.getState()).toMatchInlineSnapshot(` + Object { + "columns": Array [ + "_source", + ], + "predecessorCount": 1, + "sort": Array [ + "time", + "desc", + ], + "successorCount": 1, + } + `); + }); + test('getState -> url to appState syncing with return to a url without state', async () => { + history.push( + '/#?_a=(columns:!(_source),predecessorCount:1,sort:!(time,desc),successorCount:1)' + ); + expect(state.appState.getState()).toMatchInlineSnapshot(` + Object { + "columns": Array [ + "_source", + ], + "predecessorCount": 1, + "sort": Array [ + "time", + "desc", + ], + "successorCount": 1, + } + `); + history.push('/'); + expect(state.appState.getState()).toMatchInlineSnapshot(` + Object { + "columns": Array [ + "_source", + ], + "predecessorCount": 1, + "sort": Array [ + "time", + "desc", + ], + "successorCount": 1, + } + `); + }); + + test('getState -> filters', async () => { + const filterManager = new FilterManager(setupMock.uiSettings); + const filterGlobal = { + query: { match: { extension: { query: 'jpg', type: 'phrase' } } }, + meta: { index: 'logstash-*', negate: false, disabled: false, alias: null }, + } as Filter; + filterManager.setGlobalFilters([filterGlobal]); + const filterApp = { + query: { match: { extension: { query: 'png', type: 'phrase' } } }, + meta: { index: 'logstash-*', negate: true, disabled: false, alias: null }, + } as Filter; + filterManager.setAppFilters([filterApp]); + state.setFilters(filterManager); + expect(state.getFilters()).toMatchInlineSnapshot(` + Array [ + Object { + "$state": Object { + "store": "globalState", + }, + "meta": Object { + "alias": null, + "disabled": false, + "index": "logstash-*", + "key": "extension", + "negate": false, + "params": Object { + "query": "jpg", + }, + "type": "phrase", + "value": [Function], + }, + "query": Object { + "match": Object { + "extension": Object { + "query": "jpg", + "type": "phrase", + }, + }, + }, + }, + Object { + "$state": Object { + "store": "appState", + }, + "meta": Object { + "alias": null, + "disabled": false, + "index": "logstash-*", + "key": "extension", + "negate": true, + "params": Object { + "query": "png", + }, + "type": "phrase", + "value": [Function], + }, + "query": Object { + "match": Object { + "extension": Object { + "query": "png", + "type": "phrase", + }, + }, + }, + }, + ] + `); + state.flushToUrl(); + expect(getCurrentUrl()).toMatchInlineSnapshot( + `"/#?_g=(filters:!(('$state':(store:globalState),meta:(alias:!n,disabled:!f,index:'logstash-*',key:extension,negate:!f,params:(query:jpg),type:phrase),query:(match:(extension:(query:jpg,type:phrase))))))&_a=(columns:!(_source),filters:!(('$state':(store:appState),meta:(alias:!n,disabled:!f,index:'logstash-*',key:extension,negate:!t,params:(query:png),type:phrase),query:(match:(extension:(query:png,type:phrase))))),predecessorCount:4,sort:!(time,desc),successorCount:4)"` + ); + }); +}); diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context_state.ts b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context_state.ts new file mode 100644 index 00000000000000..8fb6140d55e319 --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context_state.ts @@ -0,0 +1,275 @@ +/* + * 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 _ from 'lodash'; +import { createBrowserHistory, History } from 'history'; +import { + createStateContainer, + createKbnUrlStateStorage, + syncStates, + BaseStateContainer, +} from '../../../../../../../plugins/kibana_utils/public'; +import { esFilters, FilterManager, Filter } from '../../../../../../../plugins/data/public'; + +interface AppState { + /** + * Columns displayed in the table, cannot be changed by UI, just in discover's main app + */ + columns: string[]; + /** + * Array of filters + */ + filters: Filter[]; + /** + * Number of records to be fetched before anchor records (newer records) + */ + predecessorCount: number; + /** + * Sorting of the records to be fetched, assumed to be a legacy parameter + */ + sort: string[]; + /** + * Number of records to be fetched after the anchor records (older records) + */ + successorCount: number; +} + +interface GlobalState { + /** + * Array of filters + */ + filters: Filter[]; +} + +interface GetStateParams { + /** + * Number of records to be fetched when 'Load' link/button is clicked + */ + defaultStepSize: string; + /** + * The timefield used for sorting + */ + timeFieldName: string; + /** + * Determins the use of long vs. short/hashed urls + */ + storeInSessionStorage?: boolean; + /** + * Browser history used for testing + */ + history?: History; +} + +interface GetStateReturn { + /** + * Global state, the _g part of the URL + */ + globalState: BaseStateContainer; + /** + * App state, the _a part of the URL + */ + appState: BaseStateContainer; + /** + * Start sync between state and URL + */ + startSync: () => void; + /** + * Stop sync between state and URL + */ + stopSync: () => void; + /** + * Set app state to with a partial new app state + */ + setAppState: (newState: Partial) => void; + /** + * Get all filters, global and app state + */ + getFilters: () => Filter[]; + /** + * Set global state and app state filters by the given FilterManager instance + * @param filterManager + */ + setFilters: (filterManager: FilterManager) => void; + /** + * sync state to URL, used for testing + */ + flushToUrl: () => void; +} +const GLOBAL_STATE_URL_KEY = '_g'; +const APP_STATE_URL_KEY = '_a'; + +/** + * Builds and returns appState and globalState containers + * provides helper functions to start/stop syncing with URL + */ +export function getState({ + defaultStepSize, + timeFieldName, + storeInSessionStorage = false, + history, +}: GetStateParams): GetStateReturn { + const stateStorage = createKbnUrlStateStorage({ + useHash: storeInSessionStorage, + history: history ? history : createBrowserHistory(), + }); + + const globalStateInitial = stateStorage.get(GLOBAL_STATE_URL_KEY) as GlobalState; + const globalStateContainer = createStateContainer(globalStateInitial); + + const appStateFromUrl = stateStorage.get(APP_STATE_URL_KEY) as AppState; + const appStateInitial = createInitialAppState(defaultStepSize, timeFieldName, appStateFromUrl); + const appStateContainer = createStateContainer(appStateInitial); + + const { start, stop } = syncStates([ + { + storageKey: GLOBAL_STATE_URL_KEY, + stateContainer: { + ...globalStateContainer, + ...{ + set: (value: GlobalState | null) => { + if (value) { + globalStateContainer.set(value); + } + }, + }, + }, + stateStorage, + }, + { + storageKey: APP_STATE_URL_KEY, + stateContainer: { + ...appStateContainer, + ...{ + set: (value: AppState | null) => { + if (value) { + appStateContainer.set(value); + } + }, + }, + }, + stateStorage, + }, + ]); + + return { + globalState: globalStateContainer, + appState: appStateContainer, + startSync: start, + stopSync: stop, + setAppState: (newState: Partial) => { + const oldState = appStateContainer.getState(); + const mergedState = { ...oldState, ...newState }; + + if (!isEqualState(oldState, mergedState)) { + appStateContainer.set(mergedState); + } + }, + getFilters: () => [ + ...getFilters(globalStateContainer.getState()), + ...getFilters(appStateContainer.getState()), + ], + setFilters: (filterManager: FilterManager) => { + // global state filters + const globalFilters = filterManager.getGlobalFilters(); + const globalFilterChanged = !isEqualFilters( + globalFilters, + getFilters(globalStateContainer.getState()) + ); + if (globalFilterChanged) { + globalStateContainer.set({ filters: globalFilters }); + } + // app state filters + const appFilters = filterManager.getAppFilters(); + const appFilterChanged = !isEqualFilters( + appFilters, + getFilters(appStateContainer.getState()) + ); + if (appFilterChanged) { + appStateContainer.set({ ...appStateContainer.getState(), ...{ filters: appFilters } }); + } + }, + // helper function just needed for testing + flushToUrl: () => stateStorage.flush(), + }; +} + +/** + * Helper function to compare 2 different filter states + */ +export function isEqualFilters(filtersA: Filter[], filtersB: Filter[]) { + if (!filtersA && !filtersB) { + return true; + } else if (!filtersA || !filtersB) { + return false; + } + return esFilters.compareFilters(filtersA, filtersB, esFilters.COMPARE_ALL_OPTIONS); +} + +/** + * Helper function to compare 2 different states, is needed since comparing filters + * works differently, doesn't work with _.isEqual + */ +function isEqualState(stateA: AppState | GlobalState, stateB: AppState | GlobalState) { + if (!stateA && !stateB) { + return true; + } else if (!stateA || !stateB) { + return false; + } + const { filters: stateAFilters = [], ...stateAPartial } = stateA; + const { filters: stateBFilters = [], ...stateBPartial } = stateB; + return ( + _.isEqual(stateAPartial, stateBPartial) && + esFilters.compareFilters(stateAFilters, stateBFilters, esFilters.COMPARE_ALL_OPTIONS) + ); +} + +/** + * Helper function to return array of filter object of a given state + */ +function getFilters(state: AppState | GlobalState): Filter[] { + if (!state || !Array.isArray(state.filters)) { + return []; + } + return state.filters; +} + +/** + * Helper function to return the initial app state, which is a merged object of url state and + * default state. The default size is the default number of successor/predecessor records to fetch + */ +function createInitialAppState( + defaultSize: string, + timeFieldName: string, + urlState: AppState +): AppState { + const defaultState = { + columns: ['_source'], + filters: [], + predecessorCount: parseInt(defaultSize, 10), + sort: [timeFieldName, 'desc'], + successorCount: parseInt(defaultSize, 10), + }; + if (typeof urlState !== 'object') { + return defaultState; + } + + return { + ...defaultState, + ...urlState, + }; +} From ad5daba2eaf6800a0c67d658f62877706033d797 Mon Sep 17 00:00:00 2001 From: patrykkopycinski Date: Tue, 18 Feb 2020 14:53:31 +0100 Subject: [PATCH 07/12] [SIEM] Replace AutoSizer with use-resize-observer (#56588) --- renovate.json5 | 8 + .../auto_sizer/__examples__/index.stories.tsx | 27 - .../public/components/auto_sizer/index.tsx | 182 ---- .../components/charts/areachart.test.tsx | 4 +- .../public/components/charts/areachart.tsx | 36 +- .../components/charts/barchart.test.tsx | 6 +- .../public/components/charts/barchart.tsx | 39 +- .../events_viewer/events_viewer.test.tsx | 5 + .../events_viewer/events_viewer.tsx | 212 +++-- .../components/events_viewer/index.test.tsx | 5 + .../__snapshots__/timeline.test.tsx.snap | 793 +++++++++++++++++- .../components/timeline/timeline.test.tsx | 5 + .../public/components/timeline/timeline.tsx | 192 ++--- .../plugins/siem/public/pages/home/index.tsx | 171 ++-- .../pages/hosts/details/details_tabs.test.tsx | 5 + x-pack/package.json | 2 + yarn.lock | 14 + 17 files changed, 1144 insertions(+), 562 deletions(-) delete mode 100644 x-pack/legacy/plugins/siem/public/components/auto_sizer/__examples__/index.stories.tsx delete mode 100644 x-pack/legacy/plugins/siem/public/components/auto_sizer/index.tsx diff --git a/renovate.json5 b/renovate.json5 index 642c4a98b57998..58a64a5d0f9679 100644 --- a/renovate.json5 +++ b/renovate.json5 @@ -913,6 +913,14 @@ '@types/tslib', ], }, + { + groupSlug: 'use-resize-observer', + groupName: 'use-resize-observer related packages', + packageNames: [ + 'use-resize-observer', + '@types/use-resize-observer', + ], + }, { groupSlug: 'uuid', groupName: 'uuid related packages', diff --git a/x-pack/legacy/plugins/siem/public/components/auto_sizer/__examples__/index.stories.tsx b/x-pack/legacy/plugins/siem/public/components/auto_sizer/__examples__/index.stories.tsx deleted file mode 100644 index 414cea0d3f40d6..00000000000000 --- a/x-pack/legacy/plugins/siem/public/components/auto_sizer/__examples__/index.stories.tsx +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -import { storiesOf } from '@storybook/react'; -import React from 'react'; -import { AutoSizer } from '..'; - -storiesOf('components/AutoSizer', module).add('example', () => ( -
- - {({ measureRef, content }) => ( -
-
- {'width: '} - {content.width} -
-
- {'height: '} - {content.height} -
-
- )} -
-
-)); diff --git a/x-pack/legacy/plugins/siem/public/components/auto_sizer/index.tsx b/x-pack/legacy/plugins/siem/public/components/auto_sizer/index.tsx deleted file mode 100644 index 8b3a85b28b8fe3..00000000000000 --- a/x-pack/legacy/plugins/siem/public/components/auto_sizer/index.tsx +++ /dev/null @@ -1,182 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import isEqual from 'lodash/fp/isEqual'; -import React from 'react'; -import ResizeObserver from 'resize-observer-polyfill'; - -interface Measurement { - width?: number; - height?: number; -} - -interface Measurements { - bounds: Measurement; - content: Measurement; - windowMeasurement: Measurement; -} - -interface AutoSizerProps { - detectAnyWindowResize?: boolean; - bounds?: boolean; - content?: boolean; - onResize?: (size: Measurements) => void; - children: ( - // eslint-disable-next-line @typescript-eslint/no-explicit-any - args: { measureRef: (instance: HTMLElement | null) => any } & Measurements - ) => React.ReactNode; -} - -interface AutoSizerState { - boundsMeasurement: Measurement; - contentMeasurement: Measurement; - windowMeasurement: Measurement; -} - -/** A hard-fork of the `infra` `AutoSizer` ಠ_ಠ */ -export class AutoSizer extends React.PureComponent { - public element: HTMLElement | null = null; - public resizeObserver: ResizeObserver | null = null; - public windowWidth: number = -1; - - public readonly state = { - boundsMeasurement: { - height: void 0, - width: void 0, - }, - contentMeasurement: { - height: void 0, - width: void 0, - }, - windowMeasurement: { - height: void 0, - width: void 0, - }, - }; - - constructor(props: AutoSizerProps) { - super(props); - if (this.props.detectAnyWindowResize) { - window.addEventListener('resize', this.updateMeasurement); - } - this.resizeObserver = new ResizeObserver(entries => { - entries.forEach(entry => { - if (entry.target === this.element) { - this.measure(entry); - } - }); - }); - } - - public componentWillUnmount() { - if (this.resizeObserver) { - this.resizeObserver.disconnect(); - this.resizeObserver = null; - } - if (this.props.detectAnyWindowResize) { - window.removeEventListener('resize', this.updateMeasurement); - } - } - - public measure = (entry: ResizeObserverEntry | null) => { - if (!this.element) { - return; - } - - const { content = true, bounds = false } = this.props; - const { - boundsMeasurement: previousBoundsMeasurement, - contentMeasurement: previousContentMeasurement, - windowMeasurement: previousWindowMeasurement, - } = this.state; - - const boundsRect = bounds ? this.element.getBoundingClientRect() : null; - const boundsMeasurement = boundsRect - ? { - height: this.element.getBoundingClientRect().height, - width: this.element.getBoundingClientRect().width, - } - : previousBoundsMeasurement; - const windowMeasurement: Measurement = { - width: window.innerWidth, - height: window.innerHeight, - }; - - if ( - this.props.detectAnyWindowResize && - boundsMeasurement && - boundsMeasurement.width && - this.windowWidth !== -1 && - this.windowWidth > window.innerWidth - ) { - const gap = this.windowWidth - window.innerWidth; - boundsMeasurement.width = boundsMeasurement.width - gap; - } - this.windowWidth = window.innerWidth; - const contentRect = content && entry ? entry.contentRect : null; - const contentMeasurement = - contentRect && entry - ? { - height: entry.contentRect.height, - width: entry.contentRect.width, - } - : previousContentMeasurement; - - if ( - isEqual(boundsMeasurement, previousBoundsMeasurement) && - isEqual(contentMeasurement, previousContentMeasurement) && - isEqual(windowMeasurement, previousWindowMeasurement) - ) { - return; - } - - requestAnimationFrame(() => { - if (!this.resizeObserver) { - return; - } - - this.setState({ boundsMeasurement, contentMeasurement, windowMeasurement }); - - if (this.props.onResize) { - this.props.onResize({ - bounds: boundsMeasurement, - content: contentMeasurement, - windowMeasurement, - }); - } - }); - }; - - public render() { - const { children } = this.props; - const { boundsMeasurement, contentMeasurement, windowMeasurement } = this.state; - - return children({ - bounds: boundsMeasurement, - content: contentMeasurement, - windowMeasurement, - measureRef: this.storeRef, - }); - } - - private updateMeasurement = () => { - window.setTimeout(() => { - this.measure(null); - }, 0); - }; - - private storeRef = (element: HTMLElement | null) => { - if (this.element && this.resizeObserver) { - this.resizeObserver.unobserve(this.element); - } - - if (element && this.resizeObserver) { - this.resizeObserver.observe(element); - } - - this.element = element; - }; -} diff --git a/x-pack/legacy/plugins/siem/public/components/charts/areachart.test.tsx b/x-pack/legacy/plugins/siem/public/components/charts/areachart.test.tsx index 342d7d35f9cb72..27f0222b96b77f 100644 --- a/x-pack/legacy/plugins/siem/public/components/charts/areachart.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/charts/areachart.test.tsx @@ -331,7 +331,7 @@ describe('AreaChart', () => { }); it(`should render area chart`, () => { - expect(shallowWrapper.find('AutoSizer')).toHaveLength(1); + expect(shallowWrapper.find('WrappedByAutoSizer')).toHaveLength(1); expect(shallowWrapper.find('ChartPlaceHolder')).toHaveLength(0); }); }); @@ -344,7 +344,7 @@ describe('AreaChart', () => { }); it(`should render a chart place holder`, () => { - expect(shallowWrapper.find('AutoSizer')).toHaveLength(0); + expect(shallowWrapper.find('WrappedByAutoSizer')).toHaveLength(0); expect(shallowWrapper.find('ChartPlaceHolder')).toHaveLength(1); }); } diff --git a/x-pack/legacy/plugins/siem/public/components/charts/areachart.tsx b/x-pack/legacy/plugins/siem/public/components/charts/areachart.tsx index 57f78080abc605..fd05b80e412354 100644 --- a/x-pack/legacy/plugins/siem/public/components/charts/areachart.tsx +++ b/x-pack/legacy/plugins/siem/public/components/charts/areachart.tsx @@ -16,7 +16,8 @@ import { RecursivePartial, } from '@elastic/charts'; import { getOr, get, isNull, isNumber } from 'lodash/fp'; -import { AutoSizer } from '../auto_sizer'; +import useResizeObserver from 'use-resize-observer'; + import { ChartPlaceHolder } from './chart_place_holder'; import { useTimeZone } from '../../lib/kibana'; import { @@ -124,35 +125,24 @@ export const AreaChartBase = React.memo(AreaChartBaseComponent); AreaChartBase.displayName = 'AreaChartBase'; -export const AreaChartComponent = ({ - areaChart, - configs, -}: { +interface AreaChartComponentProps { areaChart: ChartSeriesData[] | null | undefined; configs?: ChartSeriesConfigs | undefined; -}) => { +} + +export const AreaChartComponent: React.FC = ({ areaChart, configs }) => { + const { ref: measureRef, width, height } = useResizeObserver({}); const customHeight = get('customHeight', configs); const customWidth = get('customWidth', configs); + const chartHeight = getChartHeight(customHeight, height); + const chartWidth = getChartWidth(customWidth, width); return checkIfAnyValidSeriesExist(areaChart) ? ( - - {({ measureRef, content: { height, width } }) => ( - - - - )} - + + + ) : ( - + ); }; diff --git a/x-pack/legacy/plugins/siem/public/components/charts/barchart.test.tsx b/x-pack/legacy/plugins/siem/public/components/charts/barchart.test.tsx index d8e5079dd72a66..0b6635b04d3808 100644 --- a/x-pack/legacy/plugins/siem/public/components/charts/barchart.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/charts/barchart.test.tsx @@ -278,7 +278,7 @@ describe.each(chartDataSets)('BarChart with valid data [%o]', data => { }); it(`should render chart`, () => { - expect(shallowWrapper.find('AutoSizer')).toHaveLength(1); + expect(shallowWrapper.find('WrappedByAutoSizer')).toHaveLength(1); expect(shallowWrapper.find('ChartPlaceHolder')).toHaveLength(0); }); }); @@ -290,8 +290,8 @@ describe.each(chartHolderDataSets)('BarChart with invalid data [%o]', data => { shallowWrapper = shallow(); }); - it(`should render chart holder`, () => { - expect(shallowWrapper.find('AutoSizer')).toHaveLength(0); + it(`should render a ChartPlaceHolder`, () => { + expect(shallowWrapper.find('WrappedByAutoSizer')).toHaveLength(0); expect(shallowWrapper.find('ChartPlaceHolder')).toHaveLength(1); }); }); diff --git a/x-pack/legacy/plugins/siem/public/components/charts/barchart.tsx b/x-pack/legacy/plugins/siem/public/components/charts/barchart.tsx index d9dd302dae7244..1355926d343dfa 100644 --- a/x-pack/legacy/plugins/siem/public/components/charts/barchart.tsx +++ b/x-pack/legacy/plugins/siem/public/components/charts/barchart.tsx @@ -8,9 +8,9 @@ import React from 'react'; import { Chart, BarSeries, Axis, Position, ScaleType, Settings } from '@elastic/charts'; import { getOr, get, isNumber } from 'lodash/fp'; import deepmerge from 'deepmerge'; +import useResizeObserver from 'use-resize-observer'; import { useTimeZone } from '../../lib/kibana'; -import { AutoSizer } from '../auto_sizer'; import { ChartPlaceHolder } from './chart_place_holder'; import { chartDefaultSettings, @@ -99,40 +99,25 @@ export const BarChartBase = React.memo(BarChartBaseComponent); BarChartBase.displayName = 'BarChartBase'; -export const BarChartComponent = ({ - barChart, - configs, -}: { +interface BarChartComponentProps { barChart: ChartSeriesData[] | null | undefined; configs?: ChartSeriesConfigs | undefined; -}) => { +} + +export const BarChartComponent: React.FC = ({ barChart, configs }) => { + const { ref: measureRef, width, height } = useResizeObserver({}); const customHeight = get('customHeight', configs); const customWidth = get('customWidth', configs); + const chartHeight = getChartHeight(customHeight, height); + const chartWidth = getChartWidth(customWidth, width); return checkIfAnyValidSeriesExist(barChart) ? ( - - {({ measureRef, content: { height, width } }) => ( - - - - )} - + + + ) : ( - + ); }; -BarChartComponent.displayName = 'BarChartComponent'; - export const BarChart = React.memo(BarChartComponent); - -BarChart.displayName = 'BarChart'; diff --git a/x-pack/legacy/plugins/siem/public/components/events_viewer/events_viewer.test.tsx b/x-pack/legacy/plugins/siem/public/components/events_viewer/events_viewer.test.tsx index 3cef3e98c2f0a7..8c4228b597dbb4 100644 --- a/x-pack/legacy/plugins/siem/public/components/events_viewer/events_viewer.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/events_viewer/events_viewer.test.tsx @@ -6,6 +6,7 @@ import React from 'react'; import { MockedProvider } from 'react-apollo/test-utils'; +import useResizeObserver from 'use-resize-observer'; import { mockIndexPattern, TestProviders } from '../../mock'; import { wait } from '../../lib/helpers'; @@ -27,6 +28,10 @@ mockUseFetchIndexPatterns.mockImplementation(() => [ }, ]); +const mockUseResizeObserver: jest.Mock = useResizeObserver as jest.Mock; +jest.mock('use-resize-observer'); +mockUseResizeObserver.mockImplementation(() => ({})); + const from = 1566943856794; const to = 1566857456791; diff --git a/x-pack/legacy/plugins/siem/public/components/events_viewer/events_viewer.tsx b/x-pack/legacy/plugins/siem/public/components/events_viewer/events_viewer.tsx index 14473605a7c889..cbce1f635310a6 100644 --- a/x-pack/legacy/plugins/siem/public/components/events_viewer/events_viewer.tsx +++ b/x-pack/legacy/plugins/siem/public/components/events_viewer/events_viewer.tsx @@ -9,13 +9,13 @@ import deepEqual from 'fast-deep-equal'; import { getOr, isEmpty, isEqual, union } from 'lodash/fp'; import React, { useMemo } from 'react'; import styled from 'styled-components'; +import useResizeObserver from 'use-resize-observer'; import { BrowserFields } from '../../containers/source'; import { TimelineQuery } from '../../containers/timeline'; import { Direction } from '../../graphql/types'; import { useKibana } from '../../lib/kibana'; import { KqlMode } from '../../store/timeline/model'; -import { AutoSizer } from '../auto_sizer'; import { HeaderSection } from '../header_section'; import { ColumnHeader } from '../timeline/body/column_headers/column_header'; import { defaultHeaders } from '../timeline/body/column_headers/default_headers'; @@ -95,6 +95,7 @@ const EventsViewerComponent: React.FC = ({ toggleColumn, utilityBar, }) => { + const { ref: measureRef, width = 0 } = useResizeObserver({}); const columnsHeader = isEmpty(columns) ? defaultHeaders : columns; const kibana = useKibana(); const combinedQueries = combineQueries({ @@ -120,115 +121,108 @@ const EventsViewerComponent: React.FC = ({ return ( - - {({ measureRef, content: { width = 0 } }) => ( - <> - -
- - - {combinedQueries != null ? ( - - {({ - events, - getUpdatedAt, - inspect, - loading, - loadMore, - pageInfo, - refetch, - totalCount = 0, - }) => { - const totalCountMinusDeleted = - totalCount > 0 ? totalCount - deletedEventIds.length : 0; - - const subtitle = `${ - i18n.SHOWING - }: ${totalCountMinusDeleted.toLocaleString()} ${timelineTypeContext.unit?.( - totalCountMinusDeleted - ) ?? i18n.UNIT(totalCountMinusDeleted)}`; - - // TODO: Reset eventDeletedIds/eventLoadingIds on refresh/loadmore (getUpdatedAt) - return ( - <> - + +
+ + + {combinedQueries != null ? ( + + {({ + events, + getUpdatedAt, + inspect, + loading, + loadMore, + pageInfo, + refetch, + totalCount = 0, + }) => { + const totalCountMinusDeleted = + totalCount > 0 ? totalCount - deletedEventIds.length : 0; + + const subtitle = `${ + i18n.SHOWING + }: ${totalCountMinusDeleted.toLocaleString()} ${timelineTypeContext.unit?.( + totalCountMinusDeleted + ) ?? i18n.UNIT(totalCountMinusDeleted)}`; + + // TODO: Reset eventDeletedIds/eventLoadingIds on refresh/loadmore (getUpdatedAt) + return ( + <> + + {headerFilterGroup} + + + {utilityBar?.(refetch, totalCountMinusDeleted)} + +
+ + - {headerFilterGroup} - - - {utilityBar?.(refetch, totalCountMinusDeleted)} - -
- - - - !deletedEventIds.includes(e._id))} - id={id} - isEventViewer={true} - height={height} - sort={sort} - toggleColumn={toggleColumn} - /> - -
- -
- - ); - }} - - ) : null} - - )} - + inputId="global" + inspect={inspect} + loading={loading} + refetch={refetch} + /> + + !deletedEventIds.includes(e._id))} + id={id} + isEventViewer={true} + height={height} + sort={sort} + toggleColumn={toggleColumn} + /> + +
+ +
+ + ); + }} +
+ ) : null} + ); }; diff --git a/x-pack/legacy/plugins/siem/public/components/events_viewer/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/events_viewer/index.test.tsx index ec8d329f1dfe3e..2bedd1cb89b41c 100644 --- a/x-pack/legacy/plugins/siem/public/components/events_viewer/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/events_viewer/index.test.tsx @@ -6,6 +6,7 @@ import React from 'react'; import { MockedProvider } from 'react-apollo/test-utils'; +import useResizeObserver from 'use-resize-observer'; import { wait } from '../../lib/helpers'; import { mockIndexPattern, TestProviders } from '../../mock'; @@ -26,6 +27,10 @@ mockUseFetchIndexPatterns.mockImplementation(() => [ }, ]); +const mockUseResizeObserver: jest.Mock = useResizeObserver as jest.Mock; +jest.mock('use-resize-observer'); +mockUseResizeObserver.mockImplementation(() => ({})); + const from = 1566943856794; const to = 1566857456791; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/__snapshots__/timeline.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/timeline/__snapshots__/timeline.test.tsx.snap index 67d266d1cbf398..3fcd258b79147c 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/__snapshots__/timeline.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/timeline/__snapshots__/timeline.test.tsx.snap @@ -1,10 +1,793 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Timeline rendering renders correctly against snapshot 1`] = ` - - - + + + + + + + + `; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/timeline.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/timeline.test.tsx index f7c0d0b4757347..78899b7c5d6289 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/timeline.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/timeline.test.tsx @@ -7,6 +7,7 @@ import { shallow } from 'enzyme'; import React from 'react'; import { MockedProvider } from 'react-apollo/test-utils'; +import useResizeObserver from 'use-resize-observer'; import { timelineQuery } from '../../containers/timeline/index.gql_query'; import { mockBrowserFields } from '../../containers/source/mock'; @@ -29,6 +30,10 @@ const testFlyoutHeight = 980; jest.mock('../../lib/kibana'); +const mockUseResizeObserver: jest.Mock = useResizeObserver as jest.Mock; +jest.mock('use-resize-observer'); +mockUseResizeObserver.mockImplementation(() => ({})); + describe('Timeline', () => { const sort: Sort = { columnId: '@timestamp', diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/timeline.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/timeline.tsx index 09457c8f0285af..4b7331ab14c7e8 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/timeline.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/timeline.tsx @@ -8,13 +8,13 @@ import { EuiFlexGroup } from '@elastic/eui'; import { getOr, isEmpty } from 'lodash/fp'; import React from 'react'; import styled from 'styled-components'; +import useResizeObserver from 'use-resize-observer'; import { BrowserFields } from '../../containers/source'; import { TimelineQuery } from '../../containers/timeline'; import { Direction } from '../../graphql/types'; import { useKibana } from '../../lib/kibana'; import { KqlMode, EventType } from '../../store/timeline/model'; -import { AutoSizer } from '../auto_sizer'; import { ColumnHeader } from './body/column_headers/column_header'; import { defaultHeaders } from './body/column_headers/default_headers'; import { Sort } from './body/sort'; @@ -88,7 +88,7 @@ interface Props { } /** The parent Timeline component */ -export const TimelineComponent = ({ +export const TimelineComponent: React.FC = ({ browserFields, columns, dataProviders, @@ -118,7 +118,10 @@ export const TimelineComponent = ({ start, sort, toggleColumn, -}: Props) => { +}) => { + const { ref: measureRef, width = 0, height: timelineHeaderHeight = 0 } = useResizeObserver< + HTMLDivElement + >({}); const kibana = useKibana(); const combinedQueries = combineQueries({ config: esQuery.getEsQueryConfig(kibana.services.uiSettings), @@ -132,101 +135,98 @@ export const TimelineComponent = ({ end, }); const columnsHeader = isEmpty(columns) ? defaultHeaders : columns; + return ( - - {({ measureRef, content: { height: timelineHeaderHeight = 0, width = 0 } }) => ( - + }> + + + + {combinedQueries != null ? ( + c.id)} + sourceId="default" + limit={itemsPerPage} + filterQuery={combinedQueries.filterQuery} + sortField={{ + sortFieldId: sort.columnId, + direction: sort.sortDirection as Direction, + }} > - - - - - {combinedQueries != null ? ( - c.id)} - sourceId="default" - limit={itemsPerPage} - filterQuery={combinedQueries.filterQuery} - sortField={{ - sortFieldId: sort.columnId, - direction: sort.sortDirection as Direction, - }} - > - {({ - events, - inspect, - loading, - totalCount, - pageInfo, - loadMore, - getUpdatedAt, - refetch, - }) => ( - - - -