From 170f479d005221d17587bce2f45293444756e7c3 Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Mon, 16 Nov 2020 19:52:15 -0700 Subject: [PATCH] [Maps] saved object tagging (#83197) * add tag selector to save modal * save tag references onSave * populate tags when unwrapping attributes * tslint * update listing page to show tags * fix data-test-subj id in functional tests * i18n cleanup * tslint * remove unused import * use listingTable service for functional tests * tslint and fix mvt grid layer functional test * review feedback * add tags to all privileges and add test user to find, delete, get, get_all, and update tests * move functions to module scope Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../table_list_view/table_list_view.tsx | 5 +- .../functional/page_objects/dashboard_page.ts | 4 +- test/functional/services/listing_table.ts | 31 +- x-pack/plugins/maps/kibana.json | 2 +- x-pack/plugins/maps/public/kibana_services.ts | 1 + .../maps/public/map_attribute_service.ts | 32 +- x-pack/plugins/maps/public/plugin.ts | 2 + .../routes/list_page/maps_list_view.tsx | 572 ++++-------------- .../routes/map_page/saved_map/saved_map.ts | 34 +- .../public/routes/map_page/top_nav_config.tsx | 16 + x-pack/plugins/maps/server/plugin.ts | 4 +- .../translations/translations/ja-JP.json | 15 - .../translations/translations/zh-CN.json | 15 - .../functional/apps/maps/mvt_super_fine.js | 2 +- .../apps/maps/saved_object_management.js | 14 +- .../es_archives/maps/kibana/data.json | 2 +- .../test/functional/page_objects/gis_page.ts | 48 +- .../security_and_spaces/apis/_find.ts | 1 + .../security_and_spaces/apis/create.ts | 1 + .../security_and_spaces/apis/delete.ts | 1 + .../security_and_spaces/apis/get.ts | 1 + .../security_and_spaces/apis/get_all.ts | 1 + .../security_and_spaces/apis/update.ts | 1 + .../fixtures/es_archiver/maps/data.json | 210 +++++++ .../common/lib/authentication.ts | 18 + .../functional/tests/index.ts | 1 + .../functional/tests/maps_integration.ts | 139 +++++ 27 files changed, 603 insertions(+), 570 deletions(-) create mode 100644 x-pack/test/saved_object_tagging/common/fixtures/es_archiver/maps/data.json create mode 100644 x-pack/test/saved_object_tagging/functional/tests/maps_integration.ts diff --git a/src/plugins/kibana_react/public/table_list_view/table_list_view.tsx b/src/plugins/kibana_react/public/table_list_view/table_list_view.tsx index 4f509876a75f4..75fd2c30a995a 100644 --- a/src/plugins/kibana_react/public/table_list_view/table_list_view.tsx +++ b/src/plugins/kibana_react/public/table_list_view/table_list_view.tsx @@ -57,7 +57,7 @@ export interface TableListViewProps { listingLimit: number; initialFilter: string; initialPageSize: number; - noItemsFragment: JSX.Element; + noItemsFragment?: JSX.Element; tableColumns: Array>; tableListTitle: string; toastNotifications: ToastsStart; @@ -73,7 +73,7 @@ export interface TableListViewProps { /** * Describes the content of the table. If not specified, the caption will be "This table contains {itemCount} rows." */ - tableCaption: string; + tableCaption?: string; searchFilters?: SearchFilterConfig[]; } @@ -445,6 +445,7 @@ class TableListView extends React.Component { const elements = await find.allByCssSelector( `[data-test-subj^="${prefixMap[appName]}ListingTitleLink"]` @@ -126,14 +123,8 @@ export function ListingTableProvider({ getService, getPageObjects }: FtrProvider /** * Searches for item on Landing page and retruns items count that match `ListingTitleLink-${name}` pattern - * @param appName 'visualize' | 'dashboard' - * @param name item name */ - public async searchAndExpectItemsCount( - appName: 'visualize' | 'dashboard', - name: string, - count: number - ) { + public async searchAndExpectItemsCount(appName: AppName, name: string, count: number) { await this.searchForItemWithName(name); await retry.try(async () => { const links = await testSubjects.findAll( @@ -165,10 +156,8 @@ export function ListingTableProvider({ getService, getPageObjects }: FtrProvider /** * Clicks item on Landing page by link name if it is present - * @param appName 'dashboard' | 'visualize' - * @param name item name */ - public async clickItemLink(appName: 'dashboard' | 'visualize', name: string) { + public async clickItemLink(appName: AppName, name: string) { await testSubjects.click( `${prefixMap[appName]}ListingTitleLink-${name.split(' ').join('-')}` ); @@ -204,6 +193,12 @@ export function ListingTableProvider({ getService, getPageObjects }: FtrProvider } }); } + + public async onListingPage(appName: AppName) { + return await testSubjects.exists(`${appName}LandingPage`, { + timeout: 5000, + }); + } } return new ListingTable(); diff --git a/x-pack/plugins/maps/kibana.json b/x-pack/plugins/maps/kibana.json index 8983d4ab1a4da..e47968b027cc3 100644 --- a/x-pack/plugins/maps/kibana.json +++ b/x-pack/plugins/maps/kibana.json @@ -19,7 +19,7 @@ "savedObjects", "share" ], - "optionalPlugins": ["home"], + "optionalPlugins": ["home", "savedObjectsTagging"], "ui": true, "server": true, "extraPublicDirs": ["common/constants"], diff --git a/x-pack/plugins/maps/public/kibana_services.ts b/x-pack/plugins/maps/public/kibana_services.ts index 4dcc9193420c0..02b875257a5ac 100644 --- a/x-pack/plugins/maps/public/kibana_services.ts +++ b/x-pack/plugins/maps/public/kibana_services.ts @@ -48,6 +48,7 @@ export const getCoreI18n = () => coreStart.i18n; export const getSearchService = () => pluginsStart.data.search; export const getEmbeddableService = () => pluginsStart.embeddable; export const getNavigateToApp = () => coreStart.application.navigateToApp; +export const getSavedObjectsTagging = () => pluginsStart.savedObjectsTagging; // xpack.maps.* kibana.yml settings from this plugin let mapAppConfig: MapsConfigType; diff --git a/x-pack/plugins/maps/public/map_attribute_service.ts b/x-pack/plugins/maps/public/map_attribute_service.ts index 0e3ef1b9ea518..9b2f3105f6870 100644 --- a/x-pack/plugins/maps/public/map_attribute_service.ts +++ b/x-pack/plugins/maps/public/map_attribute_service.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { SavedObjectReference } from 'src/core/types'; import { AttributeService } from '../../../../src/plugins/embeddable/public'; import { MapSavedObjectAttributes } from '../common/map_saved_object_type'; import { MAP_SAVED_OBJECT_TYPE } from '../common/constants'; @@ -14,11 +15,9 @@ import { getCoreOverlays, getEmbeddableService, getSavedObjectsClient } from './ import { extractReferences, injectReferences } from '../common/migrations/references'; import { MapByValueInput, MapByReferenceInput } from './embeddable/types'; -export type MapAttributeService = AttributeService< - MapSavedObjectAttributes, - MapByValueInput, - MapByReferenceInput ->; +type MapDoc = MapSavedObjectAttributes & { references?: SavedObjectReference[] }; + +export type MapAttributeService = AttributeService; let mapAttributeService: MapAttributeService | null = null; @@ -28,30 +27,37 @@ export function getMapAttributeService(): MapAttributeService { } mapAttributeService = getEmbeddableService().getAttributeService< - MapSavedObjectAttributes, + MapDoc, MapByValueInput, MapByReferenceInput >(MAP_SAVED_OBJECT_TYPE, { - saveMethod: async (attributes: MapSavedObjectAttributes, savedObjectId?: string) => { - const { attributes: attributesWithExtractedReferences, references } = extractReferences({ - attributes, + saveMethod: async (attributes: MapDoc, savedObjectId?: string) => { + // AttributeService "attributes" contains "references" as a child. + // SavedObjectClient "attributes" uses "references" as a sibling. + // https://github.com/elastic/kibana/issues/83133 + const savedObjectClientReferences = attributes.references; + const savedObjectClientAttributes = { ...attributes }; + delete savedObjectClientAttributes.references; + const { attributes: updatedAttributes, references } = extractReferences({ + attributes: savedObjectClientAttributes, + references: savedObjectClientReferences, }); const savedObject = await (savedObjectId ? getSavedObjectsClient().update( MAP_SAVED_OBJECT_TYPE, savedObjectId, - attributesWithExtractedReferences, + updatedAttributes, { references } ) : getSavedObjectsClient().create( MAP_SAVED_OBJECT_TYPE, - attributesWithExtractedReferences, + updatedAttributes, { references } )); return { id: savedObject.id }; }, - unwrapMethod: async (savedObjectId: string): Promise => { + unwrapMethod: async (savedObjectId: string): Promise => { const savedObject = await getSavedObjectsClient().get( MAP_SAVED_OBJECT_TYPE, savedObjectId @@ -62,7 +68,7 @@ export function getMapAttributeService(): MapAttributeService { } const { attributes } = injectReferences(savedObject); - return attributes; + return { ...attributes, references: savedObject.references }; }, checkForDuplicateTitle: (props: OnSaveProps) => { return checkForDuplicateTitle( diff --git a/x-pack/plugins/maps/public/plugin.ts b/x-pack/plugins/maps/public/plugin.ts index 3da346aaf4443..ecb647cbb61b2 100644 --- a/x-pack/plugins/maps/public/plugin.ts +++ b/x-pack/plugins/maps/public/plugin.ts @@ -63,6 +63,7 @@ import { setLicensingPluginStart, } from './licensed_features'; import { EMSSettings } from '../common/ems_settings'; +import { SavedObjectTaggingPluginStart } from '../../saved_objects_tagging/public'; export interface MapsPluginSetupDependencies { inspector: InspectorSetupContract; @@ -86,6 +87,7 @@ export interface MapsPluginStartDependencies { visualizations: VisualizationsStart; savedObjects: SavedObjectsStart; dashboard: DashboardStart; + savedObjectsTagging?: SavedObjectTaggingPluginStart; } /** diff --git a/x-pack/plugins/maps/public/routes/list_page/maps_list_view.tsx b/x-pack/plugins/maps/public/routes/list_page/maps_list_view.tsx index 97ed3d428d341..a579e3f122cc6 100644 --- a/x-pack/plugins/maps/public/routes/list_page/maps_list_view.tsx +++ b/x-pack/plugins/maps/public/routes/list_page/maps_list_view.tsx @@ -5,45 +5,68 @@ */ import React, { MouseEvent } from 'react'; -import _ from 'lodash'; -import { - EuiTitle, - EuiFieldSearch, - EuiBasicTable, - EuiPage, - EuiPageBody, - EuiPageContent, - EuiLink, - EuiFlexGroup, - EuiFlexItem, - EuiButton, - EuiSpacer, - EuiOverlayMask, - EuiConfirmModal, - EuiCallOut, -} from '@elastic/eui'; +import { SavedObjectReference } from 'src/core/types'; import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { Direction } from '@elastic/eui'; -import { - CriteriaWithPagination, - EuiBasicTableColumn, -} from '@elastic/eui/src/components/basic_table/basic_table'; -import { EuiTableSortingType } from '@elastic/eui'; +import { EuiLink } from '@elastic/eui'; +import { EuiBasicTableColumn } from '@elastic/eui/src/components/basic_table/basic_table'; +import { TableListView } from '../../../../../../src/plugins/kibana_react/public'; import { goToSpecifiedPath } from '../../render_app'; import { APP_ID, MAP_PATH, MAP_SAVED_OBJECT_TYPE } from '../../../common/constants'; import { getMapsCapabilities, - getUiSettings, getToasts, getCoreChrome, getNavigateToApp, getSavedObjectsClient, + getSavedObjectsTagging, + getSavedObjects, } from '../../kibana_services'; import { getAppTitle } from '../../../common/i18n_getters'; import { MapSavedObjectAttributes } from '../../../common/map_saved_object_type'; -export const EMPTY_FILTER = ''; +interface MapItem { + id: string; + title: string; + description?: string; + references?: SavedObjectReference[]; +} + +const savedObjectsTagging = getSavedObjectsTagging(); +const searchFilters = savedObjectsTagging + ? [savedObjectsTagging.ui.getSearchBarFilter({ useName: true })] + : []; + +const tableColumns: Array> = [ + { + field: 'title', + name: i18n.translate('xpack.maps.mapListing.titleFieldTitle', { + defaultMessage: 'Title', + }), + sortable: true, + render: (field: string, record: MapItem) => ( + { + e.preventDefault(); + goToSpecifiedPath(`/${MAP_PATH}/${record.id}`); + }} + data-test-subj={`mapListingTitleLink-${record.title.split(' ').join('-')}`} + > + {field} + + ), + }, + { + field: 'description', + name: i18n.translate('xpack.maps.mapListing.descriptionFieldTitle', { + defaultMessage: 'Description', + }), + dataType: 'string', + sortable: true, + }, +]; +if (savedObjectsTagging) { + tableColumns.push(savedObjectsTagging.ui.getTableColumnDefinition()); +} function navigateToNewMap() { const navigateToApp = getNavigateToApp(); @@ -51,447 +74,76 @@ function navigateToNewMap() { path: MAP_PATH, }); } -interface State { - sortField?: string | number | symbol; - sortDirection?: Direction; - hasInitialFetchReturned: boolean; - isFetchingItems: boolean; - showDeleteModal: boolean; - showLimitError: boolean; - filter: string; - items: TableRow[]; - selectedIds: string[]; - page: number; - perPage: number; - readOnly: boolean; - listingLimit: number; - totalItems?: number; -} - -interface TableRow { - id: string; - title: string; - description?: string; -} - -export class MapsListView extends React.Component { - _isMounted: boolean = false; - state: State = { - hasInitialFetchReturned: false, - isFetchingItems: false, - showDeleteModal: false, - showLimitError: false, - filter: EMPTY_FILTER, - items: [], - selectedIds: [], - page: 0, - perPage: 20, - readOnly: !getMapsCapabilities().save, - listingLimit: getUiSettings().get('savedObjects:listingLimit'), - }; - componentWillUnmount() { - this._isMounted = false; - this.debouncedFetch.cancel(); - } +async function findMaps(searchQuery: string) { + let searchTerm = searchQuery; + let tagReferences; - componentDidMount() { - this._isMounted = true; - this.initMapList(); - } - - async initMapList() { - this.fetchItems(); - getCoreChrome().docTitle.change(getAppTitle()); - getCoreChrome().setBreadcrumbs([{ text: getAppTitle() }]); - } - - debouncedFetch = _.debounce(async (filter) => { - const response = await getSavedObjectsClient().find({ - type: MAP_SAVED_OBJECT_TYPE, - search: filter ? `${filter}*` : undefined, - perPage: this.state.listingLimit, - page: 1, - searchFields: ['title^3', 'description'], - defaultSearchOperator: 'AND', - fields: ['description', 'title'], - }); - - if (!this._isMounted) { - return; - } - - // We need this check to handle the case where search results come back in a different - // order than they were sent out. Only load results for the most recent search. - if (filter === this.state.filter) { - this.setState({ - hasInitialFetchReturned: true, - isFetchingItems: false, - items: response.savedObjects.map((savedObject) => { - return { - id: savedObject.id, - title: savedObject.attributes.title, - description: savedObject.attributes.description, - }; - }), - totalItems: response.total, - showLimitError: response.total > this.state.listingLimit, - }); - } - }, 300); - - fetchItems = () => { - this.setState( - { - isFetchingItems: true, - }, - this.debouncedFetch.bind(null, this.state.filter) - ); - }; - - deleteSelectedItems = async () => { - try { - const deletions = this.state.selectedIds.map((id) => { - return getSavedObjectsClient().delete(MAP_SAVED_OBJECT_TYPE, id); - }); - await Promise.all(deletions); - } catch (error) { - getToasts().addDanger({ - title: i18n.translate('xpack.maps.mapListing.unableToDeleteToastTitle', { - defaultMessage: `Unable to delete map(s)`, - }), - text: `${error}`, - }); - } - this.fetchItems(); - this.setState({ - selectedIds: [], + if (savedObjectsTagging) { + const parsed = savedObjectsTagging.ui.parseSearchQuery(searchQuery, { + useName: true, }); - this.closeDeleteModal(); - }; - - closeDeleteModal = () => { - this.setState({ showDeleteModal: false }); - }; - - openDeleteModal = () => { - this.setState({ showDeleteModal: true }); - }; - - onTableChange = ({ page, sort }: CriteriaWithPagination) => { - const { index: pageIndex, size: pageSize } = page; - - let { field: sortField, direction: sortDirection } = sort || {}; - - // 3rd sorting state that is not captured by sort - native order (no sort) - // when switching from desc to asc for the same field - use native order - if ( - this.state.sortField === sortField && - this.state.sortDirection === 'desc' && - sortDirection === 'asc' - ) { - sortField = undefined; - sortDirection = undefined; - } - - this.setState({ - page: pageIndex, - perPage: pageSize, - sortField, - sortDirection, - }); - }; - - getPageOfItems = () => { - // do not sort original list to preserve elasticsearch ranking order - const itemsCopy = this.state.items.slice(); - - if (this.state.sortField) { - itemsCopy.sort((a, b) => { - const fieldA = _.get(a, this.state.sortField!, ''); - const fieldB = _.get(b, this.state.sortField!, ''); - let order = 1; - if (this.state.sortDirection === 'desc') { - order = -1; - } - return order * fieldA.toLowerCase().localeCompare(fieldB.toLowerCase()); - }); - } - - // If begin is greater than the length of the sequence, an empty array is returned. - const startIndex = this.state.page * this.state.perPage; - // If end is greater than the length of the sequence, slice extracts through to the end of the sequence (arr.length). - const lastIndex = startIndex + this.state.perPage; - return itemsCopy.slice(startIndex, lastIndex); - }; - - hasNoItems() { - if (!this.state.isFetchingItems && this.state.items.length === 0 && !this.state.filter) { - return true; - } - - return false; + searchTerm = parsed.searchTerm; + tagReferences = parsed.tagReferences; } - renderConfirmDeleteModal() { - return ( - - -

- -

-
-
- ); - } - - renderListingLimitWarning() { - if (this.state.showLimitError) { - return ( - - -

- - - - - . -

-
- -
- ); - } - } - - renderNoResultsMessage() { - if (this.state.isFetchingItems) { - return ''; - } - - if (this.hasNoItems()) { - return i18n.translate('xpack.maps.mapListing.noItemsDescription', { - defaultMessage: `Looks like you don't have any maps. Click the create button to create one.`, - }); - } - - return i18n.translate('xpack.maps.mapListing.noMatchDescription', { - defaultMessage: 'No items matched your search.', - }); - } - - renderSearchBar() { - let deleteBtn; - if (this.state.selectedIds.length > 0) { - deleteBtn = ( - - - - - - ); - } - - return ( - - {deleteBtn} - - { - this.setState( - { - filter: e.target.value, - }, - this.fetchItems - ); - }} - data-test-subj="searchFilter" - /> - - - ); - } - - renderTable() { - const tableColumns: Array> = [ - { - field: 'title', - name: i18n.translate('xpack.maps.mapListing.titleFieldTitle', { - defaultMessage: 'Title', - }), - sortable: true, - render: (field, record) => ( - { - e.preventDefault(); - goToSpecifiedPath(`/${MAP_PATH}/${record.id}`); - }} - data-test-subj={`mapListingTitleLink-${record.title.split(' ').join('-')}`} - > - {field} - - ), - }, - { - field: 'description', - name: i18n.translate('xpack.maps.mapListing.descriptionFieldTitle', { - defaultMessage: 'Description', - }), - dataType: 'string', - sortable: true, - }, - ]; - const pagination = { - pageIndex: this.state.page, - pageSize: this.state.perPage, - totalItemCount: this.state.items.length, - pageSizeOptions: [10, 20, 50], - }; - - let selection; - if (!this.state.readOnly) { - selection = { - onSelectionChange: (s: TableRow[]) => { - this.setState({ - selectedIds: s.map((item) => { - return item.id; - }), - }); - }, - }; - } + const resp = await getSavedObjectsClient().find({ + type: MAP_SAVED_OBJECT_TYPE, + search: searchTerm ? `${searchTerm}*` : undefined, + perPage: getSavedObjects().settings.getListingLimit(), + page: 1, + searchFields: ['title^3', 'description'], + defaultSearchOperator: 'AND', + fields: ['description', 'title'], + hasReference: tagReferences, + }); - const sorting: EuiTableSortingType = {}; - if (this.state.sortField) { - sorting.sort = { - field: this.state.sortField, - direction: this.state.sortDirection!, + return { + total: resp.total, + hits: resp.savedObjects.map((savedObject) => { + return { + id: savedObject.id, + title: savedObject.attributes.title, + description: savedObject.attributes.description, + references: savedObject.references, }; - } - const items = this.state.items.length === 0 ? [] : this.getPageOfItems(); - - return ( - - ); - } - - renderListing() { - let createButton; - if (!this.state.readOnly) { - createButton = ( - - - - ); - } - return ( - - {this.state.showDeleteModal && this.renderConfirmDeleteModal()} - - - - -

- -

-
-
- - {createButton} -
- - - - {this.renderListingLimitWarning()} - - {this.renderSearchBar()} - - - - {this.renderTable()} -
- ); - } - - renderPageContent() { - if (!this.state.hasInitialFetchReturned) { - return; - } + }), + }; +} - return {this.renderListing()}; - } +async function deleteMaps(items: object[]) { + const deletions = items.map((item) => { + return getSavedObjectsClient().delete(MAP_SAVED_OBJECT_TYPE, (item as MapItem).id); + }); + await Promise.all(deletions); +} - render() { - return ( - - {this.renderPageContent()} - - ); - } +export function MapsListView() { + const isReadOnly = !getMapsCapabilities().save; + + getCoreChrome().docTitle.change(getAppTitle()); + getCoreChrome().setBreadcrumbs([{ text: getAppTitle() }]); + + return ( + + ); } diff --git a/x-pack/plugins/maps/public/routes/map_page/saved_map/saved_map.ts b/x-pack/plugins/maps/public/routes/map_page/saved_map/saved_map.ts index 036f8cf11d374..98f428f9a2999 100644 --- a/x-pack/plugins/maps/public/routes/map_page/saved_map/saved_map.ts +++ b/x-pack/plugins/maps/public/routes/map_page/saved_map/saved_map.ts @@ -33,7 +33,12 @@ import { getIsLayerTOCOpen, getOpenTOCDetails } from '../../../selectors/ui_sele import { getMapAttributeService } from '../../../map_attribute_service'; import { OnSaveProps } from '../../../../../../../src/plugins/saved_objects/public'; import { MapByReferenceInput, MapEmbeddableInput } from '../../../embeddable/types'; -import { getCoreChrome, getToasts, getIsAllowByValueEmbeddables } from '../../../kibana_services'; +import { + getCoreChrome, + getToasts, + getIsAllowByValueEmbeddables, + getSavedObjectsTagging, +} from '../../../kibana_services'; import { goToSpecifiedPath } from '../../../render_app'; import { LayerDescriptor } from '../../../../common/descriptor_types'; import { getInitialLayers } from './get_initial_layers'; @@ -51,6 +56,7 @@ export class SavedMap { private _originatingApp?: string; private readonly _stateTransfer?: EmbeddableStateTransfer; private readonly _store: MapStore; + private _tags: string[] = []; constructor({ defaultLayers = [], @@ -87,7 +93,14 @@ export class SavedMap { description: '', }; } else { - this._attributes = await getMapAttributeService().unwrapAttributes(this._mapEmbeddableInput); + const doc = await getMapAttributeService().unwrapAttributes(this._mapEmbeddableInput); + const references = doc.references; + delete doc.references; + this._attributes = doc; + const savedObjectsTagging = getSavedObjectsTagging(); + if (savedObjectsTagging && references && references.length) { + this._tags = savedObjectsTagging.ui.getTagIdsFromReferences(references); + } } if (this._attributes?.mapStateJSON) { @@ -216,6 +229,10 @@ export class SavedMap { return this._getStateTransfer().getAppNameFromId(appId); }; + public getTags(): string[] { + return this._tags; + } + public hasSaveAndReturnConfig() { const hasOriginatingApp = !!this._originatingApp; const isNewMap = !this.getSavedObjectId(); @@ -247,9 +264,11 @@ export class SavedMap { newTitle, newCopyOnSave, returnToOrigin, + newTags, saveByReference, }: OnSaveProps & { returnToOrigin: boolean; + newTags?: string[]; saveByReference: boolean; }) { if (!this._attributes) { @@ -264,8 +283,17 @@ export class SavedMap { let updatedMapEmbeddableInput: MapEmbeddableInput; try { + const savedObjectsTagging = getSavedObjectsTagging(); + // Attribute service deviates from Saved Object client by including references as a child to attributes in stead of a sibling + const attributes = + savedObjectsTagging && newTags + ? { + ...this._attributes, + references: savedObjectsTagging.ui.updateTagsReferences([], newTags), + } + : this._attributes; updatedMapEmbeddableInput = (await getMapAttributeService().wrapAttributes( - this._attributes, + attributes, saveByReference, newCopyOnSave ? undefined : this._mapEmbeddableInput )) as MapEmbeddableInput; diff --git a/x-pack/plugins/maps/public/routes/map_page/top_nav_config.tsx b/x-pack/plugins/maps/public/routes/map_page/top_nav_config.tsx index 2d0a7d967a6cf..43a74a9c73012 100644 --- a/x-pack/plugins/maps/public/routes/map_page/top_nav_config.tsx +++ b/x-pack/plugins/maps/public/routes/map_page/top_nav_config.tsx @@ -14,6 +14,7 @@ import { getCoreI18n, getSavedObjectsClient, getCoreOverlays, + getSavedObjectsTagging, } from '../../kibana_services'; import { checkForDuplicateTitle, @@ -125,6 +126,19 @@ export function getTopNavConfig({ } }, run: () => { + let selectedTags = savedMap.getTags(); + function onTagsSelected(newTags: string[]) { + selectedTags = newTags; + } + + const savedObjectsTagging = getSavedObjectsTagging(); + const tagSelector = savedObjectsTagging ? ( + + ) : undefined; + const saveModal = ( ); showSaveModal(saveModal, getCoreI18n().Context); diff --git a/x-pack/plugins/maps/server/plugin.ts b/x-pack/plugins/maps/server/plugin.ts index 65d79272494f0..a79e5353048c8 100644 --- a/x-pack/plugins/maps/server/plugin.ts +++ b/x-pack/plugins/maps/server/plugin.ts @@ -185,7 +185,7 @@ export class MapsPlugin implements Plugin { catalogue: [APP_ID], savedObject: { all: [MAP_SAVED_OBJECT_TYPE, 'query'], - read: ['index-pattern'], + read: ['index-pattern', 'tag'], }, ui: ['save', 'show', 'saveQuery'], }, @@ -194,7 +194,7 @@ export class MapsPlugin implements Plugin { catalogue: [APP_ID], savedObject: { all: [], - read: [MAP_SAVED_OBJECT_TYPE, 'index-pattern', 'query'], + read: [MAP_SAVED_OBJECT_TYPE, 'index-pattern', 'query', 'tag'], }, ui: ['show'], }, diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index ef83ea4d11460..8d8c61f09c585 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -11294,24 +11294,9 @@ "xpack.maps.layerWizardSelect.solutionsCategoryLabel": "ソリューション", "xpack.maps.loadMap.errorAttemptingToLoadSavedMap": "マップを読み込めません", "xpack.maps.map.initializeErrorTitle": "マップを初期化できません", - "xpack.maps.mapListing.advancedSettingsLinkText": "高度な設定", - "xpack.maps.mapListing.cancelTitle": "キャンセル", - "xpack.maps.mapListing.createMapButtonLabel": "マップを作成", - "xpack.maps.mapListing.deleteSelectedButtonLabel": "選択項目を削除", - "xpack.maps.mapListing.deleteSelectedItemsTitle": "選択項目を削除しますか?", - "xpack.maps.mapListing.deleteTitle": "削除", - "xpack.maps.mapListing.deleteWarning": "削除されたアイテムは復元できません。", "xpack.maps.mapListing.descriptionFieldTitle": "説明", "xpack.maps.mapListing.errorAttemptingToLoadSavedMaps": "マップを読み込めません", - "xpack.maps.mapListing.limitExceededTitle": "リスティング制限超過", - "xpack.maps.mapListing.limitHelpDescription": "{totalItems} 個のアイテムがありますが、listingLimit の設定により {listingLimit} 個までしか下の表に表示できません。この設定は次の場所で変更できます ", - "xpack.maps.mapListing.listingTableTitle": "マップ", - "xpack.maps.mapListing.noItemsDescription": "マップがないようです。作成ボタンをクリックして作成してください。", - "xpack.maps.mapListing.noMatchDescription": "検索に一致するアイテムがありません。", - "xpack.maps.mapListing.searchAriaLabel": "フィルターアイテム", - "xpack.maps.mapListing.searchPlaceholder": "検索…", "xpack.maps.mapListing.titleFieldTitle": "タイトル", - "xpack.maps.mapListing.unableToDeleteToastTitle": "マップを削除できません", "xpack.maps.maps.choropleth.rightSourcePlaceholder": "インデックスパターンを選択", "xpack.maps.mapSavedObjectLabel": "マップ", "xpack.maps.mapSettingsPanel.autoFitToBoundsLocationLabel": "自動的にマップをデータ境界に合わせる", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 2f9eef45a244b..e83420a6b1065 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -11307,24 +11307,9 @@ "xpack.maps.layerWizardSelect.solutionsCategoryLabel": "解决方案", "xpack.maps.loadMap.errorAttemptingToLoadSavedMap": "无法加载地图", "xpack.maps.map.initializeErrorTitle": "无法初始化地图", - "xpack.maps.mapListing.advancedSettingsLinkText": "高级设置", - "xpack.maps.mapListing.cancelTitle": "取消", - "xpack.maps.mapListing.createMapButtonLabel": "创建地图", - "xpack.maps.mapListing.deleteSelectedButtonLabel": "删除选定", - "xpack.maps.mapListing.deleteSelectedItemsTitle": "删除选定项?", - "xpack.maps.mapListing.deleteTitle": "删除", - "xpack.maps.mapListing.deleteWarning": "您无法恢复已删除项。", "xpack.maps.mapListing.descriptionFieldTitle": "描述", "xpack.maps.mapListing.errorAttemptingToLoadSavedMaps": "无法加载地图", - "xpack.maps.mapListing.limitExceededTitle": "已超过列表限制", - "xpack.maps.mapListing.limitHelpDescription": "您有 {totalItems} 项,但您的 listingLimit 设置阻止下表显示 {listingLimit} 项以上。此设置可在以下选项下更改: ", - "xpack.maps.mapListing.listingTableTitle": "Maps", - "xpack.maps.mapListing.noItemsDescription": "似乎您没有任何地图。单击创建按钮来创建。", - "xpack.maps.mapListing.noMatchDescription": "没有任何项匹配您的搜索。", - "xpack.maps.mapListing.searchAriaLabel": "筛选项", - "xpack.maps.mapListing.searchPlaceholder": "搜索......", "xpack.maps.mapListing.titleFieldTitle": "标题", - "xpack.maps.mapListing.unableToDeleteToastTitle": "无法删除地图", "xpack.maps.maps.choropleth.rightSourcePlaceholder": "选择索引模式", "xpack.maps.mapSavedObjectLabel": "地图", "xpack.maps.mapSettingsPanel.autoFitToBoundsLocationLabel": "使地图自适应数据边界", diff --git a/x-pack/test/functional/apps/maps/mvt_super_fine.js b/x-pack/test/functional/apps/maps/mvt_super_fine.js index b5a7935a81eb5..6d86b93c3ec44 100644 --- a/x-pack/test/functional/apps/maps/mvt_super_fine.js +++ b/x-pack/test/functional/apps/maps/mvt_super_fine.js @@ -19,7 +19,7 @@ export default function ({ getPageObjects, getService }) { ['global_maps_all', 'test_logstash_reader', 'geoshape_data_reader'], false ); - await PageObjects.maps.loadSavedMap('geo grid vector grid example (SUPER_FINE resolution)'); + await PageObjects.maps.loadSavedMap('geo grid vector grid example SUPER_FINE resolution'); }); after(async () => { 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 277a8a5651453..8c62136472921 100644 --- a/x-pack/test/functional/apps/maps/saved_object_management.js +++ b/x-pack/test/functional/apps/maps/saved_object_management.js @@ -139,8 +139,8 @@ export default function ({ getPageObjects, getService }) { await PageObjects.maps.openNewMap(); await PageObjects.maps.saveMap(MAP1_NAME); - const count = await PageObjects.maps.getMapCountWithName(MAP1_NAME); - expect(count).to.equal(1); + + await PageObjects.maps.searchAndExpectItemsCount(MAP1_NAME, 1); }); it('should allow saving map that crosses dateline', async () => { @@ -148,8 +148,8 @@ export default function ({ getPageObjects, getService }) { await PageObjects.maps.setView('64', '179', '5'); await PageObjects.maps.saveMap(MAP2_NAME); - const count = await PageObjects.maps.getMapCountWithName(MAP2_NAME); - expect(count).to.equal(1); + + await PageObjects.maps.searchAndExpectItemsCount(MAP2_NAME, 1); }); }); @@ -157,11 +157,9 @@ export default function ({ getPageObjects, getService }) { it('should delete selected saved objects', async () => { await PageObjects.maps.deleteSavedMaps(MAP_NAME_PREFIX); - const map1Count = await PageObjects.maps.getMapCountWithName(MAP1_NAME); - expect(map1Count).to.equal(0); + await PageObjects.maps.searchAndExpectItemsCount(MAP1_NAME, 0); - const map2Count = await PageObjects.maps.getMapCountWithName(MAP2_NAME); - expect(map2Count).to.equal(0); + await PageObjects.maps.searchAndExpectItemsCount(MAP2_NAME, 0); }); }); }); diff --git a/x-pack/test/functional/es_archives/maps/kibana/data.json b/x-pack/test/functional/es_archives/maps/kibana/data.json index e3a8743e60897..79e8c14cc3982 100644 --- a/x-pack/test/functional/es_archives/maps/kibana/data.json +++ b/x-pack/test/functional/es_archives/maps/kibana/data.json @@ -638,7 +638,7 @@ "description": "", "layerListJSON": "[{\"id\":\"g1xkv\",\"label\":null,\"minZoom\":0,\"maxZoom\":24,\"sourceDescriptor\":{\"resolution\": \"SUPER_FINE\",\"type\":\"ES_GEO_GRID\",\"id\":\"9305f6ea-4518-4c06-95b9-33321aa38d6a\",\"indexPatternId\":\"c698b940-e149-11e8-a35a-370a8516603a\",\"geoField\":\"geo.coordinates\",\"requestType\":\"grid\",\"metrics\":[{\"type\":\"count\"},{\"type\":\"max\",\"field\":\"bytes\"}]},\"visible\":true,\"temporary\":false,\"style\":{\"type\":\"VECTOR\",\"properties\":{\"fillColor\":{\"type\":\"DYNAMIC\",\"options\":{\"field\":{\"label\":\"max of bytes\",\"name\":\"max_of_bytes\",\"origin\":\"source\"},\"color\":\"Blues\"}},\"lineColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#cccccc\"}},\"lineWidth\":{\"type\":\"STATIC\",\"options\":{\"size\":1}},\"iconSize\":{\"type\":\"DYNAMIC\",\"options\":{\"field\":{\"label\":\"Count\",\"name\":\"doc_count\",\"origin\":\"source\"},\"minSize\":4,\"maxSize\":32}}},\"temporary\":true,\"previousStyle\":null},\"type\":\"TILED_VECTOR\"}]", "mapStateJSON": "{\"zoom\":3.59,\"center\":{\"lon\":-98.05765,\"lat\":38.32288},\"timeFilters\":{\"from\":\"2015-09-20T00:00:00.000Z\",\"to\":\"2015-09-20T01:00:00.000Z\"},\"refreshConfig\":{\"isPaused\":true,\"interval\":1000}}", - "title": "geo grid vector grid example (SUPER_FINE resolution)", + "title": "geo grid vector grid example SUPER_FINE resolution", "uiStateJSON": "{\"isDarkMode\":false}" }, "type": "map" diff --git a/x-pack/test/functional/page_objects/gis_page.ts b/x-pack/test/functional/page_objects/gis_page.ts index 3b461d4536b02..850c631f2c7bc 100644 --- a/x-pack/test/functional/page_objects/gis_page.ts +++ b/x-pack/test/functional/page_objects/gis_page.ts @@ -21,6 +21,7 @@ export function GisPageProvider({ getService, getPageObjects }: FtrProviderConte const renderable = getService('renderable'); const browser = getService('browser'); const MenuToggle = getService('MenuToggle'); + const listingTable = getService('listingTable'); const setViewPopoverToggle = new MenuToggle({ name: 'SetView Popover', @@ -120,13 +121,10 @@ export function GisPageProvider({ getService, getPageObjects }: FtrProviderConte await retry.try(async () => { await this.searchForMapWithName(name); - await this.selectMap(name); + await listingTable.clickItemLink('map', name); await PageObjects.header.waitUntilLoadingHasFinished(); - - const onMapListingPage = await this.onMapListingPage(); - if (onMapListingPage) { - throw new Error(`Failed to open map ${name}`); - } + // check Map landing page is not present + await testSubjects.missingOrFail('mapLandingPage', { timeout: 10000 }); }); await this.waitForLayersToLoad(); @@ -134,8 +132,8 @@ export function GisPageProvider({ getService, getPageObjects }: FtrProviderConte async deleteSavedMaps(search: string) { await this.searchForMapWithName(search); - await testSubjects.click('checkboxSelectAll'); - await testSubjects.click('deleteSelectedItems'); + await listingTable.checkListingSelectAllCheckbox(); + await listingTable.clickDeleteSelected(); await PageObjects.common.clickConfirmOnModal(); await PageObjects.header.waitUntilLoadingHasFinished(); @@ -150,7 +148,7 @@ export function GisPageProvider({ getService, getPageObjects }: FtrProviderConte await renderable.waitForRender(); } - async saveMap(name: string, uncheckReturnToOriginModeSwitch = false) { + async saveMap(name: string, uncheckReturnToOriginModeSwitch = false, tags?: string[]) { await testSubjects.click('mapSaveButton'); await testSubjects.setValue('savedObjectTitle', name); if (uncheckReturnToOriginModeSwitch) { @@ -162,6 +160,13 @@ export function GisPageProvider({ getService, getPageObjects }: FtrProviderConte } await testSubjects.setEuiSwitch('returnToOriginModeSwitch', 'uncheck'); } + if (tags) { + await testSubjects.click('savedObjectTagSelector'); + for (const tagName of tags) { + await testSubjects.click(`tagSelectorOption-${tagName.replace(' ', '_')}`); + } + await testSubjects.click('savedObjectTitle'); + } await testSubjects.clickWhenNotDisabled('confirmSaveSavedObjectButton'); } @@ -174,7 +179,7 @@ export function GisPageProvider({ getService, getPageObjects }: FtrProviderConte } async expectMissingCreateNewButton() { - await testSubjects.missingOrFail('newMapLink'); + await testSubjects.missingOrFail('newItemButton'); } async expectMissingAddLayerButton() { @@ -187,8 +192,7 @@ export function GisPageProvider({ getService, getPageObjects }: FtrProviderConte async onMapListingPage() { log.debug(`onMapListingPage`); - const exists = await testSubjects.exists('mapsListingPage', { timeout: 3500 }); - return exists; + return await listingTable.onListingPage('map'); } async searchForMapWithName(name: string) { @@ -196,21 +200,11 @@ export function GisPageProvider({ getService, getPageObjects }: FtrProviderConte await this.gotoMapListingPage(); - await retry.try(async () => { - const searchFilter = await testSubjects.find('searchFilter'); - await searchFilter.clearValue(); - await searchFilter.click(); - await searchFilter.type(name); - await PageObjects.common.pressEnterKey(); - }); + await listingTable.searchForItemWithName(name); await PageObjects.header.waitUntilLoadingHasFinished(); } - async selectMap(name: string) { - await testSubjects.click(`mapListingTitleLink-${name.split(' ').join('-')}`); - } - async getHits() { await inspector.open(); await inspector.openInspectorRequestsView(); @@ -232,13 +226,11 @@ export function GisPageProvider({ getService, getPageObjects }: FtrProviderConte } } - async getMapCountWithName(name: string) { + async searchAndExpectItemsCount(name: string, count: number) { await this.gotoMapListingPage(); - log.debug(`getMapCountWithName: ${name}`); - await this.searchForMapWithName(name); - const buttons = await find.allByButtonText(name); - return buttons.length; + log.debug(`searchAndExpectItemsCount: ${name}`); + await listingTable.searchAndExpectItemsCount('map', name, count); } async setView(lat: number, lon: number, zoom: number) { diff --git a/x-pack/test/saved_object_tagging/api_integration/security_and_spaces/apis/_find.ts b/x-pack/test/saved_object_tagging/api_integration/security_and_spaces/apis/_find.ts index 4f08134365e95..8734b7cf5bb68 100644 --- a/x-pack/test/saved_object_tagging/api_integration/security_and_spaces/apis/_find.ts +++ b/x-pack/test/saved_object_tagging/api_integration/security_and_spaces/apis/_find.ts @@ -60,6 +60,7 @@ export default function ({ getService }: FtrProviderContext) { USERS.DEFAULT_SPACE_SO_TAGGING_WRITE_USER, USERS.DEFAULT_SPACE_DASHBOARD_READ_USER, USERS.DEFAULT_SPACE_VISUALIZE_READ_USER, + USERS.DEFAULT_SPACE_MAPS_READ_USER, ], unauthorized: [USERS.NOT_A_KIBANA_USER, USERS.DEFAULT_SPACE_ADVANCED_SETTINGS_READ_USER], }; diff --git a/x-pack/test/saved_object_tagging/api_integration/security_and_spaces/apis/create.ts b/x-pack/test/saved_object_tagging/api_integration/security_and_spaces/apis/create.ts index 70884ba6c968b..8ca92ac472c6e 100644 --- a/x-pack/test/saved_object_tagging/api_integration/security_and_spaces/apis/create.ts +++ b/x-pack/test/saved_object_tagging/api_integration/security_and_spaces/apis/create.ts @@ -60,6 +60,7 @@ export default function ({ getService }: FtrProviderContext) { USERS.DEFAULT_SPACE_DASHBOARD_READ_USER, USERS.DEFAULT_SPACE_VISUALIZE_READ_USER, USERS.DEFAULT_SPACE_ADVANCED_SETTINGS_READ_USER, + USERS.DEFAULT_SPACE_MAPS_READ_USER, USERS.NOT_A_KIBANA_USER, ], }; diff --git a/x-pack/test/saved_object_tagging/api_integration/security_and_spaces/apis/delete.ts b/x-pack/test/saved_object_tagging/api_integration/security_and_spaces/apis/delete.ts index 64f120fd75629..a2e3630622d67 100644 --- a/x-pack/test/saved_object_tagging/api_integration/security_and_spaces/apis/delete.ts +++ b/x-pack/test/saved_object_tagging/api_integration/security_and_spaces/apis/delete.ts @@ -53,6 +53,7 @@ export default function ({ getService }: FtrProviderContext) { USERS.DEFAULT_SPACE_DASHBOARD_READ_USER, USERS.DEFAULT_SPACE_VISUALIZE_READ_USER, USERS.DEFAULT_SPACE_ADVANCED_SETTINGS_READ_USER, + USERS.DEFAULT_SPACE_MAPS_READ_USER, USERS.NOT_A_KIBANA_USER, ], }; diff --git a/x-pack/test/saved_object_tagging/api_integration/security_and_spaces/apis/get.ts b/x-pack/test/saved_object_tagging/api_integration/security_and_spaces/apis/get.ts index 1a354bbbcb660..9cde766b4f514 100644 --- a/x-pack/test/saved_object_tagging/api_integration/security_and_spaces/apis/get.ts +++ b/x-pack/test/saved_object_tagging/api_integration/security_and_spaces/apis/get.ts @@ -57,6 +57,7 @@ export default function ({ getService }: FtrProviderContext) { USERS.DEFAULT_SPACE_SO_TAGGING_WRITE_USER, USERS.DEFAULT_SPACE_DASHBOARD_READ_USER, USERS.DEFAULT_SPACE_VISUALIZE_READ_USER, + USERS.DEFAULT_SPACE_MAPS_READ_USER, ], unauthorized: [USERS.NOT_A_KIBANA_USER, USERS.DEFAULT_SPACE_ADVANCED_SETTINGS_READ_USER], }; diff --git a/x-pack/test/saved_object_tagging/api_integration/security_and_spaces/apis/get_all.ts b/x-pack/test/saved_object_tagging/api_integration/security_and_spaces/apis/get_all.ts index 61b859cf81992..677bdee56ed8b 100644 --- a/x-pack/test/saved_object_tagging/api_integration/security_and_spaces/apis/get_all.ts +++ b/x-pack/test/saved_object_tagging/api_integration/security_and_spaces/apis/get_all.ts @@ -63,6 +63,7 @@ export default function ({ getService }: FtrProviderContext) { USERS.DEFAULT_SPACE_SO_TAGGING_WRITE_USER, USERS.DEFAULT_SPACE_DASHBOARD_READ_USER, USERS.DEFAULT_SPACE_VISUALIZE_READ_USER, + USERS.DEFAULT_SPACE_MAPS_READ_USER, ], unauthorized: [USERS.NOT_A_KIBANA_USER, USERS.DEFAULT_SPACE_ADVANCED_SETTINGS_READ_USER], }; diff --git a/x-pack/test/saved_object_tagging/api_integration/security_and_spaces/apis/update.ts b/x-pack/test/saved_object_tagging/api_integration/security_and_spaces/apis/update.ts index 77bf9d7ca3287..3347eca9920d6 100644 --- a/x-pack/test/saved_object_tagging/api_integration/security_and_spaces/apis/update.ts +++ b/x-pack/test/saved_object_tagging/api_integration/security_and_spaces/apis/update.ts @@ -60,6 +60,7 @@ export default function ({ getService }: FtrProviderContext) { USERS.DEFAULT_SPACE_DASHBOARD_READ_USER, USERS.DEFAULT_SPACE_VISUALIZE_READ_USER, USERS.DEFAULT_SPACE_ADVANCED_SETTINGS_READ_USER, + USERS.DEFAULT_SPACE_MAPS_READ_USER, USERS.NOT_A_KIBANA_USER, ], }; diff --git a/x-pack/test/saved_object_tagging/common/fixtures/es_archiver/maps/data.json b/x-pack/test/saved_object_tagging/common/fixtures/es_archiver/maps/data.json new file mode 100644 index 0000000000000..cdaf4fe171ec0 --- /dev/null +++ b/x-pack/test/saved_object_tagging/common/fixtures/es_archiver/maps/data.json @@ -0,0 +1,210 @@ +{ + "type": "doc", + "value": { + "id": "space:default", + "index": ".kibana", + "source": { + "space": { + "_reserved": true, + "description": "This is the default space", + "name": "Default Space" + }, + "type": "space", + "updated_at": "2017-09-21T18:49:16.270Z" + }, + "type": "doc" + } +} + +{ + "type": "doc", + "value": { + "id": "tag:tag-1", + "index": ".kibana", + "source": { + "tag": { + "name": "tag-1", + "description": "My first tag!", + "color": "#FF00FF" + }, + "type": "tag", + "updated_at": "2017-09-21T18:49:16.270Z" + }, + "type": "doc" + } +} + +{ + "type": "doc", + "value": { + "id": "tag:tag-2", + "index": ".kibana", + "source": { + "tag": { + "name": "tag-2", + "description": "Another awesome tag", + "color": "#11FF22" + }, + "type": "tag", + "updated_at": "2017-09-21T18:49:16.270Z" + }, + "type": "doc" + } +} + +{ + "type": "doc", + "value": { + "id": "tag:tag-3", + "index": ".kibana", + "source": { + "tag": { + "name": "tag-3", + "description": "Last but not least", + "color": "#AA0077" + }, + "type": "tag", + "updated_at": "2017-09-21T18:49:16.270Z" + }, + "type": "doc" + } +} + +{ + "type": "doc", + "value": { + "id": "config:6.3.0", + "index": ".kibana", + "source": { + "config": { + "buildNum": 8467, + "defaultIndex": "0bf35f60-3dc9-11e8-8660-4d65aa086b3c" + }, + "references": [ + ], + "type": "config", + "updated_at": "2018-04-11T20:43:55.434Z" + } + } +} + +{ + "type": "doc", + "value": { + "id": "map:63af0ed0-2515-11eb-8f44-eda8d4b698b3", + "index": ".kibana", + "source": { + "map": { + "title" : "map 3 (tag-1 and tag-3)", + "description" : "", + "layerListJSON" : "[{\"sourceDescriptor\":{\"type\":\"EMS_TMS\",\"isAutoSelect\":true},\"id\":\"d897b506-e719-42b8-9927-351eedd7d357\",\"label\":null,\"minZoom\":0,\"maxZoom\":24,\"alpha\":1,\"visible\":true,\"style\":{\"type\":\"TILE\"},\"type\":\"VECTOR_TILE\"}]", + "mapStateJSON" : "{\"zoom\":0.97,\"center\":{\"lon\":0,\"lat\":19.94277},\"timeFilters\":{\"from\":\"now-15m\",\"to\":\"now\"},\"refreshConfig\":{\"isPaused\":true,\"interval\":0},\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filters\":[],\"settings\":{\"autoFitToDataBounds\":false,\"initialLocation\":\"LAST_SAVED_LOCATION\",\"fixedLocation\":{\"lat\":0,\"lon\":0,\"zoom\":2},\"browserLocation\":{\"zoom\":2},\"maxZoom\":24,\"minZoom\":0,\"showSpatialFilters\":true,\"spatialFiltersAlpa\":0.3,\"spatialFiltersFillColor\":\"#DA8B45\",\"spatialFiltersLineColor\":\"#DA8B45\"}}", + "uiStateJSON" : "{\"isLayerTOCOpen\":true,\"openTOCDetails\":[]}" + }, + "type" : "map", + "references" : [ + { + "type" : "tag", + "id" : "tag-1", + "name" : "tag-ref-tag-1" + }, + { + "type" : "tag", + "id" : "tag-3", + "name" : "tag-ref-tag-3" + } + ], + "migrationVersion" : { + "map" : "7.10.0" + }, + "updated_at" : "2020-11-12T18:32:16.189Z" + } + } +} + +{ + "type": "doc", + "value": { + "id": "map:4afc6d10-2515-11eb-8f44-eda8d4b698b3", + "index": ".kibana", + "source": { + "map" : { + "title" : "map 1 (tag-2)", + "description" : "", + "layerListJSON" : "[{\"sourceDescriptor\":{\"type\":\"EMS_TMS\",\"isAutoSelect\":true},\"id\":\"6a91fb66-465c-4193-8c59-9b3f5f262756\",\"label\":null,\"minZoom\":0,\"maxZoom\":24,\"alpha\":1,\"visible\":true,\"style\":{\"type\":\"TILE\"},\"type\":\"VECTOR_TILE\"}]", + "mapStateJSON" : "{\"zoom\":0.97,\"center\":{\"lon\":0,\"lat\":16.22097},\"timeFilters\":{\"from\":\"now-15m\",\"to\":\"now\"},\"refreshConfig\":{\"isPaused\":true,\"interval\":0},\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filters\":[],\"settings\":{\"autoFitToDataBounds\":false,\"initialLocation\":\"LAST_SAVED_LOCATION\",\"fixedLocation\":{\"lat\":0,\"lon\":0,\"zoom\":2},\"browserLocation\":{\"zoom\":2},\"maxZoom\":24,\"minZoom\":0,\"showSpatialFilters\":true,\"spatialFiltersAlpa\":0.3,\"spatialFiltersFillColor\":\"#DA8B45\",\"spatialFiltersLineColor\":\"#DA8B45\"}}", + "uiStateJSON" : "{\"isLayerTOCOpen\":true,\"openTOCDetails\":[]}" + }, + "type" : "map", + "references" : [ + { + "type" : "tag", + "id" : "tag-2", + "name" : "tag-ref-tag-2" + } + ], + "migrationVersion" : { + "map" : "7.10.0" + }, + "updated_at" : "2020-11-12T18:31:34.753Z" + } + } +} + +{ + "type": "doc", + "value": { + "id": "map:562cce50-2515-11eb-8f44-eda8d4b698b3", + "index": ".kibana", + "source": { + "map" : { + "title" : "map 2 (tag-3)", + "description" : "", + "layerListJSON" : "[{\"sourceDescriptor\":{\"type\":\"EMS_TMS\",\"isAutoSelect\":true},\"id\":\"285d5190-aaf1-4dfc-912b-9c7d9e0104a8\",\"label\":null,\"minZoom\":0,\"maxZoom\":24,\"alpha\":1,\"visible\":true,\"style\":{\"type\":\"TILE\"},\"type\":\"VECTOR_TILE\"}]", + "mapStateJSON" : "{\"zoom\":0.97,\"center\":{\"lon\":0,\"lat\":19.94277},\"timeFilters\":{\"from\":\"now-15m\",\"to\":\"now\"},\"refreshConfig\":{\"isPaused\":true,\"interval\":0},\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filters\":[],\"settings\":{\"autoFitToDataBounds\":false,\"initialLocation\":\"LAST_SAVED_LOCATION\",\"fixedLocation\":{\"lat\":0,\"lon\":0,\"zoom\":2},\"browserLocation\":{\"zoom\":2},\"maxZoom\":24,\"minZoom\":0,\"showSpatialFilters\":true,\"spatialFiltersAlpa\":0.3,\"spatialFiltersFillColor\":\"#DA8B45\",\"spatialFiltersLineColor\":\"#DA8B45\"}}", + "uiStateJSON" : "{\"isLayerTOCOpen\":true,\"openTOCDetails\":[]}" + }, + "type" : "map", + "references" : [ + { + "type" : "tag", + "id" : "tag-3", + "name" : "tag-ref-tag-3" + } + ], + "migrationVersion" : { + "map" : "7.10.0" + }, + "updated_at" : "2020-11-12T18:31:53.525Z" + } + } +} + +{ + "type": "doc", + "value": { + "id": "map:6f021340-2515-11eb-8f44-eda8d4b698b3", + "index": ".kibana", + "source": { + "map" : { + "title" : "map 4 (tag-1)", + "description" : "", + "layerListJSON" : "[{\"sourceDescriptor\":{\"type\":\"EMS_TMS\",\"isAutoSelect\":true},\"id\":\"3deeb666-33cf-4e9a-ab78-e453ed9d721d\",\"label\":null,\"minZoom\":0,\"maxZoom\":24,\"alpha\":1,\"visible\":true,\"style\":{\"type\":\"TILE\"},\"type\":\"VECTOR_TILE\"}]", + "mapStateJSON" : "{\"zoom\":0.97,\"center\":{\"lon\":0,\"lat\":19.94277},\"timeFilters\":{\"from\":\"now-15m\",\"to\":\"now\"},\"refreshConfig\":{\"isPaused\":true,\"interval\":0},\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filters\":[],\"settings\":{\"autoFitToDataBounds\":false,\"initialLocation\":\"LAST_SAVED_LOCATION\",\"fixedLocation\":{\"lat\":0,\"lon\":0,\"zoom\":2},\"browserLocation\":{\"zoom\":2},\"maxZoom\":24,\"minZoom\":0,\"showSpatialFilters\":true,\"spatialFiltersAlpa\":0.3,\"spatialFiltersFillColor\":\"#DA8B45\",\"spatialFiltersLineColor\":\"#DA8B45\"}}", + "uiStateJSON" : "{\"isLayerTOCOpen\":true,\"openTOCDetails\":[]}" + }, + "type" : "map", + "references" : [ + { + "type" : "tag", + "id" : "tag-1", + "name" : "tag-ref-tag-1" + } + ], + "migrationVersion" : { + "map" : "7.10.0" + }, + "updated_at" : "2020-11-12T18:32:35.188Z" + } + } +} diff --git a/x-pack/test/saved_object_tagging/common/lib/authentication.ts b/x-pack/test/saved_object_tagging/common/lib/authentication.ts index c318755bedcdd..8917057ad685e 100644 --- a/x-pack/test/saved_object_tagging/common/lib/authentication.ts +++ b/x-pack/test/saved_object_tagging/common/lib/authentication.ts @@ -118,6 +118,19 @@ export const ROLES = { ], }, }, + KIBANA_RBAC_DEFAULT_SPACE_MAPS_READ_USER: { + name: 'kibana_rbac_default_space_maps_read_user', + privileges: { + kibana: [ + { + feature: { + maps: ['read'], + }, + spaces: ['default'], + }, + ], + }, + }, }; export const USERS = { @@ -185,4 +198,9 @@ export const USERS = { password: 'password', roles: [ROLES.KIBANA_RBAC_DEFAULT_SPACE_ADVANCED_SETTINGS_READ_USER.name], }, + DEFAULT_SPACE_MAPS_READ_USER: { + username: 'a_kibana_rbac_default_space_maps_read_user', + password: 'password', + roles: [ROLES.KIBANA_RBAC_DEFAULT_SPACE_MAPS_READ_USER.name], + }, }; diff --git a/x-pack/test/saved_object_tagging/functional/tests/index.ts b/x-pack/test/saved_object_tagging/functional/tests/index.ts index 43673487ba74f..0ddfa64d682a8 100644 --- a/x-pack/test/saved_object_tagging/functional/tests/index.ts +++ b/x-pack/test/saved_object_tagging/functional/tests/index.ts @@ -23,5 +23,6 @@ export default function ({ loadTestFile, getService }: FtrProviderContext) { loadTestFile(require.resolve('./visualize_integration')); loadTestFile(require.resolve('./dashboard_integration')); loadTestFile(require.resolve('./feature_control')); + loadTestFile(require.resolve('./maps_integration')); }); } diff --git a/x-pack/test/saved_object_tagging/functional/tests/maps_integration.ts b/x-pack/test/saved_object_tagging/functional/tests/maps_integration.ts new file mode 100644 index 0000000000000..4e44659b4fc67 --- /dev/null +++ b/x-pack/test/saved_object_tagging/functional/tests/maps_integration.ts @@ -0,0 +1,139 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../ftr_provider_context'; + +// eslint-disable-next-line import/no-default-export +export default function ({ getPageObjects, getService }: FtrProviderContext) { + const esArchiver = getService('esArchiver'); + const listingTable = getService('listingTable'); + const testSubjects = getService('testSubjects'); + const find = getService('find'); + const PageObjects = getPageObjects(['maps', 'tagManagement', 'common']); + + /** + * Select tags in the searchbar's tag filter. + */ + const selectFilterTags = async (...tagNames: string[]) => { + // open the filter dropdown + const filterButton = await find.byCssSelector('.euiFilterGroup .euiFilterButton'); + await filterButton.click(); + // select the tags + for (const tagName of tagNames) { + await testSubjects.click( + `tag-searchbar-option-${PageObjects.tagManagement.testSubjFriendly(tagName)}` + ); + } + // click elsewhere to close the filter dropdown + const searchFilter = await find.byCssSelector('main .euiFieldSearch'); + await searchFilter.click(); + }; + + describe('maps integration', () => { + before(async () => { + await esArchiver.load('maps'); + }); + after(async () => { + await esArchiver.unload('maps'); + }); + + describe('listing', () => { + beforeEach(async () => { + await PageObjects.common.navigateToUrlWithBrowserHistory('maps', '/'); + await PageObjects.maps.gotoMapListingPage(); + }); + + it('allows to manually type tag filter query', async () => { + await listingTable.searchForItemWithName('tag:(tag-1)', { escape: false }); + + await listingTable.expectItemsCount('map', 2); + const itemNames = await listingTable.getAllItemsNames(); + expect(itemNames).to.eql(['map 3 (tag-1 and tag-3)', 'map 4 (tag-1)']); + }); + + it('allows to filter by selecting a tag in the filter menu', async () => { + await selectFilterTags('tag-3'); + + await listingTable.expectItemsCount('map', 2); + const itemNames = await listingTable.getAllItemsNames(); + expect(itemNames).to.eql(['map 3 (tag-1 and tag-3)', 'map 2 (tag-3)']); + }); + + it('allows to filter by multiple tags', async () => { + await selectFilterTags('tag-2', 'tag-3'); + + await listingTable.expectItemsCount('map', 3); + const itemNames = await listingTable.getAllItemsNames(); + expect(itemNames).to.eql(['map 3 (tag-1 and tag-3)', 'map 1 (tag-2)', 'map 2 (tag-3)']); + }); + }); + + describe('creating', () => { + beforeEach(async () => { + await PageObjects.maps.openNewMap(); + }); + + it('allows to select tags for a new map', async () => { + await PageObjects.maps.saveMap('my-new-map', false, ['tag-1', 'tag-3']); + + await PageObjects.maps.gotoMapListingPage(); + await selectFilterTags('tag-1'); + const itemNames = await listingTable.getAllItemsNames(); + expect(itemNames).to.contain('my-new-map'); + }); + + it('allows to create a tag from the tag selector', async () => { + const { tagModal } = PageObjects.tagManagement; + + await testSubjects.click('mapSaveButton'); + await testSubjects.setValue('savedObjectTitle', 'map-with-new-tag'); + + await testSubjects.click('savedObjectTagSelector'); + await testSubjects.click(`tagSelectorOption-action__create`); + + expect(await tagModal.isOpened()).to.be(true); + + await tagModal.fillForm( + { + name: 'my-new-tag', + color: '#FFCC33', + description: '', + }, + { + submit: true, + } + ); + + expect(await tagModal.isOpened()).to.be(false); + + await testSubjects.click('confirmSaveSavedObjectButton'); + + await PageObjects.maps.gotoMapListingPage(); + await selectFilterTags('my-new-tag'); + const itemNames = await listingTable.getAllItemsNames(); + expect(itemNames).to.contain('map-with-new-tag'); + }); + }); + + describe('editing', () => { + beforeEach(async () => { + await PageObjects.common.navigateToUrlWithBrowserHistory('maps', '/'); + }); + + it('allows to select tags for an existing map', async () => { + await listingTable.clickItemLink('map', 'map 4 (tag-1)'); + + await PageObjects.maps.saveMap('map 4 (tag-1)', false, ['tag-3']); + + await PageObjects.maps.gotoMapListingPage(); + await selectFilterTags('tag-3'); + const itemNames = await listingTable.getAllItemsNames(); + expect(itemNames).to.contain('map 4 (tag-1)'); + }); + }); + }); +}