From 55a73616ab706b4588e4c681a1369c2f85c1f907 Mon Sep 17 00:00:00 2001 From: Aaron Caldwell Date: Thu, 18 Jun 2020 12:56:51 -0600 Subject: [PATCH 1/6] [Maps] Migrate maps client router to react (#65079) Co-authored-by: Elastic Machine --- .../sidebar/lib/visualize_url_utils.ts | 2 +- test/functional/services/common/find.ts | 21 + x-pack/index.js | 2 - x-pack/legacy/plugins/maps/index.js | 41 -- .../maps/public/angular/map_controller.js | 686 ------------------ x-pack/legacy/plugins/maps/public/index.scss | 3 - x-pack/legacy/plugins/maps/public/index.ts | 34 - x-pack/legacy/plugins/maps/public/legacy.ts | 22 - x-pack/legacy/plugins/maps/public/plugin.ts | 70 -- x-pack/legacy/plugins/maps/public/routes.js | 108 --- .../functions/common/saved_map.ts | 3 +- .../input_type_to_expression/map.test.ts | 2 +- .../input_type_to_expression/map.ts | 2 +- x-pack/plugins/maps/common/constants.ts | 2 +- .../public/angular/listing_ng_wrapper.html | 6 - x-pack/plugins/maps/public/angular/map.html | 35 - .../es_search_source/load_index_settings.js | 3 +- .../connected_components/gis_map/view.js | 2 +- .../maps/public/embeddable/map_embeddable.tsx | 1 - x-pack/plugins/maps/public/index.ts | 2 - .../maps/public/lazy_load_bundle/index.ts | 7 +- .../public/lazy_load_bundle/lazy/index.ts | 8 +- .../maps/public/maps_vis_type_alias.js | 2 +- x-pack/plugins/maps/public/plugin.ts | 33 +- x-pack/plugins/maps/public/reducers/store.js | 13 +- .../bootstrap}/get_initial_layers.d.ts | 2 +- .../bootstrap}/get_initial_layers.js | 27 +- .../bootstrap}/get_initial_layers.test.js | 18 +- .../bootstrap}/get_initial_query.js | 4 +- .../bootstrap}/get_initial_refresh_config.js | 4 +- .../bootstrap}/get_initial_time_filters.js | 4 +- .../services/gis_map_saved_object_loader.js | 4 +- .../bootstrap}/services/saved_gis_map.js | 20 +- .../maps/public/routing/maps_router.js | 62 ++ .../routing/page_elements/breadcrumbs.js | 60 ++ .../page_elements/top_nav_menu/index.js | 44 ++ .../top_nav_menu/top_nav_menu.js | 279 +++++++ .../routes/list/load_list_and_render.js | 57 ++ .../routes/list/maps_list_view.js} | 71 +- .../public/routing/routes/maps_app/index.js | 67 ++ .../routes/maps_app/load_map_and_render.js | 58 ++ .../routing/routes/maps_app/maps_app_view.js | 476 ++++++++++++ .../state_syncing/app_state_manager.js | 44 ++ .../public/routing/state_syncing/app_sync.js | 61 ++ .../routing/state_syncing/global_sync.ts | 32 + .../maps/public/routing/store_operations.js | 11 + .../maps/server/tutorials/ems/index.ts | 3 +- .../components/embeddables/embedded_map.tsx | 3 +- .../embeddables/embedded_map_helpers.tsx | 8 +- .../line_tool_tip_content.test.tsx | 6 +- .../map_tool_tip/line_tool_tip_content.tsx | 3 +- .../embeddables/map_tool_tip/map_tool_tip.tsx | 3 +- .../point_tool_tip_content.test.tsx | 6 +- .../map_tool_tip/point_tool_tip_content.tsx | 3 +- .../network/components/embeddables/types.ts | 3 +- .../translations/translations/ja-JP.json | 4 - .../translations/translations/zh-CN.json | 4 - .../location_map/embeddables/embedded_map.tsx | 6 +- .../location_map/embeddables/map_tool_tip.tsx | 3 +- .../apps/maps/saved_object_management.js | 4 +- .../test/functional/page_objects/gis_page.js | 9 +- 61 files changed, 1457 insertions(+), 1126 deletions(-) delete mode 100644 x-pack/legacy/plugins/maps/index.js delete mode 100644 x-pack/legacy/plugins/maps/public/angular/map_controller.js delete mode 100644 x-pack/legacy/plugins/maps/public/index.scss delete mode 100644 x-pack/legacy/plugins/maps/public/index.ts delete mode 100644 x-pack/legacy/plugins/maps/public/legacy.ts delete mode 100644 x-pack/legacy/plugins/maps/public/plugin.ts delete mode 100644 x-pack/legacy/plugins/maps/public/routes.js delete mode 100644 x-pack/plugins/maps/public/angular/listing_ng_wrapper.html delete mode 100644 x-pack/plugins/maps/public/angular/map.html rename x-pack/plugins/maps/public/{angular => routing/bootstrap}/get_initial_layers.d.ts (84%) rename x-pack/plugins/maps/public/{angular => routing/bootstrap}/get_initial_layers.js (57%) rename x-pack/plugins/maps/public/{angular => routing/bootstrap}/get_initial_layers.test.js (82%) rename x-pack/plugins/maps/public/{angular => routing/bootstrap}/get_initial_query.js (84%) rename x-pack/plugins/maps/public/{angular => routing/bootstrap}/get_initial_refresh_config.js (86%) rename x-pack/plugins/maps/public/{angular => routing/bootstrap}/get_initial_time_filters.js (90%) rename x-pack/plugins/maps/public/{angular => routing/bootstrap}/services/gis_map_saved_object_loader.js (87%) rename x-pack/plugins/maps/public/{angular => routing/bootstrap}/services/saved_gis_map.js (82%) create mode 100644 x-pack/plugins/maps/public/routing/maps_router.js create mode 100644 x-pack/plugins/maps/public/routing/page_elements/breadcrumbs.js create mode 100644 x-pack/plugins/maps/public/routing/page_elements/top_nav_menu/index.js create mode 100644 x-pack/plugins/maps/public/routing/page_elements/top_nav_menu/top_nav_menu.js create mode 100644 x-pack/plugins/maps/public/routing/routes/list/load_list_and_render.js rename x-pack/plugins/maps/public/{components/map_listing.js => routing/routes/list/maps_list_view.js} (86%) create mode 100644 x-pack/plugins/maps/public/routing/routes/maps_app/index.js create mode 100644 x-pack/plugins/maps/public/routing/routes/maps_app/load_map_and_render.js create mode 100644 x-pack/plugins/maps/public/routing/routes/maps_app/maps_app_view.js create mode 100644 x-pack/plugins/maps/public/routing/state_syncing/app_state_manager.js create mode 100644 x-pack/plugins/maps/public/routing/state_syncing/app_sync.js create mode 100644 x-pack/plugins/maps/public/routing/state_syncing/global_sync.ts create mode 100644 x-pack/plugins/maps/public/routing/store_operations.js diff --git a/src/plugins/discover/public/application/components/sidebar/lib/visualize_url_utils.ts b/src/plugins/discover/public/application/components/sidebar/lib/visualize_url_utils.ts index d585c5d6f26382..d598f28a0ad120 100644 --- a/src/plugins/discover/public/application/components/sidebar/lib/visualize_url_utils.ts +++ b/src/plugins/discover/public/application/components/sidebar/lib/visualize_url_utils.ts @@ -104,7 +104,7 @@ export function getMapsAppUrl( return { app: 'maps', - path: `#/map?${mapAppParams.toString()}`, + path: `/map#?${mapAppParams.toString()}`, }; } diff --git a/test/functional/services/common/find.ts b/test/functional/services/common/find.ts index 876fb7369feac4..88357814357d48 100644 --- a/test/functional/services/common/find.ts +++ b/test/functional/services/common/find.ts @@ -162,6 +162,27 @@ export async function FindProvider({ getService }: FtrProviderContext) { return wrapAll(elements); } + public async allByButtonText( + buttonText: string, + element: WebDriver | WebElement | WebElementWrapper = driver, + timeout: number = defaultFindTimeout + ): Promise { + log.debug(`Find.byButtonText('${buttonText}') with timeout=${timeout}`); + return await retry.tryForTime(timeout, async () => { + // tslint:disable-next-line:variable-name + const _element = element instanceof WebElementWrapper ? element._webElement : element; + await this._withTimeout(0); + const allButtons = wrapAll(await _element.findElements(By.tagName('button'))); + await this._withTimeout(defaultFindTimeout); + const buttonTexts = await Promise.all( + allButtons.map(async (el) => { + return el.getVisibleText(); + }) + ); + return buttonTexts.filter((text) => text.trim() === buttonText.trim()); + }); + } + public async allByCssSelector( selector: string, timeout: number = defaultFindTimeout diff --git a/x-pack/index.js b/x-pack/index.js index 8096774d7a4915..e7dd4886e60526 100644 --- a/x-pack/index.js +++ b/x-pack/index.js @@ -9,7 +9,6 @@ import { monitoring } from './legacy/plugins/monitoring'; import { security } from './legacy/plugins/security'; import { dashboardMode } from './legacy/plugins/dashboard_mode'; import { beats } from './legacy/plugins/beats_management'; -import { maps } from './legacy/plugins/maps'; import { spaces } from './legacy/plugins/spaces'; import { ingestManager } from './legacy/plugins/ingest_manager'; @@ -21,7 +20,6 @@ module.exports = function (kibana) { security(kibana), dashboardMode(kibana), beats(kibana), - maps(kibana), ingestManager(kibana), ]; }; diff --git a/x-pack/legacy/plugins/maps/index.js b/x-pack/legacy/plugins/maps/index.js deleted file mode 100644 index d1354ef013a961..00000000000000 --- a/x-pack/legacy/plugins/maps/index.js +++ /dev/null @@ -1,41 +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 { i18n } from '@kbn/i18n'; -import { resolve } from 'path'; -import { getAppTitle } from '../../../plugins/maps/common/i18n_getters'; -import { APP_ID, APP_ICON } from '../../../plugins/maps/common/constants'; -import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/server'; - -export function maps(kibana) { - return new kibana.Plugin({ - require: ['kibana', 'elasticsearch'], - id: APP_ID, - configPrefix: 'xpack.maps', - publicDir: resolve(__dirname, 'public'), - config(Joi) { - return Joi.object({ - enabled: Joi.boolean().default(true), - }) - .unknown() - .default(); - }, - uiExports: { - app: { - title: getAppTitle(), - description: i18n.translate('xpack.maps.appDescription', { - defaultMessage: 'Map application', - }), - main: 'plugins/maps/legacy', - icon: 'plugins/maps/icon.svg', - euiIconType: APP_ICON, - category: DEFAULT_APP_CATEGORIES.kibana, - order: 4000, - }, - styleSheetPaths: `${__dirname}/public/index.scss`, - }, - }); -} diff --git a/x-pack/legacy/plugins/maps/public/angular/map_controller.js b/x-pack/legacy/plugins/maps/public/angular/map_controller.js deleted file mode 100644 index 70d5195feef423..00000000000000 --- a/x-pack/legacy/plugins/maps/public/angular/map_controller.js +++ /dev/null @@ -1,686 +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 _ from 'lodash'; -import rison from 'rison-node'; -import 'ui/directives/listen'; -import 'ui/directives/storage'; -import React from 'react'; -import { I18nProvider } from '@kbn/i18n/react'; -import { i18n } from '@kbn/i18n'; -import { render, unmountComponentAtNode } from 'react-dom'; -import { uiModules } from 'ui/modules'; -import { - getTimeFilter, - getIndexPatternService, - getInspector, - getNavigation, - getData, - getCoreI18n, - getCoreChrome, - getMapsCapabilities, - getToasts, - // eslint-disable-next-line @kbn/eslint/no-restricted-paths -} from '../../../../../plugins/maps/public/kibana_services'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { createMapStore } from '../../../../../plugins/maps/public/reducers/store'; -import { Provider } from 'react-redux'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { GisMap } from '../../../../../plugins/maps/public/connected_components/gis_map'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { addHelpMenuToAppChrome } from '../../../../../plugins/maps/public/help_menu_util'; -import { - setSelectedLayer, - setRefreshConfig, - setGotoWithCenter, - replaceLayerList, - setQuery, - setMapSettings, - enableFullScreen, - updateFlyout, - setReadOnly, - setIsLayerTOCOpen, - setOpenTOCDetails, - openMapSettings, - // eslint-disable-next-line @kbn/eslint/no-restricted-paths -} from '../../../../../plugins/maps/public/actions'; -import { - DEFAULT_IS_LAYER_TOC_OPEN, - FLYOUT_STATE, - // eslint-disable-next-line @kbn/eslint/no-restricted-paths -} from '../../../../../plugins/maps/public/reducers/ui'; -import { - getIsFullScreen, - getFlyoutDisplay, - // eslint-disable-next-line @kbn/eslint/no-restricted-paths -} from '../../../../../plugins/maps/public/selectors/ui_selectors'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { copyPersistentState } from '../../../../../plugins/maps/public/reducers/util'; -import { - getQueryableUniqueIndexPatternIds, - hasDirtyState, - getLayerListRaw, - // eslint-disable-next-line @kbn/eslint/no-restricted-paths -} from '../../../../../plugins/maps/public/selectors/map_selectors'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { getInspectorAdapters } from '../../../../../plugins/maps/public/reducers/non_serializable_instances'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { getInitialLayers } from '../../../../../plugins/maps/public/angular/get_initial_layers'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { getInitialQuery } from '../../../../../plugins/maps/public/angular/get_initial_query'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { getInitialTimeFilters } from '../../../../../plugins/maps/public/angular/get_initial_time_filters'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { getInitialRefreshConfig } from '../../../../../plugins/maps/public/angular/get_initial_refresh_config'; -import { MAP_SAVED_OBJECT_TYPE, MAP_APP_PATH } from '../../../../../plugins/maps/common/constants'; -import { npSetup, npStart } from 'ui/new_platform'; -import { esFilters } from '../../../../../../src/plugins/data/public'; -import { - SavedObjectSaveModal, - showSaveModal, -} from '../../../../../../src/plugins/saved_objects/public'; -import { loadKbnTopNavDirectives } from '../../../../../../src/plugins/kibana_legacy/public'; -import { - bindSetupCoreAndPlugins as bindNpSetupCoreAndPlugins, - bindStartCoreAndPlugins as bindNpStartCoreAndPlugins, -} from '../../../../../plugins/maps/public/plugin'; // eslint-disable-line @kbn/eslint/no-restricted-paths - -const REACT_ANCHOR_DOM_ELEMENT_ID = 'react-maps-root'; - -const app = uiModules.get(MAP_APP_PATH, []); - -// Init required services. Necessary while in legacy -const config = _.get(npSetup, 'plugins.maps.config', {}); -const kibanaVersion = npSetup.core.injectedMetadata.getKibanaVersion(); -bindNpSetupCoreAndPlugins(npSetup.core, npSetup.plugins, config, kibanaVersion); -bindNpStartCoreAndPlugins(npStart.core, npStart.plugins); - -loadKbnTopNavDirectives(getNavigation().ui); - -function getInitialLayersFromUrlParam() { - const locationSplit = window.location.href.split('?'); - if (locationSplit.length <= 1) { - return []; - } - const mapAppParams = new URLSearchParams(locationSplit[1]); - if (!mapAppParams.has('initialLayers')) { - return []; - } - - try { - return rison.decode_array(mapAppParams.get('initialLayers')); - } catch (e) { - getToasts().addWarning({ - title: i18n.translate('xpack.maps.initialLayers.unableToParseTitle', { - defaultMessage: `Inital layers not added to map`, - }), - text: i18n.translate('xpack.maps.initialLayers.unableToParseMessage', { - defaultMessage: `Unable to parse contents of 'initialLayers' parameter. Error: {errorMsg}`, - values: { errorMsg: e.message }, - }), - }); - return []; - } -} - -app.controller( - 'GisMapController', - ($scope, $route, kbnUrl, localStorage, AppState, globalState) => { - const savedQueryService = getData().query.savedQueries; - const { filterManager } = getData().query; - const savedMap = $route.current.locals.map; - $scope.screenTitle = savedMap.title; - let unsubscribe; - let initialLayerListConfig; - const $state = new AppState(); - const store = createMapStore(); - - function getAppStateFilters() { - return _.get($state, 'filters', []); - } - - const visibleSubscription = getCoreChrome() - .getIsVisible$() - .subscribe((isVisible) => { - $scope.$evalAsync(() => { - $scope.isVisible = isVisible; - }); - }); - - $scope.$listen(globalState, 'fetch_with_changes', (diff) => { - if (diff.includes('time') || diff.includes('filters')) { - onQueryChange({ - filters: [...globalState.filters, ...getAppStateFilters()], - time: globalState.time, - }); - } - if (diff.includes('refreshInterval')) { - $scope.onRefreshChange({ isPaused: globalState.pause, refreshInterval: globalState.value }); - } - }); - - $scope.$listen($state, 'fetch_with_changes', function (diff) { - if ((diff.includes('query') || diff.includes('filters')) && $state.query) { - onQueryChange({ - filters: [...globalState.filters, ...getAppStateFilters()], - query: $state.query, - }); - } - }); - - function syncAppAndGlobalState() { - $scope.$evalAsync(() => { - // appState - $state.query = $scope.query; - $state.filters = filterManager.getAppFilters(); - $state.save(); - - // globalState - globalState.time = $scope.time; - globalState.refreshInterval = { - pause: $scope.refreshConfig.isPaused, - value: $scope.refreshConfig.interval, - }; - globalState.filters = filterManager.getGlobalFilters(); - globalState.save(); - }); - } - - $scope.query = getInitialQuery({ - mapStateJSON: savedMap.mapStateJSON, - appState: $state, - userQueryLanguage: localStorage.get('kibana.userQueryLanguage'), - }); - $scope.time = getInitialTimeFilters({ - mapStateJSON: savedMap.mapStateJSON, - globalState: globalState, - }); - $scope.refreshConfig = getInitialRefreshConfig({ - mapStateJSON: savedMap.mapStateJSON, - globalState: globalState, - }); - - /* Saved Queries */ - $scope.showSaveQuery = getMapsCapabilities().saveQuery; - - $scope.$watch( - () => getMapsCapabilities().saveQuery, - (newCapability) => { - $scope.showSaveQuery = newCapability; - } - ); - - $scope.onQuerySaved = (savedQuery) => { - $scope.savedQuery = savedQuery; - }; - - $scope.onSavedQueryUpdated = (savedQuery) => { - $scope.savedQuery = { ...savedQuery }; - }; - - $scope.onClearSavedQuery = () => { - delete $scope.savedQuery; - delete $state.savedQuery; - onQueryChange({ - filters: filterManager.getGlobalFilters(), - query: { - query: '', - language: localStorage.get('kibana.userQueryLanguage'), - }, - }); - }; - - function updateStateFromSavedQuery(savedQuery) { - const savedQueryFilters = savedQuery.attributes.filters || []; - const globalFilters = filterManager.getGlobalFilters(); - const allFilters = [...savedQueryFilters, ...globalFilters]; - - if (savedQuery.attributes.timefilter) { - if (savedQuery.attributes.timefilter.refreshInterval) { - $scope.onRefreshChange({ - isPaused: savedQuery.attributes.timefilter.refreshInterval.pause, - refreshInterval: savedQuery.attributes.timefilter.refreshInterval.value, - }); - } - onQueryChange({ - filters: allFilters, - query: savedQuery.attributes.query, - time: savedQuery.attributes.timefilter, - }); - } else { - onQueryChange({ - filters: allFilters, - query: savedQuery.attributes.query, - }); - } - } - - $scope.$watch('savedQuery', (newSavedQuery) => { - if (!newSavedQuery) return; - - $state.savedQuery = newSavedQuery.id; - updateStateFromSavedQuery(newSavedQuery); - }); - - $scope.$watch( - () => $state.savedQuery, - (newSavedQueryId) => { - if (!newSavedQueryId) { - $scope.savedQuery = undefined; - return; - } - if ($scope.savedQuery && newSavedQueryId !== $scope.savedQuery.id) { - savedQueryService.getSavedQuery(newSavedQueryId).then((savedQuery) => { - $scope.$evalAsync(() => { - $scope.savedQuery = savedQuery; - updateStateFromSavedQuery(savedQuery); - }); - }); - } - } - ); - /* End of Saved Queries */ - async function onQueryChange({ filters, query, time, refresh }) { - if (filters) { - filterManager.setFilters(filters); // Maps and merges filters - $scope.filters = filterManager.getFilters(); - } - if (query) { - $scope.query = query; - } - if (time) { - $scope.time = time; - } - syncAppAndGlobalState(); - dispatchSetQuery(refresh); - } - - function dispatchSetQuery(refresh) { - store.dispatch( - setQuery({ - filters: $scope.filters, - query: $scope.query, - timeFilters: $scope.time, - refresh, - }) - ); - } - - $scope.indexPatterns = []; - $scope.onQuerySubmit = function ({ dateRange, query }) { - onQueryChange({ - query, - time: dateRange, - refresh: true, - }); - }; - $scope.updateFiltersAndDispatch = function (filters) { - onQueryChange({ - filters, - }); - }; - $scope.onRefreshChange = function ({ isPaused, refreshInterval }) { - $scope.refreshConfig = { - isPaused, - interval: refreshInterval ? refreshInterval : $scope.refreshConfig.interval, - }; - syncAppAndGlobalState(); - - store.dispatch(setRefreshConfig($scope.refreshConfig)); - }; - - function addFilters(newFilters) { - newFilters.forEach((filter) => { - filter.$state = { store: esFilters.FilterStateStore.APP_STATE }; - }); - $scope.updateFiltersAndDispatch([...$scope.filters, ...newFilters]); - } - - function hasUnsavedChanges() { - const state = store.getState(); - const layerList = getLayerListRaw(state); - const layerListConfigOnly = copyPersistentState(layerList); - - const savedLayerList = savedMap.getLayerList(); - - return !savedLayerList - ? !_.isEqual(layerListConfigOnly, initialLayerListConfig) - : // savedMap stores layerList as a JSON string using JSON.stringify. - // JSON.stringify removes undefined properties from objects. - // savedMap.getLayerList converts the JSON string back into Javascript array of objects. - // Need to perform the same process for layerListConfigOnly to compare apples to apples - // and avoid undefined properties in layerListConfigOnly triggering unsaved changes. - !_.isEqual(JSON.parse(JSON.stringify(layerListConfigOnly)), savedLayerList); - } - - function isOnMapNow() { - return window.location.hash.startsWith(`#/${MAP_SAVED_OBJECT_TYPE}`); - } - - function beforeUnload(event) { - if (!isOnMapNow()) { - return; - } - - const hasChanged = hasUnsavedChanges(); - if (hasChanged) { - event.preventDefault(); - event.returnValue = 'foobar'; //this is required for Chrome - } - } - window.addEventListener('beforeunload', beforeUnload); - - async function renderMap() { - // clear old UI state - store.dispatch(setSelectedLayer(null)); - store.dispatch(updateFlyout(FLYOUT_STATE.NONE)); - store.dispatch(setReadOnly(!getMapsCapabilities().save)); - - handleStoreChanges(store); - unsubscribe = store.subscribe(() => { - handleStoreChanges(store); - }); - - // sync store with savedMap mapState - let savedObjectFilters = []; - if (savedMap.mapStateJSON) { - const mapState = JSON.parse(savedMap.mapStateJSON); - store.dispatch( - setGotoWithCenter({ - lat: mapState.center.lat, - lon: mapState.center.lon, - zoom: mapState.zoom, - }) - ); - if (mapState.filters) { - savedObjectFilters = mapState.filters; - } - if (mapState.settings) { - store.dispatch(setMapSettings(mapState.settings)); - } - } - - if (savedMap.uiStateJSON) { - const uiState = JSON.parse(savedMap.uiStateJSON); - store.dispatch( - setIsLayerTOCOpen(_.get(uiState, 'isLayerTOCOpen', DEFAULT_IS_LAYER_TOC_OPEN)) - ); - store.dispatch(setOpenTOCDetails(_.get(uiState, 'openTOCDetails', []))); - } - - const layerList = getInitialLayers(savedMap.layerListJSON, getInitialLayersFromUrlParam()); - initialLayerListConfig = copyPersistentState(layerList); - store.dispatch(replaceLayerList(layerList)); - store.dispatch(setRefreshConfig($scope.refreshConfig)); - - const initialFilters = [ - ..._.get(globalState, 'filters', []), - ...getAppStateFilters(), - ...savedObjectFilters, - ]; - await onQueryChange({ filters: initialFilters }); - - const root = document.getElementById(REACT_ANCHOR_DOM_ELEMENT_ID); - render( - - - - - , - root - ); - } - renderMap(); - - let prevIndexPatternIds; - async function updateIndexPatterns(nextIndexPatternIds) { - const indexPatterns = []; - const getIndexPatternPromises = nextIndexPatternIds.map(async (indexPatternId) => { - try { - const indexPattern = await getIndexPatternService().get(indexPatternId); - indexPatterns.push(indexPattern); - } catch (err) { - // unable to fetch index pattern - } - }); - - await Promise.all(getIndexPatternPromises); - // ignore outdated results - if (prevIndexPatternIds !== nextIndexPatternIds) { - return; - } - $scope.$evalAsync(() => { - $scope.indexPatterns = indexPatterns; - }); - } - - $scope.isFullScreen = false; - $scope.isSaveDisabled = false; - $scope.isOpenSettingsDisabled = false; - function handleStoreChanges(store) { - const nextIsFullScreen = getIsFullScreen(store.getState()); - if (nextIsFullScreen !== $scope.isFullScreen) { - // Must trigger digest cycle for angular top nav to redraw itself when isFullScreen changes - $scope.$evalAsync(() => { - $scope.isFullScreen = nextIsFullScreen; - }); - } - - const nextIndexPatternIds = getQueryableUniqueIndexPatternIds(store.getState()); - if (nextIndexPatternIds !== prevIndexPatternIds) { - prevIndexPatternIds = nextIndexPatternIds; - updateIndexPatterns(nextIndexPatternIds); - } - - const nextIsSaveDisabled = hasDirtyState(store.getState()); - if (nextIsSaveDisabled !== $scope.isSaveDisabled) { - $scope.$evalAsync(() => { - $scope.isSaveDisabled = nextIsSaveDisabled; - }); - } - - const flyoutDisplay = getFlyoutDisplay(store.getState()); - const nextIsOpenSettingsDisabled = flyoutDisplay !== FLYOUT_STATE.NONE; - if (nextIsOpenSettingsDisabled !== $scope.isOpenSettingsDisabled) { - $scope.$evalAsync(() => { - $scope.isOpenSettingsDisabled = nextIsOpenSettingsDisabled; - }); - } - } - - $scope.$on('$destroy', () => { - window.removeEventListener('beforeunload', beforeUnload); - visibleSubscription.unsubscribe(); - getCoreChrome().setIsVisible(true); - - if (unsubscribe) { - unsubscribe(); - } - const node = document.getElementById(REACT_ANCHOR_DOM_ELEMENT_ID); - if (node) { - unmountComponentAtNode(node); - } - }); - - const updateBreadcrumbs = () => { - getCoreChrome().setBreadcrumbs([ - { - text: i18n.translate('xpack.maps.mapController.mapsBreadcrumbLabel', { - defaultMessage: 'Maps', - }), - onClick: () => { - if (isOnMapNow() && hasUnsavedChanges()) { - const navigateAway = window.confirm( - i18n.translate('xpack.maps.mapController.unsavedChangesWarning', { - defaultMessage: `Your unsaved changes might not be saved`, - }) - ); - if (navigateAway) { - window.location.hash = '#'; - } - } else { - window.location.hash = '#'; - } - }, - }, - { text: savedMap.title }, - ]); - }; - updateBreadcrumbs(); - - addHelpMenuToAppChrome(); - - async function doSave(saveOptions) { - savedMap.syncWithStore(store.getState()); - let id; - - try { - id = await savedMap.save(saveOptions); - getCoreChrome().docTitle.change(savedMap.title); - } catch (err) { - getToasts().addDanger({ - title: i18n.translate('xpack.maps.mapController.saveErrorMessage', { - defaultMessage: `Error on saving '{title}'`, - values: { title: savedMap.title }, - }), - text: err.message, - 'data-test-subj': 'saveMapError', - }); - return { error: err }; - } - - if (id) { - getToasts().addSuccess({ - title: i18n.translate('xpack.maps.mapController.saveSuccessMessage', { - defaultMessage: `Saved '{title}'`, - values: { title: savedMap.title }, - }), - 'data-test-subj': 'saveMapSuccess', - }); - - updateBreadcrumbs(); - - if (savedMap.id !== $route.current.params.id) { - $scope.$evalAsync(() => { - kbnUrl.change(`map/{{id}}`, { id: savedMap.id }); - }); - } - } - return { id }; - } - - // Hide angular timepicer/refresh UI from top nav - getTimeFilter().disableTimeRangeSelector(); - getTimeFilter().disableAutoRefreshSelector(); - $scope.showDatePicker = true; // used by query-bar directive to enable timepikcer in query bar - $scope.topNavMenu = [ - { - id: 'full-screen', - label: i18n.translate('xpack.maps.mapController.fullScreenButtonLabel', { - defaultMessage: `full screen`, - }), - description: i18n.translate('xpack.maps.mapController.fullScreenDescription', { - defaultMessage: `full screen`, - }), - testId: 'mapsFullScreenMode', - run() { - getCoreChrome().setIsVisible(false); - store.dispatch(enableFullScreen()); - }, - }, - { - id: 'inspect', - label: i18n.translate('xpack.maps.mapController.openInspectorButtonLabel', { - defaultMessage: `inspect`, - }), - description: i18n.translate('xpack.maps.mapController.openInspectorDescription', { - defaultMessage: `Open Inspector`, - }), - testId: 'openInspectorButton', - run() { - const inspectorAdapters = getInspectorAdapters(store.getState()); - getInspector().open(inspectorAdapters, {}); - }, - }, - { - id: 'mapSettings', - label: i18n.translate('xpack.maps.mapController.openSettingsButtonLabel', { - defaultMessage: `Map settings`, - }), - description: i18n.translate('xpack.maps.mapController.openSettingsDescription', { - defaultMessage: `Open map settings`, - }), - testId: 'openSettingsButton', - disableButton() { - return $scope.isOpenSettingsDisabled; - }, - run() { - store.dispatch(openMapSettings()); - }, - }, - ...(getMapsCapabilities().save - ? [ - { - id: 'save', - label: i18n.translate('xpack.maps.mapController.saveMapButtonLabel', { - defaultMessage: `save`, - }), - description: i18n.translate('xpack.maps.mapController.saveMapDescription', { - defaultMessage: `Save map`, - }), - testId: 'mapSaveButton', - disableButton() { - return $scope.isSaveDisabled; - }, - tooltip() { - if ($scope.isSaveDisabled) { - return i18n.translate('xpack.maps.mapController.saveMapDisabledButtonTooltip', { - defaultMessage: 'Save or Cancel your layer changes before saving', - }); - } - }, - run: async () => { - const onSave = ({ - newTitle, - newCopyOnSave, - isTitleDuplicateConfirmed, - onTitleDuplicate, - }) => { - const currentTitle = savedMap.title; - savedMap.title = newTitle; - savedMap.copyOnSave = newCopyOnSave; - const saveOptions = { - confirmOverwrite: false, - isTitleDuplicateConfirmed, - onTitleDuplicate, - }; - return doSave(saveOptions).then((response) => { - // If the save wasn't successful, put the original values back. - if (!response.id || response.error) { - savedMap.title = currentTitle; - } - return response; - }); - }; - - const saveModal = ( - {}} - title={savedMap.title} - showCopyOnSave={savedMap.id ? true : false} - objectType={MAP_SAVED_OBJECT_TYPE} - showDescription={false} - /> - ); - showSaveModal(saveModal, getCoreI18n().Context); - }, - }, - ] - : []), - ]; - } -); diff --git a/x-pack/legacy/plugins/maps/public/index.scss b/x-pack/legacy/plugins/maps/public/index.scss deleted file mode 100644 index b2a228f01b9210..00000000000000 --- a/x-pack/legacy/plugins/maps/public/index.scss +++ /dev/null @@ -1,3 +0,0 @@ -/* GIS plugin styles */ - -@import '../../../../plugins/maps/public/index'; diff --git a/x-pack/legacy/plugins/maps/public/index.ts b/x-pack/legacy/plugins/maps/public/index.ts deleted file mode 100644 index 89ce976de9278d..00000000000000 --- a/x-pack/legacy/plugins/maps/public/index.ts +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import '../../../../plugins/maps/public/kibana_services'; - -// import the uiExports that we want to "use" -import 'uiExports/inspectorViews'; -import 'uiExports/search'; -import 'uiExports/embeddableFactories'; -import 'uiExports/embeddableActions'; - -import 'ui/autoload/all'; -import 'react-vis/dist/style.css'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import '../../../../plugins/maps/public/angular/services/gis_map_saved_object_loader'; -import './angular/map_controller'; -import './routes'; -// @ts-ignore -import { MapsPlugin } from './plugin'; - -export const plugin = () => { - return new MapsPlugin(); -}; - -export { - RenderTooltipContentParams, - ITooltipProperty, -} from '../../../../plugins/maps/public/classes/tooltips/tooltip_property'; // eslint-disable-line @kbn/eslint/no-restricted-paths -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -export { MapEmbeddable, MapEmbeddableInput } from '../../../../plugins/maps/public/embeddable'; diff --git a/x-pack/legacy/plugins/maps/public/legacy.ts b/x-pack/legacy/plugins/maps/public/legacy.ts deleted file mode 100644 index bcbfca17755fb4..00000000000000 --- a/x-pack/legacy/plugins/maps/public/legacy.ts +++ /dev/null @@ -1,22 +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 { npSetup, npStart } from 'ui/new_platform'; -// @ts-ignore Untyped Module -import { uiModules } from 'ui/modules'; -import { plugin } from '.'; - -const pluginInstance = plugin(); - -const setupPlugins = { - __LEGACY: { - uiModules, - }, - np: npSetup.plugins, -}; - -export const setup = pluginInstance.setup(npSetup.core, setupPlugins); -export const start = pluginInstance.start(npStart.core, npStart.plugins); diff --git a/x-pack/legacy/plugins/maps/public/plugin.ts b/x-pack/legacy/plugins/maps/public/plugin.ts deleted file mode 100644 index 9605c0d3e5fd8d..00000000000000 --- a/x-pack/legacy/plugins/maps/public/plugin.ts +++ /dev/null @@ -1,70 +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 _ from 'lodash'; -import { Plugin, CoreStart, CoreSetup } from 'src/core/public'; -// @ts-ignore -import { Start as InspectorStartContract } from 'src/plugins/inspector/public'; -// @ts-ignore -import { wrapInI18nContext } from 'ui/i18n'; -// @ts-ignore -import { MapListing } from '../../../../plugins/maps/public/components/map_listing'; // eslint-disable-line @kbn/eslint/no-restricted-paths -// @ts-ignore -import { - bindSetupCoreAndPlugins as bindNpSetupCoreAndPlugins, - bindStartCoreAndPlugins as bindNpStartCoreAndPlugins, -} from '../../../../plugins/maps/public/plugin'; // eslint-disable-line @kbn/eslint/no-restricted-paths -import { HomePublicPluginSetup } from '../../../../../src/plugins/home/public'; -import { LicensingPluginSetup } from '../../../../plugins/licensing/public'; -import { - DataPublicPluginSetup, - DataPublicPluginStart, -} from '../../../../../src/plugins/data/public'; - -/** - * These are the interfaces with your public contracts. You should export these - * for other plugins to use in _their_ `SetupDeps`/`StartDeps` interfaces. - * @public - */ -export type MapsPluginSetup = ReturnType; -export type MapsPluginStart = ReturnType; - -interface MapsPluginSetupDependencies { - __LEGACY: any; - np: { - licensing?: LicensingPluginSetup; - home: HomePublicPluginSetup; - data: DataPublicPluginSetup; - }; -} - -interface MapsPluginStartDependencies { - data: DataPublicPluginStart; - inspector: InspectorStartContract; - // file_upload TODO: Export type from file upload and use here -} - -/** @internal */ -export class MapsPlugin implements Plugin { - public setup(core: CoreSetup, { __LEGACY: { uiModules }, np }: MapsPluginSetupDependencies) { - uiModules - .get('app/maps', ['ngRoute', 'react']) - .directive('mapListing', function (reactDirective: any) { - return reactDirective(wrapInI18nContext(MapListing)); - }); - - // @ts-ignore - const config = _.get(np, 'maps.config', {}); - // @ts-ignore - const kibanaVersion = core.injectedMetadata.getKibanaVersion(); - // @ts-ignore - bindNpSetupCoreAndPlugins(core, np, config, kibanaVersion); - } - - public start(core: CoreStart, plugins: MapsPluginStartDependencies) { - bindNpStartCoreAndPlugins(core, plugins); - } -} diff --git a/x-pack/legacy/plugins/maps/public/routes.js b/x-pack/legacy/plugins/maps/public/routes.js deleted file mode 100644 index 20664b1b35a26a..00000000000000 --- a/x-pack/legacy/plugins/maps/public/routes.js +++ /dev/null @@ -1,108 +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 { i18n } from '@kbn/i18n'; -import routes from 'ui/routes'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import listingTemplate from '../../../../plugins/maps/public/angular/listing_ng_wrapper.html'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import mapTemplate from '../../../../plugins/maps/public/angular/map.html'; -import { - getSavedObjectsClient, - getCoreChrome, - getMapsCapabilities, - // eslint-disable-next-line @kbn/eslint/no-restricted-paths -} from '../../../../plugins/maps/public/kibana_services'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { getMapsSavedObjectLoader } from '../../../../plugins/maps/public/angular/services/gis_map_saved_object_loader'; -import { LISTING_LIMIT_SETTING } from '../../../../../src/plugins/saved_objects/common'; - -routes.enable(); - -routes - .defaults(/.*/, { - badge: () => { - if (getMapsCapabilities().save) { - return undefined; - } - - return { - text: i18n.translate('xpack.maps.badge.readOnly.text', { - defaultMessage: 'Read only', - }), - tooltip: i18n.translate('xpack.maps.badge.readOnly.tooltip', { - defaultMessage: 'Unable to save maps', - }), - iconType: 'glasses', - }; - }, - }) - .when('/', { - template: listingTemplate, - controller($scope, config) { - const gisMapSavedObjectLoader = getMapsSavedObjectLoader(); - $scope.listingLimit = config.get(LISTING_LIMIT_SETTING); - $scope.find = (search) => { - return gisMapSavedObjectLoader.find(search, $scope.listingLimit); - }; - $scope.delete = (ids) => { - return gisMapSavedObjectLoader.delete(ids); - }; - $scope.readOnly = !getMapsCapabilities().save; - }, - resolve: { - hasMaps: function (kbnUrl) { - getSavedObjectsClient() - .find({ type: 'map', perPage: 1 }) - .then((resp) => { - // Do not show empty listing page, just redirect to a new map - if (resp.savedObjects.length === 0) { - kbnUrl.redirect('/map'); - } - return true; - }); - }, - }, - }) - .when('/map', { - template: mapTemplate, - controller: 'GisMapController', - resolve: { - map: function (redirectWhenMissing) { - const gisMapSavedObjectLoader = getMapsSavedObjectLoader(); - return gisMapSavedObjectLoader.get().catch( - redirectWhenMissing({ - map: '/', - }) - ); - }, - }, - }) - .when('/map/:id', { - template: mapTemplate, - controller: 'GisMapController', - resolve: { - map: function (redirectWhenMissing, $route) { - const gisMapSavedObjectLoader = getMapsSavedObjectLoader(); - const id = $route.current.params.id; - return gisMapSavedObjectLoader - .get(id) - .then((savedMap) => { - getCoreChrome().recentlyAccessed.add(savedMap.getFullPath(), savedMap.title, id); - getCoreChrome().docTitle.change(savedMap.title); - return savedMap; - }) - .catch( - redirectWhenMissing({ - map: '/', - }) - ); - }, - }, - }) - .otherwise({ - redirectTo: '/', - }); diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/saved_map.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/saved_map.ts index faa2937aeaa14b..2a3741e15f467a 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/saved_map.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/saved_map.ts @@ -13,7 +13,8 @@ import { EmbeddableExpression, } from '../../expression_types'; import { getFunctionHelp } from '../../../i18n'; -import { MapEmbeddableInput } from '../../../../../legacy/plugins/maps/public'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { MapEmbeddableInput } from '../../../../../plugins/maps/public/embeddable'; interface Arguments { id: string; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/input_type_to_expression/map.test.ts b/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/input_type_to_expression/map.test.ts index e0e0aeaeea272a..d910c734a69740 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/input_type_to_expression/map.test.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/input_type_to_expression/map.test.ts @@ -5,7 +5,7 @@ */ import { toExpression } from './map'; -import { MapEmbeddableInput } from '../../../../../../legacy/plugins/maps/public'; +import { MapEmbeddableInput } from '../../../../../../plugins/maps/public/embeddable'; import { fromExpression, Ast } from '@kbn/interpreter/common'; const baseSavedMapInput = { diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/input_type_to_expression/map.ts b/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/input_type_to_expression/map.ts index 1f9bec133488c7..111fdc71fa242b 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/input_type_to_expression/map.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/input_type_to_expression/map.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { MapEmbeddableInput } from '../../../../../../legacy/plugins/maps/public'; +import { MapEmbeddableInput } from '../../../../../../plugins/maps/public/embeddable'; export function toExpression(input: MapEmbeddableInput): string { const expressionParts = [] as string[]; diff --git a/x-pack/plugins/maps/common/constants.ts b/x-pack/plugins/maps/common/constants.ts index edb395633827f6..be3de22fa011e8 100644 --- a/x-pack/plugins/maps/common/constants.ts +++ b/x-pack/plugins/maps/common/constants.ts @@ -32,7 +32,7 @@ export const GIS_API_PATH = `api/${APP_ID}`; export const INDEX_SETTINGS_API_PATH = `${GIS_API_PATH}/indexSettings`; export const FONTS_API_PATH = `${GIS_API_PATH}/fonts`; -export const MAP_BASE_URL = `/${MAP_APP_PATH}#/${MAP_SAVED_OBJECT_TYPE}`; +export const MAP_BASE_URL = `/${MAP_APP_PATH}/${MAP_SAVED_OBJECT_TYPE}`; export function createMapPath(id: string) { return `${MAP_BASE_URL}/${id}`; diff --git a/x-pack/plugins/maps/public/angular/listing_ng_wrapper.html b/x-pack/plugins/maps/public/angular/listing_ng_wrapper.html deleted file mode 100644 index bfea81e13e9dfc..00000000000000 --- a/x-pack/plugins/maps/public/angular/listing_ng_wrapper.html +++ /dev/null @@ -1,6 +0,0 @@ - diff --git a/x-pack/plugins/maps/public/angular/map.html b/x-pack/plugins/maps/public/angular/map.html deleted file mode 100644 index 7d7dcf6f9c9a95..00000000000000 --- a/x-pack/plugins/maps/public/angular/map.html +++ /dev/null @@ -1,35 +0,0 @@ -
- -
-
- - -
-
- -

{{screenTitle}}

-
- -
diff --git a/x-pack/plugins/maps/public/classes/sources/es_search_source/load_index_settings.js b/x-pack/plugins/maps/public/classes/sources/es_search_source/load_index_settings.js index 811291de26d351..d5d24da225232b 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_search_source/load_index_settings.js +++ b/x-pack/plugins/maps/public/classes/sources/es_search_source/load_index_settings.js @@ -29,14 +29,13 @@ async function fetchIndexSettings(indexPatternTitle) { const http = getHttp(); const toasts = getToasts(); try { - const indexSettings = await http.fetch(`../${INDEX_SETTINGS_API_PATH}`, { + return await http.fetch(`/${INDEX_SETTINGS_API_PATH}`, { method: 'GET', credentials: 'same-origin', query: { indexPatternTitle, }, }); - return indexSettings; } catch (err) { const warningMsg = i18n.translate('xpack.maps.indexSettings.fetchErrorMsg', { defaultMessage: `Unable to fetch index settings for index pattern '{indexPatternTitle}'. diff --git a/x-pack/plugins/maps/public/connected_components/gis_map/view.js b/x-pack/plugins/maps/public/connected_components/gis_map/view.js index 122f5cdd45c1cb..7199620d69fcf4 100644 --- a/x-pack/plugins/maps/public/connected_components/gis_map/view.js +++ b/x-pack/plugins/maps/public/connected_components/gis_map/view.js @@ -14,7 +14,6 @@ import { LayerPanel } from '../layer_panel'; import { AddLayerPanel } from '../add_layer_panel'; import { EuiFlexGroup, EuiFlexItem, EuiCallOut } from '@elastic/eui'; import { ExitFullScreenButton } from '../../../../../../src/plugins/kibana_react/public'; - import { getIndexPatternsFromIds } from '../../index_pattern_util'; import { ES_GEO_FIELD_TYPE } from '../../../common/constants'; import { indexPatterns as indexPatternsUtils } from '../../../../../../src/plugins/data/public'; @@ -23,6 +22,7 @@ import uuid from 'uuid/v4'; import { FLYOUT_STATE } from '../../reducers/ui'; import { MapSettingsPanel } from '../map_settings_panel'; import { registerLayerWizards } from '../../classes/layers/load_layer_wizards'; +import 'mapbox-gl/dist/mapbox-gl.css'; const RENDER_COMPLETE_EVENT = 'renderComplete'; diff --git a/x-pack/plugins/maps/public/embeddable/map_embeddable.tsx b/x-pack/plugins/maps/public/embeddable/map_embeddable.tsx index b27f66ea085de1..6f583397922148 100644 --- a/x-pack/plugins/maps/public/embeddable/map_embeddable.tsx +++ b/x-pack/plugins/maps/public/embeddable/map_embeddable.tsx @@ -8,7 +8,6 @@ import _ from 'lodash'; import React, { Suspense, lazy } from 'react'; import { Provider } from 'react-redux'; import { render, unmountComponentAtNode } from 'react-dom'; -import 'mapbox-gl/dist/mapbox-gl.css'; import { Subscription } from 'rxjs'; import { Unsubscribe } from 'redux'; import { EuiLoadingSpinner } from '@elastic/eui'; diff --git a/x-pack/plugins/maps/public/index.ts b/x-pack/plugins/maps/public/index.ts index 6a144e84b05e05..7b5521443d974d 100644 --- a/x-pack/plugins/maps/public/index.ts +++ b/x-pack/plugins/maps/public/index.ts @@ -17,5 +17,3 @@ export const plugin: PluginInitializer = ( }; export { MAP_SAVED_OBJECT_TYPE } from '../common/constants'; -export { ITooltipProperty } from './classes/tooltips/tooltip_property'; -export { MapsPluginStart } from './plugin'; diff --git a/x-pack/plugins/maps/public/lazy_load_bundle/index.ts b/x-pack/plugins/maps/public/lazy_load_bundle/index.ts index 152412376fb094..ca4098ebfa8053 100644 --- a/x-pack/plugins/maps/public/lazy_load_bundle/index.ts +++ b/x-pack/plugins/maps/public/lazy_load_bundle/index.ts @@ -7,6 +7,7 @@ import { AnyAction } from 'redux'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { IndexPatternsService } from 'src/plugins/data/public/index_patterns'; +import { ReactElement } from 'react'; import { Embeddable, IContainer } from '../../../../../src/plugins/embeddable/public'; import { LayerDescriptor } from '../../common/descriptor_types'; import { MapStore, MapStoreState } from '../reducers/store'; @@ -36,6 +37,7 @@ interface LazyLoadedMapModules { initialLayers?: LayerDescriptor[] ) => LayerDescriptor[]; mergeInputWithSavedMap: any; + renderApp: (context: unknown, params: unknown) => ReactElement; createSecurityLayerDescriptors: ( indexPatternId: string, indexPatternTitle: string @@ -49,7 +51,7 @@ export async function lazyLoadMapModules(): Promise { loadModulesPromise = new Promise(async (resolve) => { const { - // @ts-ignore + // @ts-expect-error getMapsSavedObjectLoader, getQueryableUniqueIndexPatternIds, MapEmbeddable, @@ -60,6 +62,8 @@ export async function lazyLoadMapModules(): Promise { addLayerWithoutDataSync, getInitialLayers, mergeInputWithSavedMap, + // @ts-expect-error + renderApp, createSecurityLayerDescriptors, } = await import('./lazy'); @@ -74,6 +78,7 @@ export async function lazyLoadMapModules(): Promise { addLayerWithoutDataSync, getInitialLayers, mergeInputWithSavedMap, + renderApp, createSecurityLayerDescriptors, }); }); diff --git a/x-pack/plugins/maps/public/lazy_load_bundle/lazy/index.ts b/x-pack/plugins/maps/public/lazy_load_bundle/lazy/index.ts index 0600b8d5073c6b..4f9f01f8a1b37c 100644 --- a/x-pack/plugins/maps/public/lazy_load_bundle/lazy/index.ts +++ b/x-pack/plugins/maps/public/lazy_load_bundle/lazy/index.ts @@ -7,13 +7,15 @@ // These are map-dependencies of the embeddable. // By lazy-loading these, the Maps-app can register the embeddable when the plugin mounts, without actually pulling all the code. -// @ts-ignore -export * from '../../angular/services/gis_map_saved_object_loader'; +// @ts-expect-error +export * from '../../routing/bootstrap/services/gis_map_saved_object_loader'; export * from '../../embeddable/map_embeddable'; export * from '../../kibana_services'; export * from '../../reducers/store'; export * from '../../actions'; export * from '../../selectors/map_selectors'; -export * from '../../angular/get_initial_layers'; +export * from '../../routing/bootstrap/get_initial_layers'; export * from '../../embeddable/merge_input_with_saved_map'; +// @ts-expect-error +export * from '../../routing/maps_router'; export * from '../../classes/layers/solution_layers/security'; diff --git a/x-pack/plugins/maps/public/maps_vis_type_alias.js b/x-pack/plugins/maps/public/maps_vis_type_alias.js index 14f31d7660a54d..cb7b3db17eab58 100644 --- a/x-pack/plugins/maps/public/maps_vis_type_alias.js +++ b/x-pack/plugins/maps/public/maps_vis_type_alias.js @@ -28,7 +28,7 @@ The Maps app offers more functionality and is easier to use.`, return { aliasApp: APP_ID, - aliasPath: `#/${MAP_SAVED_OBJECT_TYPE}`, + aliasPath: `/${MAP_SAVED_OBJECT_TYPE}`, name: APP_ID, title: i18n.translate('xpack.maps.visTypeAlias.title', { defaultMessage: 'Maps', diff --git a/x-pack/plugins/maps/public/plugin.ts b/x-pack/plugins/maps/public/plugin.ts index e0639c9d0f3650..412e8832453bc6 100644 --- a/x-pack/plugins/maps/public/plugin.ts +++ b/x-pack/plugins/maps/public/plugin.ts @@ -4,8 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ -import { CoreSetup, CoreStart, Plugin, PluginInitializerContext } from 'src/core/public'; import { Setup as InspectorSetupContract } from 'src/plugins/inspector/public'; +import { + CoreSetup, + CoreStart, + Plugin, + PluginInitializerContext, + DEFAULT_APP_CATEGORIES, +} from '../../../../src/core/public'; // @ts-ignore import { MapView } from './inspector/views/map_view'; import { @@ -41,11 +47,13 @@ import { featureCatalogueEntry } from './feature_catalogue_entry'; import { getMapsVisTypeAlias } from './maps_vis_type_alias'; import { HomePublicPluginSetup } from '../../../../src/plugins/home/public'; import { VisualizationsSetup } from '../../../../src/plugins/visualizations/public'; -import { APP_ID, MAP_SAVED_OBJECT_TYPE } from '../common/constants'; +import { APP_ICON, APP_ID, MAP_SAVED_OBJECT_TYPE } from '../common/constants'; import { MapEmbeddableFactory } from './embeddable/map_embeddable_factory'; import { EmbeddableSetup } from '../../../../src/plugins/embeddable/public'; -import { MapsConfigType, MapsXPackConfig } from '../config'; +import { MapsXPackConfig, MapsConfigType } from '../config'; +import { getAppTitle } from '../common/i18n_getters'; import { ILicense } from '../../licensing/common/types'; +import { lazyLoadMapModules } from './lazy_load_bundle'; import { MapsStartApi } from './api'; import { createSecurityLayerDescriptors } from './api/create_security_layer_descriptors'; @@ -140,9 +148,22 @@ export class MapsPlugin home.featureCatalogue.register(featureCatalogueEntry); visualizations.registerAlias(getMapsVisTypeAlias()); embeddable.registerEmbeddableFactory(MAP_SAVED_OBJECT_TYPE, new MapEmbeddableFactory()); - return { - config, - }; + + core.application.register({ + id: APP_ID, + title: getAppTitle(), + order: 4000, + icon: `plugins/${APP_ID}/icon.svg`, + euiIconType: APP_ICON, + category: DEFAULT_APP_CATEGORIES.kibana, + // @ts-expect-error + async mount(context, params) { + const [coreStart, startPlugins] = await core.getStartServices(); + bindStartCoreAndPlugins(coreStart, startPlugins); + const { renderApp } = await lazyLoadMapModules(); + return renderApp(context, params); + }, + }); } public start(core: CoreStart, plugins: any): MapsStartApi { diff --git a/x-pack/plugins/maps/public/reducers/store.js b/x-pack/plugins/maps/public/reducers/store.js index a1bc6b8e701394..c63f5f7fd82fcc 100644 --- a/x-pack/plugins/maps/public/reducers/store.js +++ b/x-pack/plugins/maps/public/reducers/store.js @@ -9,6 +9,7 @@ import thunk from 'redux-thunk'; import { ui, DEFAULT_MAP_UI_STATE } from './ui'; import { map, DEFAULT_MAP_STATE } from './map'; // eslint-disable-line import/named import { nonSerializableInstances } from './non_serializable_instances'; +import { MAP_DESTROYED } from '../actions'; export const DEFAULT_MAP_STORE_STATE = { ui: { ...DEFAULT_MAP_UI_STATE }, @@ -17,11 +18,21 @@ export const DEFAULT_MAP_STORE_STATE = { export function createMapStore() { const enhancers = [applyMiddleware(thunk)]; - const rootReducer = combineReducers({ + const combinedReducers = combineReducers({ map, ui, nonSerializableInstances, }); + + const rootReducer = (state, action) => { + // Reset store on map destroyed + if (action.type === MAP_DESTROYED) { + state = undefined; + } + + return combinedReducers(state, action); + }; + const storeConfig = {}; return createStore(rootReducer, storeConfig, compose(...enhancers)); } diff --git a/x-pack/plugins/maps/public/angular/get_initial_layers.d.ts b/x-pack/plugins/maps/public/routing/bootstrap/get_initial_layers.d.ts similarity index 84% rename from x-pack/plugins/maps/public/angular/get_initial_layers.d.ts rename to x-pack/plugins/maps/public/routing/bootstrap/get_initial_layers.d.ts index 5b77ea6f514fc7..a23e715a082958 100644 --- a/x-pack/plugins/maps/public/angular/get_initial_layers.d.ts +++ b/x-pack/plugins/maps/public/routing/bootstrap/get_initial_layers.d.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { LayerDescriptor } from '../../common/descriptor_types'; +import { LayerDescriptor } from '../../../common/descriptor_types'; export function getInitialLayers( layerListJSON?: string, diff --git a/x-pack/plugins/maps/public/angular/get_initial_layers.js b/x-pack/plugins/maps/public/routing/bootstrap/get_initial_layers.js similarity index 57% rename from x-pack/plugins/maps/public/angular/get_initial_layers.js rename to x-pack/plugins/maps/public/routing/bootstrap/get_initial_layers.js index 598fd6ce324d02..cf6f51c8aeacfe 100644 --- a/x-pack/plugins/maps/public/angular/get_initial_layers.js +++ b/x-pack/plugins/maps/public/routing/bootstrap/get_initial_layers.js @@ -5,25 +5,24 @@ */ import _ from 'lodash'; // Import each layer type, even those not used, to init in registry -import '../classes/sources/wms_source'; -import '../classes/sources/ems_file_source'; -import '../classes/sources/es_search_source'; -import '../classes/sources/es_pew_pew_source'; -import '../classes/sources/kibana_regionmap_source'; -import '../classes/sources/es_geo_grid_source'; -import '../classes/sources/xyz_tms_source'; -import { KibanaTilemapSource } from '../classes/sources/kibana_tilemap_source'; -import { TileLayer } from '../classes/layers/tile_layer/tile_layer'; -import { EMSTMSSource } from '../classes/sources/ems_tms_source'; -import { VectorTileLayer } from '../classes/layers/vector_tile_layer/vector_tile_layer'; -import { getIsEmsEnabled } from '../kibana_services'; -import { getKibanaTileMap } from '../meta'; +import '../../classes/sources/wms_source'; +import '../../classes/sources/ems_file_source'; +import '../../classes/sources/es_search_source'; +import '../../classes/sources/es_pew_pew_source'; +import '../../classes/sources/kibana_regionmap_source'; +import '../../classes/sources/es_geo_grid_source'; +import '../../classes/sources/xyz_tms_source'; +import { KibanaTilemapSource } from '../../classes/sources/kibana_tilemap_source'; +import { TileLayer } from '../../classes/layers/tile_layer/tile_layer'; +import { EMSTMSSource } from '../../classes/sources/ems_tms_source'; +import { VectorTileLayer } from '../../classes/layers/vector_tile_layer/vector_tile_layer'; +import { getIsEmsEnabled } from '../../kibana_services'; +import { getKibanaTileMap } from '../../meta'; export function getInitialLayers(layerListJSON, initialLayers = []) { if (layerListJSON) { return JSON.parse(layerListJSON); } - const tilemapSourceFromKibana = getKibanaTileMap(); if (_.get(tilemapSourceFromKibana, 'url')) { const layerDescriptor = TileLayer.createDescriptor({ diff --git a/x-pack/plugins/maps/public/angular/get_initial_layers.test.js b/x-pack/plugins/maps/public/routing/bootstrap/get_initial_layers.test.js similarity index 82% rename from x-pack/plugins/maps/public/angular/get_initial_layers.test.js rename to x-pack/plugins/maps/public/routing/bootstrap/get_initial_layers.test.js index 8ddcd7cb5bbbb3..4de29e6f028e15 100644 --- a/x-pack/plugins/maps/public/angular/get_initial_layers.test.js +++ b/x-pack/plugins/maps/public/routing/bootstrap/get_initial_layers.test.js @@ -4,10 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -jest.mock('../meta', () => { +jest.mock('../../meta', () => { return {}; }); -jest.mock('../kibana_services'); +jest.mock('../../kibana_services'); import { getInitialLayers } from './get_initial_layers'; @@ -15,7 +15,7 @@ const layerListNotProvided = undefined; describe('Saved object has layer list', () => { beforeEach(() => { - require('../kibana_services').getIsEmsEnabled = () => true; + require('../../kibana_services').getIsEmsEnabled = () => true; }); it('Should get initial layers from saved object', () => { @@ -32,7 +32,7 @@ describe('Saved object has layer list', () => { describe('kibana.yml configured with map.tilemap.url', () => { beforeAll(() => { - require('../meta').getKibanaTileMap = () => { + require('../../meta').getKibanaTileMap = () => { return { url: 'myTileUrl', }; @@ -62,11 +62,11 @@ describe('kibana.yml configured with map.tilemap.url', () => { describe('EMS is enabled', () => { beforeAll(() => { - require('../meta').getKibanaTileMap = () => { + require('../../meta').getKibanaTileMap = () => { return null; }; - require('../kibana_services').getIsEmsEnabled = () => true; - require('../kibana_services').getEmsTileLayerId = () => ({ + require('../../kibana_services').getIsEmsEnabled = () => true; + require('../../kibana_services').getEmsTileLayerId = () => ({ bright: 'road_map', desaturated: 'road_map_desaturated', dark: 'dark_map', @@ -98,10 +98,10 @@ describe('EMS is enabled', () => { describe('EMS is not enabled', () => { beforeAll(() => { - require('../meta').getKibanaTileMap = () => { + require('../../meta').getKibanaTileMap = () => { return null; }; - require('../kibana_services').getIsEmsEnabled = () => false; + require('../../kibana_services').getIsEmsEnabled = () => false; }); it('Should return empty layer list since there are no configured tile layers', () => { diff --git a/x-pack/plugins/maps/public/angular/get_initial_query.js b/x-pack/plugins/maps/public/routing/bootstrap/get_initial_query.js similarity index 84% rename from x-pack/plugins/maps/public/angular/get_initial_query.js rename to x-pack/plugins/maps/public/routing/bootstrap/get_initial_query.js index 84f431cf2b3b63..dfc3a1c9de96af 100644 --- a/x-pack/plugins/maps/public/angular/get_initial_query.js +++ b/x-pack/plugins/maps/public/routing/bootstrap/get_initial_query.js @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { getUiSettings } from '../kibana_services'; -import { UI_SETTINGS } from '../../../../../src/plugins/data/public'; +import { getUiSettings } from '../../kibana_services'; +import { UI_SETTINGS } from '../../../../../../src/plugins/data/public'; export function getInitialQuery({ mapStateJSON, appState = {}, userQueryLanguage }) { const settings = getUiSettings(); diff --git a/x-pack/plugins/maps/public/angular/get_initial_refresh_config.js b/x-pack/plugins/maps/public/routing/bootstrap/get_initial_refresh_config.js similarity index 86% rename from x-pack/plugins/maps/public/angular/get_initial_refresh_config.js rename to x-pack/plugins/maps/public/routing/bootstrap/get_initial_refresh_config.js index 17a50c6c5f6852..d7b3bbf5b4ab2c 100644 --- a/x-pack/plugins/maps/public/angular/get_initial_refresh_config.js +++ b/x-pack/plugins/maps/public/routing/bootstrap/get_initial_refresh_config.js @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { getUiSettings } from '../kibana_services'; -import { UI_SETTINGS } from '../../../../../src/plugins/data/public'; +import { getUiSettings } from '../../kibana_services'; +import { UI_SETTINGS } from '../../../../../../src/plugins/data/public'; export function getInitialRefreshConfig({ mapStateJSON, globalState = {} }) { const uiSettings = getUiSettings(); diff --git a/x-pack/plugins/maps/public/angular/get_initial_time_filters.js b/x-pack/plugins/maps/public/routing/bootstrap/get_initial_time_filters.js similarity index 90% rename from x-pack/plugins/maps/public/angular/get_initial_time_filters.js rename to x-pack/plugins/maps/public/routing/bootstrap/get_initial_time_filters.js index 75d9f0e95ccf08..9c11dabe039234 100644 --- a/x-pack/plugins/maps/public/angular/get_initial_time_filters.js +++ b/x-pack/plugins/maps/public/routing/bootstrap/get_initial_time_filters.js @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { getUiSettings } from '../kibana_services'; +import { getUiSettings } from '../../kibana_services'; -export function getInitialTimeFilters({ mapStateJSON, globalState = {} }) { +export function getInitialTimeFilters({ mapStateJSON, globalState }) { if (mapStateJSON) { const mapState = JSON.parse(mapStateJSON); if (mapState.timeFilters) { diff --git a/x-pack/plugins/maps/public/angular/services/gis_map_saved_object_loader.js b/x-pack/plugins/maps/public/routing/bootstrap/services/gis_map_saved_object_loader.js similarity index 87% rename from x-pack/plugins/maps/public/angular/services/gis_map_saved_object_loader.js rename to x-pack/plugins/maps/public/routing/bootstrap/services/gis_map_saved_object_loader.js index ea48eecf4e421f..6c7e141c2ab5a7 100644 --- a/x-pack/plugins/maps/public/angular/services/gis_map_saved_object_loader.js +++ b/x-pack/plugins/maps/public/routing/bootstrap/services/gis_map_saved_object_loader.js @@ -6,14 +6,14 @@ import _ from 'lodash'; import { createSavedGisMapClass } from './saved_gis_map'; -import { SavedObjectLoader } from '../../../../../../src/plugins/saved_objects/public'; +import { SavedObjectLoader } from '../../../../../../../src/plugins/saved_objects/public'; import { getCoreChrome, getSavedObjectsClient, getIndexPatternService, getCoreOverlays, getData, -} from '../../kibana_services'; +} from '../../../kibana_services'; export const getMapsSavedObjectLoader = _.once(function () { const services = { diff --git a/x-pack/plugins/maps/public/angular/services/saved_gis_map.js b/x-pack/plugins/maps/public/routing/bootstrap/services/saved_gis_map.js similarity index 82% rename from x-pack/plugins/maps/public/angular/services/saved_gis_map.js rename to x-pack/plugins/maps/public/routing/bootstrap/services/saved_gis_map.js index a0beaa768888a8..f24c7be65afa35 100644 --- a/x-pack/plugins/maps/public/angular/services/saved_gis_map.js +++ b/x-pack/plugins/maps/public/routing/bootstrap/services/saved_gis_map.js @@ -5,7 +5,7 @@ */ import _ from 'lodash'; -import { createSavedObjectClass } from '../../../../../../src/plugins/saved_objects/public'; +import { createSavedObjectClass } from '../../../../../../../src/plugins/saved_objects/public'; import { getTimeFilters, getMapZoom, @@ -15,11 +15,12 @@ import { getQuery, getFilters, getMapSettings, -} from '../../selectors/map_selectors'; -import { getIsLayerTOCOpen, getOpenTOCDetails } from '../../selectors/ui_selectors'; -import { copyPersistentState } from '../../reducers/util'; -import { extractReferences, injectReferences } from '../../../common/migrations/references'; -import { MAP_SAVED_OBJECT_TYPE } from '../../../common/constants'; +} from '../../../selectors/map_selectors'; +import { getIsLayerTOCOpen, getOpenTOCDetails } from '../../../selectors/ui_selectors'; +import { copyPersistentState } from '../../../reducers/util'; +import { extractReferences, injectReferences } from '../../../../common/migrations/references'; +import { MAP_BASE_URL, MAP_SAVED_OBJECT_TYPE } from '../../../../common/constants'; +import { getStore } from '../../store_operations'; export function createSavedGisMapClass(services) { const SavedObjectClass = createSavedObjectClass(services); @@ -73,14 +74,17 @@ export function createSavedGisMapClass(services) { }); this.showInRecentlyAccessed = true; } + getFullPath() { - return `/app/maps#map/${this.id}`; + return `${MAP_BASE_URL}/${this.id}`; } + getLayerList() { return this.layerListJSON ? JSON.parse(this.layerListJSON) : null; } - syncWithStore(state) { + syncWithStore() { + const state = getStore().getState(); const layerList = getLayerListRaw(state); const layerListConfigOnly = copyPersistentState(layerList); this.layerListJSON = JSON.stringify(layerListConfigOnly); diff --git a/x-pack/plugins/maps/public/routing/maps_router.js b/x-pack/plugins/maps/public/routing/maps_router.js new file mode 100644 index 00000000000000..840d4f2c669227 --- /dev/null +++ b/x-pack/plugins/maps/public/routing/maps_router.js @@ -0,0 +1,62 @@ +/* + * 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 { render, unmountComponentAtNode } from 'react-dom'; +import { Router, Switch, Route, Redirect } from 'react-router-dom'; +import { getCoreI18n } from '../kibana_services'; +import { createKbnUrlStateStorage } from '../../../../../src/plugins/kibana_utils/public'; +import { getStore } from './store_operations'; +import { Provider } from 'react-redux'; +import { LoadListAndRender } from './routes/list/load_list_and_render'; +import { LoadMapAndRender } from './routes/maps_app/load_map_and_render'; + +export let goToSpecifiedPath; +export let kbnUrlStateStorage; + +export async function renderApp(context, { appBasePath, element, history }) { + goToSpecifiedPath = (path) => history.push(path); + kbnUrlStateStorage = createKbnUrlStateStorage({ useHash: false, history }); + + render(, element); + + return () => { + unmountComponentAtNode(element); + }; +} + +const App = ({ history, appBasePath }) => { + const store = getStore(); + const I18nContext = getCoreI18n().Context; + + return ( + + + + + + + // Redirect other routes to list, or if hash-containing, their non-hash equivalents + { + if (hash) { + // Remove leading hash + const newPath = hash.substr(1); + return ; + } else if (pathname === '/' || pathname === '') { + return ; + } else { + return ; + } + }} + /> + + + + + ); +}; diff --git a/x-pack/plugins/maps/public/routing/page_elements/breadcrumbs.js b/x-pack/plugins/maps/public/routing/page_elements/breadcrumbs.js new file mode 100644 index 00000000000000..36a355719d945c --- /dev/null +++ b/x-pack/plugins/maps/public/routing/page_elements/breadcrumbs.js @@ -0,0 +1,60 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; +import { getCoreChrome } from '../../kibana_services'; +import { MAP_SAVED_OBJECT_TYPE } from '../../../common/constants'; +import _ from 'lodash'; +import { getLayerListRaw } from '../../selectors/map_selectors'; +import { copyPersistentState } from '../../reducers/util'; +import { getStore } from '../store_operations'; +import { goToSpecifiedPath } from '../maps_router'; + +function hasUnsavedChanges(savedMap, initialLayerListConfig) { + const state = getStore().getState(); + const layerList = getLayerListRaw(state); + const layerListConfigOnly = copyPersistentState(layerList); + + const savedLayerList = savedMap.getLayerList(); + + return !savedLayerList + ? !_.isEqual(layerListConfigOnly, initialLayerListConfig) + : // savedMap stores layerList as a JSON string using JSON.stringify. + // JSON.stringify removes undefined properties from objects. + // savedMap.getLayerList converts the JSON string back into Javascript array of objects. + // Need to perform the same process for layerListConfigOnly to compare apples to apples + // and avoid undefined properties in layerListConfigOnly triggering unsaved changes. + !_.isEqual(JSON.parse(JSON.stringify(layerListConfigOnly)), savedLayerList); +} + +export const updateBreadcrumbs = (savedMap, initialLayerListConfig, currentPath = '') => { + const isOnMapNow = currentPath.startsWith(`/${MAP_SAVED_OBJECT_TYPE}`); + const breadCrumbs = isOnMapNow + ? [ + { + text: i18n.translate('xpack.maps.mapController.mapsBreadcrumbLabel', { + defaultMessage: 'Maps', + }), + onClick: () => { + if (hasUnsavedChanges(savedMap, initialLayerListConfig)) { + const navigateAway = window.confirm( + i18n.translate('xpack.maps.breadCrumbs.unsavedChangesWarning', { + defaultMessage: `Your unsaved changes might not be saved`, + }) + ); + if (navigateAway) { + goToSpecifiedPath('/'); + } + } else { + goToSpecifiedPath('/'); + } + }, + }, + { text: savedMap.title }, + ] + : []; + getCoreChrome().setBreadcrumbs(breadCrumbs); +}; diff --git a/x-pack/plugins/maps/public/routing/page_elements/top_nav_menu/index.js b/x-pack/plugins/maps/public/routing/page_elements/top_nav_menu/index.js new file mode 100644 index 00000000000000..2575bbb9df9209 --- /dev/null +++ b/x-pack/plugins/maps/public/routing/page_elements/top_nav_menu/index.js @@ -0,0 +1,44 @@ +/* + * 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 { connect } from 'react-redux'; +import { MapsTopNavMenu } from './top_nav_menu'; +import { + enableFullScreen, + openMapSettings, + removePreviewLayers, + setRefreshConfig, + setSelectedLayer, + updateFlyout, +} from '../../../actions'; +import { FLYOUT_STATE } from '../../../reducers/ui'; +import { getInspectorAdapters } from '../../../reducers/non_serializable_instances'; +import { getFlyoutDisplay } from '../../../selectors/ui_selectors'; +import { hasDirtyState } from '../../../selectors/map_selectors'; + +function mapStateToProps(state = {}) { + return { + isOpenSettingsDisabled: getFlyoutDisplay(state) !== FLYOUT_STATE.NONE, + inspectorAdapters: getInspectorAdapters(state), + isSaveDisabled: hasDirtyState(state), + }; +} + +function mapDispatchToProps(dispatch) { + return { + closeFlyout: () => { + dispatch(setSelectedLayer(null)); + dispatch(updateFlyout(FLYOUT_STATE.NONE)); + dispatch(removePreviewLayers()); + }, + setRefreshStoreConfig: (refreshConfig) => dispatch(setRefreshConfig(refreshConfig)), + enableFullScreen: () => dispatch(enableFullScreen()), + openMapSettings: () => dispatch(openMapSettings()), + }; +} + +const connectedMapsTopNavMenu = connect(mapStateToProps, mapDispatchToProps)(MapsTopNavMenu); +export { connectedMapsTopNavMenu as MapsTopNavMenu }; diff --git a/x-pack/plugins/maps/public/routing/page_elements/top_nav_menu/top_nav_menu.js b/x-pack/plugins/maps/public/routing/page_elements/top_nav_menu/top_nav_menu.js new file mode 100644 index 00000000000000..762d61d6d33f9c --- /dev/null +++ b/x-pack/plugins/maps/public/routing/page_elements/top_nav_menu/top_nav_menu.js @@ -0,0 +1,279 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import { + getNavigation, + getCoreChrome, + getMapsCapabilities, + getInspector, + getToasts, + getCoreI18n, + getData, + getUiSettings, +} from '../../../kibana_services'; +import { + SavedObjectSaveModal, + showSaveModal, +} from '../../../../../../../src/plugins/saved_objects/public'; +import { MAP_SAVED_OBJECT_TYPE } from '../../../../common/constants'; +import { updateBreadcrumbs } from '../breadcrumbs'; +import { goToSpecifiedPath } from '../../maps_router'; + +export function MapsTopNavMenu({ + savedMap, + query, + onQueryChange, + onQuerySaved, + onSavedQueryUpdated, + savedQuery, + time, + refreshConfig, + setRefreshConfig, + setRefreshStoreConfig, + initialLayerListConfig, + indexPatterns, + updateFiltersAndDispatch, + isSaveDisabled, + closeFlyout, + enableFullScreen, + openMapSettings, + inspectorAdapters, + syncAppAndGlobalState, + currentPath, + isOpenSettingsDisabled, +}) { + const { TopNavMenu } = getNavigation().ui; + const { filterManager } = getData().query; + const showSaveQuery = getMapsCapabilities().saveQuery; + const onClearSavedQuery = () => { + onQuerySaved(undefined); + onQueryChange({ + filters: filterManager.getGlobalFilters(), + query: { + query: '', + language: getUiSettings().get('search:queryLanguage'), + }, + }); + }; + + // Nav settings + const config = getTopNavConfig( + savedMap, + initialLayerListConfig, + isOpenSettingsDisabled, + isSaveDisabled, + closeFlyout, + enableFullScreen, + openMapSettings, + inspectorAdapters, + currentPath + ); + + const submitQuery = function ({ dateRange, query }) { + onQueryChange({ + query, + time: dateRange, + refresh: true, + }); + }; + + const onRefreshChange = function ({ isPaused, refreshInterval }) { + const newRefreshConfig = { + isPaused, + interval: isNaN(refreshInterval) ? refreshConfig.interval : refreshInterval, + }; + setRefreshConfig(newRefreshConfig, () => { + setRefreshStoreConfig(newRefreshConfig); + syncAppAndGlobalState(); + }); + }; + + return ( + + ); +} + +function getTopNavConfig( + savedMap, + initialLayerListConfig, + isOpenSettingsDisabled, + isSaveDisabled, + closeFlyout, + enableFullScreen, + openMapSettings, + inspectorAdapters, + currentPath +) { + return [ + { + id: 'full-screen', + label: i18n.translate('xpack.maps.mapController.fullScreenButtonLabel', { + defaultMessage: `full screen`, + }), + description: i18n.translate('xpack.maps.mapController.fullScreenDescription', { + defaultMessage: `full screen`, + }), + testId: 'mapsFullScreenMode', + run() { + getCoreChrome().setIsVisible(false); + enableFullScreen(); + }, + }, + { + id: 'inspect', + label: i18n.translate('xpack.maps.mapController.openInspectorButtonLabel', { + defaultMessage: `inspect`, + }), + description: i18n.translate('xpack.maps.mapController.openInspectorDescription', { + defaultMessage: `Open Inspector`, + }), + testId: 'openInspectorButton', + run() { + getInspector().open(inspectorAdapters, {}); + }, + }, + { + id: 'mapSettings', + label: i18n.translate('xpack.maps.mapController.openSettingsButtonLabel', { + defaultMessage: `Map settings`, + }), + description: i18n.translate('xpack.maps.mapController.openSettingsDescription', { + defaultMessage: `Open map settings`, + }), + testId: 'openSettingsButton', + disableButton() { + return isOpenSettingsDisabled; + }, + run() { + openMapSettings(); + }, + }, + ...(getMapsCapabilities().save + ? [ + { + id: 'save', + label: i18n.translate('xpack.maps.mapController.saveMapButtonLabel', { + defaultMessage: `save`, + }), + description: i18n.translate('xpack.maps.mapController.saveMapDescription', { + defaultMessage: `Save map`, + }), + testId: 'mapSaveButton', + disableButton() { + return isSaveDisabled; + }, + tooltip() { + if (isSaveDisabled) { + return i18n.translate('xpack.maps.mapController.saveMapDisabledButtonTooltip', { + defaultMessage: 'Save or Cancel your layer changes before saving', + }); + } + }, + run: async () => { + const onSave = ({ + newTitle, + newCopyOnSave, + isTitleDuplicateConfirmed, + onTitleDuplicate, + }) => { + const currentTitle = savedMap.title; + savedMap.title = newTitle; + savedMap.copyOnSave = newCopyOnSave; + const saveOptions = { + confirmOverwrite: false, + isTitleDuplicateConfirmed, + onTitleDuplicate, + }; + return doSave( + savedMap, + saveOptions, + initialLayerListConfig, + closeFlyout, + currentPath + ).then((response) => { + // If the save wasn't successful, put the original values back. + if (!response.id || response.error) { + savedMap.title = currentTitle; + } + return response; + }); + }; + + const saveModal = ( + {}} + title={savedMap.title} + showCopyOnSave={!!savedMap.id} + objectType={MAP_SAVED_OBJECT_TYPE} + showDescription={false} + /> + ); + showSaveModal(saveModal, getCoreI18n().Context); + }, + }, + ] + : []), + ]; +} + +async function doSave(savedMap, saveOptions, initialLayerListConfig, closeFlyout, currentPath) { + closeFlyout(); + savedMap.syncWithStore(); + let id; + + try { + id = await savedMap.save(saveOptions); + getCoreChrome().docTitle.change(savedMap.title); + } catch (err) { + getToasts().addDanger({ + title: i18n.translate('xpack.maps.mapController.saveErrorMessage', { + defaultMessage: `Error on saving '{title}'`, + values: { title: savedMap.title }, + }), + text: err.message, + 'data-test-subj': 'saveMapError', + }); + return { error: err }; + } + + if (id) { + goToSpecifiedPath(`/map/${id}${window.location.hash}`); + updateBreadcrumbs(savedMap, initialLayerListConfig, currentPath); + + getToasts().addSuccess({ + title: i18n.translate('xpack.maps.mapController.saveSuccessMessage', { + defaultMessage: `Saved '{title}'`, + values: { title: savedMap.title }, + }), + 'data-test-subj': 'saveMapSuccess', + }); + } + return { id }; +} diff --git a/x-pack/plugins/maps/public/routing/routes/list/load_list_and_render.js b/x-pack/plugins/maps/public/routing/routes/list/load_list_and_render.js new file mode 100644 index 00000000000000..ee1307956071ac --- /dev/null +++ b/x-pack/plugins/maps/public/routing/routes/list/load_list_and_render.js @@ -0,0 +1,57 @@ +/* + * 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 { getMapsSavedObjectLoader } from '../../bootstrap/services/gis_map_saved_object_loader'; +import { getToasts } from '../../../kibana_services'; +import { i18n } from '@kbn/i18n'; +import { MapsListView } from './maps_list_view'; +import { Redirect } from 'react-router-dom'; + +export class LoadListAndRender extends React.Component { + state = { + mapsLoaded: false, + hasSavedMaps: null, + }; + + componentDidMount() { + this._isMounted = true; + this._loadMapsList(); + } + + componentWillUnmount() { + this._isMounted = false; + } + + async _loadMapsList() { + try { + const { hits = [] } = await getMapsSavedObjectLoader().find('', 1); + if (this._isMounted) { + this.setState({ mapsLoaded: true, hasSavedMaps: !!hits.length }); + } + } catch (err) { + if (this._isMounted) { + this.setState({ mapsLoaded: true, hasSavedMaps: false }); + getToasts().addDanger({ + title: i18n.translate('xpack.maps.mapListing.errorAttemptingToLoadSavedMaps', { + defaultMessage: `Unable to load maps`, + }), + text: `${err}`, + }); + } + } + } + + render() { + const { mapsLoaded, hasSavedMaps } = this.state; + + if (mapsLoaded) { + return hasSavedMaps ? : ; + } else { + return null; + } + } +} diff --git a/x-pack/plugins/maps/public/components/map_listing.js b/x-pack/plugins/maps/public/routing/routes/list/maps_list_view.js similarity index 86% rename from x-pack/plugins/maps/public/components/map_listing.js rename to x-pack/plugins/maps/public/routing/routes/list/maps_list_view.js index 030b67185c1063..a32bd00dbae510 100644 --- a/x-pack/plugins/maps/public/components/map_listing.js +++ b/x-pack/plugins/maps/public/routing/routes/list/maps_list_view.js @@ -5,10 +5,14 @@ */ import React from 'react'; -import PropTypes from 'prop-types'; import _ from 'lodash'; - -import { getToasts } from '../kibana_services'; +import { getMapsSavedObjectLoader } from '../../bootstrap/services/gis_map_saved_object_loader'; +import { + getMapsCapabilities, + getUiSettings, + getToasts, + getCoreChrome, +} from '../../../kibana_services'; import { EuiTitle, EuiFieldSearch, @@ -27,11 +31,14 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { addHelpMenuToAppChrome } from '../help_menu_util'; +import { addHelpMenuToAppChrome } from '../../../help_menu_util'; +import { Link } from 'react-router-dom'; +import { updateBreadcrumbs } from '../../page_elements/breadcrumbs'; +import { goToSpecifiedPath } from '../../maps_router'; export const EMPTY_FILTER = ''; -export class MapListing extends React.Component { +export class MapsListView extends React.Component { state = { hasInitialFetchReturned: false, isFetchingItems: false, @@ -42,10 +49,13 @@ export class MapListing extends React.Component { selectedIds: [], page: 0, perPage: 20, + readOnly: !getMapsCapabilities().save, + listingLimit: getUiSettings().get('savedObjects:listingLimit'), }; UNSAFE_componentWillMount() { this._isMounted = true; + updateBreadcrumbs(); } componentWillUnmount() { @@ -54,12 +64,21 @@ export class MapListing extends React.Component { } componentDidMount() { + this.initMapList(); + } + + async initMapList() { this.fetchItems(); addHelpMenuToAppChrome(); + getCoreChrome().docTitle.change('Maps'); } + _find = (search) => getMapsSavedObjectLoader().find(search, this.state.listingLimit); + + _delete = (ids) => getMapsSavedObjectLoader().delete(ids); + debouncedFetch = _.debounce(async (filter) => { - const response = await this.props.find(filter); + const response = await this._find(filter); if (!this._isMounted) { return; @@ -73,7 +92,7 @@ export class MapListing extends React.Component { isFetchingItems: false, items: response.hits, totalItems: response.total, - showLimitError: response.total > this.props.listingLimit, + showLimitError: response.total > this.state.listingLimit, }); } }, 300); @@ -89,7 +108,7 @@ export class MapListing extends React.Component { deleteSelectedItems = async () => { try { - await this.props.delete(this.state.selectedIds); + await this._delete(this.state.selectedIds); } catch (error) { getToasts().addDanger({ title: i18n.translate('xpack.maps.mapListing.unableToDeleteToastTitle', { @@ -211,11 +230,11 @@ export class MapListing extends React.Component { listingLimit setting prevents the table below from displaying more than {listingLimit}. + You can change this setting under " values={{ totalItems: this.state.totalItems, - listingLimit: this.props.listingLimit, + listingLimit: this.state.listingLimit, }} /> @@ -307,7 +326,10 @@ export class MapListing extends React.Component { sortable: true, render: (field, record) => ( { + e.preventDefault(); + goToSpecifiedPath(`/map/${record.id}`); + }} data-test-subj={`mapListingTitleLink-${record.title.split(' ').join('-')}`} > {field} @@ -331,7 +353,7 @@ export class MapListing extends React.Component { }; let selection = false; - if (!this.props.readOnly) { + if (!this.state.readOnly) { selection = { onSelectionChange: (selection) => { this.setState({ @@ -369,14 +391,16 @@ export class MapListing extends React.Component { renderListing() { let createButton; - if (!this.props.readOnly) { + if (!this.state.readOnly) { createButton = ( - - - + + + + + ); } return ( @@ -427,10 +451,3 @@ export class MapListing extends React.Component { ); } } - -MapListing.propTypes = { - readOnly: PropTypes.bool.isRequired, - find: PropTypes.func.isRequired, - delete: PropTypes.func.isRequired, - listingLimit: PropTypes.number.isRequired, -}; diff --git a/x-pack/plugins/maps/public/routing/routes/maps_app/index.js b/x-pack/plugins/maps/public/routing/routes/maps_app/index.js new file mode 100644 index 00000000000000..6b47ac6e0352af --- /dev/null +++ b/x-pack/plugins/maps/public/routing/routes/maps_app/index.js @@ -0,0 +1,67 @@ +/* + * 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 { connect } from 'react-redux'; +import { MapsAppView } from './maps_app_view'; +import { getFlyoutDisplay, getIsFullScreen } from '../../../selectors/ui_selectors'; +import { + getFilters, + getQueryableUniqueIndexPatternIds, + getRefreshConfig, +} from '../../../selectors/map_selectors'; +import { + replaceLayerList, + setGotoWithCenter, + setIsLayerTOCOpen, + setMapSettings, + setOpenTOCDetails, + setQuery, + setReadOnly, + setRefreshConfig, + setSelectedLayer, + updateFlyout, +} from '../../../actions'; +import { FLYOUT_STATE } from '../../../reducers/ui'; +import { getMapsCapabilities } from '../../../kibana_services'; + +function mapStateToProps(state = {}) { + return { + isFullScreen: getIsFullScreen(state), + nextIndexPatternIds: getQueryableUniqueIndexPatternIds(state), + flyoutDisplay: getFlyoutDisplay(state), + refreshConfig: getRefreshConfig(state), + filters: getFilters(state), + }; +} + +function mapDispatchToProps(dispatch) { + return { + dispatchSetQuery: (refresh, filters, query, time) => { + dispatch( + setQuery({ + filters, + query, + timeFilters: time, + refresh, + }) + ); + }, + setRefreshConfig: (refreshConfig) => dispatch(setRefreshConfig(refreshConfig)), + replaceLayerList: (layerList) => dispatch(replaceLayerList(layerList)), + setGotoWithCenter: (latLonZoom) => dispatch(setGotoWithCenter(latLonZoom)), + setMapSettings: (mapSettings) => dispatch(setMapSettings(mapSettings)), + setIsLayerTOCOpen: (isLayerTOCOpen) => dispatch(setIsLayerTOCOpen(isLayerTOCOpen)), + setOpenTOCDetails: (openTOCDetails) => dispatch(setOpenTOCDetails(openTOCDetails)), + clearUi: () => { + dispatch(setSelectedLayer(null)); + dispatch(updateFlyout(FLYOUT_STATE.NONE)); + dispatch(setReadOnly(!getMapsCapabilities().save)); + }, + }; +} + +const connectedMapsAppView = connect(mapStateToProps, mapDispatchToProps)(MapsAppView); +export { connectedMapsAppView as MapsAppView }; diff --git a/x-pack/plugins/maps/public/routing/routes/maps_app/load_map_and_render.js b/x-pack/plugins/maps/public/routing/routes/maps_app/load_map_and_render.js new file mode 100644 index 00000000000000..a17b83502e0481 --- /dev/null +++ b/x-pack/plugins/maps/public/routing/routes/maps_app/load_map_and_render.js @@ -0,0 +1,58 @@ +/* + * 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 { MapsAppView } from '.'; +import { getMapsSavedObjectLoader } from '../../bootstrap/services/gis_map_saved_object_loader'; +import { getToasts } from '../../../kibana_services'; +import { i18n } from '@kbn/i18n'; +import { Redirect } from 'react-router-dom'; + +export const LoadMapAndRender = class extends React.Component { + state = { + savedMap: null, + failedToLoad: false, + }; + + componentDidMount() { + this._isMounted = true; + this._loadSavedMap(); + } + + componentWillUnmount() { + this._isMounted = false; + } + + async _loadSavedMap() { + const { savedMapId } = this.props.match.params; + try { + const savedMap = await getMapsSavedObjectLoader().get(savedMapId); + if (this._isMounted) { + this.setState({ savedMap }); + } + } catch (err) { + if (this._isMounted) { + this.setState({ failedToLoad: true }); + getToasts().addWarning({ + title: i18n.translate('xpack.maps.loadMap.errorAttemptingToLoadSavedMap', { + defaultMessage: `Unable to load map`, + }), + text: `${err.message}`, + }); + } + } + } + + render() { + const { savedMap, failedToLoad } = this.state; + if (failedToLoad) { + return ; + } + + const currentPath = this.props.match.url; + return savedMap ? : null; + } +}; diff --git a/x-pack/plugins/maps/public/routing/routes/maps_app/maps_app_view.js b/x-pack/plugins/maps/public/routing/routes/maps_app/maps_app_view.js new file mode 100644 index 00000000000000..bf92f5a3371211 --- /dev/null +++ b/x-pack/plugins/maps/public/routing/routes/maps_app/maps_app_view.js @@ -0,0 +1,476 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import 'mapbox-gl/dist/mapbox-gl.css'; +import _ from 'lodash'; +import { DEFAULT_IS_LAYER_TOC_OPEN } from '../../../reducers/ui'; +import { + getIndexPatternService, + getToasts, + getData, + getUiSettings, + getCoreChrome, +} from '../../../kibana_services'; +import { copyPersistentState } from '../../../reducers/util'; +import { getInitialLayers } from '../../bootstrap/get_initial_layers'; +import rison from 'rison-node'; +import { getInitialTimeFilters } from '../../bootstrap/get_initial_time_filters'; +import { getInitialRefreshConfig } from '../../bootstrap/get_initial_refresh_config'; +import { getInitialQuery } from '../../bootstrap/get_initial_query'; +import { MapsTopNavMenu } from '../../page_elements/top_nav_menu'; +import { + getGlobalState, + updateGlobalState, + useGlobalStateSyncing, +} from '../../state_syncing/global_sync'; +import { AppStateManager } from '../../state_syncing/app_state_manager'; +import { useAppStateSyncing } from '../../state_syncing/app_sync'; +import { updateBreadcrumbs } from '../../page_elements/breadcrumbs'; +import { esFilters } from '../../../../../../../src/plugins/data/public'; +import { GisMap } from '../../../connected_components/gis_map'; + +export class MapsAppView extends React.Component { + _visibleSubscription = null; + _globalSyncUnsubscribe = null; + _globalSyncChangeMonitorSubscription = null; + _appSyncUnsubscribe = null; + _appStateManager = new AppStateManager(); + + constructor(props) { + super(props); + this.state = { + indexPatterns: [], + prevIndexPatternIds: [], + initialized: false, + isVisible: true, + savedQuery: '', + currentPath: '', + initialLayerListConfig: null, + }; + } + + componentDidMount() { + const { savedMap, currentPath } = this.props; + this.setState({ currentPath }); + + getCoreChrome().docTitle.change(savedMap.title); + getCoreChrome().recentlyAccessed.add(savedMap.getFullPath(), savedMap.title, savedMap.id); + + // Init sync utils + // eslint-disable-next-line react-hooks/rules-of-hooks + this._globalSyncUnsubscribe = useGlobalStateSyncing(); + // eslint-disable-next-line react-hooks/rules-of-hooks + this._appSyncUnsubscribe = useAppStateSyncing(this._appStateManager); + this._globalSyncChangeMonitorSubscription = getData().query.state$.subscribe( + this._updateFromGlobalState + ); + + // Check app state in case of refresh + const initAppState = this._appStateManager.getAppState(); + this._onQueryChange(initAppState); + if (initAppState.savedQuery) { + this._updateStateFromSavedQuery(initAppState.savedQuery); + } + + // Monitor visibility + this._visibleSubscription = getCoreChrome() + .getIsVisible$() + .subscribe((isVisible) => this.setState({ isVisible })); + this._initMap(); + } + + _initBreadcrumbUpdater = () => { + const { initialLayerListConfig, currentPath } = this.state; + updateBreadcrumbs(this.props.savedMap, initialLayerListConfig, currentPath); + }; + + componentDidUpdate(prevProps, prevState) { + const { currentPath: prevCurrentPath } = prevState; + const { currentPath, initialLayerListConfig } = this.state; + const { savedMap } = this.props; + if (savedMap && initialLayerListConfig && currentPath !== prevCurrentPath) { + updateBreadcrumbs(savedMap, initialLayerListConfig, currentPath); + } + // TODO: Handle null when converting to TS + this._handleStoreChanges(); + } + + _updateFromGlobalState = ({ changes, state: globalState }) => { + if (!changes || !globalState) { + return; + } + const newState = {}; + Object.keys(changes).forEach((key) => { + if (changes[key]) { + newState[key] = globalState[key]; + } + }); + + this.setState(newState, () => { + this._appStateManager.setQueryAndFilters({ + filters: getData().query.filterManager.getAppFilters(), + }); + const { time, filters, refreshInterval } = globalState; + this.props.dispatchSetQuery(refreshInterval, filters, this.state.query, time); + }); + }; + + componentWillUnmount() { + if (this._globalSyncUnsubscribe) { + this._globalSyncUnsubscribe(); + } + if (this._appSyncUnsubscribe) { + this._appSyncUnsubscribe(); + } + if (this._visibleSubscription) { + this._visibleSubscription.unsubscribe(); + } + if (this._globalSyncChangeMonitorSubscription) { + this._globalSyncChangeMonitorSubscription.unsubscribe(); + } + + // Clean up app state filters + const { filterManager } = getData().query; + filterManager.filters.forEach((filter) => { + if (filter.$state.store === esFilters.FilterStateStore.APP_STATE) { + filterManager.removeFilter(filter); + } + }); + } + + _getInitialLayersFromUrlParam() { + const locationSplit = window.location.href.split('?'); + if (locationSplit.length <= 1) { + return []; + } + const mapAppParams = new URLSearchParams(locationSplit[1]); + if (!mapAppParams.has('initialLayers')) { + return []; + } + + try { + let mapInitLayers = mapAppParams.get('initialLayers'); + if (mapInitLayers[mapInitLayers.length - 1] === '#') { + mapInitLayers = mapInitLayers.substr(0, mapInitLayers.length - 1); + } + return rison.decode_array(mapInitLayers); + } catch (e) { + getToasts().addWarning({ + title: i18n.translate('xpack.maps.initialLayers.unableToParseTitle', { + defaultMessage: `Initial layers not added to map`, + }), + text: i18n.translate('xpack.maps.initialLayers.unableToParseMessage', { + defaultMessage: `Unable to parse contents of 'initialLayers' parameter. Error: {errorMsg}`, + values: { errorMsg: e.message }, + }), + }); + return []; + } + } + + async _updateIndexPatterns(nextIndexPatternIds) { + const indexPatterns = []; + const getIndexPatternPromises = nextIndexPatternIds.map(async (indexPatternId) => { + try { + const indexPattern = await getIndexPatternService().get(indexPatternId); + indexPatterns.push(indexPattern); + } catch (err) { + // unable to fetch index pattern + } + }); + + await Promise.all(getIndexPatternPromises); + this.setState({ + indexPatterns, + }); + } + + _handleStoreChanges = () => { + const { prevIndexPatternIds } = this.state; + const { nextIndexPatternIds } = this.props; + + if (nextIndexPatternIds !== prevIndexPatternIds) { + this.setState({ prevIndexPatternIds: nextIndexPatternIds }); + this._updateIndexPatterns(nextIndexPatternIds); + } + }; + + _getAppStateFilters = () => { + return this._appStateManager.getFilters() || []; + }; + + _syncAppAndGlobalState = () => { + const { query, time, initialized } = this.state; + const { refreshConfig } = this.props; + const { filterManager } = getData().query; + + // appState + this._appStateManager.setQueryAndFilters({ + query: query, + filters: filterManager.getAppFilters(), + }); + + // globalState + const refreshInterval = { + pause: refreshConfig.isPaused, + value: refreshConfig.interval, + }; + updateGlobalState( + { + time: time, + refreshInterval, + filters: filterManager.getGlobalFilters(), + }, + !initialized + ); + this.setState({ refreshInterval }); + }; + + _onQueryChange = async ({ filters, query, time, refresh }) => { + const { filterManager } = getData().query; + const { dispatchSetQuery } = this.props; + const newState = {}; + let newFilters; + if (filters) { + filterManager.setFilters(filters); // Maps and merges filters + newFilters = filterManager.getFilters(); + } + if (query) { + newState.query = query; + } + if (time) { + newState.time = time; + } + this.setState(newState, () => { + this._syncAppAndGlobalState(); + dispatchSetQuery( + refresh, + newFilters || this.props.filters, + query || this.state.query, + time || this.state.time + ); + }); + }; + + _initQueryTimeRefresh() { + const { setRefreshConfig, savedMap } = this.props; + // TODO: Handle null when converting to TS + const globalState = getGlobalState(); + const mapStateJSON = savedMap ? savedMap.mapStateJSON : undefined; + const newState = { + query: getInitialQuery({ + mapStateJSON, + appState: this._appStateManager.getAppState(), + userQueryLanguage: getUiSettings().get('search:queryLanguage'), + }), + time: getInitialTimeFilters({ + mapStateJSON, + globalState, + }), + refreshConfig: getInitialRefreshConfig({ + mapStateJSON, + globalState, + }), + }; + this.setState({ query: newState.query, time: newState.time }); + updateGlobalState( + { + time: newState.time, + refreshInterval: { + value: newState.refreshConfig.interval, + pause: newState.refreshConfig.isPaused, + }, + }, + !this.state.initialized + ); + setRefreshConfig(newState.refreshConfig); + } + + _initMapAndLayerSettings() { + const { savedMap } = this.props; + // Get saved map & layer settings + this._initQueryTimeRefresh(); + + const layerList = getInitialLayers( + savedMap.layerListJSON, + this._getInitialLayersFromUrlParam() + ); + this.props.replaceLayerList(layerList); + this.setState( + { + initialLayerListConfig: copyPersistentState(layerList), + savedMap, + }, + this._initBreadcrumbUpdater + ); + } + + _updateFiltersAndDispatch = (filters) => { + this._onQueryChange({ + filters, + }); + }; + + _onRefreshChange = ({ isPaused, refreshInterval }) => { + const { refreshConfig } = this.props; + const newRefreshConfig = { + isPaused, + interval: isNaN(refreshInterval) ? refreshConfig.interval : refreshInterval, + }; + this.setState({ refreshConfig: newRefreshConfig }, this._syncAppAndGlobalState); + this.props.setRefreshConfig(newRefreshConfig); + }; + + _updateStateFromSavedQuery(savedQuery) { + if (!savedQuery) { + this.setState({ savedQuery: '' }); + return; + } + const { filterManager } = getData().query; + const savedQueryFilters = savedQuery.attributes.filters || []; + const globalFilters = filterManager.getGlobalFilters(); + const allFilters = [...savedQueryFilters, ...globalFilters]; + + if (savedQuery.attributes.timefilter) { + if (savedQuery.attributes.timefilter.refreshInterval) { + this._onRefreshChange({ + isPaused: savedQuery.attributes.timefilter.refreshInterval.pause, + refreshInterval: savedQuery.attributes.timefilter.refreshInterval.value, + }); + } + this._onQueryChange({ + filters: allFilters, + query: savedQuery.attributes.query, + time: savedQuery.attributes.timefilter, + }); + } else { + this._onQueryChange({ + filters: allFilters, + query: savedQuery.attributes.query, + }); + } + } + + _syncStoreAndGetFilters() { + const { + savedMap, + setGotoWithCenter, + setMapSettings, + setIsLayerTOCOpen, + setOpenTOCDetails, + } = this.props; + let savedObjectFilters = []; + if (savedMap.mapStateJSON) { + const mapState = JSON.parse(savedMap.mapStateJSON); + setGotoWithCenter({ + lat: mapState.center.lat, + lon: mapState.center.lon, + zoom: mapState.zoom, + }); + if (mapState.filters) { + savedObjectFilters = mapState.filters; + } + if (mapState.settings) { + setMapSettings(mapState.settings); + } + } + + if (savedMap.uiStateJSON) { + const uiState = JSON.parse(savedMap.uiStateJSON); + setIsLayerTOCOpen(_.get(uiState, 'isLayerTOCOpen', DEFAULT_IS_LAYER_TOC_OPEN)); + setOpenTOCDetails(_.get(uiState, 'openTOCDetails', [])); + } + return savedObjectFilters; + } + + async _initMap() { + const { clearUi, savedMap } = this.props; + // TODO: Handle null when converting to TS + const globalState = getGlobalState(); + this._initMapAndLayerSettings(); + clearUi(); + + const savedObjectFilters = this._syncStoreAndGetFilters(savedMap); + await this._onQueryChange({ + filters: [ + ..._.get(globalState, 'filters', []), + ...this._getAppStateFilters(), + ...savedObjectFilters, + ], + }); + this.setState({ initialized: true }); + } + + _renderTopNav() { + const { + query, + time, + savedQuery, + initialLayerListConfig, + isVisible, + indexPatterns, + currentPath, + } = this.state; + const { savedMap, refreshConfig } = this.props; + + return isVisible ? ( + { + this.setState( + { + refreshConfig: newConfig, + }, + callback + ); + }} + initialLayerListConfig={initialLayerListConfig} + indexPatterns={indexPatterns} + updateFiltersAndDispatch={this._updateFiltersAndDispatch} + onQuerySaved={(query) => { + this.setState({ savedQuery: query }); + this._appStateManager.setQueryAndFilters({ savedQuery: query }); + this._updateStateFromSavedQuery(query); + }} + onSavedQueryUpdated={(query) => { + this.setState({ savedQuery: { ...query } }); + this._appStateManager.setQueryAndFilters({ savedQuery: query }); + this._updateStateFromSavedQuery(query); + }} + syncAppAndGlobalState={this._syncAppAndGlobalState} + currentPath={currentPath} + /> + ) : null; + } + + render() { + const { filters, isFullScreen } = this.props; + + return this.state.initialized ? ( +
+ {this._renderTopNav()} +

{`screenTitle placeholder`}

+
+ { + newFilters.forEach((filter) => { + filter.$state = { store: esFilters.FilterStateStore.APP_STATE }; + }); + this._updateFiltersAndDispatch([...filters, ...newFilters]); + }} + /> +
+
+ ) : null; + } +} diff --git a/x-pack/plugins/maps/public/routing/state_syncing/app_state_manager.js b/x-pack/plugins/maps/public/routing/state_syncing/app_state_manager.js new file mode 100644 index 00000000000000..19118c6130805d --- /dev/null +++ b/x-pack/plugins/maps/public/routing/state_syncing/app_state_manager.js @@ -0,0 +1,44 @@ +/* + * 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 { Subject } from 'rxjs'; + +export class AppStateManager { + _query = ''; + _savedQuery = ''; + _filters = []; + + _updated$ = new Subject(); + + setQueryAndFilters({ query, savedQuery, filters }) { + if (this._query !== query) { + this._query = query; + } + if (this._savedQuery !== savedQuery) { + this._savedQuery = savedQuery; + } + if (this._filters !== filters) { + this._filters = filters; + } + this._updated$.next(); + } + + getQuery() { + return this._query; + } + + getFilters() { + return this._filters; + } + + getAppState() { + return { + query: this._query, + savedQuery: this._savedQuery, + filters: this._filters, + }; + } +} diff --git a/x-pack/plugins/maps/public/routing/state_syncing/app_sync.js b/x-pack/plugins/maps/public/routing/state_syncing/app_sync.js new file mode 100644 index 00000000000000..36b20174f2436d --- /dev/null +++ b/x-pack/plugins/maps/public/routing/state_syncing/app_sync.js @@ -0,0 +1,61 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { connectToQueryState, esFilters } from '../../../../../../src/plugins/data/public'; +import { syncState } from '../../../../../../src/plugins/kibana_utils/public'; +import { map } from 'rxjs/operators'; +import { getData } from '../../kibana_services'; +import { kbnUrlStateStorage } from '../maps_router'; + +export function useAppStateSyncing(appStateManager) { + // get appStateContainer + // sync app filters with app state container from data.query to state container + const { query } = getData(); + + const stateContainer = { + get: () => ({ + query: appStateManager.getQuery(), + filters: appStateManager.getFilters(), + }), + set: (state) => + state && appStateManager.setQueryAndFilters({ query: state.query, filters: state.filters }), + state$: appStateManager._updated$.pipe( + map(() => ({ + query: appStateManager.getQuery(), + filters: appStateManager.getFilters(), + })) + ), + }; + const stopSyncingQueryAppStateWithStateContainer = connectToQueryState(query, stateContainer, { + filters: esFilters.FilterStateStore.APP_STATE, + }); + + // sets up syncing app state container with url + const { start: startSyncingAppStateWithUrl, stop: stopSyncingAppStateWithUrl } = syncState({ + storageKey: '_a', + stateStorage: kbnUrlStateStorage, + stateContainer, + }); + + // merge initial state from app state container and current state in url + const initialAppState = { + ...stateContainer.get(), + ...kbnUrlStateStorage.get('_a'), + }; + // trigger state update. actually needed in case some data was in url + stateContainer.set(initialAppState); + + // set current url to whatever is in app state container + kbnUrlStateStorage.set('_a', initialAppState); + + // finally start syncing state containers with url + startSyncingAppStateWithUrl(); + + return () => { + stopSyncingQueryAppStateWithStateContainer(); + stopSyncingAppStateWithUrl(); + }; +} diff --git a/x-pack/plugins/maps/public/routing/state_syncing/global_sync.ts b/x-pack/plugins/maps/public/routing/state_syncing/global_sync.ts new file mode 100644 index 00000000000000..b466f254e4d08e --- /dev/null +++ b/x-pack/plugins/maps/public/routing/state_syncing/global_sync.ts @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { syncQueryStateWithUrl } from '../../../../../../src/plugins/data/public'; +import { getData } from '../../kibana_services'; +// @ts-ignore +import { kbnUrlStateStorage } from '../maps_router'; + +export function useGlobalStateSyncing() { + const { stop } = syncQueryStateWithUrl(getData().query, kbnUrlStateStorage); + return stop; +} + +export function getGlobalState() { + return kbnUrlStateStorage.get('_g'); +} + +export function updateGlobalState(newState: unknown, flushUrlState = false) { + const globalState = getGlobalState(); + kbnUrlStateStorage.set('_g', { + // @ts-ignore + ...globalState, + // @ts-ignore + ...newState, + }); + if (flushUrlState) { + kbnUrlStateStorage.flush({ replace: true }); + } +} diff --git a/x-pack/plugins/maps/public/routing/store_operations.js b/x-pack/plugins/maps/public/routing/store_operations.js new file mode 100644 index 00000000000000..53ebbb3328ff98 --- /dev/null +++ b/x-pack/plugins/maps/public/routing/store_operations.js @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { createMapStore } from '../reducers/store'; + +const store = createMapStore(); + +export const getStore = () => store; diff --git a/x-pack/plugins/maps/server/tutorials/ems/index.ts b/x-pack/plugins/maps/server/tutorials/ems/index.ts index 15dc403d942a36..e96af89e526851 100644 --- a/x-pack/plugins/maps/server/tutorials/ems/index.ts +++ b/x-pack/plugins/maps/server/tutorials/ems/index.ts @@ -6,6 +6,7 @@ import { i18n } from '@kbn/i18n'; import { TutorialsCategory } from '../../../../../../src/plugins/home/server'; +import { MAP_BASE_URL } from '../../../common/constants'; export function emsBoundariesSpecProvider({ emsLandingPageUrl, @@ -63,7 +64,7 @@ Indexing EMS administrative boundaries in Elasticsearch allows for search on bou 2. Click `Add layer`, then select `Upload GeoJSON`.\n\ 3. Upload the GeoJSON file and click `Import file`.', values: { - newMapUrl: prependBasePath('/app/maps#/map'), + newMapUrl: prependBasePath(MAP_BASE_URL), }, }), }, diff --git a/x-pack/plugins/security_solution/public/network/components/embeddables/embedded_map.tsx b/x-pack/plugins/security_solution/public/network/components/embeddables/embedded_map.tsx index 39b253439aff0d..6470fc270d0bf4 100644 --- a/x-pack/plugins/security_solution/public/network/components/embeddables/embedded_map.tsx +++ b/x-pack/plugins/security_solution/public/network/components/embeddables/embedded_map.tsx @@ -22,7 +22,8 @@ import { IndexPatternsMissingPrompt } from './index_patterns_missing_prompt'; import { MapToolTip } from './map_tool_tip/map_tool_tip'; import * as i18n from './translations'; import { SetQuery } from './types'; -import { MapEmbeddable } from '../../../../../../legacy/plugins/maps/public'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { MapEmbeddable } from '../../../../../../plugins/maps/public/embeddable'; import { Query, Filter } from '../../../../../../../src/plugins/data/public'; import { useKibana, useUiSetting$ } from '../../../common/lib/kibana'; diff --git a/x-pack/plugins/security_solution/public/network/components/embeddables/embedded_map_helpers.tsx b/x-pack/plugins/security_solution/public/network/components/embeddables/embedded_map_helpers.tsx index 558133104ac1e4..e50dcd7a8c8d83 100644 --- a/x-pack/plugins/security_solution/public/network/components/embeddables/embedded_map_helpers.tsx +++ b/x-pack/plugins/security_solution/public/network/components/embeddables/embedded_map_helpers.tsx @@ -13,9 +13,13 @@ import { getLayerList } from './map_config'; import { MAP_SAVED_OBJECT_TYPE } from '../../../../../maps/public'; import { MapEmbeddable, - RenderTooltipContentParams, MapEmbeddableInput, -} from '../../../../../../legacy/plugins/maps/public'; + // eslint-disable-next-line @kbn/eslint/no-restricted-paths +} from '../../../../../../plugins/maps/public/embeddable'; +import { + RenderTooltipContentParams, + // eslint-disable-next-line @kbn/eslint/no-restricted-paths +} from '../../../../../../plugins/maps/public/classes/tooltips/tooltip_property'; import * as i18n from './translations'; import { Query, Filter } from '../../../../../../../src/plugins/data/public'; import { diff --git a/x-pack/plugins/security_solution/public/network/components/embeddables/map_tool_tip/line_tool_tip_content.test.tsx b/x-pack/plugins/security_solution/public/network/components/embeddables/map_tool_tip/line_tool_tip_content.test.tsx index aef41ddaef5aee..648e52e3aba34a 100644 --- a/x-pack/plugins/security_solution/public/network/components/embeddables/map_tool_tip/line_tool_tip_content.test.tsx +++ b/x-pack/plugins/security_solution/public/network/components/embeddables/map_tool_tip/line_tool_tip_content.test.tsx @@ -13,8 +13,10 @@ import { SUM_OF_SERVER_BYTES, SUM_OF_SOURCE_BYTES, } from '../map_config'; -import { ITooltipProperty } from '../../../../../../maps/public'; -import { TooltipProperty } from '../../../../../../maps/public/classes/tooltips/tooltip_property'; +import { + ITooltipProperty, + TooltipProperty, +} from '../../../../../../maps/public/classes/tooltips/tooltip_property'; describe('LineToolTipContent', () => { const mockFeatureProps: ITooltipProperty[] = [ diff --git a/x-pack/plugins/security_solution/public/network/components/embeddables/map_tool_tip/line_tool_tip_content.tsx b/x-pack/plugins/security_solution/public/network/components/embeddables/map_tool_tip/line_tool_tip_content.tsx index 9238f7ec65a205..62d0e76287f2db 100644 --- a/x-pack/plugins/security_solution/public/network/components/embeddables/map_tool_tip/line_tool_tip_content.tsx +++ b/x-pack/plugins/security_solution/public/network/components/embeddables/map_tool_tip/line_tool_tip_content.tsx @@ -16,7 +16,8 @@ import { } from '../map_config'; import * as i18n from '../translations'; -import { ITooltipProperty } from '../../../../../../maps/public'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { ITooltipProperty } from '../../../../../../maps/public/classes/tooltips/tooltip_property'; const FlowBadge = (styled(EuiBadge)` height: 45px; diff --git a/x-pack/plugins/security_solution/public/network/components/embeddables/map_tool_tip/map_tool_tip.tsx b/x-pack/plugins/security_solution/public/network/components/embeddables/map_tool_tip/map_tool_tip.tsx index d2b8322c8121eb..d3ca23f74dfdff 100644 --- a/x-pack/plugins/security_solution/public/network/components/embeddables/map_tool_tip/map_tool_tip.tsx +++ b/x-pack/plugins/security_solution/public/network/components/embeddables/map_tool_tip/map_tool_tip.tsx @@ -17,7 +17,8 @@ import { LineToolTipContent } from './line_tool_tip_content'; import { PointToolTipContent } from './point_tool_tip_content'; import { Loader } from '../../../../common/components/loader'; import * as i18n from '../translations'; -import { ITooltipProperty } from '../../../../../../maps/public'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { ITooltipProperty } from '../../../../../../maps/public/classes/tooltips/tooltip_property'; export const MapToolTipComponent = ({ closeTooltip, diff --git a/x-pack/plugins/security_solution/public/network/components/embeddables/map_tool_tip/point_tool_tip_content.test.tsx b/x-pack/plugins/security_solution/public/network/components/embeddables/map_tool_tip/point_tool_tip_content.test.tsx index 36b9f44e196300..e7c9dcfdc9a535 100644 --- a/x-pack/plugins/security_solution/public/network/components/embeddables/map_tool_tip/point_tool_tip_content.test.tsx +++ b/x-pack/plugins/security_solution/public/network/components/embeddables/map_tool_tip/point_tool_tip_content.test.tsx @@ -11,8 +11,10 @@ import { TestProviders } from '../../../../common/mock'; import { getEmptyStringTag } from '../../../../common/components/empty_value'; import { HostDetailsLink, IPDetailsLink } from '../../../../common/components/links'; import { FlowTarget } from '../../../../graphql/types'; -import { ITooltipProperty } from '../../../../../../maps/public'; -import { TooltipProperty } from '../../../../../../maps/public/classes/tooltips/tooltip_property'; +import { + TooltipProperty, + ITooltipProperty, +} from '../../../../../../maps/public/classes/tooltips/tooltip_property'; describe('PointToolTipContent', () => { const mockFeatureProps: ITooltipProperty[] = [ diff --git a/x-pack/plugins/security_solution/public/network/components/embeddables/map_tool_tip/point_tool_tip_content.tsx b/x-pack/plugins/security_solution/public/network/components/embeddables/map_tool_tip/point_tool_tip_content.tsx index 3196e7d7a6c3ae..f38f3e054a645f 100644 --- a/x-pack/plugins/security_solution/public/network/components/embeddables/map_tool_tip/point_tool_tip_content.tsx +++ b/x-pack/plugins/security_solution/public/network/components/embeddables/map_tool_tip/point_tool_tip_content.tsx @@ -14,7 +14,8 @@ import { DescriptionListStyled } from '../../../../common/components/page'; import { HostDetailsLink, IPDetailsLink } from '../../../../common/components/links'; import { DefaultFieldRenderer } from '../../../../timelines/components/field_renderers/field_renderers'; import { FlowTarget } from '../../../../graphql/types'; -import { ITooltipProperty } from '../../../../../../maps/public'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { ITooltipProperty } from '../../../../../../maps/public/classes/tooltips/tooltip_property'; interface PointToolTipContentProps { contextId: string; diff --git a/x-pack/plugins/security_solution/public/network/components/embeddables/types.ts b/x-pack/plugins/security_solution/public/network/components/embeddables/types.ts index 9a49046634c3ba..f91fd677ba7fe8 100644 --- a/x-pack/plugins/security_solution/public/network/components/embeddables/types.ts +++ b/x-pack/plugins/security_solution/public/network/components/embeddables/types.ts @@ -4,7 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { RenderTooltipContentParams } from '../../../../../../legacy/plugins/maps/public'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { RenderTooltipContentParams } from '../../../../../maps/public/classes/tooltips/tooltip_property'; import { inputsModel } from '../../../common/store/inputs'; export interface IndexPatternMapping { diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 8d604c59daac95..fb8a4c3464d118 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -8971,10 +8971,7 @@ "xpack.maps.addLayerPanel.footer.cancelButtonLabel": "キャンセル", "xpack.maps.addLayerPanel.importFile": "ファイルのインポート", "xpack.maps.aggs.defaultCountLabel": "カウント", - "xpack.maps.appDescription": "マップアプリケーション", "xpack.maps.appTitle": "マップ", - "xpack.maps.badge.readOnly.text": "読み込み専用", - "xpack.maps.badge.readOnly.tooltip": "マップを保存できませんで", "xpack.maps.blendedVectorLayer.clusteredLayerName": "クラスター化 {displayName}", "xpack.maps.common.esSpatialRelation.containsLabel": "contains", "xpack.maps.common.esSpatialRelation.disjointLabel": "disjoint", @@ -9105,7 +9102,6 @@ "xpack.maps.mapController.saveMapDescription": "マップを保存", "xpack.maps.mapController.saveMapDisabledButtonTooltip": "保存する前に、レイヤーの変更を保存するか、キャンセルしてください", "xpack.maps.mapController.saveSuccessMessage": "「{title}」が保存されました", - "xpack.maps.mapController.unsavedChangesWarning": "保存されていない変更は保存されない可能性があります", "xpack.maps.mapEmbeddableFactory.invalidLayerList": "不正な形式のレイヤーリストによりマップを読み込めません", "xpack.maps.mapEmbeddableFactory.invalidSavedObject": "不正な形式の保存済みオブジェクトによりマップを読み込めません", "xpack.maps.mapListing.advancedSettingsLinkText": "高度な設定", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 5dbe9e5ccc07e7..3fe58b62c00c33 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -8975,10 +8975,7 @@ "xpack.maps.addLayerPanel.footer.cancelButtonLabel": "鍙栨秷", "xpack.maps.addLayerPanel.importFile": "导入文件", "xpack.maps.aggs.defaultCountLabel": "计数", - "xpack.maps.appDescription": "地图应用程序", "xpack.maps.appTitle": "Maps", - "xpack.maps.badge.readOnly.text": "只读", - "xpack.maps.badge.readOnly.tooltip": "无法保存地图", "xpack.maps.blendedVectorLayer.clusteredLayerName": "集群 {displayName}", "xpack.maps.common.esSpatialRelation.containsLabel": "contains", "xpack.maps.common.esSpatialRelation.disjointLabel": "disjoint", @@ -9109,7 +9106,6 @@ "xpack.maps.mapController.saveMapDescription": "保存地图", "xpack.maps.mapController.saveMapDisabledButtonTooltip": "保存或在保存之前取消您的图层更改", "xpack.maps.mapController.saveSuccessMessage": "已保存“{title}”", - "xpack.maps.mapController.unsavedChangesWarning": "可能不会保存您未保存的更改", "xpack.maps.mapEmbeddableFactory.invalidLayerList": "无法加载地图,图层列表格式不正确", "xpack.maps.mapEmbeddableFactory.invalidSavedObject": "无法加载地图,已保存对象格式错误", "xpack.maps.mapListing.advancedSettingsLinkText": "高级设置", diff --git a/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/embeddables/embedded_map.tsx b/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/embeddables/embedded_map.tsx index 648418c02489a3..85ac0163333630 100644 --- a/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/embeddables/embedded_map.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/embeddables/embedded_map.tsx @@ -11,7 +11,8 @@ import { createPortalNode, InPortal, OutPortal } from 'react-reverse-portal'; import { MapEmbeddable, MapEmbeddableInput, -} from '../../../../../../../../legacy/plugins/maps/public'; + // eslint-disable-next-line @kbn/eslint/no-restricted-paths +} from '../../../../../../../maps/public/embeddable'; import * as i18n from './translations'; import { GeoPoint } from '../../../../../../common/runtime_types'; import { getLayerList } from './map_config'; @@ -23,7 +24,8 @@ import { } from '../../../../../../../../../src/plugins/embeddable/public'; import { MAP_SAVED_OBJECT_TYPE } from '../../../../../../../maps/public'; import { MapToolTipComponent } from './map_tool_tip'; -import { RenderTooltipContentParams } from '../../../../../../../../legacy/plugins/maps/public'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { RenderTooltipContentParams } from '../../../../../../../maps/public/classes/tooltips/tooltip_property'; export interface EmbeddedMapProps { upPoints: LocationPoint[]; diff --git a/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/embeddables/map_tool_tip.tsx b/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/embeddables/map_tool_tip.tsx index 0d54d91007a8d4..bf403846dcec46 100644 --- a/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/embeddables/map_tool_tip.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/embeddables/map_tool_tip.tsx @@ -21,7 +21,8 @@ import { AppState } from '../../../../../state'; import { monitorLocationsSelector } from '../../../../../state/selectors'; import { useMonitorId } from '../../../../../hooks'; import { MonitorLocation } from '../../../../../../common/runtime_types/monitor'; -import { RenderTooltipContentParams } from '../../../../../../../../legacy/plugins/maps/public'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { RenderTooltipContentParams } from '../../../../../../../maps/public/classes/tooltips/tooltip_property'; import { formatAvailabilityValue } from '../../availability_reporting/availability_reporting'; import { LastCheckLabel } from '../../translations'; diff --git a/x-pack/test/functional/apps/maps/saved_object_management.js b/x-pack/test/functional/apps/maps/saved_object_management.js index 62b9286d494bad..810df8e9950643 100644 --- a/x-pack/test/functional/apps/maps/saved_object_management.js +++ b/x-pack/test/functional/apps/maps/saved_object_management.js @@ -77,9 +77,9 @@ export default function ({ getPageObjects, getService }) { it('should override query stored with map when query is provided in app state', async () => { const currentUrl = await browser.getCurrentUrl(); - const kibanaBaseUrl = currentUrl.substring(0, currentUrl.indexOf('#')); + const kibanaBaseUrl = currentUrl.substring(0, currentUrl.indexOf('/maps/')); const appState = `_a=(query:(language:kuery,query:'machine.os.raw%20:%20"win%208"'))`; - const urlWithQueryInAppState = `${kibanaBaseUrl}#/map/8eabdab0-144f-11e9-809f-ad25bb78262c?${appState}`; + const urlWithQueryInAppState = `${kibanaBaseUrl}/maps/map/8eabdab0-144f-11e9-809f-ad25bb78262c#?${appState}`; await browser.get(urlWithQueryInAppState, true); await PageObjects.maps.waitForLayersToLoad(); diff --git a/x-pack/test/functional/page_objects/gis_page.js b/x-pack/test/functional/page_objects/gis_page.js index c3862379be904f..93b9d9b4b3f7ba 100644 --- a/x-pack/test/functional/page_objects/gis_page.js +++ b/x-pack/test/functional/page_objects/gis_page.js @@ -5,6 +5,7 @@ */ import _ from 'lodash'; +import { APP_ID } from '../../../plugins/maps/common/constants'; export function GisPageProvider({ getService, getPageObjects }) { const PageObjects = getPageObjects(['common', 'header', 'timePicker']); @@ -159,7 +160,7 @@ export function GisPageProvider({ getService, getPageObjects }) { async onMapListingPage() { log.debug(`onMapListingPage`); - const exists = await testSubjects.exists('mapsListingPage'); + const exists = await testSubjects.exists('mapsListingPage', { timeout: 3500 }); return exists; } @@ -197,7 +198,7 @@ export function GisPageProvider({ getService, getPageObjects }) { const onPage = await this.onMapListingPage(); if (!onPage) { await retry.try(async () => { - await PageObjects.common.navigateToUrl('maps', '/', { basePath: this.basePath }); + await PageObjects.common.navigateToUrlWithBrowserHistory(APP_ID, '/'); const onMapListingPage = await this.onMapListingPage(); if (!onMapListingPage) throw new Error('Not on map listing page.'); }); @@ -209,8 +210,8 @@ export function GisPageProvider({ getService, getPageObjects }) { log.debug(`getMapCountWithName: ${name}`); await this.searchForMapWithName(name); - const links = await find.allByLinkText(name); - return links.length; + const buttons = await find.allByButtonText(name); + return buttons.length; } async isSetViewPopoverOpen() { From 82ce718b013b88537b7f3a62d5bad105f28503c6 Mon Sep 17 00:00:00 2001 From: Alison Goryachev Date: Thu, 18 Jun 2020 15:15:15 -0400 Subject: [PATCH 2/6] [CCR] Fix follower indices table not updating after pausing (#69228) --- .../components/follower_indices_table/follower_indices_table.js | 2 +- .../public/app/store/actions/follower_index.js | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/x-pack/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/components/follower_indices_table/follower_indices_table.js b/x-pack/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/components/follower_indices_table/follower_indices_table.js index 183609355fdfd7..0c57b3f7330cfd 100644 --- a/x-pack/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/components/follower_indices_table/follower_indices_table.js +++ b/x-pack/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/components/follower_indices_table/follower_indices_table.js @@ -66,7 +66,7 @@ export class FollowerIndicesTable extends PureComponent { if (prevFollowerIndices !== followerIndices) { return { prevFollowerIndices: followerIndices, - filteredClusters: getFilteredIndices(followerIndices, queryText), + filteredIndices: getFilteredIndices(followerIndices, queryText), }; } diff --git a/x-pack/plugins/cross_cluster_replication/public/app/store/actions/follower_index.js b/x-pack/plugins/cross_cluster_replication/public/app/store/actions/follower_index.js index 1af5a95a29b983..d7f03931474ee8 100644 --- a/x-pack/plugins/cross_cluster_replication/public/app/store/actions/follower_index.js +++ b/x-pack/plugins/cross_cluster_replication/public/app/store/actions/follower_index.js @@ -149,7 +149,6 @@ export const resumeFollowerIndex = (id) => scope, handler: async () => resumeFollowerIndexRequest(id), onSuccess(response, dispatch) { - console.log('response', response); /** * We can have 1 or more follower index resume operation * that can fail or succeed. We will show 1 toast notification for each. From b2a2aff75e6880ddc1543a7151077fdff56ff238 Mon Sep 17 00:00:00 2001 From: Aleh Zasypkin Date: Thu, 18 Jun 2020 21:22:48 +0200 Subject: [PATCH 3/6] Document authentication settings. (#69284) Co-authored-by: Kaarina Tungseth Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> --- docs/settings/security-settings.asciidoc | 147 ++++++++++++++++++ docs/user/security/access-agreement.asciidoc | 27 ++++ docs/user/security/audit-logging.asciidoc | 2 +- .../security/authentication/index.asciidoc | 19 ++- .../user/security/images/access-agreement.png | Bin 0 -> 62680 bytes docs/user/security/images/kibana-login.jpg | Bin 22181 -> 0 bytes docs/user/security/images/kibana-login.png | Bin 0 -> 33169 bytes docs/user/security/securing-kibana.asciidoc | 1 + 8 files changed, 192 insertions(+), 4 deletions(-) create mode 100644 docs/user/security/access-agreement.asciidoc create mode 100644 docs/user/security/images/access-agreement.png delete mode 100644 docs/user/security/images/kibana-login.jpg create mode 100644 docs/user/security/images/kibana-login.png diff --git a/docs/settings/security-settings.asciidoc b/docs/settings/security-settings.asciidoc index 2957c9c27b624f..dc7f585f3e4c35 100644 --- a/docs/settings/security-settings.asciidoc +++ b/docs/settings/security-settings.asciidoc @@ -30,6 +30,150 @@ You do not need to configure any additional settings to use the |=== +[float] +[[authentication-security-settings]] +==== Authentication security settings + +You configure authentication settings in the `xpack.security.authc` namespace in `kibana.yml`. + +For example: + +[source,yaml] +---------------------------------------- +xpack.security.authc: + providers: + basic.basic1: <1> + order: 0 <2> + ... + + saml.saml1: <3> + order: 1 + ... + + saml.saml2: <4> + order: 2 + ... + + pki.realm3: + order: 3 + ... + ... +---------------------------------------- +<1> Specifies the type of authentication provider (for example, `basic`, `token`, `saml`, `oidc`, `kerberos`, `pki`) and the provider name. This setting is mandatory. +<2> Specifies the order of the provider in the authentication chain and on the Login Selector UI. This setting is mandatory. +<3> Specifies the settings for the SAML authentication provider with a `saml1` name. +<4> Specifies the settings for the SAML authentication provider with a `saml2` name. + +The valid settings in the `xpack.security.authc.providers` namespace vary depending on the authentication provider type. For more information, refer to <>. + +[float] +[[authentication-provider-settings]] +===== Valid settings for all authentication providers + +[cols="2*<"] +|=== +| `xpack.security.authc.providers.` +`..enabled` +| Determines if the authentication provider should be enabled. By default, {kib} enables the provider as soon as you configure any of its properties. + +| `xpack.security.authc.providers.` +`..order` +| Order of the provider in the authentication chain and on the Login Selector UI. + +| `xpack.security.authc.providers.` +`..description` +| Custom description of the provider entry displayed on the Login Selector UI. + +| `xpack.security.authc.providers.` +`..hint` +| Custom hint for the provider entry displayed on the Login Selector UI. + +| `xpack.security.authc.providers.` +`..icon` +| Custom icon for the provider entry displayed on the Login Selector UI. + +| `xpack.security.authc.providers.` +`..showInSelector` +| Flag that indicates if the provider should have an entry on the Login Selector UI. Setting this to `false` doesn't remove the provider from the authentication chain. + +|=== + +[NOTE] +============ +You are unable to set this setting to `false` for `basic` and `token` authentication providers. +============ + +[cols="2*<"] +|=== + +| `xpack.security.authc.providers.` +`..accessAgreement.message` +| Access agreement text in Markdown format. For more information, refer to <>. + +|=== + +[float] +[[saml-authentication-provider-settings]] +===== SAML authentication provider settings + +In addition to <>, you can specify the following settings: + +[cols="2*<"] +|=== +| `xpack.security.authc.providers.` +`saml..realm` +| SAML realm in {es} that provider should use. + +| `xpack.security.authc.providers.` +`saml..maxRedirectURLSize` +| The maximum size of the URL that {kib} is allowed to store during the authentication SAML handshake. For more information, refer to <>. + +|=== + +[float] +[[oidc-authentication-provider-settings]] +===== OpenID Connect authentication provider settings + +In addition to <>, you can specify the following settings: + +[cols="2*<"] +|=== +| `xpack.security.authc.providers.` +`oidc..realm` +| OpenID Connect realm in {es} that the provider should use. + +|=== + +[float] +[[http-authentication-settings]] +===== HTTP authentication settings + +There is a very limited set of cases when you'd want to change these settings. For more information, refer to <>. + +[cols="2*<"] +|=== +| `xpack.security.authc.http.enabled` +| Determines if HTTP authentication should be enabled. By default, this setting is set to `true`. + +| `xpack.security.authc.http.autoSchemesEnabled` +| Determines if HTTP authentication schemes used by the enabled authentication providers should be automatically supported during HTTP authentication. By default, this setting is set to `true`. + +| `xpack.security.authc.http.schemes` +| List of HTTP authentication schemes that {kib} HTTP authentication should support. By default, this setting is set to `['apikey']` to support HTTP authentication with <> scheme. + +|=== + +[float] +[[login-selector-settings]] +===== Login Selector UI settings + +[cols="2*<"] +|=== +| `xpack.security.authc.selector.enabled` +| Determines if the Login Selector UI should be enabled. By default, this setting is set to `true` if more than one authentication provider is configured. + +|=== + [float] [[security-ui-settings]] ==== User interface security settings @@ -96,4 +240,7 @@ string of `[ms|s|m|h|d|w|M|Y]` (e.g. '70ms', '5s', '3d', '1Y'). | `xpack.security.loginAssistanceMessage` | Adds a message to the login screen. Useful for displaying information about maintenance windows, links to corporate sign up pages etc. +| `xpack.security.loginHelp` + | Adds a message accessible at the Login Selector UI with additional help information for the login process. + |=== diff --git a/docs/user/security/access-agreement.asciidoc b/docs/user/security/access-agreement.asciidoc new file mode 100644 index 00000000000000..362eb805012103 --- /dev/null +++ b/docs/user/security/access-agreement.asciidoc @@ -0,0 +1,27 @@ +[role="xpack"] +[[xpack-security-access-agreement]] +=== Access agreement + +Some work environments require you to acknowledge and accept an agreement before you can access {kib}, which can contain sensitive information. The agreement text supports Markdown format and can be specified using the `xpack.security.authc.providers...accessAgreement.message` setting. + +[NOTE] +============================================================================ +You need to acknowledge the access agreement only once per session, and {kib} reports the acknowledgement in the audit logs. +============================================================================ + +Here is how your `kibana.yml` can look like if you define an access agreement: + +[source,yaml] +-------------------------------------------------------------------------------- +xpack.security.authc.providers: + basic.basic1: + order: 0 + accessAgreement: + message: "**You are accessing a system with a sensitive information** \n\n + By logging in, you acknowledge that (shortened ...)" +-------------------------------------------------------------------------------- + +When you authenticate using `basic.basic1`, you'll see the following agreement that you must acknowledge before you can access {kib}: + +[role="screenshot"] +image::user/security/images/access-agreement.png["Access Agreement UI"] diff --git a/docs/user/security/audit-logging.asciidoc b/docs/user/security/audit-logging.asciidoc index f72ae0dcf9c934..a7359af38c1cb2 100644 --- a/docs/user/security/audit-logging.asciidoc +++ b/docs/user/security/audit-logging.asciidoc @@ -1,6 +1,6 @@ [role="xpack"] [[xpack-security-audit-logging]] -=== Audit Logging +=== Audit logs You can enable auditing to keep track of security-related events such as authorization success and failures. Logging these events enables you diff --git a/docs/user/security/authentication/index.asciidoc b/docs/user/security/authentication/index.asciidoc index 4c0e863b05d315..fe93e38151b826 100644 --- a/docs/user/security/authentication/index.asciidoc +++ b/docs/user/security/authentication/index.asciidoc @@ -17,22 +17,29 @@ Enable multiple authentication mechanisms at the same time specifying a prioritized list of the authentication _providers_ (typically of various types) in the configuration. Providers are consulted in ascending order. Make sure each configured provider has a unique name (e.g. `basic1` or `saml1` in the configuration example) and `order` setting. In the event that two or more providers have the same name or `order`, {kib} will fail to start. -When two or more providers are configured, you can choose the provider you want to use on the Login Selector UI. The order the providers appear is determined by the order setting. The appearance of the specific provider entry can be customized with the `description` setting. +When two or more providers are configured, you can choose the provider you want to use on the Login Selector UI. The order the providers appear is determined by the `order` setting. The appearance of the specific provider entry can be customized with the `description`, `hint`, and `icon` settings. + +TIP: To provide login instructions to users, use the `xpack.security.loginHelp` setting, which supports Markdown format. When you specify the `xpack.security.loginHelp` setting, the Login Selector UI displays a `Need help?` link that lets users access login help information. If you don't want a specific provider to show up at the Login Selector UI (e.g. to only support third-party initiated login) you can hide it with `showInSelector` setting set to `false`. However, in this case, the provider is presented in the provider chain and may be consulted during authentication based on its `order`. To disable the provider, use the `enabled` setting. TIP: The Login Selector UI can also be disabled or enabled with `xpack.security.authc.selector.enabled` setting. -Here is how your `kibana.yml` can look like if you deal with multiple authentication providers: +Here is how your `kibana.yml` and Login Selector UI can look like if you deal with multiple authentication providers: +[source,yaml] -------------------------------------------------------------------------------- +xpack.security.loginHelp: "**Help** info with a [link](...)" xpack.security.authc.providers: basic.basic1: order: 0 + icon: "logoElasticsearch" + hint: "Typically for administrators" saml.saml1: order: 1 realm: saml1 description: "Log in with SSO" + icon: "https://my-company.xyz/saml-logo.svg" saml.saml2: order: 2 realm: saml2 @@ -42,6 +49,11 @@ xpack.security.authc.providers: enabled: false -------------------------------------------------------------------------------- +[role="screenshot"] +image::user/security/images/kibana-login.png["Login Selector UI"] + +For more information, refer to <>. + [[basic-authentication]] ==== Basic authentication @@ -170,6 +182,7 @@ Basic authentication is supported _only_ if the `basic` authentication provider To support basic authentication for the applications like `curl` or when the `Authorization: Basic base64(username:password)` HTTP header is included in the request (for example, by reverse proxy), add `Basic` scheme to the list of supported schemes for the <>. [float] +[[security-saml-and-long-urls]] ===== SAML and long URLs At the beginning of the SAML handshake, {kib} stores the initial URL in the session cookie, so it can redirect the user back to that URL after successful SAML authentication. @@ -325,4 +338,4 @@ NOTE: Don't forget to explicitly specify default `apikey` scheme when you just w xpack.security.authc.http.schemes: [apikey, basic, something-custom] -------------------------------------------------------------------------------- -With this configuration, you can send requests to {kib} with the `Authorization` header using `ApiKey`, `Basic` or `Something-Custom` HTTP schemes (case insensitive). Under the hood, {kib} relays this header to {es}, then {es} authenticates the request using the credentials in the header. \ No newline at end of file +With this configuration, you can send requests to {kib} with the `Authorization` header using `ApiKey`, `Basic` or `Something-Custom` HTTP schemes (case insensitive). Under the hood, {kib} relays this header to {es}, then {es} authenticates the request using the credentials in the header. diff --git a/docs/user/security/images/access-agreement.png b/docs/user/security/images/access-agreement.png new file mode 100644 index 0000000000000000000000000000000000000000..ecb6122875cb85165519090a642f76b88de1f483 GIT binary patch literal 62680 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
    +;%#z{|m9wZ`V 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 literal 0 HcmV?d00001 diff --git a/docs/user/security/images/kibana-login.jpg b/docs/user/security/images/kibana-login.jpg deleted file mode 100644 index 9a1916d5217dc61faf725bbaee5a60c25f75f0c7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 22181 zcmeHv2|QHY`}mclk|u>hOhu6;C8Cm>im|+sgrZD4l2j@Yu1Lx@p@_;@Lqdx!l__Mc z#=aJ25Lsuev)=h1y>D;o?fd<{-{trH{r~^Zd$}{`+`0Fh^E~G{&-0vTxx6M`2eiOQ z-%uaoOTGrrzE<6$iS*D~}D{0{L3 zA+25Z&gYz6?43_9Tfb&4q_xe^Wad=s;P_Hr`K7p^PCBTW06m%c*?Z3I0me2j-YZCC zHuP@BLw>#$&Rr@OgsYp_w8A3zx6cowdm9kl>0lqH32z<7Ufld;D5#cQs9J{o%7dbLNUKULqm6 za+SQoYDIO84VqdTH*Mdcr;jr*G&0{~VQIB@pY@TW$Bx_CJDhMicm9IwMK^cfD}Gn~ zuU!ua3%_|QBJy@rbbLZ$(*5L=)CW(VW@YE(<~_?VEGm9eQd(ACQBzx2-_Y39+(Myt zc6ER1>HXYC9~m7RpO|DYSyONUeEtd+`1}>JAK(%JaLt%GlYge*6kL2WT&DmRnJKV* z-K>SWW`c*#ELx#V{XvUX=e+HN(ets}hGr=JsC@^&h&KCS~%>M0|`{fXtI)r~ec%TtJ&<1#T7WfyQ zBRJ>x-~M!g_Ysh#60ZfC#}5{S2)_sfLz76$=7o#|eVu5JP97wy;=$|=m>h0dFht*Q z1NmHna3!-lvim7K$Pe3}2EX)BHs+eY0yQ4=`K;{78=1YS3h5c1+)!+U!pIv#y~RQ+ zU}dxJfUtly;yb+dCRB>86y2|Ujq?~6p=hL#w@-w5n4rAprrGHyzdamJIg0BJ(0;tz zOs}x)TFJUSYjz!^Tl**3eFzbjimErhNl|TFoI@O+bmBn?VFlL+L#kC)WUEf@hDNkt zkTiDIs_HQhO2fxyRxvP?%zi94j|Zvg+>2lhnVf1OyuZd$h$n4@FZ%>2#LoENI16GJ znyXFc7{H%dNLTi#u%|37_%X#nkgM~K6H(w20DqK4S2EWRYj5H~p@}>QAa^VU%d48; zK`nx)zLPxq;y4egPO}xU9cFLkL1pj2O^reAEDgs(rN$y2G#75PMBEwsc#vF-AV-x4 z8Q(DT)$B)@!ef@3U(8klS+SmLT8ZVTE`(WV-I#d(kr}}%L(T}0STy>U=fkUug8NBIUYn!gPpMb=u#%a zxZZ5XVG}=MQ3HnQtRPAD(;x0k0feGU>p{J%k@j+!@nB09lF2yDgRb3+K*-Oy)jX(+ zaD`_7I`cV9+2yg32gR!6Cr3gb~IimKq0dV({VaJKmq#UD3}`^Yg4&-LEOWz@8(u=>0J zWf!gI*aPZVNkRo*qw2_aRSi~@-bEfnU*MwJ<^O>PF=am^)VW>A=tL$efJL`*p8hZ} z(9-t<9ajs0smng$%Q4RYf^~V&0xN((Vm3lk0CmA`B4<5ZLSW6X;Xxf9*iPK{Reb7A zGae)&sD>JNl6g=VhT-!J%i*T+pvyQObOToAL6j&k=&FTaHuyVukR<#=o%>5XfpKkP zH@D>Jut2$n8z7G|=o zc+d<24_fwTaF6|l`DI{fD~0kPatnzeiZJ;{$fyLd6UXIK;XyPV41nUhYGz#pK=3u+ zC`3-DhX<7la&-3-P_KG0&;-Efun`FD?G(Ct=Qsmo(f|}c)V=PT;W#^%Rx+1Q!l(iK z*R20F>p!mT+jRVE=j#P?Ylt@V`2j~B`9~$^MsN1>b3b)sxpEqj`kMdQ4ATC7OmWqA z7s|WcZIqdz#>Y;FJXDjg-Y`?d<`74>l)d-CjlkgXCl2VzPbC+XZeE(ho?9fLh{;zM=yM0fE`Kk@n= ztAM<>HDz(`FjYKy@Om8i>2*;gAIC8sv1_~N3bZmH87~8c%M9jKPh!5;3NW_xAGCtk z{l9Akmo5U9^}AMJ)e9H3GZ%2S@Sua0c&@n8D3zp(${=O_lwtNPYaZ15*r=fN9+kkp zg{6B14cNyAbi)h5@V&o3A98Ih9#yON2V0rn)O9-P*X7bQ|COs8P0&U69A$O+wz-47 zp@E%P@y`YGs#i8>EXpH<&DJd^X_jb_N>8_%mQ>}!;4NRy8`bGliJosuvV zKffZu8OXV>x6+59`YE32VrwaU$w_8ziN*FrF6C< z55iw)XKhsKH9)Cc0Wbeoh%mwiKb?Z#Iw#w)Crm$I z)U~w%9?+CW!)+QHZjImvJbBQHS&wqA}9v!+hM_nr|eeBT+$8m^L!IC@yt zvem~`m597v$AcW4gYZj+UpRF*2e>~(R}yryYqK{?TMj9N^(}WF-PTa^Vzp#h;_FZP zZ}@{At@K{PY3IfvCy9Zi1zz$D@-K~DHRJ6wGoE@LIzmx#HMu%zaJAheFRVpkZH!)7 zzKY1rw@4RL?21}9anHei`^$*4+oewsa@m1bhJ{Dinx4B(DSvZL0IMxS_jCSHmFSFo4( znmn)5u6i!@?&Abr^_02Pq+M`w9e&s2kk87AdDl{EGPZJP%zCnh)972_AYaa6fi*I2EaoD-b>@?LVw8YtS_fnQFlt1>q-LN44^j~>s7wLz&?(KAA=@7!OgcUdvz z_*OpFxJ%0CiQPD{?zd~TOGO@rDK6FtSmJ}s?ePzJq1d*bNMY11N87S%W9yGU{h(Ru za9;Vww!tR1upJbfc~)Z)KFhA~p8QU_U7(H@|VMs*-zb}W|Ttq^)S9K)j#`} zelHDSJxlt`fPJSD0;Kk+VeaiXvl>>c5^RkTql?F7?zL^}m((cnX^8e4jMj5iR#UIC zuBa|OmoAk5(B;8(k-h#FTIVgxGOoU7RqhHTKh6uFYIl0YX%_YawJsGW+(zk84m)_@7Nvb&M-+~wyNOLq8 zcv{7Kwh`k1<(*W~hDcW-7gxvlmLdls^5b2bOJ`fmH{1H2Z`m%vcr!gw=$|<)cangz zvjvB~1WdTv$+|u~6X+ippg(0Ag6)`%<;?<(K0BL^$T&v09S@SZ&^}(G%7rWfGQX># zF%bhm_3Gt_i~)|BSoI^rQ0gUt#Xq7{4p>h(vNH4Dj~DJz36lmq=(^W0Z1Q}dyxGAt zn5!tytNAy%KKb9~`Vw-X#lgxCMHYx3KjIyG)^Llr7+v)Z{u;S+%MJ(D(JS^1&bPfg z2Nzu5Hmdm3ZU{gG^VzYqtqkCB1v$^mNxf%)+((b3^O*a7RCIUIu$~A5c$i zZL=Hn5Gcf31-yM6I@w-G)N8LF8+g&?Cu(LxOu77d&l<1KX+2-IJJIV?X?KC_;&`=y z4e-ZYG{lIt5FU3N6C3^bv#Z~4bkIMw(HYx5p#!)z|6U&S=mU{u;*7VpDr$d%3x2UB z`LlKD6^+ehPO9An?DP3=sq3^&=bgzZY!V1f=+=EddrSWyW-wSDMS!7VxDTOQFyzH| zr7mrFddB@ZDmbC)za(En%XMCy`arb%K&|`w4(t--^;+Z@X$gb4hrVY5U0!QfKh#qY z({9>e7f^4PADVQtRLXgzKJkLng9|>VW9KWW0%G*B@T#yrp5&Zjin*?#&=GndUG;ow zV$R+9sfr4_>ACv*DgJ^!dS2I&lMe7G8|cl5`P3GkeFQv9Y(>~CBFDmG_-W*kp)HA< zBjZD~j_i%!mEx_KvILqB@mJ05c;QM*reqc~9#Rr(hEr@+f>FI`{3XAAf zr}qi1e%+#vv+n0XIqF0vH8a!ehd4 z5o@PEC0O3{2`zDCfA58fhBJENceKmp&KjP45W8b2+~8rQ&-O>~#h%-DCN46>&SdoI z$P-`bNQYb0zu-Z+2MX@+8=pSqbG^AKSw%r_R4UC$LVm_e)9>KIwo00E&{iJQUI)w+ zDHCT0WNM_z>U7Oluusf-x@e3F-R|<3G=48}06jlAcxAnbX7lW|ZVxLw$O@Ochu*g} zrISQC3Jix%&m($tOdv-!$x)K%zj9yJoytms?wB_W zM4M`mo{4r8P3A#jxTlc^CvFV6n0stsAZI8qtzI|EEUoFT$}S%oI2Yt83GcZ(KF?KG)_%$mc zE~Wj|fx-JBZEYJZlJcUe{C4aDJe$Q=PR%W(^wp&=*`|_HT^UkR73Y8NM6J>44Xt)v z0z{5mE7G`xG{$GCs35yeSJcGkNbrKts#_3Z6hTVre#Tj3s49Dh@;Zo1`A;O(1nOv$>u@Ii$VPoLQ{G_I1Syx78X{qI* zRIlV21&j0ZpB3asPmEZ`P$?~jM_nRZPXP`XvuXZ1o88HX8!?F{KgaWw>c%Z0LRlSrUa|f_dVN{eE-DjL6@jUx%a?=2*rv=NNAW?ANidNHZjoCvQeFuc! zRbZ3ek}!m|B#fW}rjMHuauz%}h!hgO6*Jrb^^!GtP=ORjVI&D44M-{vO@`SZ_B1OC ziKMOOhE$@SaX_>nG`tXu0M22L6V_zk$xx(`Mun?t@M`wwp7TIDM|M z(<}R@ck`|HuTUzo1CZ&r|IKH2PoL2VLY}8JTv0&%Inh{hzR_2~&xq#dP^L5gF3|w& z{7c04H?jJk=*&C5=#W;3Jr5*e+l6Y?k&Nnx|_ql3;lbF|lR}2f zaYVBSE-7`BAHgRPW8u8YhXy6v5gL2)O$-6#E+hx|;* z>d8a#?3kWdiB&loTXq-WYJ2K6R!0cg991)vJvSGQ+5!zZb_~lf+JIX~vKId?UrFn1 z#D!uW^ng(RTmqXW^HGrhUTRSk6B|n-%ro6kJi4dmrk8v>JMG>0K!Me3`g~IAMWPka zx}NA++!MYSk-9)A|Vk@=YQGz7$V!r^VLvWACzDPHf{r1M5syT(v)FaQMapyP+Z1mDjpD z_+N=lL`GsKtwzK~T>ogGdzJSjvHOnYC)cOB6=i4{jq10_W20YJfRzbpL+f*i?ow_w zbFMaUmgU3C@zNf8T;MM$r(ZH>NV9bqlC32%%W3b4cBvP`*cSDRjcT%r>oo7!xU4AE zezL#f+J1!~g>CbxX9eR?GHGcygPho(W4nyTExq#MNXMWt8(W+&R-ATbVsp{Lx54xJ z#E$#ef6zAdHs}-Z7;K&t$5qMfM?PY2BvcaE2_vE`h37)cWB$7G2W(_WQ8lk*sV=^_ zc~4FDoxPue(~n*JkkURcB`M6H^}&-zGer(`9|acQkr{PJe+|MFug{Tu__;o)W1tqd zMiXOHAal-9&q&Qged0tZ_2m4T);CH=)bvBn)GEgkdv@1ff*GSZJSbxw(mUg!)mU9F zd^vSP#~ncSp@*hh^{2`|Sn@yrQ1AI$mi_vFgDWPadlh+rwG)QxAhDs(JczZ@k=}Ja zv&q>PKVraH3=IC#Fb~?1ZWqA7T&K6AdbgcfOt61{S_6#y9dKmfRAfYG)E@D}5Widu zG$Z1f3Ephb*^3$5u?*o~+nn zw0&fmBX&Y6oCg`E_xEqduS9>iWP~DAB2&t0oGZe;L+Rm!-!`S8579%i+n9%4D;e=ylyi$!6c4ysY`9Qk^U&2@a&NPZ(0PaMW~uF~N5z^ewdP6SDs9Izk=H9x z5y$+Y~ z9-FDbh;;$_HovAu*kEs>kaX7#Ls@D|!(!Dt=lPYlN>B@cXJFWx$xpR=gAm%Jxyh)s z#|E06?_zVtrA(X3Riq55&DRUwS{(lvn9{XZC1~Y}%_}X}&zb#;)2aeeytmFqh6~+! z!j~>MxRH*fkODmxDC=PD#nV*%#~(YK5p2Pql-aAfd$aDjQ$lYq6hAm90x>@byZIMk zTKFgOrp!Qr1sm!9MS3+HXNtq{qQdBnwVBHomuxO~U9P7AxxNOyx`27OhCW*{y zYX$z%OC`iCc<_9pS9u$Tkwh83ig#oLru8Am>eXHDw;4}dTGqHiC_FjQrw^OW+%+Sl ztJRI2Sf-z8!YHK^CI~db8&lr`DH_IG|0!iy{7GiuDz@_Iy^U?g=R>_3mj()_pR={f5}qb z@W0DNmDZcr3EZ_YpJm04oxrxiwRKeow2aPnhjaseI&wM7n^MMXLS}C%NFEcbs0rH} zaoA8^E+#SB2osy2XHHO@tME|R&YZpJj?D#bKjt=}Ly#5iu)i-s-$E{KVMul=^Gb6o z@s{cE5?9$r6VmP;d}PIFr@6=)_!(!!}Y=s?k|qSM%JA}(mjRs>e2cs6X0 zvZ_$h&k<=(Z!}y(X4S?qZ|Lr}xV$^jXZGuvr*#Ey==N>i4H&D0&XV*xmy4xEU~ihZ ztQ~P)!h@_69##m_Q`oOODP%KpMTd{tM#1R8pqsaeH{#d*ZFFG~QDiz6q)97kGz;8&KIYXz%0 z`gA8>2U|J1Soe7N4cm`b%=8%>rHa>;zTj_uvL(%0M!HkXAph2pN>8UtoGhG^+-U(G=W^Wkmjq0(CO;4Qr%IC5E>@B);`BHo1N&8O?^^6{strS}T90;Oc zb>gqz|B?ft@qbI&$4A=_b~+r>vXGGHOlI&iFR%I-D%}0y!s#e{c(v7YtSLQ1(jhTM zKf?UkZQv8}o)FXM{ty{1f%n$3KmpIkTBqp_QBL>WJT-YpPt0;;NJ(Md$Y?X}g3e9p z(7^C+n_Jmi^iD2ko<5G`k?PoUDH2gl&%@r9PU#9^RH{vCv|GqN(y}drk5U}%)3Anq zMT%>4ZR3Cdme$2fcpPDXFr?Y`XWrPcEm5Y+CRSwJWgZ~MQ-aX@=0O$_kK7Gi@t<<>7v2%?+_Y@G zmZj7_uiw>OJQntJSbm`CRVYTGvpG_*sq4Z+>p?gv)xvMD;?0lL`r4t}-zfrdb^pV* zAjk}_;sq2Ji5JGSp-Cg!@Rf$y&Z>jmFQSL2s@ig9`6Vf#-TI&8!iw+m1ugCJsOCY! zN+2NBX98xZ0D&BsdGqbXQa+zzV_`GHip|YRu6@-keH`V&ndpuS391x76{iQFAx}P3+@$o41&_mA%AT0+GP;c3hqJ0;G=3bvfI0ryN2kwGyH9YRzm_2 zx~c#HK41)teE1JA6>qIXB9OB{qWOa0WT0We4oC+u<#%CfE0jhtsH7DL5f&IgXf{|L zv>5jXfUZe6kC2D(Um~N)aL=GBXD*&|`OnS)fJlU21>MI5>c*GY>>dsmVDB=3;s$K0 zkogw=5o3RVh`Q_;sS$+bQauQ0XQNswo?joN*WFUb^K-=coZCg$!l^9#FgeSB`a8eg9VHD`;{Tz+(hyp> z6Y2_Y$$M$MOEb(VD0Hr8HMbM|DYiXMVJ5b0_0oekW2myZRj%V2zWW773xtF!UM5b? zRY;WM3x6dl<6V9%WJ$|Dn-dDMHz=_rIaHriM)vTZ&i}&I|Cftwj1yxS=T!qKq zg?QeKW-5}5GfxhV3vC=&Q)MXd)=4zLUG$6(EQ`OVv%K1l_UV~t-2SeYH{UHd!ZFyW z4>;0t!Ttk#j-5X;i4k^*#0x14;D*iX5kDRj&Z!RhvY$S|{1P#cUd=Kwy+Rr0L7fx7 zXTgR3ngtj0)2m-{<3MOL$onZ}29JR#MXz%1I_y1>F*7|cZ-oCVOjB@uJ+VYMyN5$) z15EjmIxs2k${tDgv|!sJWn2mmV)L8wpd=x@Vq?crE3nmdN7F`d6R!Tm5tD;Fs1WRE zpMi$(+MYLl;<$mV4mfvUB|b71PEyZj5YDLjsSjF8&+oUe3@i&Y%QNT-`4D&Gmwgd? z-**Saeo%YWvozSVngTO%7y@bFQ7@5&yT42DP5%7L85fhSpU~Q`o@1xmSk~PapSZBW zOd?~=s)Ggw)o$`o*iJEE(iUB)<4=bN4zVpRZZ4YCSxv_jNuLl_dk=4-?p^ihWtf6@ z<>uCw1LV^)LTp6BpwG7N*z->;WGEGqvKE&J3(NALD+#Z7(0rMm?RlZD+z7{<=j#p9 z`q4mf)A-J~jU)JGJTV?vr_Dv-68;aj8d?(FQ%2U29vL93n$t#d@V`kIZtX2X)ZEKn3)F+iliL0);|`k zD{&~4=t_4TaA2D;Fh!cE8r``l*wnQ8T$1Z!LQnrnh73zK~k) zO;+t*3Qw}R_X=dZ>f1Ilw&&_Mwo^9{DaKCQFypM_QK{Ltiu+F%>6k4#v1nk6SPp$e z0gL6RaAP&W;F1^1^IMTYVr=g(e#2GbwYkIT(KAru{lrq;8 zr{*sHE-a>3k{pmds9A_gXI&UT>0khV2)N@S(*3a4)J6B&-kOqRMvOk zN=%%s=|q2ZXF-scm|Rr$wm|-A&V~{4eTDg6=N275zCv@k6X(dX{aM4XgJ2dHNa+aIHh3(f- zh<-dXQQN*x{i8m`sryB3lE9fo%6Hndnuf41f)w^T($FGQHwQc3J!@QNN|3+N0 zWe-^8UG_kI9Z1s2-r28MGdu6`+l++kWxeDZGcPM<^8 zC-%E>^%gV=zFhUwMfR_`=YNu7@^=zXzfUm%j;Jh-TAc^simgz#H&wq%o!f=XUYm(E zXU*kANIQEST&%XGjoKFgT42So~qs+{QX%z+Qdpj*?0vzw-CfZ4T?o`>) z&b})FHQ`yI<4dJ(Yp-(FcdaqK#WFU1-OM5QfAtpiP5HQ62%jbYY})0o$(V68Y!My> zp~|gWvD=!}KS8P)PM~A` zIXmcUYyGZFIGx1}g&`XIWpnkEUu2xQd(Z_h}Z7WQ}Zo`LEX&N)D>C)Z&o$3gDk zv~pnVgv-Fb__S$VsZTE-5PV8J{7%efFb5tT7X&fE^n`E4_}(pd*?*_|#kvDMO;7bK z*-iuwS9dCinPWe_oR6{7c@xdn?C>we`?N_gFjUjah@|#%D!kC4r9E&teUou)pLou! z4Y^s_K5}*u7n9~av592rueG}CqeLZ61LF@Mc|NB&x4;zF*O)-@7*z&e$Gzhip%d;a z-vg3XivWY}yuH5|=cS(3Ro;`)c-%I(upP+CqgY?}R}6oeF=cT8 zzQo;qQ9O?N(tY#2MMn4PN>&ETK6@7R*6iub3h}{bYJ0uhx!-2$59mA6KkP%w-kH{6 zVpl3o6dgOtxK67;l+nbW?nlolhy7=vd?T* zr3gm`X<&U3j0YnAl1iL^LPlC8(=j4aeC_rWC*^H{sUHT;ndd3#@bM2XMO~)P-1PF) zEFP&s=QA>DDa2wTKckJ+X2W)#)5kiMpz%Urq2slIea0&!maKSsxui2oCi$i3oK&xx zeCf6%wn=+wY#-!tA=gKG-7gKzwUVok9X>^l@}Fsd8PYC#-5Ccjo!-;*@|RwRottqJ z)z7&^?afrujEh8&9;JHqd|t+>vDQltafV|rcT-ng-M50heTAE*$YV9XgN0l7&Kku! zzmBNR+7Qt&>{Io6-Y>XqNAz~u1RhKqS zC|6q)w1Vw1ybGC%OPAD{URmSYOn!VkAbp}0%Vhb2xN%1HwBk9LKd}VU$wI&wPz_=w zoAAHzpqf0#=xIyE@hRg{@&N{+q(%HrT*}Kl$y+xEZane~)jhNHi9y#wF$>YKowH7F zx`ELb-e`5@NSf19H~%48w&I>@xmM>Lw+KyFzX+M#XNyC+qK(kou+M`3psc9@xny7S z8G*g`%Uce{*}B06t7_o>pS*kmY~fA$Q6L(V%9K8n=Xupkwn@<1A}M7YO@WHf7f!d# ziDup?TfXagz_r=?JGIK+c$DhxkCRX`#G2hbsx`0SuETLrX6NGbY1V)Zr(^d68_vEH z*fB*Aoz(Ff_%LKbNscN8K8p(*Iy!y1u|0yHC)}Ab0fa-Qxjm>I8N>){QAjKSU_tqk zXKuw!RPDMDW{N2Wrnx%}z|z+qZO2h}8s-WCS>AC3p+_@wK$6h>QtOVBf#_x65dnOB z1XqKnRp*k%RDpQE9L$4C?13F7_$BbNxMMldtmh~gH!f%A4mCrdY2aanN7zsEd4|;X-#09%$dYCK<(eP0RPY{ z?kVBmU+w*Wj=yX;3Mlu@l2#n14bpxM?3)P#!Aht{I%&8Vp;(KW4Rkhb$u|82atXMh zNeKB?E|)>zC`_Y6u{b@M_gj2h_n>x@KQxu>Q}MnoA}2Rsq5hl@jcvGsD#drA3VJ7Q z+}oFZyva;YY{%D~{&(M=i)lnu&kgP$aR#reN%CAy3wEJ6%!j)trs{Xi(9c-9YB|x3 z(#{F$K?MOT-T@Lh2rcW3E+*(hGUu3NRNtD(majrBSb9{1@8b41R_hKh0*!L(R^{pk z$VV9}1i6YWk@eZCQEEs-D6yX@{mXd}1amB0cNdqeHM1K_p2euX*~MIiRW3G5=`m4n zG0S=4Z2V5=CBgTsr?Aw^V9zQq5f9D5*nNQEf)NDV2bhazoB}8MFbY~6>#us zp5N>8Q+a;$2zNPGu;0POJBPW zJLUQZa-f_z@6?3)sR@lqn4<{$W5*!i<}UzZNenlw4n&j~u7NT&Rlpl^-KPw^gg_?q zBL@A2vHx-D_C0G{vS^OO&Wt^HVercrd;5sY(NO5zov=GXy{ne+~=|ma^^F3*2t9>iOU>bd)~=^`AwhMoAZ6v+klM6 z%LI9SkdRSe%4n>kHHuQ6bemZQ5jAphjVB$Sas0+^TzRp*_@L4;`1u|&3sr|CCih-? zB{^62GDZMQhejrM1OcyZxfX@|_QECLO0a`RS&{89x|O+Eu&Ua( z*Rdanm}$31PHzdOt~kbnMu(7d$QMu0>bqbheRvNbzWKX8V7KdI;xZqSDApGjquvdq z$?`-n#SR^K7C+FG+U2MGT+;{di!QqlC>c)m90lZw34i1%yhP_}!Q($X^l!J}0Qlh7 zF#%uHDm)OaJ(IX!eL2(6_h+Wy?>+w&{eN!?|9bv!bV`c*#CkdhdZu=ZT{ULd|+|D$<-f!&VZ>9O=$?v*Y%q!Q>%<&KGg z6n}5So*h{*Z4s*R!9IT$A-37mUq3sf=EJk_%Af_$??`C(WGI!tzt|`k7@KN-C446< ishPi$eHVQCdQ`Q*#CTn9 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%0DGiF-<2B7!?!mxqLtb8??+Z!>%IvAaY9cfK9Ufv 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 literal 0 HcmV?d00001 diff --git a/docs/user/security/securing-kibana.asciidoc b/docs/user/security/securing-kibana.asciidoc index 6fc2b0af318a4c..b30acd0ed2e53e 100644 --- a/docs/user/security/securing-kibana.asciidoc +++ b/docs/user/security/securing-kibana.asciidoc @@ -145,3 +145,4 @@ include::authentication/index.asciidoc[] include::securing-communications/index.asciidoc[] include::securing-communications/elasticsearch-mutual-tls.asciidoc[] include::audit-logging.asciidoc[] +include::access-agreement.asciidoc[] From 00a60d8cbded32d20cb4c8112f6107007d8dc301 Mon Sep 17 00:00:00 2001 From: Frank Hassanabad Date: Thu, 18 Jun 2020 13:52:38 -0600 Subject: [PATCH 4/6] [SIEM][Detection Engine] Fixes 7.8 and 7.9 upgrade issue within rules where you can get the error "params invalid: [lists]: definition for this key is missing" ## Summary * https://github.com/elastic/kibana/issues/69463 * See here for manual backport to 7.8: https://github.com/elastic/kibana/pull/69434 This fixes a bug where if you import rules and set your overwrite to `true` multiple times in a row within 7.7 you can end up with a lists array. When upgrading to 7.8, we change the name of `lists` to `exceptions_lists` and suddenly when you enable/disable a rule you can get the following error below: ![image](https://user-images.githubusercontent.com/1151048/84945824-fa60e280-b0a4-11ea-8e05-bffdec2e4765.png) The fix is to allow the lists array still if it is present within saved objects to avoid seeing this error screen and being tolerant. We also fix the area of code that is causing the data bug so it cannot happen again with `exceptions_list` which is what the name of lists was renamed to causing this problem. Note that this has unit tests and I also manually tested this by intentionally injecting a `lists` and `exceptions_lists` and using the UI to verify there wasn't another validation spot that needed to be relaxed to allow for the data. ### Checklist - [x] [Unit or functional tests](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#cross-browser-compatibility) were updated or added to match the most common scenarios --- .../routes/rules/utils.test.ts | 29 ++++- .../signals/signal_params_schema.mock.ts | 45 ++++++++ .../signals/signal_params_schema.test.ts | 105 ++++++++++++++++++ .../signals/signal_params_schema.ts | 2 + 4 files changed, 180 insertions(+), 1 deletion(-) create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_params_schema.mock.ts create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_params_schema.test.ts diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils.test.ts index 3b514b92e1479e..891c241661a1b8 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils.test.ts @@ -115,7 +115,7 @@ describe('utils', () => { expect(rule).toEqual(expected); }); - it('transforms ML Rule fields', () => { + test('transforms ML Rule fields', () => { const mlRule = getResult(); mlRule.params.anomalyThreshold = 55; mlRule.params.machineLearningJobId = 'some_job_id'; @@ -130,6 +130,33 @@ describe('utils', () => { }) ); }); + + // This has to stay here until we do data migration of saved objects and lists is removed from: + // signal_params_schema.ts + test('does not leak a lists structure in the transform which would cause validation issues', () => { + const result: RuleAlertType & { lists: [] } = { lists: [], ...getResult() }; + const rule = transformAlertToRule(result); + expect(rule).toEqual( + expect.not.objectContaining({ + lists: [], + }) + ); + }); + + // This has to stay here until we do data migration of saved objects and exceptions_list is removed from: + // signal_params_schema.ts + test('does not leak an exceptions_list structure in the transform which would cause validation issues', () => { + const result: RuleAlertType & { exceptions_list: [] } = { + exceptions_list: [], + ...getResult(), + }; + const rule = transformAlertToRule(result); + expect(rule).toEqual( + expect.not.objectContaining({ + exceptions_list: [], + }) + ); + }); }); describe('getIdError', () => { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_params_schema.mock.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_params_schema.mock.ts new file mode 100644 index 00000000000000..d60509b28f7da6 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_params_schema.mock.ts @@ -0,0 +1,45 @@ +/* + * 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 { SignalParamsSchema } from './signal_params_schema'; + +export const getSignalParamsSchemaMock = (): Partial => ({ + description: 'Detecting root and admin users', + query: 'user.name: root or user.name: admin', + severity: 'high', + type: 'query', + riskScore: 55, + language: 'kuery', + ruleId: 'rule-1', + from: 'now-6m', + to: 'now', +}); + +export const getSignalParamsSchemaDecodedMock = (): SignalParamsSchema => ({ + description: 'Detecting root and admin users', + falsePositives: [], + filters: null, + from: 'now-6m', + immutable: false, + index: null, + language: 'kuery', + maxSignals: 100, + meta: null, + note: null, + outputIndex: null, + query: 'user.name: root or user.name: admin', + references: [], + riskScore: 55, + ruleId: 'rule-1', + savedId: null, + severity: 'high', + threat: null, + timelineId: null, + timelineTitle: null, + to: 'now', + type: 'query', + version: 1, +}); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_params_schema.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_params_schema.test.ts new file mode 100644 index 00000000000000..a7404b4066387b --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_params_schema.test.ts @@ -0,0 +1,105 @@ +/* + * 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 { signalParamsSchema, SignalParamsSchema } from './signal_params_schema'; +import { + getSignalParamsSchemaDecodedMock, + getSignalParamsSchemaMock, +} from './signal_params_schema.mock'; +import { DEFAULT_MAX_SIGNALS } from '../../../../common/constants'; + +describe('signal_params_schema', () => { + test('it works with expected basic mock data set', () => { + const schema = signalParamsSchema(); + expect(schema.validate(getSignalParamsSchemaMock())).toEqual( + getSignalParamsSchemaDecodedMock() + ); + }); + + test('it works on older lists data structures if they exist as an empty array', () => { + const schema = signalParamsSchema(); + const mock: Partial = { lists: [], ...getSignalParamsSchemaMock() }; + const expected: Partial = { + lists: [], + ...getSignalParamsSchemaDecodedMock(), + }; + expect(schema.validate(mock)).toEqual(expected); + }); + + test('it works on older exceptions_list data structures if they exist as an empty array', () => { + const schema = signalParamsSchema(); + const mock: Partial = { + exceptions_list: [], + ...getSignalParamsSchemaMock(), + }; + const expected: Partial = { + exceptions_list: [], + ...getSignalParamsSchemaDecodedMock(), + }; + expect(schema.validate(mock)).toEqual(expected); + }); + + test('it throws if given an invalid value', () => { + const schema = signalParamsSchema(); + const mock: Partial & { madeUpValue: string } = { + madeUpValue: 'something', + ...getSignalParamsSchemaMock(), + }; + expect(() => schema.validate(mock)).toThrow( + '[madeUpValue]: definition for this key is missing' + ); + }); + + test('if risk score is a string then it will be converted into a number before being inserted as data', () => { + const schema = signalParamsSchema(); + const mock: Omit, 'riskScore'> & { riskScore: string } = { + ...getSignalParamsSchemaMock(), + riskScore: '5', + }; + expect(schema.validate(mock).riskScore).toEqual(5); + expect(typeof schema.validate(mock).riskScore).toEqual('number'); + }); + + test('if risk score is a number then it will work as a number', () => { + const schema = signalParamsSchema(); + const mock: Partial = { + ...getSignalParamsSchemaMock(), + riskScore: 5, + }; + expect(schema.validate(mock).riskScore).toEqual(5); + expect(typeof schema.validate(mock).riskScore).toEqual('number'); + }); + + test('maxSignals will default to "DEFAULT_MAX_SIGNALS" if not set', () => { + const schema = signalParamsSchema(); + const { maxSignals, ...withoutMockData } = getSignalParamsSchemaMock(); + expect(schema.validate(withoutMockData).maxSignals).toEqual(DEFAULT_MAX_SIGNALS); + }); + + test('version will default to "1" if not set', () => { + const schema = signalParamsSchema(); + const { version, ...withoutVersion } = getSignalParamsSchemaMock(); + expect(schema.validate(withoutVersion).version).toEqual(1); + }); + + test('references will default to an empty array if not set', () => { + const schema = signalParamsSchema(); + const { references, ...withoutReferences } = getSignalParamsSchemaMock(); + expect(schema.validate(withoutReferences).references).toEqual([]); + }); + + test('immutable will default to false if not set', () => { + const schema = signalParamsSchema(); + const { immutable, ...withoutImmutable } = getSignalParamsSchemaMock(); + expect(schema.validate(withoutImmutable).immutable).toEqual(false); + }); + + test('falsePositives will default to an empty array if not set', () => { + const schema = signalParamsSchema(); + const { falsePositives, ...withoutFalsePositives } = getSignalParamsSchemaMock(); + expect(schema.validate(withoutFalsePositives).falsePositives).toEqual([]); + }); +}); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_params_schema.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_params_schema.ts index 461b2589babcc8..5f95f635a6bd85 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_params_schema.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_params_schema.ts @@ -34,6 +34,8 @@ const signalSchema = schema.object({ type: schema.string(), references: schema.arrayOf(schema.string(), { defaultValue: [] }), version: schema.number({ defaultValue: 1 }), + lists: schema.maybe(schema.arrayOf(schema.object({}, { unknowns: 'allow' }))), // For backwards compatibility with customers that had a data bug in 7.7. Once we use a migration script please remove this. + exceptions_list: schema.maybe(schema.arrayOf(schema.object({}, { unknowns: 'allow' }))), // For backwards compatibility with customers that had a data bug in 7.8. Once we use a migration script please remove this. exceptionsList: schema.maybe(schema.arrayOf(schema.object({}, { unknowns: 'allow' }))), }); From d26cbef38992bc58dd4f796f99ddd6b620f9ce3e Mon Sep 17 00:00:00 2001 From: Lisa Cawley Date: Thu, 18 Jun 2020 13:19:10 -0700 Subject: [PATCH 5/6] [DOCS] Adds kibana-pull attribute for release docs (#69554) --- .../development-plugin-feature-registration.asciidoc | 8 ++++---- .../development-plugin-functional-tests.asciidoc | 2 +- .../plugin/development-plugin-localization.asciidoc | 6 +++--- .../plugin/development-plugin-resources.asciidoc | 10 +++++----- .../visualize/development-visualize-index.asciidoc | 2 +- docs/gs-index.asciidoc | 8 ++------ docs/index.asciidoc | 6 +----- docs/plugins/known-plugins.asciidoc | 2 +- packages/kbn-release-notes/src/formats/asciidoc.ts | 2 +- x-pack/dev-tools/xkb_release_notes.pl | 4 ++-- 10 files changed, 21 insertions(+), 29 deletions(-) diff --git a/docs/developer/plugin/development-plugin-feature-registration.asciidoc b/docs/developer/plugin/development-plugin-feature-registration.asciidoc index d594a6d4255b2e..203cc201ee6262 100644 --- a/docs/developer/plugin/development-plugin-feature-registration.asciidoc +++ b/docs/developer/plugin/development-plugin-feature-registration.asciidoc @@ -22,7 +22,7 @@ init(server) { ----------- ===== Feature details -Registering a feature consists of the following fields. For more information, consult the {repo}blob/{branch}/x-pack/plugins/features/server/feature_registry.ts[feature registry interface]. +Registering a feature consists of the following fields. For more information, consult the {kib-repo}blob/{branch}/x-pack/plugins/features/server/feature_registry.ts[feature registry interface]. [cols="1a, 1a, 1a, 1a"] @@ -45,12 +45,12 @@ Registering a feature consists of the following fields. For more information, co |An array of applications this feature enables. Typically, all of your plugin's apps (from `uiExports`) will be included here. |`privileges` (required) -|{repo}blob/{branch}/x-pack/plugins/features/common/feature.ts[`FeatureConfig`]. +|{kib-repo}blob/{branch}/x-pack/plugins/features/common/feature.ts[`FeatureConfig`]. |See <> and <> |The set of privileges this feature requires to function. |`subFeatures` (optional) -|{repo}blob/{branch}/x-pack/plugins/features/common/feature.ts[`FeatureConfig`]. +|{kib-repo}blob/{branch}/x-pack/plugins/features/common/feature.ts[`FeatureConfig`]. |See <> |The set of subfeatures that enables finer access control than the `all` and `read` feature privileges. These options are only available in the Gold subscription level and higher. @@ -68,7 +68,7 @@ Registering a feature consists of the following fields. For more information, co ===== Privilege definition The `privileges` section of feature registration allows plugins to implement read/write and read-only modes for their applications. -For a full explanation of fields and options, consult the {repo}blob/{branch}/x-pack/plugins/features/server/feature_registry.ts[feature registry interface]. +For a full explanation of fields and options, consult the {kib-repo}blob/{branch}/x-pack/plugins/features/server/feature_registry.ts[feature registry interface]. ==== Using UI Capabilities diff --git a/docs/developer/plugin/development-plugin-functional-tests.asciidoc b/docs/developer/plugin/development-plugin-functional-tests.asciidoc index 1227682b1dcf93..eda2ceb627fced 100644 --- a/docs/developer/plugin/development-plugin-functional-tests.asciidoc +++ b/docs/developer/plugin/development-plugin-functional-tests.asciidoc @@ -85,5 +85,5 @@ node ../../kibana/scripts/functional_test_runner [float] ==== Using esArchiver -We're working on documentation for this, but for now the best place to look is the original {pull}10359[pull request]. +We're working on documentation for this, but for now the best place to look is the original {kibana-pull}10359[pull request]. diff --git a/docs/developer/plugin/development-plugin-localization.asciidoc b/docs/developer/plugin/development-plugin-localization.asciidoc index 1fb8b6aa0cbde7..b0b543bd9fe33a 100644 --- a/docs/developer/plugin/development-plugin-localization.asciidoc +++ b/docs/developer/plugin/development-plugin-localization.asciidoc @@ -109,7 +109,7 @@ export const HELLO_WORLD = i18n.translate('hello.wonderful.world', { }); ----------- -Full details are {repo}tree/master/packages/kbn-i18n#vanilla-js[here]. +Full details are {kib-repo}tree/master/packages/kbn-i18n#vanilla-js[here]. [float] ===== i18n for React @@ -133,7 +133,7 @@ export const Component = () => { }; ----------- -Full details are {repo}tree/master/packages/kbn-i18n#react[here]. +Full details are {kib-repo}tree/master/packages/kbn-i18n#react[here]. @@ -153,7 +153,7 @@ The translation directive has the following syntax: > ----------- -Full details are {repo}tree/master/packages/kbn-i18n#angularjs[here]. +Full details are {kib-repo}tree/master/packages/kbn-i18n#angularjs[here]. [float] diff --git a/docs/developer/plugin/development-plugin-resources.asciidoc b/docs/developer/plugin/development-plugin-resources.asciidoc index a2fd0e23d0be4a..3a32c49e40e0f8 100644 --- a/docs/developer/plugin/development-plugin-resources.asciidoc +++ b/docs/developer/plugin/development-plugin-resources.asciidoc @@ -5,12 +5,12 @@ Here are some resources that are helpful for getting started with plugin develop [float] ==== Some light reading -Our {repo}blob/master/CONTRIBUTING.md[contributing guide] can help you get a development environment going. +Our {kib-repo}blob/master/CONTRIBUTING.md[contributing guide] can help you get a development environment going. [float] ==== Plugin Generator -We recommend that you kick-start your plugin by generating it with the {repo}tree/{branch}/packages/kbn-plugin-generator[Kibana Plugin Generator]. Run the following in the Kibana repo, and you will be asked a couple questions, see some progress bars, and have a freshly generated plugin ready for you to play with in Kibana's `plugins` folder. +We recommend that you kick-start your plugin by generating it with the {kib-repo}tree/{branch}/packages/kbn-plugin-generator[Kibana Plugin Generator]. Run the following in the Kibana repo, and you will be asked a couple questions, see some progress bars, and have a freshly generated plugin ready for you to play with in Kibana's `plugins` folder. ["source","shell"] ----------- @@ -34,7 +34,7 @@ The Kibana directory must be named `kibana`, and your plugin directory should be [float] ==== References in the code - - {repo}blob/{branch}/src/legacy/server/plugins/lib/plugin.js[Plugin class]: What options does the `kibana.Plugin` class accept? + - {kib-repo}blob/{branch}/src/legacy/server/plugins/lib/plugin.js[Plugin class]: What options does the `kibana.Plugin` class accept? - <>: What type of exports are available? [float] @@ -65,9 +65,9 @@ To enable TypeScript support, create a `tsconfig.json` file at the root of your TypeScript code is automatically converted into JavaScript during development, but not in the distributable version of Kibana. If you use the -{repo}blob/{branch}/packages/kbn-plugin-helpers[@kbn/plugin-helpers] to build your plugin, then your `.ts` and `.tsx` files will be permanently transpiled before your plugin is archived. If you have your own build process, make sure to run the TypeScript compiler on your source files and ship the compilation output so that your plugin will work with the distributable version of Kibana. +{kib-repo}blob/{branch}/packages/kbn-plugin-helpers[@kbn/plugin-helpers] to build your plugin, then your `.ts` and `.tsx` files will be permanently transpiled before your plugin is archived. If you have your own build process, make sure to run the TypeScript compiler on your source files and ship the compilation output so that your plugin will work with the distributable version of Kibana. ==== {kib} platform migration guide -{repo}blob/{branch}/src/core/MIGRATION.md#migrating-legacy-plugins-to-the-new-platform[This guide] +{kib-repo}blob/{branch}/src/core/MIGRATION.md#migrating-legacy-plugins-to-the-new-platform[This guide] provides an action plan for moving a legacy plugin to the new platform. diff --git a/docs/developer/visualize/development-visualize-index.asciidoc b/docs/developer/visualize/development-visualize-index.asciidoc index daefc434e1f183..ac824b4702a3c2 100644 --- a/docs/developer/visualize/development-visualize-index.asciidoc +++ b/docs/developer/visualize/development-visualize-index.asciidoc @@ -22,5 +22,5 @@ here are a few resources: * The <> documentation, where we try to capture any changes to the APIs as they occur across minors. * link:https://github.com/elastic/kibana/issues/44121[Meta issue] which is tracking the move of the plugin to the new Kibana platform * Our link:https://www.elastic.co/blog/join-our-elastic-stack-workspace-on-slack[Elastic Stack workspace on Slack]. -* The {repo}blob/{branch}/src/plugins/visualizations[source code], which will continue to be +* The {kib-repo}blob/{branch}/src/plugins/visualizations[source code], which will continue to be the most accurate source of information. diff --git a/docs/gs-index.asciidoc b/docs/gs-index.asciidoc index c5446dda676061..06996d382d90f8 100644 --- a/docs/gs-index.asciidoc +++ b/docs/gs-index.asciidoc @@ -11,14 +11,10 @@ release-state can be: released | prerelease | unreleased :docker-image: docker.elastic.co/kibana/kibana:{version} :es-ref: https://www.elastic.co/guide/en/elasticsearch/reference/{branch}/ -:kibana-ref: https://www.elastic.co/guide/en/kibana/{branch} -:xpack-ref: https://www.elastic.co/guide/en/x-pack/current/ -:repo: https://github.com/elastic/kibana/ -:issue: {repo}issues/ -:pull: {repo}pull/ -:commit: {repo}commit/ :security: https://www.elastic.co/community/security/ +include::{docs-root}/shared/attributes.asciidoc[] + include::introduction.asciidoc[] include::setup/install.asciidoc[] diff --git a/docs/index.asciidoc b/docs/index.asciidoc index b8dec183d378b6..66ad2f7ec306ab 100644 --- a/docs/index.asciidoc +++ b/docs/index.asciidoc @@ -11,11 +11,7 @@ include::{docs-root}/shared/versions/stack/{source_branch}.asciidoc[] :docker-repo: docker.elastic.co/kibana/kibana :docker-image: docker.elastic.co/kibana/kibana:{version} -:repo: https://github.com/elastic/kibana/ -:issue: {repo}issues/ -:pull: {repo}pull/ -:commit: {repo}commit/ -:blob: {repo}blob/{branch}/ +:blob: {kib-repo}blob/{branch}/ :security-ref: https://www.elastic.co/community/security/ include::{docs-root}/shared/attributes.asciidoc[] diff --git a/docs/plugins/known-plugins.asciidoc b/docs/plugins/known-plugins.asciidoc index cb70a1d1c387a4..cd07596ad37ef5 100644 --- a/docs/plugins/known-plugins.asciidoc +++ b/docs/plugins/known-plugins.asciidoc @@ -71,4 +71,4 @@ Use it to create, edit and embed visualizations, and also to search inside an em * https://github.com/datasweet-fr/kibana-datasweet-formula[Datasweet Formula] (datasweet) - enables calculated metric on any standard Kibana visualization. * https://github.com/pjhampton/kibana-prometheus-exporter[Prometheus Exporter] - exports the Kibana metrics in the prometheus format -NOTE: If you want your plugin to be added to this page, open a {repo}tree/{branch}/docs/plugins/known-plugins.asciidoc[pull request]. +NOTE: If you want your plugin to be added to this page, open a {kib-repo}tree/{branch}/docs/plugins/known-plugins.asciidoc[pull request]. diff --git a/packages/kbn-release-notes/src/formats/asciidoc.ts b/packages/kbn-release-notes/src/formats/asciidoc.ts index d6c707f009f323..b749e89048a8e9 100644 --- a/packages/kbn-release-notes/src/formats/asciidoc.ts +++ b/packages/kbn-release-notes/src/formats/asciidoc.ts @@ -73,7 +73,7 @@ export class AsciidocFormat extends Format { for (const pr of prsInArea) { const fixes = pr.fixes.length ? `[Fixes ${pr.fixes.join(', ')}] ` : ''; const strippedTitle = pr.title.replace(/^\s*\[[^\]]+\]\s*/, ''); - yield `* ${fixes}${strippedTitle} {pull}${pr.number}[#${pr.number}]\n`; + yield `* ${fixes}${strippedTitle} {kibana-pull}${pr.number}[#${pr.number}]\n`; if (pr.note) { yield ` - ${pr.note}\n`; } diff --git a/x-pack/dev-tools/xkb_release_notes.pl b/x-pack/dev-tools/xkb_release_notes.pl index 08da967c35e687..a635a88392657e 100755 --- a/x-pack/dev-tools/xkb_release_notes.pl +++ b/x-pack/dev-tools/xkb_release_notes.pl @@ -111,7 +111,7 @@ sub dump_issues { } my $number = $issue->{number}; -# print encode_utf8("* $title {pull}${number}[#${number}]"); +# print encode_utf8("* $title {kibana-pull}${number}[#${number}]"); print encode_utf8("* $title"); print "\n"; print encode_utf8("// https://github.com/${User_Repo}pull/${number}[#${number}]"); @@ -120,7 +120,7 @@ sub dump_issues { print keys %uniq > 1 ? " (issues: " : " (issue: "; -# print join ", ", map {"{issue}${_}[#${_}]"} +# print join ", ", map {"{kib-issue}${_}[#${_}]"} # print join ", ", map {"#${_}"} print join ", ", map {"https://github.com/${User_Repo}issues/${_}[#${_}]"} sort keys %uniq; From bdb65920f057cf588e9f012da15f6b4e665abd98 Mon Sep 17 00:00:00 2001 From: Jonathan Budzenski Date: Thu, 18 Jun 2020 15:19:46 -0500 Subject: [PATCH 6/6] skip tests using hostDetailsPolicyResponseActionBadge --- .../public/management/pages/endpoint_hosts/view/index.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx index af027c2e106c3d..68c4e960741819 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx @@ -371,7 +371,7 @@ describe('when on the hosts page', () => { }); }); - describe('when showing host Policy Response panel', () => { + describe.skip('when showing host Policy Response panel', () => { let renderResult: ReturnType; beforeEach(async () => { coreStart.http.post.mockImplementation(async (requestOptions) => {