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; } 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_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 04d2785137719..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 @@ -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(); } /** @@ -357,6 +365,7 @@ export class IndexPatternsService { fieldAttrs, allowNoIndex, }, + namespaces, } = savedObject; const parsedSourceFilters = sourceFilters ? JSON.parse(sourceFilters) : undefined; @@ -382,6 +391,7 @@ export class IndexPatternsService { fieldAttrs: parsedFieldAttrs, allowNoIndex, runtimeFieldMap: parsedRuntimeFieldMap, + namespaces, }; }; @@ -390,7 +400,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'); } @@ -462,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 @@ -475,6 +502,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..aabb7ba57a732 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, @@ -245,8 +246,17 @@ export interface IndexPatternSpec { runtimeFieldMap?: Record; fieldAttrs?: FieldAttrs; allowNoIndex?: boolean; + namespaces?: string[]; } 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, diff --git a/src/plugins/data/server/saved_objects/index_patterns.ts b/src/plugins/data/server/saved_objects/index_patterns.ts index f570e239c3c64..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,8 @@ import { indexPatternSavedObjectTypeMigrations } from './index_pattern_migration export const indexPatternSavedObjectType: SavedObjectsType = { name: 'index-pattern', hidden: false, - namespaceType: 'single', + namespaceType: 'multiple', + 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/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}`, + }; +} 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" }, ] }