From 5464af69234e48b1787bf310259309a5f21b6c31 Mon Sep 17 00:00:00 2001 From: Rudolf Meijering Date: Wed, 8 Sep 2021 22:15:52 +0200 Subject: [PATCH] legacy dashboards import/export API: deprecation logs and usage data (#111283) * Move legacy dashboards API to core and adds usage data * More legacy_export plugin removal * Log a warning for deprecated dashboard import/export API * Review comments --- .github/CODEOWNERS | 1 - docs/developer/plugin-list.asciidoc | 4 - docs/user/api.asciidoc | 8 -- .../core_usage_stats_client.mock.ts | 2 + .../core_usage_stats_client.test.ts | 112 ++++++++++++++++++ .../core_usage_stats_client.ts | 13 ++ src/core/server/core_usage_data/types.ts | 15 +++ src/core/server/saved_objects/routes/index.ts | 12 ++ .../routes/legacy_import_export}/export.ts | 21 +++- .../routes/legacy_import_export}/import.ts | 22 +++- .../integration_tests/export.test.ts | 89 ++++++++++++++ .../integration_tests/import.test.ts | 84 +++++++++++++ .../lib}/collect_references_deep.test.ts | 2 +- .../lib}/collect_references_deep.ts | 0 .../lib}/export_dashboards.ts | 0 .../lib}/import_dashboards.test.ts | 4 +- .../lib}/import_dashboards.ts | 0 .../routes/legacy_import_export/lib/index.ts} | 7 +- .../saved_objects/saved_objects_service.ts | 1 + src/core/server/server.api.md | 28 +++++ .../collectors/core/core_usage_collector.ts | 84 ++++++++++++- src/plugins/legacy_export/README.md | 3 - src/plugins/legacy_export/kibana.json | 10 -- src/plugins/legacy_export/server/index.ts | 12 -- src/plugins/legacy_export/server/lib/index.ts | 10 -- src/plugins/legacy_export/server/plugin.ts | 34 ------ .../legacy_export/server/routes/index.ts | 20 ---- src/plugins/legacy_export/tsconfig.json | 15 --- src/plugins/telemetry/schema/oss_plugins.json | 86 +++++++++++++- test/tsconfig.json | 1 - x-pack/test/tsconfig.json | 1 - 31 files changed, 565 insertions(+), 136 deletions(-) rename src/{plugins/legacy_export/server/routes => core/server/saved_objects/routes/legacy_import_export}/export.ts (65%) rename src/{plugins/legacy_export/server/routes => core/server/saved_objects/routes/legacy_import_export}/import.ts (66%) create mode 100644 src/core/server/saved_objects/routes/legacy_import_export/integration_tests/export.test.ts create mode 100644 src/core/server/saved_objects/routes/legacy_import_export/integration_tests/import.test.ts rename src/{plugins/legacy_export/server/lib/export => core/server/saved_objects/routes/legacy_import_export/lib}/collect_references_deep.test.ts (98%) rename src/{plugins/legacy_export/server/lib/export => core/server/saved_objects/routes/legacy_import_export/lib}/collect_references_deep.ts (100%) rename src/{plugins/legacy_export/server/lib/export => core/server/saved_objects/routes/legacy_import_export/lib}/export_dashboards.ts (100%) rename src/{plugins/legacy_export/server/lib/import => core/server/saved_objects/routes/legacy_import_export/lib}/import_dashboards.test.ts (95%) rename src/{plugins/legacy_export/server/lib/import => core/server/saved_objects/routes/legacy_import_export/lib}/import_dashboards.ts (100%) rename src/{plugins/legacy_export/jest.config.js => core/server/saved_objects/routes/legacy_import_export/lib/index.ts} (75%) delete mode 100644 src/plugins/legacy_export/README.md delete mode 100644 src/plugins/legacy_export/kibana.json delete mode 100644 src/plugins/legacy_export/server/index.ts delete mode 100644 src/plugins/legacy_export/server/lib/index.ts delete mode 100644 src/plugins/legacy_export/server/plugin.ts delete mode 100644 src/plugins/legacy_export/server/routes/index.ts delete mode 100644 src/plugins/legacy_export/tsconfig.json diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 0949c5d74243..837d80f28153 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -250,7 +250,6 @@ /src/plugins/kibana_overview/ @elastic/kibana-core /x-pack/plugins/global_search_bar/ @elastic/kibana-core #CC# /src/core/server/csp/ @elastic/kibana-core -#CC# /src/plugins/legacy_export/ @elastic/kibana-core #CC# /src/plugins/xpack_legacy/ @elastic/kibana-core #CC# /src/plugins/saved_objects/ @elastic/kibana-core #CC# /x-pack/plugins/cloud/ @elastic/kibana-core diff --git a/docs/developer/plugin-list.asciidoc b/docs/developer/plugin-list.asciidoc index dfb62f23445e..319ac1e8476f 100644 --- a/docs/developer/plugin-list.asciidoc +++ b/docs/developer/plugin-list.asciidoc @@ -172,10 +172,6 @@ in Kibana, e.g. visualizations. It has the form of a flyout panel. |Utilities for building Kibana plugins. -|{kib-repo}blob/{branch}/src/plugins/legacy_export/README.md[legacyExport] -|The legacyExport plugin adds support for the legacy saved objects export format. - - |{kib-repo}blob/{branch}/src/plugins/management/README.md[management] |This plugins contains the "Stack Management" page framework. It offers navigation and an API to link individual managment section into it. This plugin does not contain any individual diff --git a/docs/user/api.asciidoc b/docs/user/api.asciidoc index 82f3355759b6..00aa3c545df6 100644 --- a/docs/user/api.asciidoc +++ b/docs/user/api.asciidoc @@ -51,14 +51,6 @@ Calls to the API endpoints require different operations. To interact with the {k * *DELETE* - Removes the information. -For example, the following `curl` command exports a dashboard: - -[source,sh] --------------------------------------------- -curl -X POST api/kibana/dashboards/export?dashboard=942dcef0-b2cd-11e8-ad8e-85441f0c2e5c --------------------------------------------- -// KIBANA - [float] [[api-request-headers]] === Request headers diff --git a/src/core/server/core_usage_data/core_usage_stats_client.mock.ts b/src/core/server/core_usage_data/core_usage_stats_client.mock.ts index 1c9c0b8fae57..35471234676b 100644 --- a/src/core/server/core_usage_data/core_usage_stats_client.mock.ts +++ b/src/core/server/core_usage_data/core_usage_stats_client.mock.ts @@ -23,6 +23,8 @@ const createUsageStatsClientMock = () => incrementSavedObjectsImport: jest.fn().mockResolvedValue(null), incrementSavedObjectsResolveImportErrors: jest.fn().mockResolvedValue(null), incrementSavedObjectsExport: jest.fn().mockResolvedValue(null), + incrementLegacyDashboardsImport: jest.fn().mockResolvedValue(null), + incrementLegacyDashboardsExport: jest.fn().mockResolvedValue(null), } as unknown) as jest.Mocked); export const coreUsageStatsClientMock = { diff --git a/src/core/server/core_usage_data/core_usage_stats_client.test.ts b/src/core/server/core_usage_data/core_usage_stats_client.test.ts index 384e3d7b932c..d14c248bfa1b 100644 --- a/src/core/server/core_usage_data/core_usage_stats_client.test.ts +++ b/src/core/server/core_usage_data/core_usage_stats_client.test.ts @@ -25,6 +25,8 @@ import { IMPORT_STATS_PREFIX, RESOLVE_IMPORT_STATS_PREFIX, EXPORT_STATS_PREFIX, + LEGACY_DASHBOARDS_IMPORT_STATS_PREFIX, + LEGACY_DASHBOARDS_EXPORT_STATS_PREFIX, } from './core_usage_stats_client'; import { CoreUsageStatsClient } from '.'; import { DEFAULT_NAMESPACE_STRING } from '../saved_objects/service/lib/utils'; @@ -1007,4 +1009,114 @@ describe('CoreUsageStatsClient', () => { ); }); }); + + describe('#incrementLegacyDashboardsImport', () => { + it('does not throw an error if repository incrementCounter operation fails', async () => { + const { usageStatsClient, repositoryMock } = setup(); + repositoryMock.incrementCounter.mockRejectedValue(new Error('Oh no!')); + + const request = httpServerMock.createKibanaRequest(); + await expect( + usageStatsClient.incrementLegacyDashboardsImport({ + request, + } as IncrementSavedObjectsExportOptions) + ).resolves.toBeUndefined(); + expect(repositoryMock.incrementCounter).toHaveBeenCalled(); + }); + + it('handles the default namespace string and first party request appropriately', async () => { + const { usageStatsClient, repositoryMock } = setup(DEFAULT_NAMESPACE_STRING); + + const request = httpServerMock.createKibanaRequest({ headers: firstPartyRequestHeaders }); + await usageStatsClient.incrementLegacyDashboardsImport({ + request, + } as IncrementSavedObjectsExportOptions); + expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(1); + expect(repositoryMock.incrementCounter).toHaveBeenCalledWith( + CORE_USAGE_STATS_TYPE, + CORE_USAGE_STATS_ID, + [ + `${LEGACY_DASHBOARDS_IMPORT_STATS_PREFIX}.total`, + `${LEGACY_DASHBOARDS_IMPORT_STATS_PREFIX}.namespace.default.total`, + `${LEGACY_DASHBOARDS_IMPORT_STATS_PREFIX}.namespace.default.kibanaRequest.yes`, + ], + incrementOptions + ); + }); + + it('handles a non-default space and and third party request appropriately', async () => { + const { usageStatsClient, repositoryMock } = setup('foo'); + + const request = httpServerMock.createKibanaRequest(); + await usageStatsClient.incrementLegacyDashboardsImport({ + request, + } as IncrementSavedObjectsExportOptions); + expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(1); + expect(repositoryMock.incrementCounter).toHaveBeenCalledWith( + CORE_USAGE_STATS_TYPE, + CORE_USAGE_STATS_ID, + [ + `${LEGACY_DASHBOARDS_IMPORT_STATS_PREFIX}.total`, + `${LEGACY_DASHBOARDS_IMPORT_STATS_PREFIX}.namespace.custom.total`, + `${LEGACY_DASHBOARDS_IMPORT_STATS_PREFIX}.namespace.custom.kibanaRequest.no`, + ], + incrementOptions + ); + }); + }); + + describe('#incrementLegacyDashboardsExport', () => { + it('does not throw an error if repository incrementCounter operation fails', async () => { + const { usageStatsClient, repositoryMock } = setup(); + repositoryMock.incrementCounter.mockRejectedValue(new Error('Oh no!')); + + const request = httpServerMock.createKibanaRequest(); + await expect( + usageStatsClient.incrementLegacyDashboardsExport({ + request, + } as IncrementSavedObjectsExportOptions) + ).resolves.toBeUndefined(); + expect(repositoryMock.incrementCounter).toHaveBeenCalled(); + }); + + it('handles the default namespace string and first party request appropriately', async () => { + const { usageStatsClient, repositoryMock } = setup(DEFAULT_NAMESPACE_STRING); + + const request = httpServerMock.createKibanaRequest({ headers: firstPartyRequestHeaders }); + await usageStatsClient.incrementLegacyDashboardsExport({ + request, + } as IncrementSavedObjectsExportOptions); + expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(1); + expect(repositoryMock.incrementCounter).toHaveBeenCalledWith( + CORE_USAGE_STATS_TYPE, + CORE_USAGE_STATS_ID, + [ + `${LEGACY_DASHBOARDS_EXPORT_STATS_PREFIX}.total`, + `${LEGACY_DASHBOARDS_EXPORT_STATS_PREFIX}.namespace.default.total`, + `${LEGACY_DASHBOARDS_EXPORT_STATS_PREFIX}.namespace.default.kibanaRequest.yes`, + ], + incrementOptions + ); + }); + + it('handles a non-default space and and third party request appropriately', async () => { + const { usageStatsClient, repositoryMock } = setup('foo'); + + const request = httpServerMock.createKibanaRequest(); + await usageStatsClient.incrementLegacyDashboardsExport({ + request, + } as IncrementSavedObjectsExportOptions); + expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(1); + expect(repositoryMock.incrementCounter).toHaveBeenCalledWith( + CORE_USAGE_STATS_TYPE, + CORE_USAGE_STATS_ID, + [ + `${LEGACY_DASHBOARDS_EXPORT_STATS_PREFIX}.total`, + `${LEGACY_DASHBOARDS_EXPORT_STATS_PREFIX}.namespace.custom.total`, + `${LEGACY_DASHBOARDS_EXPORT_STATS_PREFIX}.namespace.custom.kibanaRequest.no`, + ], + incrementOptions + ); + }); + }); }); diff --git a/src/core/server/core_usage_data/core_usage_stats_client.ts b/src/core/server/core_usage_data/core_usage_stats_client.ts index 29d6e875c796..fb5340f16420 100644 --- a/src/core/server/core_usage_data/core_usage_stats_client.ts +++ b/src/core/server/core_usage_data/core_usage_stats_client.ts @@ -45,6 +45,9 @@ export const UPDATE_STATS_PREFIX = 'apiCalls.savedObjectsUpdate'; export const IMPORT_STATS_PREFIX = 'apiCalls.savedObjectsImport'; export const RESOLVE_IMPORT_STATS_PREFIX = 'apiCalls.savedObjectsResolveImportErrors'; export const EXPORT_STATS_PREFIX = 'apiCalls.savedObjectsExport'; +export const LEGACY_DASHBOARDS_IMPORT_STATS_PREFIX = 'apiCalls.legacyDashboardImport'; +export const LEGACY_DASHBOARDS_EXPORT_STATS_PREFIX = 'apiCalls.legacyDashboardExport'; + export const REPOSITORY_RESOLVE_OUTCOME_STATS = { EXACT_MATCH: 'savedObjectsRepository.resolvedOutcome.exactMatch', ALIAS_MATCH: 'savedObjectsRepository.resolvedOutcome.aliasMatch', @@ -73,6 +76,8 @@ const ALL_COUNTER_FIELDS = [ `${RESOLVE_IMPORT_STATS_PREFIX}.createNewCopiesEnabled.yes`, `${RESOLVE_IMPORT_STATS_PREFIX}.createNewCopiesEnabled.no`, ...getFieldsForCounter(EXPORT_STATS_PREFIX), + ...getFieldsForCounter(LEGACY_DASHBOARDS_IMPORT_STATS_PREFIX), + ...getFieldsForCounter(LEGACY_DASHBOARDS_EXPORT_STATS_PREFIX), `${EXPORT_STATS_PREFIX}.allTypesSelected.yes`, `${EXPORT_STATS_PREFIX}.allTypesSelected.no`, // Saved Objects Repository counters; these are included here for stats collection, but are incremented in the repository itself @@ -170,6 +175,14 @@ export class CoreUsageStatsClient { await this.updateUsageStats(counterFieldNames, EXPORT_STATS_PREFIX, options); } + public async incrementLegacyDashboardsImport(options: BaseIncrementOptions) { + await this.updateUsageStats([], LEGACY_DASHBOARDS_IMPORT_STATS_PREFIX, options); + } + + public async incrementLegacyDashboardsExport(options: BaseIncrementOptions) { + await this.updateUsageStats([], LEGACY_DASHBOARDS_EXPORT_STATS_PREFIX, options); + } + private async updateUsageStats( counterFieldNames: string[], prefix: string, diff --git a/src/core/server/core_usage_data/types.ts b/src/core/server/core_usage_data/types.ts index 68e0b56c56db..006f9848e8f3 100644 --- a/src/core/server/core_usage_data/types.ts +++ b/src/core/server/core_usage_data/types.ts @@ -110,6 +110,21 @@ export interface CoreUsageStats { 'apiCalls.savedObjectsExport.namespace.custom.kibanaRequest.no'?: number; 'apiCalls.savedObjectsExport.allTypesSelected.yes'?: number; 'apiCalls.savedObjectsExport.allTypesSelected.no'?: number; + // Legacy Dashboard Import/Export API + 'apiCalls.legacyDashboardExport.total'?: number; + 'apiCalls.legacyDashboardExport.namespace.default.total'?: number; + 'apiCalls.legacyDashboardExport.namespace.default.kibanaRequest.yes'?: number; + 'apiCalls.legacyDashboardExport.namespace.default.kibanaRequest.no'?: number; + 'apiCalls.legacyDashboardExport.namespace.custom.total'?: number; + 'apiCalls.legacyDashboardExport.namespace.custom.kibanaRequest.yes'?: number; + 'apiCalls.legacyDashboardExport.namespace.custom.kibanaRequest.no'?: number; + 'apiCalls.legacyDashboardImport.total'?: number; + 'apiCalls.legacyDashboardImport.namespace.default.total'?: number; + 'apiCalls.legacyDashboardImport.namespace.default.kibanaRequest.yes'?: number; + 'apiCalls.legacyDashboardImport.namespace.default.kibanaRequest.no'?: number; + 'apiCalls.legacyDashboardImport.namespace.custom.total'?: number; + 'apiCalls.legacyDashboardImport.namespace.custom.kibanaRequest.yes'?: number; + 'apiCalls.legacyDashboardImport.namespace.custom.kibanaRequest.no'?: number; // Saved Objects Repository counters 'savedObjectsRepository.resolvedOutcome.exactMatch'?: number; 'savedObjectsRepository.resolvedOutcome.aliasMatch'?: number; diff --git a/src/core/server/saved_objects/routes/index.ts b/src/core/server/saved_objects/routes/index.ts index 3ab870c276d2..8511b59a0758 100644 --- a/src/core/server/saved_objects/routes/index.ts +++ b/src/core/server/saved_objects/routes/index.ts @@ -24,6 +24,8 @@ import { registerExportRoute } from './export'; import { registerImportRoute } from './import'; import { registerResolveImportErrorsRoute } from './resolve_import_errors'; import { registerMigrateRoute } from './migrate'; +import { registerLegacyImportRoute } from './legacy_import_export/import'; +import { registerLegacyExportRoute } from './legacy_import_export/export'; export function registerRoutes({ http, @@ -31,12 +33,14 @@ export function registerRoutes({ logger, config, migratorPromise, + kibanaVersion, }: { http: InternalHttpServiceSetup; coreUsageData: InternalCoreUsageDataSetup; logger: Logger; config: SavedObjectConfig; migratorPromise: Promise; + kibanaVersion: string; }) { const router = http.createRouter('/api/saved_objects/'); @@ -53,6 +57,14 @@ export function registerRoutes({ registerImportRoute(router, { config, coreUsageData }); registerResolveImportErrorsRoute(router, { config, coreUsageData }); + const legacyRouter = http.createRouter(''); + registerLegacyImportRoute(legacyRouter, { + maxImportPayloadBytes: config.maxImportPayloadBytes, + coreUsageData, + logger, + }); + registerLegacyExportRoute(legacyRouter, { kibanaVersion, coreUsageData, logger }); + const internalRouter = http.createRouter('/internal/saved_objects/'); registerMigrateRoute(internalRouter, migratorPromise); diff --git a/src/plugins/legacy_export/server/routes/export.ts b/src/core/server/saved_objects/routes/legacy_import_export/export.ts similarity index 65% rename from src/plugins/legacy_export/server/routes/export.ts rename to src/core/server/saved_objects/routes/legacy_import_export/export.ts index 8d5bd71c0b7e..c9a954db07e7 100644 --- a/src/plugins/legacy_export/server/routes/export.ts +++ b/src/core/server/saved_objects/routes/legacy_import_export/export.ts @@ -8,10 +8,18 @@ import moment from 'moment'; import { schema } from '@kbn/config-schema'; -import { IRouter } from 'src/core/server'; -import { exportDashboards } from '../lib'; +import { InternalCoreUsageDataSetup } from 'src/core/server/core_usage_data'; +import { IRouter, Logger } from '../../..'; +import { exportDashboards } from './lib'; -export const registerExportRoute = (router: IRouter, kibanaVersion: string) => { +export const registerLegacyExportRoute = ( + router: IRouter, + { + kibanaVersion, + coreUsageData, + logger, + }: { kibanaVersion: string; coreUsageData: InternalCoreUsageDataSetup; logger: Logger } +) => { router.get( { path: '/api/kibana/dashboards/export', @@ -25,9 +33,16 @@ export const registerExportRoute = (router: IRouter, kibanaVersion: string) => { }, }, async (ctx, req, res) => { + logger.warn( + "The export dashboard API '/api/kibana/dashboards/export' is deprecated. Use the saved objects export objects API '/api/saved_objects/_export' instead." + ); + const ids = Array.isArray(req.query.dashboard) ? req.query.dashboard : [req.query.dashboard]; const { client } = ctx.core.savedObjects; + const usageStatsClient = coreUsageData.getClient(); + usageStatsClient.incrementLegacyDashboardsExport({ request: req }).catch(() => {}); + const exported = await exportDashboards(ids, client, kibanaVersion); const filename = `kibana-dashboards.${moment.utc().format('YYYY-MM-DD-HH-mm-ss')}.json`; const body = JSON.stringify(exported, null, ' '); diff --git a/src/plugins/legacy_export/server/routes/import.ts b/src/core/server/saved_objects/routes/legacy_import_export/import.ts similarity index 66% rename from src/plugins/legacy_export/server/routes/import.ts rename to src/core/server/saved_objects/routes/legacy_import_export/import.ts index 4a2dbecd3e02..09027af81014 100644 --- a/src/plugins/legacy_export/server/routes/import.ts +++ b/src/core/server/saved_objects/routes/legacy_import_export/import.ts @@ -7,10 +7,18 @@ */ import { schema } from '@kbn/config-schema'; -import { IRouter, SavedObject } from 'src/core/server'; -import { importDashboards } from '../lib'; +import { IRouter, Logger, SavedObject } from '../../..'; +import { InternalCoreUsageDataSetup } from '../../../core_usage_data'; +import { importDashboards } from './lib'; -export const registerImportRoute = (router: IRouter, maxImportPayloadBytes: number) => { +export const registerLegacyImportRoute = ( + router: IRouter, + { + maxImportPayloadBytes, + coreUsageData, + logger, + }: { maxImportPayloadBytes: number; coreUsageData: InternalCoreUsageDataSetup; logger: Logger } +) => { router.post( { path: '/api/kibana/dashboards/import', @@ -34,9 +42,17 @@ export const registerImportRoute = (router: IRouter, maxImportPayloadBytes: numb }, }, async (ctx, req, res) => { + logger.warn( + "The import dashboard API '/api/kibana/dashboards/import' is deprecated. Use the saved objects import objects API '/api/saved_objects/_import' instead." + ); + const { client } = ctx.core.savedObjects; const objects = req.body.objects as SavedObject[]; const { force, exclude } = req.query; + + const usageStatsClient = coreUsageData.getClient(); + usageStatsClient.incrementLegacyDashboardsImport({ request: req }).catch(() => {}); + const result = await importDashboards(client, objects, { overwrite: force, exclude: Array.isArray(exclude) ? exclude : [exclude], diff --git a/src/core/server/saved_objects/routes/legacy_import_export/integration_tests/export.test.ts b/src/core/server/saved_objects/routes/legacy_import_export/integration_tests/export.test.ts new file mode 100644 index 000000000000..f9f654023d5f --- /dev/null +++ b/src/core/server/saved_objects/routes/legacy_import_export/integration_tests/export.test.ts @@ -0,0 +1,89 @@ +/* + * 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. + */ + +const exportObjects = [ + { + id: '1', + type: 'index-pattern', + attributes: {}, + references: [], + }, + { + id: '2', + type: 'search', + attributes: {}, + references: [ + { + name: 'ref_0', + type: 'index-pattern', + id: '1', + }, + ], + }, +]; + +jest.mock('../lib/export_dashboards', () => ({ + exportDashboards: jest.fn().mockResolvedValue({ version: 'mockversion', objects: exportObjects }), +})); + +import supertest from 'supertest'; +import type { UnwrapPromise } from '@kbn/utility-types'; +import { CoreUsageStatsClient } from '../../../../core_usage_data'; +import { coreUsageStatsClientMock } from '../../../../core_usage_data/core_usage_stats_client.mock'; +import { coreUsageDataServiceMock } from '../../../../core_usage_data/core_usage_data_service.mock'; +import { registerLegacyExportRoute } from '../export'; +import { setupServer } from '../../test_utils'; +import { loggerMock } from 'src/core/server/logging/logger.mock'; + +type SetupServerReturn = UnwrapPromise>; +let coreUsageStatsClient: jest.Mocked; + +describe('POST /api/dashboards/export', () => { + let server: SetupServerReturn['server']; + let httpSetup: SetupServerReturn['httpSetup']; + + beforeEach(async () => { + ({ server, httpSetup } = await setupServer()); + + const router = httpSetup.createRouter(''); + + coreUsageStatsClient = coreUsageStatsClientMock.create(); + coreUsageStatsClient.incrementLegacyDashboardsExport.mockRejectedValue(new Error('Oh no!')); // intentionally throw this error, which is swallowed, so we can assert that the operation does not fail + const coreUsageData = coreUsageDataServiceMock.createSetupContract(coreUsageStatsClient); + registerLegacyExportRoute(router, { + kibanaVersion: '7.14.0', + coreUsageData, + logger: loggerMock.create(), + }); + + await server.start(); + }); + + afterEach(async () => { + jest.clearAllMocks(); + await server.stop(); + }); + + it('calls exportDashboards and records usage stats', async () => { + const result = await supertest(httpSetup.server.listener).get( + '/api/kibana/dashboards/export?dashboard=942dcef0-b2cd-11e8-ad8e-85441f0c2e5c' + ); + + expect(result.status).toBe(200); + expect(result.header['content-type']).toEqual('application/json; charset=utf-8'); + expect(result.header['content-disposition']).toMatch( + /attachment; filename="kibana-dashboards.*\.json/ + ); + + expect(result.body.objects).toEqual(exportObjects); + expect(result.body.version).toEqual('mockversion'); + expect(coreUsageStatsClient.incrementLegacyDashboardsExport).toHaveBeenCalledWith({ + request: expect.anything(), + }); + }); +}); diff --git a/src/core/server/saved_objects/routes/legacy_import_export/integration_tests/import.test.ts b/src/core/server/saved_objects/routes/legacy_import_export/integration_tests/import.test.ts new file mode 100644 index 000000000000..5ced77550c08 --- /dev/null +++ b/src/core/server/saved_objects/routes/legacy_import_export/integration_tests/import.test.ts @@ -0,0 +1,84 @@ +/* + * 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. + */ + +const importObjects = [ + { + id: '1', + type: 'index-pattern', + attributes: {}, + references: [], + }, + { + id: '2', + type: 'search', + attributes: {}, + references: [ + { + name: 'ref_0', + type: 'index-pattern', + id: '1', + }, + ], + }, +]; + +jest.mock('../lib/import_dashboards', () => ({ + importDashboards: jest.fn().mockResolvedValue({ objects: importObjects }), +})); + +import supertest from 'supertest'; +import type { UnwrapPromise } from '@kbn/utility-types'; +import { CoreUsageStatsClient } from '../../../../core_usage_data'; +import { coreUsageStatsClientMock } from '../../../../core_usage_data/core_usage_stats_client.mock'; +import { coreUsageDataServiceMock } from '../../../../core_usage_data/core_usage_data_service.mock'; +import { registerLegacyImportRoute } from '../import'; +import { setupServer } from '../../test_utils'; +import { loggerMock } from 'src/core/server/logging/logger.mock'; + +type SetupServerReturn = UnwrapPromise>; +let coreUsageStatsClient: jest.Mocked; + +describe('POST /api/dashboards/import', () => { + let server: SetupServerReturn['server']; + let httpSetup: SetupServerReturn['httpSetup']; + + beforeEach(async () => { + ({ server, httpSetup } = await setupServer()); + + const router = httpSetup.createRouter(''); + + coreUsageStatsClient = coreUsageStatsClientMock.create(); + coreUsageStatsClient.incrementLegacyDashboardsImport.mockRejectedValue(new Error('Oh no!')); // intentionally throw this error, which is swallowed, so we can assert that the operation does not fail + const coreUsageData = coreUsageDataServiceMock.createSetupContract(coreUsageStatsClient); + registerLegacyImportRoute(router, { + maxImportPayloadBytes: 26214400, + coreUsageData, + logger: loggerMock.create(), + }); + + await server.start(); + }); + + afterEach(async () => { + jest.clearAllMocks(); + await server.stop(); + }); + + it('calls importDashboards and records usage stats', async () => { + const result = await supertest(httpSetup.server.listener) + .post('/api/kibana/dashboards/import') + .send({ version: '7.14.0', objects: importObjects }); + + expect(result.status).toBe(200); + + expect(result.body.objects).toEqual(importObjects); + expect(coreUsageStatsClient.incrementLegacyDashboardsImport).toHaveBeenCalledWith({ + request: expect.anything(), + }); + }); +}); diff --git a/src/plugins/legacy_export/server/lib/export/collect_references_deep.test.ts b/src/core/server/saved_objects/routes/legacy_import_export/lib/collect_references_deep.test.ts similarity index 98% rename from src/plugins/legacy_export/server/lib/export/collect_references_deep.test.ts rename to src/core/server/saved_objects/routes/legacy_import_export/lib/collect_references_deep.test.ts index c86ce9124eaa..4f5da2a783cd 100644 --- a/src/plugins/legacy_export/server/lib/export/collect_references_deep.test.ts +++ b/src/core/server/saved_objects/routes/legacy_import_export/lib/collect_references_deep.test.ts @@ -7,7 +7,7 @@ */ import { SavedObject, SavedObjectAttributes } from 'src/core/server'; -import { savedObjectsClientMock } from '../../../../../core/server/mocks'; +import { savedObjectsClientMock } from '../../../../mocks'; import { collectReferencesDeep } from './collect_references_deep'; const data: Array> = [ diff --git a/src/plugins/legacy_export/server/lib/export/collect_references_deep.ts b/src/core/server/saved_objects/routes/legacy_import_export/lib/collect_references_deep.ts similarity index 100% rename from src/plugins/legacy_export/server/lib/export/collect_references_deep.ts rename to src/core/server/saved_objects/routes/legacy_import_export/lib/collect_references_deep.ts diff --git a/src/plugins/legacy_export/server/lib/export/export_dashboards.ts b/src/core/server/saved_objects/routes/legacy_import_export/lib/export_dashboards.ts similarity index 100% rename from src/plugins/legacy_export/server/lib/export/export_dashboards.ts rename to src/core/server/saved_objects/routes/legacy_import_export/lib/export_dashboards.ts diff --git a/src/plugins/legacy_export/server/lib/import/import_dashboards.test.ts b/src/core/server/saved_objects/routes/legacy_import_export/lib/import_dashboards.test.ts similarity index 95% rename from src/plugins/legacy_export/server/lib/import/import_dashboards.test.ts rename to src/core/server/saved_objects/routes/legacy_import_export/lib/import_dashboards.test.ts index 64214e87336f..3d23fb1b9022 100644 --- a/src/plugins/legacy_export/server/lib/import/import_dashboards.test.ts +++ b/src/core/server/saved_objects/routes/legacy_import_export/lib/import_dashboards.test.ts @@ -6,8 +6,8 @@ * Side Public License, v 1. */ -import { savedObjectsClientMock } from '../../../../../core/server/mocks'; -import { SavedObject } from '../../../../../core/server'; +import { savedObjectsClientMock } from '../../../../mocks'; +import { SavedObject } from '../../../..'; import { importDashboards } from './import_dashboards'; describe('importDashboards(req)', () => { diff --git a/src/plugins/legacy_export/server/lib/import/import_dashboards.ts b/src/core/server/saved_objects/routes/legacy_import_export/lib/import_dashboards.ts similarity index 100% rename from src/plugins/legacy_export/server/lib/import/import_dashboards.ts rename to src/core/server/saved_objects/routes/legacy_import_export/lib/import_dashboards.ts diff --git a/src/plugins/legacy_export/jest.config.js b/src/core/server/saved_objects/routes/legacy_import_export/lib/index.ts similarity index 75% rename from src/plugins/legacy_export/jest.config.js rename to src/core/server/saved_objects/routes/legacy_import_export/lib/index.ts index d5dd37c8249f..7c2fc5568256 100644 --- a/src/plugins/legacy_export/jest.config.js +++ b/src/core/server/saved_objects/routes/legacy_import_export/lib/index.ts @@ -6,8 +6,5 @@ * Side Public License, v 1. */ -module.exports = { - preset: '@kbn/test', - rootDir: '../../..', - roots: ['/src/plugins/legacy_export'], -}; +export { exportDashboards } from './export_dashboards'; +export { importDashboards } from './import_dashboards'; diff --git a/src/core/server/saved_objects/saved_objects_service.ts b/src/core/server/saved_objects/saved_objects_service.ts index 074eae55acae..b298396a2aee 100644 --- a/src/core/server/saved_objects/saved_objects_service.ts +++ b/src/core/server/saved_objects/saved_objects_service.ts @@ -306,6 +306,7 @@ export class SavedObjectsService logger: this.logger, config: this.config, migratorPromise: this.migrator$.pipe(first()).toPromise(), + kibanaVersion: this.coreContext.env.packageInfo.version, }); registerCoreObjectTypes(this.typeRegistry); diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index 5abd1171a193..e48ec859e80a 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -491,6 +491,34 @@ export interface CoreUsageDataStart { // @internal export interface CoreUsageStats { + // (undocumented) + 'apiCalls.legacyDashboardExport.namespace.custom.kibanaRequest.no'?: number; + // (undocumented) + 'apiCalls.legacyDashboardExport.namespace.custom.kibanaRequest.yes'?: number; + // (undocumented) + 'apiCalls.legacyDashboardExport.namespace.custom.total'?: number; + // (undocumented) + 'apiCalls.legacyDashboardExport.namespace.default.kibanaRequest.no'?: number; + // (undocumented) + 'apiCalls.legacyDashboardExport.namespace.default.kibanaRequest.yes'?: number; + // (undocumented) + 'apiCalls.legacyDashboardExport.namespace.default.total'?: number; + // (undocumented) + 'apiCalls.legacyDashboardExport.total'?: number; + // (undocumented) + 'apiCalls.legacyDashboardImport.namespace.custom.kibanaRequest.no'?: number; + // (undocumented) + 'apiCalls.legacyDashboardImport.namespace.custom.kibanaRequest.yes'?: number; + // (undocumented) + 'apiCalls.legacyDashboardImport.namespace.custom.total'?: number; + // (undocumented) + 'apiCalls.legacyDashboardImport.namespace.default.kibanaRequest.no'?: number; + // (undocumented) + 'apiCalls.legacyDashboardImport.namespace.default.kibanaRequest.yes'?: number; + // (undocumented) + 'apiCalls.legacyDashboardImport.namespace.default.total'?: number; + // (undocumented) + 'apiCalls.legacyDashboardImport.total'?: number; // (undocumented) 'apiCalls.savedObjectsBulkCreate.namespace.custom.kibanaRequest.no'?: number; // (undocumented) diff --git a/src/plugins/kibana_usage_collection/server/collectors/core/core_usage_collector.ts b/src/plugins/kibana_usage_collection/server/collectors/core/core_usage_collector.ts index f7a16b3f563b..853681c47cf8 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/core/core_usage_collector.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/core/core_usage_collector.ts @@ -936,11 +936,51 @@ export function getCoreUsageCollector( 'How many times this API has been called by a non-Kibana client in a custom space.', }, }, - 'apiCalls.savedObjectsExport.allTypesSelected.yes': { + // Legacy dashboard import/export APIs + 'apiCalls.legacyDashboardExport.total': { + type: 'long', + _meta: { description: 'How many times this API has been called.' }, + }, + 'apiCalls.legacyDashboardExport.namespace.default.total': { + type: 'long', + _meta: { description: 'How many times this API has been called in the Default space.' }, + }, + 'apiCalls.legacyDashboardExport.namespace.default.kibanaRequest.yes': { type: 'long', _meta: { description: - 'How many times this API has been called with the `createNewCopiesEnabled` option.', + 'How many times this API has been called by the Kibana client in the Default space.', + }, + }, + 'apiCalls.legacyDashboardExport.namespace.default.kibanaRequest.no': { + type: 'long', + _meta: { + description: + 'How many times this API has been called by a non-Kibana client in the Default space.', + }, + }, + 'apiCalls.legacyDashboardExport.namespace.custom.total': { + type: 'long', + _meta: { description: 'How many times this API has been called in a custom space.' }, + }, + 'apiCalls.legacyDashboardExport.namespace.custom.kibanaRequest.yes': { + type: 'long', + _meta: { + description: + 'How many times this API has been called by the Kibana client in a custom space.', + }, + }, + 'apiCalls.legacyDashboardExport.namespace.custom.kibanaRequest.no': { + type: 'long', + _meta: { + description: + 'How many times this API has been called by a non-Kibana client in a custom space.', + }, + }, + 'apiCalls.savedObjectsExport.allTypesSelected.yes': { + type: 'long', + _meta: { + description: 'How many times this API has been called with all types selected.', }, }, 'apiCalls.savedObjectsExport.allTypesSelected.no': { @@ -949,6 +989,46 @@ export function getCoreUsageCollector( description: 'How many times this API has been called without all types selected.', }, }, + 'apiCalls.legacyDashboardImport.total': { + type: 'long', + _meta: { description: 'How many times this API has been called.' }, + }, + 'apiCalls.legacyDashboardImport.namespace.default.total': { + type: 'long', + _meta: { description: 'How many times this API has been called in the Default space.' }, + }, + 'apiCalls.legacyDashboardImport.namespace.default.kibanaRequest.yes': { + type: 'long', + _meta: { + description: + 'How many times this API has been called by the Kibana client in the Default space.', + }, + }, + 'apiCalls.legacyDashboardImport.namespace.default.kibanaRequest.no': { + type: 'long', + _meta: { + description: + 'How many times this API has been called by a non-Kibana client in the Default space.', + }, + }, + 'apiCalls.legacyDashboardImport.namespace.custom.total': { + type: 'long', + _meta: { description: 'How many times this API has been called in a custom space.' }, + }, + 'apiCalls.legacyDashboardImport.namespace.custom.kibanaRequest.yes': { + type: 'long', + _meta: { + description: + 'How many times this API has been called by the Kibana client in a custom space.', + }, + }, + 'apiCalls.legacyDashboardImport.namespace.custom.kibanaRequest.no': { + type: 'long', + _meta: { + description: + 'How many times this API has been called by a non-Kibana client in a custom space.', + }, + }, // Saved Objects Repository counters 'savedObjectsRepository.resolvedOutcome.exactMatch': { type: 'long', diff --git a/src/plugins/legacy_export/README.md b/src/plugins/legacy_export/README.md deleted file mode 100644 index 551487a1122f..000000000000 --- a/src/plugins/legacy_export/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# `legacyExport` plugin [deprecated] - -The `legacyExport` plugin adds support for the legacy saved objects export format. diff --git a/src/plugins/legacy_export/kibana.json b/src/plugins/legacy_export/kibana.json deleted file mode 100644 index 6c1b9ab5e959..000000000000 --- a/src/plugins/legacy_export/kibana.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "id": "legacyExport", - "owner": { - "name": "Kibana Core", - "githubTeam": "kibana-core" - }, - "version": "kibana", - "server": true, - "ui": false -} diff --git a/src/plugins/legacy_export/server/index.ts b/src/plugins/legacy_export/server/index.ts deleted file mode 100644 index 95716cdbd307..000000000000 --- a/src/plugins/legacy_export/server/index.ts +++ /dev/null @@ -1,12 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 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 { PluginInitializer } from 'src/core/server'; -import { LegacyExportPlugin } from './plugin'; - -export const plugin: PluginInitializer<{}, {}> = (context) => new LegacyExportPlugin(context); diff --git a/src/plugins/legacy_export/server/lib/index.ts b/src/plugins/legacy_export/server/lib/index.ts deleted file mode 100644 index 5ad29d1eab9f..000000000000 --- a/src/plugins/legacy_export/server/lib/index.ts +++ /dev/null @@ -1,10 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 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 { exportDashboards } from './export/export_dashboards'; -export { importDashboards } from './import/import_dashboards'; diff --git a/src/plugins/legacy_export/server/plugin.ts b/src/plugins/legacy_export/server/plugin.ts deleted file mode 100644 index a6bdcdc19b0a..000000000000 --- a/src/plugins/legacy_export/server/plugin.ts +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 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 { Plugin, CoreSetup, PluginInitializerContext } from 'kibana/server'; -import { registerRoutes } from './routes'; - -/** @deprecated */ -export class LegacyExportPlugin implements Plugin<{}, {}> { - constructor(private readonly initContext: PluginInitializerContext) {} - - public setup({ http }: CoreSetup) { - const globalConfig = this.initContext.config.legacy.get(); - - const router = http.createRouter(); - registerRoutes( - router, - this.initContext.env.packageInfo.version, - globalConfig.savedObjects.maxImportPayloadBytes.getValueInBytes() - ); - - return {}; - } - - public start() { - return {}; - } - - public stop() {} -} diff --git a/src/plugins/legacy_export/server/routes/index.ts b/src/plugins/legacy_export/server/routes/index.ts deleted file mode 100644 index c3153ae603ea..000000000000 --- a/src/plugins/legacy_export/server/routes/index.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 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 { IRouter } from 'src/core/server'; -import { registerImportRoute } from './import'; -import { registerExportRoute } from './export'; - -export const registerRoutes = ( - router: IRouter, - kibanaVersion: string, - maxImportPayloadBytes: number -) => { - registerExportRoute(router, kibanaVersion); - registerImportRoute(router, maxImportPayloadBytes); -}; diff --git a/src/plugins/legacy_export/tsconfig.json b/src/plugins/legacy_export/tsconfig.json deleted file mode 100644 index 2f071b5ba6c5..000000000000 --- a/src/plugins/legacy_export/tsconfig.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "extends": "../../../tsconfig.base.json", - "compilerOptions": { - "outDir": "./target/types", - "emitDeclarationOnly": true, - "declaration": true, - "declarationMap": true - }, - "include": [ - "server/**/*", - ], - "references": [ - { "path": "../../core/tsconfig.json" } - ] -} diff --git a/src/plugins/telemetry/schema/oss_plugins.json b/src/plugins/telemetry/schema/oss_plugins.json index ad2c763ecc96..07c765b493db 100644 --- a/src/plugins/telemetry/schema/oss_plugins.json +++ b/src/plugins/telemetry/schema/oss_plugins.json @@ -6850,10 +6850,52 @@ "description": "How many times this API has been called by a non-Kibana client in a custom space." } }, + "apiCalls.legacyDashboardExport.total": { + "type": "long", + "_meta": { + "description": "How many times this API has been called." + } + }, + "apiCalls.legacyDashboardExport.namespace.default.total": { + "type": "long", + "_meta": { + "description": "How many times this API has been called in the Default space." + } + }, + "apiCalls.legacyDashboardExport.namespace.default.kibanaRequest.yes": { + "type": "long", + "_meta": { + "description": "How many times this API has been called by the Kibana client in the Default space." + } + }, + "apiCalls.legacyDashboardExport.namespace.default.kibanaRequest.no": { + "type": "long", + "_meta": { + "description": "How many times this API has been called by a non-Kibana client in the Default space." + } + }, + "apiCalls.legacyDashboardExport.namespace.custom.total": { + "type": "long", + "_meta": { + "description": "How many times this API has been called in a custom space." + } + }, + "apiCalls.legacyDashboardExport.namespace.custom.kibanaRequest.yes": { + "type": "long", + "_meta": { + "description": "How many times this API has been called by the Kibana client in a custom space." + } + }, + "apiCalls.legacyDashboardExport.namespace.custom.kibanaRequest.no": { + "type": "long", + "_meta": { + "description": "How many times this API has been called by a non-Kibana client in a custom space." + } + }, "apiCalls.savedObjectsExport.allTypesSelected.yes": { "type": "long", "_meta": { - "description": "How many times this API has been called with the `createNewCopiesEnabled` option." + "description": "How many times this API has been called with all types selected." } }, "apiCalls.savedObjectsExport.allTypesSelected.no": { @@ -6862,6 +6904,48 @@ "description": "How many times this API has been called without all types selected." } }, + "apiCalls.legacyDashboardImport.total": { + "type": "long", + "_meta": { + "description": "How many times this API has been called." + } + }, + "apiCalls.legacyDashboardImport.namespace.default.total": { + "type": "long", + "_meta": { + "description": "How many times this API has been called in the Default space." + } + }, + "apiCalls.legacyDashboardImport.namespace.default.kibanaRequest.yes": { + "type": "long", + "_meta": { + "description": "How many times this API has been called by the Kibana client in the Default space." + } + }, + "apiCalls.legacyDashboardImport.namespace.default.kibanaRequest.no": { + "type": "long", + "_meta": { + "description": "How many times this API has been called by a non-Kibana client in the Default space." + } + }, + "apiCalls.legacyDashboardImport.namespace.custom.total": { + "type": "long", + "_meta": { + "description": "How many times this API has been called in a custom space." + } + }, + "apiCalls.legacyDashboardImport.namespace.custom.kibanaRequest.yes": { + "type": "long", + "_meta": { + "description": "How many times this API has been called by the Kibana client in a custom space." + } + }, + "apiCalls.legacyDashboardImport.namespace.custom.kibanaRequest.no": { + "type": "long", + "_meta": { + "description": "How many times this API has been called by a non-Kibana client in a custom space." + } + }, "savedObjectsRepository.resolvedOutcome.exactMatch": { "type": "long", "_meta": { diff --git a/test/tsconfig.json b/test/tsconfig.json index c94d4445dd24..660850ffeb6c 100644 --- a/test/tsconfig.json +++ b/test/tsconfig.json @@ -52,7 +52,6 @@ { "path": "../src/plugins/url_forwarding/tsconfig.json" }, { "path": "../src/plugins/usage_collection/tsconfig.json" }, { "path": "../src/plugins/index_pattern_management/tsconfig.json" }, - { "path": "../src/plugins/legacy_export/tsconfig.json" }, { "path": "../src/plugins/visualize/tsconfig.json" }, { "path": "plugin_functional/plugins/core_app_status/tsconfig.json" }, { "path": "plugin_functional/plugins/core_provider_plugin/tsconfig.json" }, diff --git a/x-pack/test/tsconfig.json b/x-pack/test/tsconfig.json index 0744af077659..173403743235 100644 --- a/x-pack/test/tsconfig.json +++ b/x-pack/test/tsconfig.json @@ -34,7 +34,6 @@ { "path": "../../src/plugins/kibana_react/tsconfig.json" }, { "path": "../../src/plugins/kibana_usage_collection/tsconfig.json" }, { "path": "../../src/plugins/kibana_utils/tsconfig.json" }, - { "path": "../../src/plugins/legacy_export/tsconfig.json" }, { "path": "../../src/plugins/management/tsconfig.json" }, { "path": "../../src/plugins/navigation/tsconfig.json" }, { "path": "../../src/plugins/newsfeed/tsconfig.json" },