From 6010f29c12e72ca005d85f3a81b25fe4c174ec5d Mon Sep 17 00:00:00 2001 From: Joe Portner <5295965+jportner@users.noreply.github.com> Date: Mon, 26 Apr 2021 11:37:32 -0400 Subject: [PATCH 1/4] Core changes Add support for `resolve` method to the public SOC client abstraction. This was overlooked in Sharing Saved Objects Phase 2, but it will be added in Phase 3. --- src/core/public/index.ts | 2 +- src/core/public/saved_objects/index.ts | 1 + .../resolved_simple_saved_object.ts | 27 ++++++++ .../saved_objects_client.test.ts | 61 +++++++++++++++++++ .../saved_objects/saved_objects_client.ts | 22 +++++++ .../saved_objects_service.mock.ts | 1 + .../saved_objects/simple_saved_object.ts | 3 + 7 files changed, 116 insertions(+), 1 deletion(-) create mode 100644 src/core/public/saved_objects/resolved_simple_saved_object.ts diff --git a/src/core/public/index.ts b/src/core/public/index.ts index ca432d6b8269f..91fdd64d24572 100644 --- a/src/core/public/index.ts +++ b/src/core/public/index.ts @@ -109,7 +109,7 @@ export type { NavigateToAppOptions, } from './application'; -export { SimpleSavedObject } from './saved_objects'; +export { ResolvedSimpleSavedObject, SimpleSavedObject } from './saved_objects'; export type { SavedObjectsBatchResponse, SavedObjectsBulkCreateObject, diff --git a/src/core/public/saved_objects/index.ts b/src/core/public/saved_objects/index.ts index e8aef50376841..c7754cdcdf9d8 100644 --- a/src/core/public/saved_objects/index.ts +++ b/src/core/public/saved_objects/index.ts @@ -19,6 +19,7 @@ export type { SavedObjectsUpdateOptions, SavedObjectsBulkUpdateOptions, } from './saved_objects_client'; +export { ResolvedSimpleSavedObject } from './resolved_simple_saved_object'; export { SimpleSavedObject } from './simple_saved_object'; export type { SavedObjectsStart } from './saved_objects_service'; export type { diff --git a/src/core/public/saved_objects/resolved_simple_saved_object.ts b/src/core/public/saved_objects/resolved_simple_saved_object.ts new file mode 100644 index 0000000000000..3a8f8247cc022 --- /dev/null +++ b/src/core/public/saved_objects/resolved_simple_saved_object.ts @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import type { SavedObjectsResolveResponse } from '../../server'; +import type { SimpleSavedObject } from './simple_saved_object'; + +/** + * This class is a very simple wrapper for SavedObjects loaded from the server + * with the {@link SavedObjectsClient}. + * + * It provides basic functionality for creating/saving/deleting saved objects, + * but doesn't include any type-specific implementations. + * + * @public + */ +export class ResolvedSimpleSavedObject { + constructor( + public savedObject: SimpleSavedObject, + public outcome: SavedObjectsResolveResponse['outcome'], + public aliasTargetId: SavedObjectsResolveResponse['aliasTargetId'] + ) {} +} diff --git a/src/core/public/saved_objects/saved_objects_client.test.ts b/src/core/public/saved_objects/saved_objects_client.test.ts index 14421c871fc2b..d377fb4ef2b37 100644 --- a/src/core/public/saved_objects/saved_objects_client.test.ts +++ b/src/core/public/saved_objects/saved_objects_client.test.ts @@ -6,9 +6,12 @@ * Side Public License, v 1. */ +import type { SavedObjectsResolveResponse } from 'src/core/server'; + import { SavedObjectsClient } from './saved_objects_client'; import { SimpleSavedObject } from './simple_saved_object'; import { httpServiceMock } from '../http/http_service.mock'; +import { ResolvedSimpleSavedObject } from './resolved_simple_saved_object'; describe('SavedObjectsClient', () => { const doc = { @@ -147,6 +150,64 @@ describe('SavedObjectsClient', () => { }); }); + describe('#resolve', () => { + beforeEach(() => { + beforeEach(() => { + http.fetch.mockResolvedValue({ + saved_object: doc, + outcome: 'conflict', + aliasTargetId: 'another-id', + } as SavedObjectsResolveResponse); + }); + }); + + test('rejects if `type` is undefined', async () => { + expect(savedObjectsClient.resolve(undefined as any, doc.id)).rejects.toMatchInlineSnapshot( + `[Error: requires type and id]` + ); + }); + + test('rejects if `id` is undefined', async () => { + expect(savedObjectsClient.resolve(doc.type, undefined as any)).rejects.toMatchInlineSnapshot( + `[Error: requires type and id]` + ); + }); + + test('makes HTTP call', () => { + savedObjectsClient.resolve(doc.type, doc.id); + expect(http.fetch.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + "/api/saved_objects/resolve/config/AVwSwFxtcMV38qjDZoQg", + Object { + "body": undefined, + "method": undefined, + "query": undefined, + }, + ], + ] + `); + }); + + test('rejects when HTTP call fails', async () => { + http.fetch.mockRejectedValueOnce(new Error('Request failed')); + await expect(savedObjectsClient.resolve(doc.type, doc.id)).rejects.toMatchInlineSnapshot( + `[Error: Request failed]` + ); + }); + + test('resolves with ResolvedSimpleSavedObject instance', async () => { + const response = savedObjectsClient.resolve(doc.type, doc.id); + await expect(response).resolves.toBeInstanceOf(ResolvedSimpleSavedObject); + + const result = await response; + expect(result.savedObject.type).toBe(doc.type); + expect(result.savedObject.get('title')).toBe('Example title'); + expect(result.outcome).toBe('conflict'); + expect(result.aliasTargetId).toBe('another-id'); + }); + }); + describe('#delete', () => { beforeEach(() => { http.fetch.mockResolvedValue({}); diff --git a/src/core/public/saved_objects/saved_objects_client.ts b/src/core/public/saved_objects/saved_objects_client.ts index 782ffa6897048..6ad87890d1d29 100644 --- a/src/core/public/saved_objects/saved_objects_client.ts +++ b/src/core/public/saved_objects/saved_objects_client.ts @@ -16,8 +16,10 @@ import { SavedObjectsClientContract as SavedObjectsApi, SavedObjectsFindOptions as SavedObjectFindOptionsServer, SavedObjectsMigrationVersion, + SavedObjectsResolveResponse, } from '../../server'; +import { ResolvedSimpleSavedObject } from './resolved_simple_saved_object'; import { SimpleSavedObject } from './simple_saved_object'; import { HttpFetchOptions, HttpSetup } from '../http'; @@ -422,6 +424,26 @@ export class SavedObjectsClient { return request; } + /** + * Resolves a single object + * + * @param {string} type + * @param {string} id + * @returns The resolve result for the saved object for the given type and id. + */ + public resolve = (type: string, id: string) => { + if (!type || !id) { + return Promise.reject(new Error('requires type and id')); + } + + const path = `${this.getPath(['resolve'])}/${type}/${id}`; + const request: Promise> = this.savedObjectsFetch(path, {}); + return request.then(({ saved_object: object, outcome, aliasTargetId }) => { + const savedObject = new SimpleSavedObject(this, cloneDeep(object)); + return new ResolvedSimpleSavedObject(savedObject, outcome, aliasTargetId); + }); + }; + /** * Updates an object * diff --git a/src/core/public/saved_objects/saved_objects_service.mock.ts b/src/core/public/saved_objects/saved_objects_service.mock.ts index 625ea6b5dd2da..2ceef1c077c39 100644 --- a/src/core/public/saved_objects/saved_objects_service.mock.ts +++ b/src/core/public/saved_objects/saved_objects_service.mock.ts @@ -18,6 +18,7 @@ const createStartContractMock = () => { bulkGet: jest.fn(), find: jest.fn(), get: jest.fn(), + resolve: jest.fn(), update: jest.fn(), }, }; diff --git a/src/core/public/saved_objects/simple_saved_object.ts b/src/core/public/saved_objects/simple_saved_object.ts index b44991535bc25..95ed4b5996230 100644 --- a/src/core/public/saved_objects/simple_saved_object.ts +++ b/src/core/public/saved_objects/simple_saved_object.ts @@ -30,6 +30,7 @@ export class SimpleSavedObject { public coreMigrationVersion: SavedObjectType['coreMigrationVersion']; public error: SavedObjectType['error']; public references: SavedObjectType['references']; + public namespaces: SavedObjectType['namespaces']; constructor( private client: SavedObjectsClientContract, @@ -42,6 +43,7 @@ export class SimpleSavedObject { references, migrationVersion, coreMigrationVersion, + namespaces, }: SavedObjectType ) { this.id = id; @@ -51,6 +53,7 @@ export class SimpleSavedObject { this._version = version; this.migrationVersion = migrationVersion; this.coreMigrationVersion = coreMigrationVersion; + this.namespaces = namespaces; if (error) { this.error = error; } From 7d974af55ba7bbfaa0a4f366147bdc7053f24e5a Mon Sep 17 00:00:00 2001 From: Joe Portner <5295965+jportner@users.noreply.github.com> Date: Mon, 26 Apr 2021 11:37:37 -0400 Subject: [PATCH 2/4] Add support for resolving index patterns in data plugin SOC abstraction Splitting this out into a separate commit as it is a prerequisite for converting index patterns to namespaceType 'multiple-isolated' but it is really a separate change in a different plugin. --- .../data/common/index_patterns/index.ts | 2 +- .../index_patterns/_pattern_cache.ts | 12 ++--- .../index_patterns/index_patterns.ts | 48 +++++++++++++++++-- .../data/common/index_patterns/types.ts | 9 ++++ src/plugins/data/public/index.ts | 1 + .../data/public/index_patterns/index.ts | 1 + .../saved_objects_client_wrapper.ts | 9 +++- src/plugins/data/server/index.ts | 1 + .../saved_objects_client_wrapper.ts | 3 ++ 9 files changed, 74 insertions(+), 12 deletions(-) diff --git a/src/plugins/data/common/index_patterns/index.ts b/src/plugins/data/common/index_patterns/index.ts index 7f6249caceb52..97cab7ca2e1fd 100644 --- a/src/plugins/data/common/index_patterns/index.ts +++ b/src/plugins/data/common/index_patterns/index.ts @@ -9,7 +9,7 @@ export * from './fields'; export * from './types'; export { IndexPatternsService, IndexPatternsContract } from './index_patterns'; -export type { IndexPattern } from './index_patterns'; +export type { IndexPattern, ResolvedIndexPattern } from './index_patterns'; export * from './errors'; export * from './expressions'; export * from './constants'; diff --git a/src/plugins/data/common/index_patterns/index_patterns/_pattern_cache.ts b/src/plugins/data/common/index_patterns/index_patterns/_pattern_cache.ts index a58a349a46975..80ae0f82d188e 100644 --- a/src/plugins/data/common/index_patterns/index_patterns/_pattern_cache.ts +++ b/src/plugins/data/common/index_patterns/index_patterns/_pattern_cache.ts @@ -6,18 +6,16 @@ * Side Public License, v 1. */ -import { IndexPattern } from './index_pattern'; - -export interface PatternCache { - get: (id: string) => Promise | undefined; - set: (id: string, value: Promise) => Promise; +export interface PatternCache { + get: (id: string) => Promise | undefined; + set: (id: string, value: Promise) => Promise; clear: (id: string) => void; clearAll: () => void; } -export function createIndexPatternCache(): PatternCache { +export function createIndexPatternCache(): PatternCache { const vals: Record = {}; - const cache: PatternCache = { + const cache: PatternCache = { get: (id: string) => { return vals[id]; }, diff --git a/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts b/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts index 04d2785137719..be15780222a76 100644 --- a/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts +++ b/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts @@ -10,8 +10,8 @@ import { i18n } from '@kbn/i18n'; import { PublicMethodsOf } from '@kbn/utility-types'; import { SavedObjectsClientCommon } from '../..'; -import { createIndexPatternCache } from '.'; -import type { RuntimeField } from '../types'; +import { PatternCache, createIndexPatternCache } from './_pattern_cache'; +import type { ResolvedSavedObjectOutcome, RuntimeField } from '../types'; import { IndexPattern } from './index_pattern'; import { createEnsureDefaultIndexPattern, @@ -54,6 +54,12 @@ interface IndexPatternsServiceDeps { onRedirectNoIndexPattern?: () => void; } +export interface ResolvedIndexPattern { + indexPattern: IndexPattern; + outcome: ResolvedSavedObjectOutcome; + aliasTargetId?: string; +} + export class IndexPatternsService { private config: UiSettingsCommon; private savedObjectsClient: SavedObjectsClientCommon; @@ -62,7 +68,8 @@ export class IndexPatternsService { private fieldFormats: FieldFormatsStartCommon; private onNotification: OnNotification; private onError: OnError; - private indexPatternCache: ReturnType; + private indexPatternCache: PatternCache; + private resolvedIndexPatternCache: PatternCache; ensureDefaultIndexPattern: EnsureDefaultIndexPattern; @@ -87,6 +94,7 @@ export class IndexPatternsService { ); this.indexPatternCache = createIndexPatternCache(); + this.resolvedIndexPatternCache = createIndexPatternCache(); } /** @@ -390,7 +398,23 @@ export class IndexPatternsService { savedObjectType, id ); + return this.savedObjectToIndexPattern(savedObject); + }; + + private resolveSavedObjectAndInit = async (id: string): Promise => { + const resolveResult = await this.savedObjectsClient.resolve( + savedObjectType, + id + ); + return { + indexPattern: await this.savedObjectToIndexPattern(resolveResult.saved_object), + outcome: resolveResult.outcome, + aliasTargetId: resolveResult.aliasTargetId, + }; + }; + private savedObjectToIndexPattern = async (savedObject: SavedObject) => { + const { id } = savedObject; if (!savedObject.version) { throw new SavedObjectNotFound(savedObjectType, id, 'management/kibana/indexPatterns'); } @@ -475,6 +499,24 @@ export class IndexPatternsService { return indexPatternPromise; }; + /** + * Resolve an index pattern by id. Cache optimized + * @param id + */ + + resolve = async (id: string): Promise => { + const indexPatternPromise = + this.resolvedIndexPatternCache.get(id) || + this.resolvedIndexPatternCache.set(id, this.resolveSavedObjectAndInit(id)); + + // don't cache failed requests + indexPatternPromise.catch(() => { + this.indexPatternCache.clear(id); + }); + + return indexPatternPromise; + }; + /** * Create a new index pattern instance * @param spec diff --git a/src/plugins/data/common/index_patterns/types.ts b/src/plugins/data/common/index_patterns/types.ts index c906b809b08c4..272bed902c774 100644 --- a/src/plugins/data/common/index_patterns/types.ts +++ b/src/plugins/data/common/index_patterns/types.ts @@ -101,6 +101,7 @@ export interface SavedObjectsClientCommonFindArgs { export interface SavedObjectsClientCommon { find: (options: SavedObjectsClientCommonFindArgs) => Promise>>; get: (type: string, id: string) => Promise>; + resolve: (type: string, id: string) => Promise>; update: ( type: string, id: string, @@ -250,3 +251,11 @@ export interface IndexPatternSpec { export interface SourceFilter { value: string; } + +export type ResolvedSavedObjectOutcome = 'exactMatch' | 'aliasMatch' | 'conflict'; +export interface ResolvedSavedObject { + // TODO: refactor types and use SavedObjectsResolveResponse instead + saved_object: SavedObject; + outcome: ResolvedSavedObjectOutcome; + aliasTargetId?: string; +} diff --git a/src/plugins/data/public/index.ts b/src/plugins/data/public/index.ts index f2a61e94a07d9..5e5229dac5c9a 100644 --- a/src/plugins/data/public/index.ts +++ b/src/plugins/data/public/index.ts @@ -251,6 +251,7 @@ export const indexPatterns = { export { IndexPatternsContract, IndexPattern, + ResolvedIndexPattern, IIndexPatternFieldList, IndexPatternField, } from './index_patterns'; diff --git a/src/plugins/data/public/index_patterns/index.ts b/src/plugins/data/public/index_patterns/index.ts index 1bdd17af2a78d..f706ea0218617 100644 --- a/src/plugins/data/public/index_patterns/index.ts +++ b/src/plugins/data/public/index_patterns/index.ts @@ -22,6 +22,7 @@ export { IndexPatternsService, IndexPatternsContract, IndexPattern, + ResolvedIndexPattern, IndexPatternsApiClient, } from './index_patterns'; export { UiSettingsPublicToCommon } from './ui_settings_wrapper'; diff --git a/src/plugins/data/public/index_patterns/saved_objects_client_wrapper.ts b/src/plugins/data/public/index_patterns/saved_objects_client_wrapper.ts index f6e94a120d3d2..5e97c2cba6195 100644 --- a/src/plugins/data/public/index_patterns/saved_objects_client_wrapper.ts +++ b/src/plugins/data/public/index_patterns/saved_objects_client_wrapper.ts @@ -14,7 +14,10 @@ import { SavedObject, } from '../../common/index_patterns'; -type SOClient = Pick; +type SOClient = Pick< + SavedObjectsClient, + 'find' | 'get' | 'resolve' | 'update' | 'create' | 'delete' +>; const simpleSavedObjectToSavedObject = (simpleSavedObject: SimpleSavedObject): SavedObject => ({ @@ -36,6 +39,10 @@ export class SavedObjectsClientPublicToCommon implements SavedObjectsClientCommo const response = await this.savedObjectClient.get(type, id); return simpleSavedObjectToSavedObject(response); } + async resolve(type: string, id: string) { + const { savedObject, ...otherFields } = await this.savedObjectClient.resolve(type, id); + return { ...otherFields, saved_object: simpleSavedObjectToSavedObject(savedObject) }; + } async update( type: string, id: string, diff --git a/src/plugins/data/server/index.ts b/src/plugins/data/server/index.ts index c4e132e33fc3b..ef55b3d9b9b0a 100644 --- a/src/plugins/data/server/index.ts +++ b/src/plugins/data/server/index.ts @@ -147,6 +147,7 @@ export { IndexPatternAttributes, UI_SETTINGS, IndexPattern, + ResolvedIndexPattern, IndexPatternLoadExpressionFunctionDefinition, IndexPatternsService, IndexPatternsService as IndexPatternsCommonService, diff --git a/src/plugins/data/server/index_patterns/saved_objects_client_wrapper.ts b/src/plugins/data/server/index_patterns/saved_objects_client_wrapper.ts index 32791c3c8faca..5e2cedc9ff080 100644 --- a/src/plugins/data/server/index_patterns/saved_objects_client_wrapper.ts +++ b/src/plugins/data/server/index_patterns/saved_objects_client_wrapper.ts @@ -25,6 +25,9 @@ export class SavedObjectsClientServerToCommon implements SavedObjectsClientCommo async get(type: string, id: string) { return await this.savedObjectClient.get(type, id); } + async resolve(type: string, id: string) { + return await this.savedObjectClient.resolve(type, id); + } async update( type: string, id: string, From afc34903624f169c6dc98313a6b2e299affc643f Mon Sep 17 00:00:00 2001 From: Joe Portner <5295965+jportner@users.noreply.github.com> Date: Mon, 26 Apr 2021 11:37:39 -0400 Subject: [PATCH 3/4] Convert index patterns to namespaceType 'multiple-isolated' This will happen in the 8.0 release. It requires that index patterns are retrieved with the `resolve` API, and that the various resolve outcomes are handled appropriately on the client side using the appropriate Spaces APIs. --- .../server/saved_objects/index_patterns.ts | 3 +- .../index_pattern_management/kibana.json | 2 +- .../edit_index_pattern/edit_index_pattern.tsx | 52 +++++++++++++++++-- .../edit_index_pattern_container.tsx | 14 ++--- .../public/constants.ts | 11 ++++ .../mount_management_section.tsx | 3 +- .../index_pattern_management/public/plugin.ts | 8 ++- .../index_pattern_management/public/types.ts | 2 + .../index_pattern_management/tsconfig.json | 1 + 9 files changed, 78 insertions(+), 18 deletions(-) create mode 100644 src/plugins/index_pattern_management/public/constants.ts diff --git a/src/plugins/data/server/saved_objects/index_patterns.ts b/src/plugins/data/server/saved_objects/index_patterns.ts index f570e239c3c64..00282535e261d 100644 --- a/src/plugins/data/server/saved_objects/index_patterns.ts +++ b/src/plugins/data/server/saved_objects/index_patterns.ts @@ -12,7 +12,8 @@ import { indexPatternSavedObjectTypeMigrations } from './index_pattern_migration export const indexPatternSavedObjectType: SavedObjectsType = { name: 'index-pattern', hidden: false, - namespaceType: 'single', + namespaceType: 'multiple-isolated', + convertToMultiNamespaceTypeVersion: '8.0.0', management: { icon: 'indexPatternApp', defaultSearchField: 'title', diff --git a/src/plugins/index_pattern_management/kibana.json b/src/plugins/index_pattern_management/kibana.json index 60e382fb395f7..af364ccac6f08 100644 --- a/src/plugins/index_pattern_management/kibana.json +++ b/src/plugins/index_pattern_management/kibana.json @@ -3,6 +3,6 @@ "version": "kibana", "server": true, "ui": true, - "requiredPlugins": ["management", "data", "urlForwarding", "indexPatternFieldEditor"], + "requiredPlugins": ["management", "data", "urlForwarding", "indexPatternFieldEditor", "spacesOss"], "requiredBundles": ["kibanaReact", "kibanaUtils"] } diff --git a/src/plugins/index_pattern_management/public/components/edit_index_pattern/edit_index_pattern.tsx b/src/plugins/index_pattern_management/public/components/edit_index_pattern/edit_index_pattern.tsx index e314c00bc8176..8d4c63e881410 100644 --- a/src/plugins/index_pattern_management/public/components/edit_index_pattern/edit_index_pattern.tsx +++ b/src/plugins/index_pattern_management/public/components/edit_index_pattern/edit_index_pattern.tsx @@ -21,14 +21,15 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { IndexPattern, IndexPatternField } from '../../../../../plugins/data/public'; +import { ResolvedIndexPattern, IndexPatternField } from '../../../../../plugins/data/public'; import { useKibana } from '../../../../../plugins/kibana_react/public'; +import { IPM_APP_ID } from '../../constants'; import { IndexPatternManagmentContext } from '../../types'; import { Tabs } from './tabs'; import { IndexHeader } from './index_header'; export interface EditIndexPatternProps extends RouteComponentProps { - indexPattern: IndexPattern; + resolvedIndexPattern: ResolvedIndexPattern; } const mappingAPILink = i18n.translate( @@ -55,14 +56,18 @@ const confirmModalOptionsDelete = { }; export const EditIndexPattern = withRouter( - ({ indexPattern, history, location }: EditIndexPatternProps) => { + ({ resolvedIndexPattern, history, location }: EditIndexPatternProps) => { + const { indexPattern } = resolvedIndexPattern; const { uiSettings, indexPatternManagementStart, overlays, chrome, + http, data, + spacesOss, } = useKibana().services; + const { basePath } = http; const [fields, setFields] = useState(indexPattern.getNonScriptedFields()); const [conflictedFields, setConflictedFields] = useState( indexPattern.fields.getAll().filter((field) => field.type === 'conflict') @@ -144,6 +149,46 @@ export const EditIndexPattern = withRouter( const showTagsSection = Boolean(indexPattern.timeFieldName || (tags && tags.length > 0)); const kibana = useKibana(); const docsUrl = kibana.services.docLinks!.links.elasticsearch.mapping; + + useEffect(() => { + if (resolvedIndexPattern.outcome === 'aliasMatch' && spacesOss.isSpacesAvailable) { + // This index pattern has been resolved from a legacy URL, we should redirect the user to the new URL and display a toast. + const path = basePath.prepend( + `kibana/${IPM_APP_ID}/patterns/${indexPattern.id}${window.location.hash}` + ); + const objectNoun = 'index pattern'; // TODO: i18n + spacesOss.ui.redirectLegacyUrl(path, objectNoun); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + const getLegacyUrlConflictCallout = () => { + if ( + resolvedIndexPattern.outcome === 'conflict' && + resolvedIndexPattern.aliasTargetId && + spacesOss.isSpacesAvailable + ) { + // We have resolved to one index pattern, but there is another one with a legacy URL associated with this page. We should display a + // callout with a warning for the user, and provide a way for them to navigate to the other index pattern. + const otherObjectId = resolvedIndexPattern.aliasTargetId; + const otherObjectPath = basePath.prepend( + `kibana/${IPM_APP_ID}/patterns/${otherObjectId}${window.location.hash}` + ); + return ( + <> + + {spacesOss.ui.components.getLegacyUrlConflict({ + objectNoun: 'index pattern', // TODO: i18n + currentObjectId: indexPattern.id!, + otherObjectId, + otherObjectPath, + })} + + ); + } + return null; + }; + return (
@@ -185,6 +230,7 @@ export const EditIndexPattern = withRouter( />{' '}

+ {getLegacyUrlConflictCallout()} {conflictedFields.length > 0 && ( <> diff --git a/src/plugins/index_pattern_management/public/components/edit_index_pattern/edit_index_pattern_container.tsx b/src/plugins/index_pattern_management/public/components/edit_index_pattern/edit_index_pattern_container.tsx index 35edd7d6ce8fa..5dbafac3d9ebc 100644 --- a/src/plugins/index_pattern_management/public/components/edit_index_pattern/edit_index_pattern_container.tsx +++ b/src/plugins/index_pattern_management/public/components/edit_index_pattern/edit_index_pattern_container.tsx @@ -8,7 +8,7 @@ import React, { useEffect, useState } from 'react'; import { withRouter, RouteComponentProps } from 'react-router-dom'; -import { IndexPattern } from '../../../../../plugins/data/public'; +import { ResolvedIndexPattern } from '../../../../../plugins/data/public'; import { useKibana } from '../../../../../plugins/kibana_react/public'; import { IndexPatternManagmentContext } from '../../types'; import { getEditBreadcrumbs } from '../breadcrumbs'; @@ -17,17 +17,17 @@ import { EditIndexPattern } from '../edit_index_pattern'; const EditIndexPatternCont: React.FC> = ({ ...props }) => { const { data, setBreadcrumbs } = useKibana().services; - const [indexPattern, setIndexPattern] = useState(); + const [resolvedIndexPattern, setResolvedIndexPattern] = useState(); useEffect(() => { - data.indexPatterns.get(props.match.params.id).then((ip: IndexPattern) => { - setIndexPattern(ip); - setBreadcrumbs(getEditBreadcrumbs(ip)); + data.indexPatterns.resolve(props.match.params.id).then((ip: ResolvedIndexPattern) => { + setResolvedIndexPattern(ip); + setBreadcrumbs(getEditBreadcrumbs(ip.indexPattern)); }); }, [data.indexPatterns, props.match.params.id, setBreadcrumbs]); - if (indexPattern) { - return ; + if (resolvedIndexPattern) { + return ; } else { return <>; } diff --git a/src/plugins/index_pattern_management/public/constants.ts b/src/plugins/index_pattern_management/public/constants.ts new file mode 100644 index 0000000000000..7106465e40b96 --- /dev/null +++ b/src/plugins/index_pattern_management/public/constants.ts @@ -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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export const IPM_APP_ID = 'indexPatterns'; +export const newAppPath = `management/kibana/${IPM_APP_ID}`; +export const legacyPatternsPath = 'management/kibana/index_patterns'; diff --git a/src/plugins/index_pattern_management/public/management_app/mount_management_section.tsx b/src/plugins/index_pattern_management/public/management_app/mount_management_section.tsx index 355f529fe0f75..240d6f2080809 100644 --- a/src/plugins/index_pattern_management/public/management_app/mount_management_section.tsx +++ b/src/plugins/index_pattern_management/public/management_app/mount_management_section.tsx @@ -42,7 +42,7 @@ export async function mountManagementSection( ) { const [ { chrome, application, uiSettings, notifications, overlays, http, docLinks }, - { data, indexPatternFieldEditor }, + { data, indexPatternFieldEditor, spacesOss }, indexPatternManagementStart, ] = await getStartServices(); const canSave = Boolean(application.capabilities.indexPatterns.save); @@ -61,6 +61,7 @@ export async function mountManagementSection( docLinks, data, indexPatternFieldEditor, + spacesOss, indexPatternManagementStart: indexPatternManagementStart as IndexPatternManagementStart, setBreadcrumbs: params.setBreadcrumbs, getMlCardState, diff --git a/src/plugins/index_pattern_management/public/plugin.ts b/src/plugins/index_pattern_management/public/plugin.ts index ed92172c8b91c..874c1d9b313ea 100644 --- a/src/plugins/index_pattern_management/public/plugin.ts +++ b/src/plugins/index_pattern_management/public/plugin.ts @@ -18,6 +18,8 @@ import { import { ManagementSetup } from '../../management/public'; import { IndexPatternFieldEditorStart } from '../../index_pattern_field_editor/public'; +import type { SpacesOssPluginStart } from '../../spaces_oss/public'; +import { IPM_APP_ID, legacyPatternsPath, newAppPath } from './constants'; export interface IndexPatternManagementSetupDependencies { management: ManagementSetup; @@ -27,6 +29,7 @@ export interface IndexPatternManagementSetupDependencies { export interface IndexPatternManagementStartDependencies { data: DataPublicPluginStart; indexPatternFieldEditor: IndexPatternFieldEditorStart; + spacesOss: SpacesOssPluginStart; } export type IndexPatternManagementSetup = IndexPatternManagementServiceSetup; @@ -37,8 +40,6 @@ const sectionsHeader = i18n.translate('indexPatternManagement.indexPattern.secti defaultMessage: 'Index Patterns', }); -const IPM_APP_ID = 'indexPatterns'; - export class IndexPatternManagementPlugin implements Plugin< @@ -61,9 +62,6 @@ export class IndexPatternManagementPlugin throw new Error('`kibana` management section not found.'); } - const newAppPath = `management/kibana/${IPM_APP_ID}`; - const legacyPatternsPath = 'management/kibana/index_patterns'; - urlForwarding.forwardApp('management/kibana/index_pattern', newAppPath, (path) => '/create'); urlForwarding.forwardApp(legacyPatternsPath, newAppPath, (path) => { const pathInApp = path.substr(legacyPatternsPath.length + 1); diff --git a/src/plugins/index_pattern_management/public/types.ts b/src/plugins/index_pattern_management/public/types.ts index 58a138df633fd..1fe6e9d818037 100644 --- a/src/plugins/index_pattern_management/public/types.ts +++ b/src/plugins/index_pattern_management/public/types.ts @@ -20,6 +20,7 @@ import { ManagementAppMountParams } from '../../management/public'; import { IndexPatternManagementStart } from './index'; import { KibanaReactContextValue } from '../../kibana_react/public'; import { IndexPatternFieldEditorStart } from '../../index_pattern_field_editor/public'; +import type { SpacesOssPluginStart } from '../../spaces_oss/public'; export interface IndexPatternManagmentContext { chrome: ChromeStart; @@ -31,6 +32,7 @@ export interface IndexPatternManagmentContext { docLinks: DocLinksStart; data: DataPublicPluginStart; indexPatternFieldEditor: IndexPatternFieldEditorStart; + spacesOss: SpacesOssPluginStart; indexPatternManagementStart: IndexPatternManagementStart; setBreadcrumbs: ManagementAppMountParams['setBreadcrumbs']; getMlCardState: () => MlCardState; diff --git a/src/plugins/index_pattern_management/tsconfig.json b/src/plugins/index_pattern_management/tsconfig.json index 37bd3e4aa5bbb..892755bcd03a5 100644 --- a/src/plugins/index_pattern_management/tsconfig.json +++ b/src/plugins/index_pattern_management/tsconfig.json @@ -20,5 +20,6 @@ { "path": "../kibana_utils/tsconfig.json" }, { "path": "../es_ui_shared/tsconfig.json" }, { "path": "../index_pattern_field_editor/tsconfig.json" }, + { "path": "../spaces_oss/tsconfig.json" }, ] } From f90490c6de458bf6f787905be45dece8daf8eaa2 Mon Sep 17 00:00:00 2001 From: Joe Portner <5295965+jportner@users.noreply.github.com> Date: Mon, 26 Apr 2021 11:39:18 -0400 Subject: [PATCH 4/4] Convert index patterns to namespaceType 'multiple' This will happen in an 8.x release. It allows index patterns to be shared to multiple spaces. This commit also changes the index pattern UI to add a spaces list column and a share to space action. --- .../index_patterns/index_pattern.ts | 4 + .../index_patterns/index_patterns.ts | 7 +- .../data/common/index_patterns/types.ts | 1 + .../server/saved_objects/index_patterns.ts | 2 +- .../index_pattern_table.tsx | 159 +++++++++++++----- .../public/components/types.ts | 5 +- .../public/components/utils.ts | 68 ++++++-- 7 files changed, 183 insertions(+), 63 deletions(-) diff --git a/src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts b/src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts index 41ce7ba4bab4a..97cd3f258a2b7 100644 --- a/src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts +++ b/src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts @@ -71,6 +71,8 @@ export class IndexPattern implements IIndexPattern { * SavedObject version */ public version: string | undefined; + /** SavedObject namespaces */ + public namespaces: string[]; public sourceFilters?: SourceFilter[]; private originalSavedObjectBody: SavedObjectBody = {}; private shortDotsEnable: boolean = false; @@ -109,6 +111,7 @@ export class IndexPattern implements IIndexPattern { this.fieldFormatMap = spec.fieldFormats || {}; this.version = spec.version; + this.namespaces = spec.namespaces || []; this.title = spec.title || ''; this.timeFieldName = spec.timeFieldName; @@ -209,6 +212,7 @@ export class IndexPattern implements IIndexPattern { return { id: this.id, version: this.version, + namespaces: this.namespaces, title: this.title, timeFieldName: this.timeFieldName, diff --git a/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts b/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts index be15780222a76..a5fe558b3900a 100644 --- a/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts +++ b/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts @@ -365,6 +365,7 @@ export class IndexPatternsService { fieldAttrs, allowNoIndex, }, + namespaces, } = savedObject; const parsedSourceFilters = sourceFilters ? JSON.parse(sourceFilters) : undefined; @@ -390,6 +391,7 @@ export class IndexPatternsService { fieldAttrs: parsedFieldAttrs, allowNoIndex, runtimeFieldMap: parsedRuntimeFieldMap, + namespaces, }; }; @@ -486,9 +488,10 @@ export class IndexPatternsService { * @param id */ - get = async (id: string): Promise => { + get = async (id: string, options: { forceRefresh?: boolean } = {}): Promise => { + const { forceRefresh } = options; const indexPatternPromise = - this.indexPatternCache.get(id) || + (!forceRefresh && this.indexPatternCache.get(id)) || this.indexPatternCache.set(id, this.getSavedObjectAndInit(id)); // don't cache failed requests diff --git a/src/plugins/data/common/index_patterns/types.ts b/src/plugins/data/common/index_patterns/types.ts index 272bed902c774..aabb7ba57a732 100644 --- a/src/plugins/data/common/index_patterns/types.ts +++ b/src/plugins/data/common/index_patterns/types.ts @@ -246,6 +246,7 @@ export interface IndexPatternSpec { runtimeFieldMap?: Record; fieldAttrs?: FieldAttrs; allowNoIndex?: boolean; + namespaces?: string[]; } export interface SourceFilter { diff --git a/src/plugins/data/server/saved_objects/index_patterns.ts b/src/plugins/data/server/saved_objects/index_patterns.ts index 00282535e261d..fd0070066ad8d 100644 --- a/src/plugins/data/server/saved_objects/index_patterns.ts +++ b/src/plugins/data/server/saved_objects/index_patterns.ts @@ -12,7 +12,7 @@ import { indexPatternSavedObjectTypeMigrations } from './index_pattern_migration export const indexPatternSavedObjectType: SavedObjectsType = { name: 'index-pattern', hidden: false, - namespaceType: 'multiple-isolated', + namespaceType: 'multiple', convertToMultiNamespaceTypeVersion: '8.0.0', management: { icon: 'indexPatternApp', diff --git a/src/plugins/index_pattern_management/public/components/index_pattern_table/index_pattern_table.tsx b/src/plugins/index_pattern_management/public/components/index_pattern_table/index_pattern_table.tsx index b09246b5af8ad..3481059043560 100644 --- a/src/plugins/index_pattern_management/public/components/index_pattern_table/index_pattern_table.tsx +++ b/src/plugins/index_pattern_management/public/components/index_pattern_table/index_pattern_table.tsx @@ -17,21 +17,28 @@ import { EuiBadgeGroup, EuiPageContent, EuiTitle, + EuiBasicTableColumn, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { withRouter, RouteComponentProps } from 'react-router-dom'; -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, useMemo, PropsWithChildren } from 'react'; import { i18n } from '@kbn/i18n'; import { reactRouterNavigate, useKibana } from '../../../../../plugins/kibana_react/public'; +import { + ShareToSpaceFlyoutProps, + SpaceListProps, + SpacesContextProps, +} from '../../../../spaces_oss/public'; import { IndexPatternManagmentContext } from '../../types'; import { CreateButton } from '../create_button'; import { IndexPatternTableItem, IndexPatternCreationOption } from '../types'; -import { getIndexPatterns } from '../utils'; +import { getIndexPatterns, refreshIndexPattern } from '../utils'; import { getListBreadcrumbs } from '../breadcrumbs'; import { EmptyState } from './empty_state'; import { MatchedItem, ResolveIndexResponseItemAlias } from '../create_index_pattern_wizard/types'; import { EmptyIndexPatternPrompt } from './empty_index_pattern_prompt'; import { getIndices } from '../create_index_pattern_wizard/lib'; +import { IPM_APP_ID } from '../../constants'; const pagination = { initialPageSize: 10, @@ -66,6 +73,8 @@ interface Props extends RouteComponentProps { canSave: boolean; } +const getEmptyElement = () => ({ children }: PropsWithChildren) => <>{children}; + export const IndexPatternTable = ({ canSave, history }: Props) => { const { setBreadcrumbs, @@ -76,6 +85,7 @@ export const IndexPatternTable = ({ canSave, history }: Props) => { application, http, data, + spacesOss, getMlCardState, } = useKibana().services; const [indexPatterns, setIndexPatterns] = useState([]); @@ -126,20 +136,11 @@ export const IndexPatternTable = ({ canSave, history }: Props) => { chrome.docTitle.change(title); - const columns = [ + const columns: Array> = [ { field: 'title', name: 'Pattern', - render: ( - name: string, - index: { - id: string; - tags?: Array<{ - key: string; - name: string; - }>; - } - ) => ( + render: (name, index) => ( <> {name} @@ -158,6 +159,74 @@ export const IndexPatternTable = ({ canSave, history }: Props) => { }, ]; + const LazySpacesContextProvider = useMemo( + () => + spacesOss.isSpacesAvailable + ? spacesOss.ui.components.getSpacesContextProvider + : getEmptyElement(), + [spacesOss] + ); + const LazySpaceList = useMemo( + () => + spacesOss.isSpacesAvailable + ? spacesOss.ui.components.getSpaceList + : getEmptyElement(), + [spacesOss] + ); + const LazyShareToSpaceFlyout = useMemo( + () => + spacesOss.isSpacesAvailable + ? spacesOss.ui.components.getShareToSpaceFlyout + : getEmptyElement(), + [spacesOss] + ); + + const [shareToSpaceFlyout, setShareToSpaceFlyout] = useState(null); + if (spacesOss.isSpacesAvailable) { + columns.push( + { + field: 'namespaces', + name: 'Shared spaces', // TODO: i18n + width: '30%', + render: (namespaces: string[]) => , + }, + { + name: 'Actions', + width: '66px', + actions: [ + { + name: 'Changed shared spaces', // TODO: i18n + description: 'Change the spaces this index pattern is shared to', // TODO: i18n + icon: 'share', + type: 'icon', + onClick: (item) => { + const props: ShareToSpaceFlyoutProps = { + savedObjectTarget: { + type: 'index-pattern', + id: item.id, + namespaces: item.namespaces, + title: item.title, + icon: 'indexPatternApp', + noun: 'index pattern', // TODO: i18n + }, + onUpdate: () => { + refreshIndexPattern( + item, + indexPatterns, + indexPatternManagementStart, + data.indexPatterns + ).then((newIndexPatterns) => setIndexPatterns(newIndexPatterns)); + }, + onClose: () => setShareToSpaceFlyout(null), + }; + setShareToSpaceFlyout(); + }, + }, + ], + } + ); + } + const createButton = canSave ? ( { } return ( - - - - -

{title}

-
- - -

- -

-
-
- {createButton} -
- - -
+ + + + + +

{title}

+
+ + +

+ +

+
+
+ {createButton} +
+ + + {shareToSpaceFlyout} +
+
); }; diff --git a/src/plugins/index_pattern_management/public/components/types.ts b/src/plugins/index_pattern_management/public/components/types.ts index 3ead700732b91..040dd9c5b485d 100644 --- a/src/plugins/index_pattern_management/public/components/types.ts +++ b/src/plugins/index_pattern_management/public/components/types.ts @@ -6,6 +6,8 @@ * Side Public License, v 1. */ +import type { IndexPatternTag } from '../service/list/config'; + export interface IndexPatternCreationOption { text: string; description?: string; @@ -16,6 +18,7 @@ export interface IndexPatternTableItem { id: string; title: string; default: boolean; - tag?: string[]; + tags?: IndexPatternTag[]; + namespaces: string[]; sort: string; } diff --git a/src/plugins/index_pattern_management/public/components/utils.ts b/src/plugins/index_pattern_management/public/components/utils.ts index 68e78199798b4..75b84c95f0ea0 100644 --- a/src/plugins/index_pattern_management/public/components/utils.ts +++ b/src/plugins/index_pattern_management/public/components/utils.ts @@ -8,6 +8,7 @@ import { IndexPatternsContract } from 'src/plugins/data/public'; import { IndexPatternManagementStart } from '../plugin'; +import type { IndexPatternTableItem } from './types'; export async function getIndexPatterns( defaultIndex: string, @@ -16,24 +17,14 @@ export async function getIndexPatterns( ) { const existingIndexPatterns = await indexPatternsService.getIdsWithTitle(true); const indexPatternsListItems = await Promise.all( - existingIndexPatterns.map(async ({ id, title }) => { + existingIndexPatterns.map(async ({ id }) => { const isDefault = defaultIndex === id; - const pattern = await indexPatternsService.get(id); - const tags = (indexPatternManagementStart as IndexPatternManagementStart).list.getIndexPatternTags( - pattern, - isDefault - ); - - return { + return await getIndexPattern( id, - title, - default: isDefault, - tags, - // the prepending of 0 at the default pattern takes care of prioritization - // so the sorting will but the default index on top - // or on bottom of a the table - sort: `${isDefault ? '0' : '1'}${title}`, - }; + isDefault, + indexPatternManagementStart, + indexPatternsService + ); }) ); @@ -49,3 +40,48 @@ export async function getIndexPatterns( }) || [] ); } + +export async function refreshIndexPattern( + indexPattern: IndexPatternTableItem, + indexPatterns: IndexPatternTableItem[], + indexPatternManagementStart: IndexPatternManagementStart, + indexPatternsService: IndexPatternsContract +) { + const newIndexPattern = await getIndexPattern( + indexPattern.id, + indexPattern.default, + indexPatternManagementStart, + indexPatternsService, + { forceRefresh: true } + ); + const newIndexPatterns = [...indexPatterns]; + const index = newIndexPatterns.findIndex(({ id }) => id === indexPattern.id); + newIndexPatterns[index] = newIndexPattern; + return newIndexPatterns; +} + +async function getIndexPattern( + id: string, + isDefault: boolean, + indexPatternManagementStart: IndexPatternManagementStart, + indexPatternsService: IndexPatternsContract, + options?: { forceRefresh?: boolean } +): Promise { + const pattern = await indexPatternsService.get(id, options); + const tags = (indexPatternManagementStart as IndexPatternManagementStart).list.getIndexPatternTags( + pattern, + isDefault + ); + + return { + id, + title: pattern.title, + default: isDefault, + tags, + namespaces: pattern.namespaces, + // the prepending of 0 at the default pattern takes care of prioritization + // so the sorting will but the default index on top + // or on bottom of a the table + sort: `${isDefault ? '0' : '1'}${pattern.title}`, + }; +}