mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
Disable checking for conflicts when copying saved objects (#83575)
This commit is contained in:
parent
ac71d2e941
commit
c39d14fef4
72 changed files with 1769 additions and 191 deletions
|
@ -51,9 +51,17 @@ You can request to overwrite any objects that already exist in the target space
|
|||
(Optional, boolean) When set to `true`, all saved objects related to the specified saved objects will also be copied into the target
|
||||
spaces. The default value is `false`.
|
||||
|
||||
`createNewCopies`::
|
||||
(Optional, boolean) Creates new copies of saved objects, regenerates each object ID, and resets the origin. When used, potential conflict
|
||||
errors are avoided. The default value is `true`.
|
||||
+
|
||||
NOTE: This cannot be used with the `overwrite` option.
|
||||
|
||||
`overwrite`::
|
||||
(Optional, boolean) When set to `true`, all conflicts are automatically overidden. When a saved object with a matching `type` and `id`
|
||||
exists in the target space, that version is replaced with the version from the source space. The default value is `false`.
|
||||
+
|
||||
NOTE: This cannot be used with the `createNewCopies` option.
|
||||
|
||||
[role="child_attributes"]
|
||||
[[spaces-api-copy-saved-objects-response-body]]
|
||||
|
@ -128,8 +136,7 @@ $ curl -X POST api/spaces/_copy_saved_objects
|
|||
"id": "my-dashboard"
|
||||
}],
|
||||
"spaces": ["marketing"],
|
||||
"includeReferences": true,
|
||||
"createNewcopies": true
|
||||
"includeReferences": true
|
||||
}
|
||||
----
|
||||
// KIBANA
|
||||
|
@ -193,7 +200,8 @@ $ curl -X POST api/spaces/_copy_saved_objects
|
|||
"id": "my-dashboard"
|
||||
}],
|
||||
"spaces": ["marketing"],
|
||||
"includeReferences": true
|
||||
"includeReferences": true,
|
||||
"createNewCopies": false
|
||||
}
|
||||
----
|
||||
// KIBANA
|
||||
|
@ -254,7 +262,8 @@ $ curl -X POST api/spaces/_copy_saved_objects
|
|||
"id": "my-dashboard"
|
||||
}],
|
||||
"spaces": ["marketing", "sales"],
|
||||
"includeReferences": true
|
||||
"includeReferences": true,
|
||||
"createNewCopies": false
|
||||
}
|
||||
----
|
||||
// KIBANA
|
||||
|
@ -405,7 +414,8 @@ $ curl -X POST api/spaces/_copy_saved_objects
|
|||
"id": "my-dashboard"
|
||||
}],
|
||||
"spaces": ["marketing"],
|
||||
"includeReferences": true
|
||||
"includeReferences": true,
|
||||
"createNewCopies": false
|
||||
}
|
||||
----
|
||||
// KIBANA
|
||||
|
|
|
@ -45,6 +45,10 @@ Execute the <<spaces-api-copy-saved-objects,copy saved objects to space API>>, w
|
|||
`includeReferences`::
|
||||
(Optional, boolean) When set to `true`, all saved objects related to the specified saved objects are copied into the target spaces. The `includeReferences` must be the same values used during the failed <<spaces-api-copy-saved-objects, copy saved objects to space API>> operation. The default value is `false`.
|
||||
|
||||
`createNewCopies`::
|
||||
(Optional, boolean) Creates new copies of the saved objects, regenerates each object ID, and resets the origin. When enabled during the
|
||||
initial copy, also enable when resolving copy errors. The default value is `true`.
|
||||
|
||||
`retries`::
|
||||
(Required, object) The retry operations to attempt, which can specify how to resolve different types of errors. Object keys represent the
|
||||
target space IDs.
|
||||
|
@ -148,6 +152,7 @@ $ curl -X POST api/spaces/_resolve_copy_saved_objects_errors
|
|||
"id": "my-dashboard"
|
||||
}],
|
||||
"includeReferences": true,
|
||||
"createNewCopies": false,
|
||||
"retries": {
|
||||
"sales": [
|
||||
{
|
||||
|
@ -246,6 +251,7 @@ $ curl -X POST api/spaces/_resolve_copy_saved_objects_errors
|
|||
"id": "my-dashboard"
|
||||
}],
|
||||
"includeReferences": true,
|
||||
"createNewCopies": false,
|
||||
"retries": {
|
||||
"marketing": [
|
||||
{
|
||||
|
|
|
@ -9,7 +9,7 @@ Increments all the specified counter fields by one. Creates the document if one
|
|||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
incrementCounter(type: string, id: string, counterFieldNames: string[], options?: SavedObjectsIncrementCounterOptions): Promise<SavedObject>;
|
||||
incrementCounter<T = unknown>(type: string, id: string, counterFieldNames: string[], options?: SavedObjectsIncrementCounterOptions): Promise<SavedObject<T>>;
|
||||
```
|
||||
|
||||
## Parameters
|
||||
|
@ -23,7 +23,7 @@ incrementCounter(type: string, id: string, counterFieldNames: string[], options?
|
|||
|
||||
<b>Returns:</b>
|
||||
|
||||
`Promise<SavedObject>`
|
||||
`Promise<SavedObject<T>>`
|
||||
|
||||
The saved object after the specified fields were incremented
|
||||
|
||||
|
|
24
src/core/server/core_usage_data/constants.ts
Normal file
24
src/core/server/core_usage_data/constants.ts
Normal file
|
@ -0,0 +1,24 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
/** @internal */
|
||||
export const CORE_USAGE_STATS_TYPE = 'core-usage-stats';
|
||||
|
||||
/** @internal */
|
||||
export const CORE_USAGE_STATS_ID = 'core-usage-stats';
|
|
@ -20,7 +20,16 @@
|
|||
import { PublicMethodsOf } from '@kbn/utility-types';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
import { CoreUsageDataService } from './core_usage_data_service';
|
||||
import { CoreUsageData, CoreUsageDataStart } from './types';
|
||||
import { coreUsageStatsClientMock } from './core_usage_stats_client.mock';
|
||||
import { CoreUsageData, CoreUsageDataSetup, CoreUsageDataStart } from './types';
|
||||
|
||||
const createSetupContractMock = (usageStatsClient = coreUsageStatsClientMock.create()) => {
|
||||
const setupContract: jest.Mocked<CoreUsageDataSetup> = {
|
||||
registerType: jest.fn(),
|
||||
getClient: jest.fn().mockReturnValue(usageStatsClient),
|
||||
};
|
||||
return setupContract;
|
||||
};
|
||||
|
||||
const createStartContractMock = () => {
|
||||
const startContract: jest.Mocked<CoreUsageDataStart> = {
|
||||
|
@ -140,7 +149,7 @@ const createStartContractMock = () => {
|
|||
|
||||
const createMock = () => {
|
||||
const mocked: jest.Mocked<PublicMethodsOf<CoreUsageDataService>> = {
|
||||
setup: jest.fn(),
|
||||
setup: jest.fn().mockReturnValue(createSetupContractMock()),
|
||||
start: jest.fn().mockReturnValue(createStartContractMock()),
|
||||
stop: jest.fn(),
|
||||
};
|
||||
|
@ -149,5 +158,6 @@ const createMock = () => {
|
|||
|
||||
export const coreUsageDataServiceMock = {
|
||||
create: createMock,
|
||||
createSetupContract: createSetupContractMock,
|
||||
createStartContract: createStartContractMock,
|
||||
};
|
||||
|
|
|
@ -34,6 +34,9 @@ import { savedObjectsServiceMock } from '../saved_objects/saved_objects_service.
|
|||
|
||||
import { CoreUsageDataService } from './core_usage_data_service';
|
||||
import { elasticsearchServiceMock } from '../elasticsearch/elasticsearch_service.mock';
|
||||
import { typeRegistryMock } from '../saved_objects/saved_objects_type_registry.mock';
|
||||
import { CORE_USAGE_STATS_TYPE } from './constants';
|
||||
import { CoreUsageStatsClient } from './core_usage_stats_client';
|
||||
|
||||
describe('CoreUsageDataService', () => {
|
||||
const getTestScheduler = () =>
|
||||
|
@ -63,11 +66,67 @@ describe('CoreUsageDataService', () => {
|
|||
service = new CoreUsageDataService(coreContext);
|
||||
});
|
||||
|
||||
describe('setup', () => {
|
||||
it('creates internal repository', async () => {
|
||||
const metrics = metricsServiceMock.createInternalSetupContract();
|
||||
const savedObjectsStartPromise = Promise.resolve(
|
||||
savedObjectsServiceMock.createStartContract()
|
||||
);
|
||||
service.setup({ metrics, savedObjectsStartPromise });
|
||||
|
||||
const savedObjects = await savedObjectsStartPromise;
|
||||
expect(savedObjects.createInternalRepository).toHaveBeenCalledTimes(1);
|
||||
expect(savedObjects.createInternalRepository).toHaveBeenCalledWith([CORE_USAGE_STATS_TYPE]);
|
||||
});
|
||||
|
||||
describe('#registerType', () => {
|
||||
it('registers core usage stats type', async () => {
|
||||
const metrics = metricsServiceMock.createInternalSetupContract();
|
||||
const savedObjectsStartPromise = Promise.resolve(
|
||||
savedObjectsServiceMock.createStartContract()
|
||||
);
|
||||
const coreUsageData = service.setup({
|
||||
metrics,
|
||||
savedObjectsStartPromise,
|
||||
});
|
||||
const typeRegistry = typeRegistryMock.create();
|
||||
|
||||
coreUsageData.registerType(typeRegistry);
|
||||
expect(typeRegistry.registerType).toHaveBeenCalledTimes(1);
|
||||
expect(typeRegistry.registerType).toHaveBeenCalledWith({
|
||||
name: CORE_USAGE_STATS_TYPE,
|
||||
hidden: true,
|
||||
namespaceType: 'agnostic',
|
||||
mappings: expect.anything(),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#getClient', () => {
|
||||
it('returns client', async () => {
|
||||
const metrics = metricsServiceMock.createInternalSetupContract();
|
||||
const savedObjectsStartPromise = Promise.resolve(
|
||||
savedObjectsServiceMock.createStartContract()
|
||||
);
|
||||
const coreUsageData = service.setup({
|
||||
metrics,
|
||||
savedObjectsStartPromise,
|
||||
});
|
||||
|
||||
const usageStatsClient = coreUsageData.getClient();
|
||||
expect(usageStatsClient).toBeInstanceOf(CoreUsageStatsClient);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('start', () => {
|
||||
describe('getCoreUsageData', () => {
|
||||
it('returns core metrics for default config', () => {
|
||||
it('returns core metrics for default config', async () => {
|
||||
const metrics = metricsServiceMock.createInternalSetupContract();
|
||||
service.setup({ metrics });
|
||||
const savedObjectsStartPromise = Promise.resolve(
|
||||
savedObjectsServiceMock.createStartContract()
|
||||
);
|
||||
service.setup({ metrics, savedObjectsStartPromise });
|
||||
const elasticsearch = elasticsearchServiceMock.createStart();
|
||||
elasticsearch.client.asInternalUser.cat.indices.mockResolvedValueOnce({
|
||||
body: [
|
||||
|
@ -243,8 +302,11 @@ describe('CoreUsageDataService', () => {
|
|||
observables.push(newObservable);
|
||||
return newObservable as Observable<any>;
|
||||
});
|
||||
const savedObjectsStartPromise = Promise.resolve(
|
||||
savedObjectsServiceMock.createStartContract()
|
||||
);
|
||||
|
||||
service.setup({ metrics });
|
||||
service.setup({ metrics, savedObjectsStartPromise });
|
||||
|
||||
// Use the stopTimer$ to delay calling stop() until the third frame
|
||||
const stopTimer$ = cold('---a|');
|
||||
|
|
|
@ -21,20 +21,29 @@ import { Subject } from 'rxjs';
|
|||
import { takeUntil } from 'rxjs/operators';
|
||||
|
||||
import { CoreService } from 'src/core/types';
|
||||
import { SavedObjectsServiceStart } from 'src/core/server';
|
||||
import { Logger, SavedObjectsServiceStart, SavedObjectTypeRegistry } from 'src/core/server';
|
||||
import { CoreContext } from '../core_context';
|
||||
import { ElasticsearchConfigType } from '../elasticsearch/elasticsearch_config';
|
||||
import { HttpConfigType } from '../http';
|
||||
import { LoggingConfigType } from '../logging';
|
||||
import { SavedObjectsConfigType } from '../saved_objects/saved_objects_config';
|
||||
import { CoreServicesUsageData, CoreUsageData, CoreUsageDataStart } from './types';
|
||||
import {
|
||||
CoreServicesUsageData,
|
||||
CoreUsageData,
|
||||
CoreUsageDataStart,
|
||||
CoreUsageDataSetup,
|
||||
} from './types';
|
||||
import { isConfigured } from './is_configured';
|
||||
import { ElasticsearchServiceStart } from '../elasticsearch';
|
||||
import { KibanaConfigType } from '../kibana_config';
|
||||
import { coreUsageStatsType } from './core_usage_stats';
|
||||
import { CORE_USAGE_STATS_TYPE } from './constants';
|
||||
import { CoreUsageStatsClient } from './core_usage_stats_client';
|
||||
import { MetricsServiceSetup, OpsMetrics } from '..';
|
||||
|
||||
export interface SetupDeps {
|
||||
metrics: MetricsServiceSetup;
|
||||
savedObjectsStartPromise: Promise<SavedObjectsServiceStart>;
|
||||
}
|
||||
|
||||
export interface StartDeps {
|
||||
|
@ -60,7 +69,8 @@ const kibanaOrTaskManagerIndex = (index: string, kibanaConfigIndex: string) => {
|
|||
return index === kibanaConfigIndex ? '.kibana' : '.kibana_task_manager';
|
||||
};
|
||||
|
||||
export class CoreUsageDataService implements CoreService<void, CoreUsageDataStart> {
|
||||
export class CoreUsageDataService implements CoreService<CoreUsageDataSetup, CoreUsageDataStart> {
|
||||
private logger: Logger;
|
||||
private elasticsearchConfig?: ElasticsearchConfigType;
|
||||
private configService: CoreContext['configService'];
|
||||
private httpConfig?: HttpConfigType;
|
||||
|
@ -69,8 +79,10 @@ export class CoreUsageDataService implements CoreService<void, CoreUsageDataStar
|
|||
private stop$: Subject<void>;
|
||||
private opsMetrics?: OpsMetrics;
|
||||
private kibanaConfig?: KibanaConfigType;
|
||||
private coreUsageStatsClient?: CoreUsageStatsClient;
|
||||
|
||||
constructor(core: CoreContext) {
|
||||
this.logger = core.logger.get('core-usage-stats-service');
|
||||
this.configService = core.configService;
|
||||
this.stop$ = new Subject();
|
||||
}
|
||||
|
@ -130,8 +142,15 @@ export class CoreUsageDataService implements CoreService<void, CoreUsageDataStar
|
|||
throw new Error('Unable to read config values. Ensure that setup() has completed.');
|
||||
}
|
||||
|
||||
if (!this.coreUsageStatsClient) {
|
||||
throw new Error(
|
||||
'Core usage stats client is not initialized. Ensure that setup() has completed.'
|
||||
);
|
||||
}
|
||||
|
||||
const es = this.elasticsearchConfig;
|
||||
const soUsageData = await this.getSavedObjectIndicesUsageData(savedObjects, elasticsearch);
|
||||
const coreUsageStatsData = await this.coreUsageStatsClient.getUsageStats();
|
||||
|
||||
const http = this.httpConfig;
|
||||
return {
|
||||
|
@ -225,10 +244,11 @@ export class CoreUsageDataService implements CoreService<void, CoreUsageDataStar
|
|||
services: {
|
||||
savedObjects: soUsageData,
|
||||
},
|
||||
...coreUsageStatsData,
|
||||
};
|
||||
}
|
||||
|
||||
setup({ metrics }: SetupDeps) {
|
||||
setup({ metrics, savedObjectsStartPromise }: SetupDeps) {
|
||||
metrics
|
||||
.getOpsMetrics$()
|
||||
.pipe(takeUntil(this.stop$))
|
||||
|
@ -268,6 +288,24 @@ export class CoreUsageDataService implements CoreService<void, CoreUsageDataStar
|
|||
.subscribe((config) => {
|
||||
this.kibanaConfig = config;
|
||||
});
|
||||
|
||||
const internalRepositoryPromise = savedObjectsStartPromise.then((savedObjects) =>
|
||||
savedObjects.createInternalRepository([CORE_USAGE_STATS_TYPE])
|
||||
);
|
||||
|
||||
const registerType = (typeRegistry: SavedObjectTypeRegistry) => {
|
||||
typeRegistry.registerType(coreUsageStatsType);
|
||||
};
|
||||
|
||||
const getClient = () => {
|
||||
const debugLogger = (message: string) => this.logger.debug(message);
|
||||
|
||||
return new CoreUsageStatsClient(debugLogger, internalRepositoryPromise);
|
||||
};
|
||||
|
||||
this.coreUsageStatsClient = getClient();
|
||||
|
||||
return { registerType, getClient } as CoreUsageDataSetup;
|
||||
}
|
||||
|
||||
start({ savedObjects, elasticsearch }: StartDeps) {
|
||||
|
|
32
src/core/server/core_usage_data/core_usage_stats.ts
Normal file
32
src/core/server/core_usage_data/core_usage_stats.ts
Normal file
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { SavedObjectsType } from '../saved_objects';
|
||||
import { CORE_USAGE_STATS_TYPE } from './constants';
|
||||
|
||||
/** @internal */
|
||||
export const coreUsageStatsType: SavedObjectsType = {
|
||||
name: CORE_USAGE_STATS_TYPE,
|
||||
hidden: true,
|
||||
namespaceType: 'agnostic',
|
||||
mappings: {
|
||||
dynamic: false, // we aren't querying or aggregating over this data, so we don't need to specify any fields
|
||||
properties: {},
|
||||
},
|
||||
};
|
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { CoreUsageStatsClient } from '.';
|
||||
|
||||
const createUsageStatsClientMock = () =>
|
||||
(({
|
||||
getUsageStats: jest.fn().mockResolvedValue({}),
|
||||
incrementSavedObjectsImport: jest.fn().mockResolvedValue(null),
|
||||
incrementSavedObjectsResolveImportErrors: jest.fn().mockResolvedValue(null),
|
||||
incrementSavedObjectsExport: jest.fn().mockResolvedValue(null),
|
||||
} as unknown) as jest.Mocked<CoreUsageStatsClient>);
|
||||
|
||||
export const coreUsageStatsClientMock = {
|
||||
create: createUsageStatsClientMock,
|
||||
};
|
227
src/core/server/core_usage_data/core_usage_stats_client.test.ts
Normal file
227
src/core/server/core_usage_data/core_usage_stats_client.test.ts
Normal file
|
@ -0,0 +1,227 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { savedObjectsRepositoryMock } from '../mocks';
|
||||
import { CORE_USAGE_STATS_TYPE, CORE_USAGE_STATS_ID } from './constants';
|
||||
import {
|
||||
IncrementSavedObjectsImportOptions,
|
||||
IncrementSavedObjectsResolveImportErrorsOptions,
|
||||
IncrementSavedObjectsExportOptions,
|
||||
IMPORT_STATS_PREFIX,
|
||||
RESOLVE_IMPORT_STATS_PREFIX,
|
||||
EXPORT_STATS_PREFIX,
|
||||
} from './core_usage_stats_client';
|
||||
import { CoreUsageStatsClient } from '.';
|
||||
|
||||
describe('CoreUsageStatsClient', () => {
|
||||
const setup = () => {
|
||||
const debugLoggerMock = jest.fn();
|
||||
const repositoryMock = savedObjectsRepositoryMock.create();
|
||||
const usageStatsClient = new CoreUsageStatsClient(
|
||||
debugLoggerMock,
|
||||
Promise.resolve(repositoryMock)
|
||||
);
|
||||
return { usageStatsClient, debugLoggerMock, repositoryMock };
|
||||
};
|
||||
|
||||
const firstPartyRequestHeaders = { 'kbn-version': 'a', origin: 'b', referer: 'c' }; // as long as these three header fields are truthy, this will be treated like a first-party request
|
||||
const incrementOptions = { refresh: false };
|
||||
|
||||
describe('#getUsageStats', () => {
|
||||
it('returns empty object when encountering a repository error', async () => {
|
||||
const { usageStatsClient, repositoryMock } = setup();
|
||||
repositoryMock.get.mockRejectedValue(new Error('Oh no!'));
|
||||
|
||||
const result = await usageStatsClient.getUsageStats();
|
||||
expect(result).toEqual({});
|
||||
});
|
||||
|
||||
it('returns object attributes when usage stats exist', async () => {
|
||||
const { usageStatsClient, repositoryMock } = setup();
|
||||
const usageStats = { foo: 'bar' };
|
||||
repositoryMock.incrementCounter.mockResolvedValue({
|
||||
type: CORE_USAGE_STATS_TYPE,
|
||||
id: CORE_USAGE_STATS_ID,
|
||||
attributes: usageStats,
|
||||
references: [],
|
||||
});
|
||||
|
||||
const result = await usageStatsClient.getUsageStats();
|
||||
expect(result).toEqual(usageStats);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#incrementSavedObjectsImport', () => {
|
||||
it('does not throw an error if repository incrementCounter operation fails', async () => {
|
||||
const { usageStatsClient, repositoryMock } = setup();
|
||||
repositoryMock.incrementCounter.mockRejectedValue(new Error('Oh no!'));
|
||||
|
||||
await expect(
|
||||
usageStatsClient.incrementSavedObjectsImport({} as IncrementSavedObjectsImportOptions)
|
||||
).resolves.toBeUndefined();
|
||||
expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('handles falsy options appropriately', async () => {
|
||||
const { usageStatsClient, repositoryMock } = setup();
|
||||
|
||||
await usageStatsClient.incrementSavedObjectsImport({} as IncrementSavedObjectsImportOptions);
|
||||
expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(1);
|
||||
expect(repositoryMock.incrementCounter).toHaveBeenCalledWith(
|
||||
CORE_USAGE_STATS_TYPE,
|
||||
CORE_USAGE_STATS_ID,
|
||||
[
|
||||
`${IMPORT_STATS_PREFIX}.total`,
|
||||
`${IMPORT_STATS_PREFIX}.kibanaRequest.no`,
|
||||
`${IMPORT_STATS_PREFIX}.createNewCopiesEnabled.no`,
|
||||
`${IMPORT_STATS_PREFIX}.overwriteEnabled.no`,
|
||||
],
|
||||
incrementOptions
|
||||
);
|
||||
});
|
||||
|
||||
it('handles truthy options appropriately', async () => {
|
||||
const { usageStatsClient, repositoryMock } = setup();
|
||||
|
||||
await usageStatsClient.incrementSavedObjectsImport({
|
||||
headers: firstPartyRequestHeaders,
|
||||
createNewCopies: true,
|
||||
overwrite: true,
|
||||
} as IncrementSavedObjectsImportOptions);
|
||||
expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(1);
|
||||
expect(repositoryMock.incrementCounter).toHaveBeenCalledWith(
|
||||
CORE_USAGE_STATS_TYPE,
|
||||
CORE_USAGE_STATS_ID,
|
||||
[
|
||||
`${IMPORT_STATS_PREFIX}.total`,
|
||||
`${IMPORT_STATS_PREFIX}.kibanaRequest.yes`,
|
||||
`${IMPORT_STATS_PREFIX}.createNewCopiesEnabled.yes`,
|
||||
`${IMPORT_STATS_PREFIX}.overwriteEnabled.yes`,
|
||||
],
|
||||
incrementOptions
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#incrementSavedObjectsResolveImportErrors', () => {
|
||||
it('does not throw an error if repository incrementCounter operation fails', async () => {
|
||||
const { usageStatsClient, repositoryMock } = setup();
|
||||
repositoryMock.incrementCounter.mockRejectedValue(new Error('Oh no!'));
|
||||
|
||||
await expect(
|
||||
usageStatsClient.incrementSavedObjectsResolveImportErrors(
|
||||
{} as IncrementSavedObjectsResolveImportErrorsOptions
|
||||
)
|
||||
).resolves.toBeUndefined();
|
||||
expect(repositoryMock.incrementCounter).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('handles falsy options appropriately', async () => {
|
||||
const { usageStatsClient, repositoryMock } = setup();
|
||||
|
||||
await usageStatsClient.incrementSavedObjectsResolveImportErrors(
|
||||
{} as IncrementSavedObjectsResolveImportErrorsOptions
|
||||
);
|
||||
expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(1);
|
||||
expect(repositoryMock.incrementCounter).toHaveBeenCalledWith(
|
||||
CORE_USAGE_STATS_TYPE,
|
||||
CORE_USAGE_STATS_ID,
|
||||
[
|
||||
`${RESOLVE_IMPORT_STATS_PREFIX}.total`,
|
||||
`${RESOLVE_IMPORT_STATS_PREFIX}.kibanaRequest.no`,
|
||||
`${RESOLVE_IMPORT_STATS_PREFIX}.createNewCopiesEnabled.no`,
|
||||
],
|
||||
incrementOptions
|
||||
);
|
||||
});
|
||||
|
||||
it('handles truthy options appropriately', async () => {
|
||||
const { usageStatsClient, repositoryMock } = setup();
|
||||
|
||||
await usageStatsClient.incrementSavedObjectsResolveImportErrors({
|
||||
headers: firstPartyRequestHeaders,
|
||||
createNewCopies: true,
|
||||
} as IncrementSavedObjectsResolveImportErrorsOptions);
|
||||
expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(1);
|
||||
expect(repositoryMock.incrementCounter).toHaveBeenCalledWith(
|
||||
CORE_USAGE_STATS_TYPE,
|
||||
CORE_USAGE_STATS_ID,
|
||||
[
|
||||
`${RESOLVE_IMPORT_STATS_PREFIX}.total`,
|
||||
`${RESOLVE_IMPORT_STATS_PREFIX}.kibanaRequest.yes`,
|
||||
`${RESOLVE_IMPORT_STATS_PREFIX}.createNewCopiesEnabled.yes`,
|
||||
],
|
||||
incrementOptions
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#incrementSavedObjectsExport', () => {
|
||||
it('does not throw an error if repository incrementCounter operation fails', async () => {
|
||||
const { usageStatsClient, repositoryMock } = setup();
|
||||
repositoryMock.incrementCounter.mockRejectedValue(new Error('Oh no!'));
|
||||
|
||||
await expect(
|
||||
usageStatsClient.incrementSavedObjectsExport({} as IncrementSavedObjectsExportOptions)
|
||||
).resolves.toBeUndefined();
|
||||
expect(repositoryMock.incrementCounter).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('handles falsy options appropriately', async () => {
|
||||
const { usageStatsClient, repositoryMock } = setup();
|
||||
|
||||
await usageStatsClient.incrementSavedObjectsExport({
|
||||
types: undefined,
|
||||
supportedTypes: ['foo', 'bar'],
|
||||
} as IncrementSavedObjectsExportOptions);
|
||||
expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(1);
|
||||
expect(repositoryMock.incrementCounter).toHaveBeenCalledWith(
|
||||
CORE_USAGE_STATS_TYPE,
|
||||
CORE_USAGE_STATS_ID,
|
||||
[
|
||||
`${EXPORT_STATS_PREFIX}.total`,
|
||||
`${EXPORT_STATS_PREFIX}.kibanaRequest.no`,
|
||||
`${EXPORT_STATS_PREFIX}.allTypesSelected.no`,
|
||||
],
|
||||
incrementOptions
|
||||
);
|
||||
});
|
||||
|
||||
it('handles truthy options appropriately', async () => {
|
||||
const { usageStatsClient, repositoryMock } = setup();
|
||||
|
||||
await usageStatsClient.incrementSavedObjectsExport({
|
||||
headers: firstPartyRequestHeaders,
|
||||
types: ['foo', 'bar'],
|
||||
supportedTypes: ['foo', 'bar'],
|
||||
} as IncrementSavedObjectsExportOptions);
|
||||
expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(1);
|
||||
expect(repositoryMock.incrementCounter).toHaveBeenCalledWith(
|
||||
CORE_USAGE_STATS_TYPE,
|
||||
CORE_USAGE_STATS_ID,
|
||||
[
|
||||
`${EXPORT_STATS_PREFIX}.total`,
|
||||
`${EXPORT_STATS_PREFIX}.kibanaRequest.yes`,
|
||||
`${EXPORT_STATS_PREFIX}.allTypesSelected.yes`,
|
||||
],
|
||||
incrementOptions
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
154
src/core/server/core_usage_data/core_usage_stats_client.ts
Normal file
154
src/core/server/core_usage_data/core_usage_stats_client.ts
Normal file
|
@ -0,0 +1,154 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { CORE_USAGE_STATS_TYPE, CORE_USAGE_STATS_ID } from './constants';
|
||||
import { CoreUsageStats } from './types';
|
||||
import {
|
||||
Headers,
|
||||
ISavedObjectsRepository,
|
||||
SavedObjectsImportOptions,
|
||||
SavedObjectsResolveImportErrorsOptions,
|
||||
SavedObjectsExportOptions,
|
||||
} from '..';
|
||||
|
||||
interface BaseIncrementOptions {
|
||||
headers?: Headers;
|
||||
}
|
||||
/** @internal */
|
||||
export type IncrementSavedObjectsImportOptions = BaseIncrementOptions &
|
||||
Pick<SavedObjectsImportOptions, 'createNewCopies' | 'overwrite'>;
|
||||
/** @internal */
|
||||
export type IncrementSavedObjectsResolveImportErrorsOptions = BaseIncrementOptions &
|
||||
Pick<SavedObjectsResolveImportErrorsOptions, 'createNewCopies'>;
|
||||
/** @internal */
|
||||
export type IncrementSavedObjectsExportOptions = BaseIncrementOptions &
|
||||
Pick<SavedObjectsExportOptions, 'types'> & { supportedTypes: string[] };
|
||||
|
||||
export const IMPORT_STATS_PREFIX = 'apiCalls.savedObjectsImport';
|
||||
export const RESOLVE_IMPORT_STATS_PREFIX = 'apiCalls.savedObjectsResolveImportErrors';
|
||||
export const EXPORT_STATS_PREFIX = 'apiCalls.savedObjectsExport';
|
||||
const ALL_COUNTER_FIELDS = [
|
||||
`${IMPORT_STATS_PREFIX}.total`,
|
||||
`${IMPORT_STATS_PREFIX}.kibanaRequest.yes`,
|
||||
`${IMPORT_STATS_PREFIX}.kibanaRequest.no`,
|
||||
`${IMPORT_STATS_PREFIX}.createNewCopiesEnabled.yes`,
|
||||
`${IMPORT_STATS_PREFIX}.createNewCopiesEnabled.no`,
|
||||
`${IMPORT_STATS_PREFIX}.overwriteEnabled.yes`,
|
||||
`${IMPORT_STATS_PREFIX}.overwriteEnabled.no`,
|
||||
`${RESOLVE_IMPORT_STATS_PREFIX}.total`,
|
||||
`${RESOLVE_IMPORT_STATS_PREFIX}.kibanaRequest.yes`,
|
||||
`${RESOLVE_IMPORT_STATS_PREFIX}.kibanaRequest.no`,
|
||||
`${RESOLVE_IMPORT_STATS_PREFIX}.createNewCopiesEnabled.yes`,
|
||||
`${RESOLVE_IMPORT_STATS_PREFIX}.createNewCopiesEnabled.no`,
|
||||
`${EXPORT_STATS_PREFIX}.total`,
|
||||
`${EXPORT_STATS_PREFIX}.kibanaRequest.yes`,
|
||||
`${EXPORT_STATS_PREFIX}.kibanaRequest.no`,
|
||||
`${EXPORT_STATS_PREFIX}.allTypesSelected.yes`,
|
||||
`${EXPORT_STATS_PREFIX}.allTypesSelected.no`,
|
||||
];
|
||||
|
||||
/** @internal */
|
||||
export class CoreUsageStatsClient {
|
||||
constructor(
|
||||
private readonly debugLogger: (message: string) => void,
|
||||
private readonly repositoryPromise: Promise<ISavedObjectsRepository>
|
||||
) {}
|
||||
|
||||
public async getUsageStats() {
|
||||
this.debugLogger('getUsageStats() called');
|
||||
let coreUsageStats: CoreUsageStats = {};
|
||||
try {
|
||||
const repository = await this.repositoryPromise;
|
||||
const result = await repository.incrementCounter<CoreUsageStats>(
|
||||
CORE_USAGE_STATS_TYPE,
|
||||
CORE_USAGE_STATS_ID,
|
||||
ALL_COUNTER_FIELDS,
|
||||
{ initialize: true } // set all counter fields to 0 if they don't exist
|
||||
);
|
||||
coreUsageStats = result.attributes;
|
||||
} catch (err) {
|
||||
// do nothing
|
||||
}
|
||||
return coreUsageStats;
|
||||
}
|
||||
|
||||
public async incrementSavedObjectsImport({
|
||||
headers,
|
||||
createNewCopies,
|
||||
overwrite,
|
||||
}: IncrementSavedObjectsImportOptions) {
|
||||
const isKibanaRequest = getIsKibanaRequest(headers);
|
||||
const counterFieldNames = [
|
||||
'total',
|
||||
`kibanaRequest.${isKibanaRequest ? 'yes' : 'no'}`,
|
||||
`createNewCopiesEnabled.${createNewCopies ? 'yes' : 'no'}`,
|
||||
`overwriteEnabled.${overwrite ? 'yes' : 'no'}`,
|
||||
];
|
||||
await this.updateUsageStats(counterFieldNames, IMPORT_STATS_PREFIX);
|
||||
}
|
||||
|
||||
public async incrementSavedObjectsResolveImportErrors({
|
||||
headers,
|
||||
createNewCopies,
|
||||
}: IncrementSavedObjectsResolveImportErrorsOptions) {
|
||||
const isKibanaRequest = getIsKibanaRequest(headers);
|
||||
const counterFieldNames = [
|
||||
'total',
|
||||
`kibanaRequest.${isKibanaRequest ? 'yes' : 'no'}`,
|
||||
`createNewCopiesEnabled.${createNewCopies ? 'yes' : 'no'}`,
|
||||
];
|
||||
await this.updateUsageStats(counterFieldNames, RESOLVE_IMPORT_STATS_PREFIX);
|
||||
}
|
||||
|
||||
public async incrementSavedObjectsExport({
|
||||
headers,
|
||||
types,
|
||||
supportedTypes,
|
||||
}: IncrementSavedObjectsExportOptions) {
|
||||
const isKibanaRequest = getIsKibanaRequest(headers);
|
||||
const isAllTypesSelected = !!types && supportedTypes.every((x) => types.includes(x));
|
||||
const counterFieldNames = [
|
||||
'total',
|
||||
`kibanaRequest.${isKibanaRequest ? 'yes' : 'no'}`,
|
||||
`allTypesSelected.${isAllTypesSelected ? 'yes' : 'no'}`,
|
||||
];
|
||||
await this.updateUsageStats(counterFieldNames, EXPORT_STATS_PREFIX);
|
||||
}
|
||||
|
||||
private async updateUsageStats(counterFieldNames: string[], prefix: string) {
|
||||
const options = { refresh: false };
|
||||
try {
|
||||
const repository = await this.repositoryPromise;
|
||||
await repository.incrementCounter(
|
||||
CORE_USAGE_STATS_TYPE,
|
||||
CORE_USAGE_STATS_ID,
|
||||
counterFieldNames.map((x) => `${prefix}.${x}`),
|
||||
options
|
||||
);
|
||||
} catch (err) {
|
||||
// do nothing
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getIsKibanaRequest(headers?: Headers) {
|
||||
// The presence of these three request headers gives us a good indication that this is a first-party request from the Kibana client.
|
||||
// We can't be 100% certain, but this is a reasonable attempt.
|
||||
return headers && headers['kbn-version'] && headers.origin && headers.referer;
|
||||
}
|
|
@ -16,16 +16,24 @@
|
|||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
export { CoreUsageDataStart } from './types';
|
||||
export { CoreUsageDataSetup, CoreUsageDataStart } from './types';
|
||||
export { CoreUsageDataService } from './core_usage_data_service';
|
||||
export { CoreUsageStatsClient } from './core_usage_stats_client';
|
||||
|
||||
// Because of #79265 we need to explicity import, then export these types for
|
||||
// scripts/telemetry_check.js to work as expected
|
||||
import {
|
||||
CoreUsageStats,
|
||||
CoreUsageData,
|
||||
CoreConfigUsageData,
|
||||
CoreEnvironmentUsageData,
|
||||
CoreServicesUsageData,
|
||||
} from './types';
|
||||
|
||||
export { CoreUsageData, CoreConfigUsageData, CoreEnvironmentUsageData, CoreServicesUsageData };
|
||||
export {
|
||||
CoreUsageStats,
|
||||
CoreUsageData,
|
||||
CoreConfigUsageData,
|
||||
CoreEnvironmentUsageData,
|
||||
CoreServicesUsageData,
|
||||
};
|
||||
|
|
|
@ -17,11 +17,40 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import { CoreUsageStatsClient } from './core_usage_stats_client';
|
||||
import { ISavedObjectTypeRegistry, SavedObjectTypeRegistry } from '..';
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* CoreUsageStats are collected over time while Kibana is running. This is related to CoreUsageData, which is a superset of this that also
|
||||
* includes point-in-time configuration information.
|
||||
* */
|
||||
export interface CoreUsageStats {
|
||||
'apiCalls.savedObjectsImport.total'?: number;
|
||||
'apiCalls.savedObjectsImport.kibanaRequest.yes'?: number;
|
||||
'apiCalls.savedObjectsImport.kibanaRequest.no'?: number;
|
||||
'apiCalls.savedObjectsImport.createNewCopiesEnabled.yes'?: number;
|
||||
'apiCalls.savedObjectsImport.createNewCopiesEnabled.no'?: number;
|
||||
'apiCalls.savedObjectsImport.overwriteEnabled.yes'?: number;
|
||||
'apiCalls.savedObjectsImport.overwriteEnabled.no'?: number;
|
||||
'apiCalls.savedObjectsResolveImportErrors.total'?: number;
|
||||
'apiCalls.savedObjectsResolveImportErrors.kibanaRequest.yes'?: number;
|
||||
'apiCalls.savedObjectsResolveImportErrors.kibanaRequest.no'?: number;
|
||||
'apiCalls.savedObjectsResolveImportErrors.createNewCopiesEnabled.yes'?: number;
|
||||
'apiCalls.savedObjectsResolveImportErrors.createNewCopiesEnabled.no'?: number;
|
||||
'apiCalls.savedObjectsExport.total'?: number;
|
||||
'apiCalls.savedObjectsExport.kibanaRequest.yes'?: number;
|
||||
'apiCalls.savedObjectsExport.kibanaRequest.no'?: number;
|
||||
'apiCalls.savedObjectsExport.allTypesSelected.yes'?: number;
|
||||
'apiCalls.savedObjectsExport.allTypesSelected.no'?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Type describing Core's usage data payload
|
||||
* @internal
|
||||
*/
|
||||
export interface CoreUsageData {
|
||||
export interface CoreUsageData extends CoreUsageStats {
|
||||
config: CoreConfigUsageData;
|
||||
services: CoreServicesUsageData;
|
||||
environment: CoreEnvironmentUsageData;
|
||||
|
@ -141,6 +170,14 @@ export interface CoreConfigUsageData {
|
|||
// };
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export interface CoreUsageDataSetup {
|
||||
registerType(
|
||||
typeRegistry: ISavedObjectTypeRegistry & Pick<SavedObjectTypeRegistry, 'registerType'>
|
||||
): void;
|
||||
getClient(): CoreUsageStatsClient;
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal API for getting Core's usage data payload.
|
||||
*
|
||||
|
|
|
@ -69,13 +69,20 @@ import { I18nServiceSetup } from './i18n';
|
|||
// Because of #79265 we need to explicity import, then export these types for
|
||||
// scripts/telemetry_check.js to work as expected
|
||||
import {
|
||||
CoreUsageStats,
|
||||
CoreUsageData,
|
||||
CoreConfigUsageData,
|
||||
CoreEnvironmentUsageData,
|
||||
CoreServicesUsageData,
|
||||
} from './core_usage_data';
|
||||
|
||||
export { CoreUsageData, CoreConfigUsageData, CoreEnvironmentUsageData, CoreServicesUsageData };
|
||||
export {
|
||||
CoreUsageStats,
|
||||
CoreUsageData,
|
||||
CoreConfigUsageData,
|
||||
CoreEnvironmentUsageData,
|
||||
CoreServicesUsageData,
|
||||
};
|
||||
|
||||
export { bootstrap } from './bootstrap';
|
||||
export { Capabilities, CapabilitiesProvider, CapabilitiesSwitcher } from './capabilities';
|
||||
|
|
|
@ -22,11 +22,20 @@ import stringify from 'json-stable-stringify';
|
|||
import { createPromiseFromStreams, createMapStream, createConcatStream } from '@kbn/utils';
|
||||
|
||||
import { IRouter } from '../../http';
|
||||
import { CoreUsageDataSetup } from '../../core_usage_data';
|
||||
import { SavedObjectConfig } from '../saved_objects_config';
|
||||
import { exportSavedObjectsToStream } from '../export';
|
||||
import { validateTypes, validateObjects } from './utils';
|
||||
|
||||
export const registerExportRoute = (router: IRouter, config: SavedObjectConfig) => {
|
||||
interface RouteDependencies {
|
||||
config: SavedObjectConfig;
|
||||
coreUsageData: CoreUsageDataSetup;
|
||||
}
|
||||
|
||||
export const registerExportRoute = (
|
||||
router: IRouter,
|
||||
{ config, coreUsageData }: RouteDependencies
|
||||
) => {
|
||||
const { maxImportExportSize } = config;
|
||||
|
||||
const referenceSchema = schema.object({
|
||||
|
@ -95,6 +104,12 @@ export const registerExportRoute = (router: IRouter, config: SavedObjectConfig)
|
|||
}
|
||||
}
|
||||
|
||||
const { headers } = req;
|
||||
const usageStatsClient = coreUsageData.getClient();
|
||||
usageStatsClient
|
||||
.incrementSavedObjectsExport({ headers, types, supportedTypes })
|
||||
.catch(() => {});
|
||||
|
||||
const exportStream = await exportSavedObjectsToStream({
|
||||
savedObjectsClient,
|
||||
types,
|
||||
|
|
|
@ -21,17 +21,26 @@ import { Readable } from 'stream';
|
|||
import { extname } from 'path';
|
||||
import { schema } from '@kbn/config-schema';
|
||||
import { IRouter } from '../../http';
|
||||
import { CoreUsageDataSetup } from '../../core_usage_data';
|
||||
import { importSavedObjectsFromStream } from '../import';
|
||||
import { SavedObjectConfig } from '../saved_objects_config';
|
||||
import { createSavedObjectsStreamFromNdJson } from './utils';
|
||||
|
||||
interface RouteDependencies {
|
||||
config: SavedObjectConfig;
|
||||
coreUsageData: CoreUsageDataSetup;
|
||||
}
|
||||
|
||||
interface FileStream extends Readable {
|
||||
hapi: {
|
||||
filename: string;
|
||||
};
|
||||
}
|
||||
|
||||
export const registerImportRoute = (router: IRouter, config: SavedObjectConfig) => {
|
||||
export const registerImportRoute = (
|
||||
router: IRouter,
|
||||
{ config, coreUsageData }: RouteDependencies
|
||||
) => {
|
||||
const { maxImportExportSize, maxImportPayloadBytes } = config;
|
||||
|
||||
router.post(
|
||||
|
@ -65,6 +74,13 @@ export const registerImportRoute = (router: IRouter, config: SavedObjectConfig)
|
|||
},
|
||||
router.handleLegacyErrors(async (context, req, res) => {
|
||||
const { overwrite, createNewCopies } = req.query;
|
||||
|
||||
const { headers } = req;
|
||||
const usageStatsClient = coreUsageData.getClient();
|
||||
usageStatsClient
|
||||
.incrementSavedObjectsImport({ headers, createNewCopies, overwrite })
|
||||
.catch(() => {});
|
||||
|
||||
const file = req.body.file as FileStream;
|
||||
const fileExtension = extname(file.hapi.filename).toLowerCase();
|
||||
if (fileExtension !== '.ndjson') {
|
||||
|
|
|
@ -18,6 +18,7 @@
|
|||
*/
|
||||
|
||||
import { InternalHttpServiceSetup } from '../../http';
|
||||
import { CoreUsageDataSetup } from '../../core_usage_data';
|
||||
import { Logger } from '../../logging';
|
||||
import { SavedObjectConfig } from '../saved_objects_config';
|
||||
import { IKibanaMigrator } from '../migrations';
|
||||
|
@ -37,11 +38,13 @@ import { registerMigrateRoute } from './migrate';
|
|||
|
||||
export function registerRoutes({
|
||||
http,
|
||||
coreUsageData,
|
||||
logger,
|
||||
config,
|
||||
migratorPromise,
|
||||
}: {
|
||||
http: InternalHttpServiceSetup;
|
||||
coreUsageData: CoreUsageDataSetup;
|
||||
logger: Logger;
|
||||
config: SavedObjectConfig;
|
||||
migratorPromise: Promise<IKibanaMigrator>;
|
||||
|
@ -57,9 +60,9 @@ export function registerRoutes({
|
|||
registerBulkCreateRoute(router);
|
||||
registerBulkUpdateRoute(router);
|
||||
registerLogLegacyImportRoute(router, logger);
|
||||
registerExportRoute(router, config);
|
||||
registerImportRoute(router, config);
|
||||
registerResolveImportErrorsRoute(router, config);
|
||||
registerExportRoute(router, { config, coreUsageData });
|
||||
registerImportRoute(router, { config, coreUsageData });
|
||||
registerResolveImportErrorsRoute(router, { config, coreUsageData });
|
||||
|
||||
const internalRouter = http.createRouter('/internal/saved_objects/');
|
||||
|
||||
|
|
|
@ -25,6 +25,9 @@ import * as exportMock from '../../export';
|
|||
import supertest from 'supertest';
|
||||
import type { UnwrapPromise } from '@kbn/utility-types';
|
||||
import { createListStream } from '@kbn/utils';
|
||||
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 { SavedObjectConfig } from '../../saved_objects_config';
|
||||
import { registerExportRoute } from '../export';
|
||||
import { setupServer, createExportableType } from '../test_utils';
|
||||
|
@ -36,6 +39,7 @@ const config = {
|
|||
maxImportPayloadBytes: 26214400,
|
||||
maxImportExportSize: 10000,
|
||||
} as SavedObjectConfig;
|
||||
let coreUsageStatsClient: jest.Mocked<CoreUsageStatsClient>;
|
||||
|
||||
describe('POST /api/saved_objects/_export', () => {
|
||||
let server: SetupServerReturn['server'];
|
||||
|
@ -49,7 +53,10 @@ describe('POST /api/saved_objects/_export', () => {
|
|||
);
|
||||
|
||||
const router = httpSetup.createRouter('/api/saved_objects/');
|
||||
registerExportRoute(router, config);
|
||||
coreUsageStatsClient = coreUsageStatsClientMock.create();
|
||||
coreUsageStatsClient.incrementSavedObjectsExport.mockRejectedValue(new Error('Oh no!')); // this error is intentionally swallowed so the export does not fail
|
||||
const coreUsageData = coreUsageDataServiceMock.createSetupContract(coreUsageStatsClient);
|
||||
registerExportRoute(router, { config, coreUsageData });
|
||||
|
||||
await server.start();
|
||||
});
|
||||
|
@ -59,7 +66,7 @@ describe('POST /api/saved_objects/_export', () => {
|
|||
await server.stop();
|
||||
});
|
||||
|
||||
it('formats successful response', async () => {
|
||||
it('formats successful response and records usage stats', async () => {
|
||||
const sortedObjects = [
|
||||
{
|
||||
id: '1',
|
||||
|
@ -110,5 +117,10 @@ describe('POST /api/saved_objects/_export', () => {
|
|||
types: ['search'],
|
||||
})
|
||||
);
|
||||
expect(coreUsageStatsClient.incrementSavedObjectsExport).toHaveBeenCalledWith({
|
||||
headers: expect.anything(),
|
||||
types: ['search'],
|
||||
supportedTypes: ['index-pattern', 'search'],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -22,6 +22,9 @@ import supertest from 'supertest';
|
|||
import { UnwrapPromise } from '@kbn/utility-types';
|
||||
import { registerImportRoute } from '../import';
|
||||
import { savedObjectsClientMock } from '../../../../../core/server/mocks';
|
||||
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 { SavedObjectConfig } from '../../saved_objects_config';
|
||||
import { setupServer, createExportableType } from '../test_utils';
|
||||
import { SavedObjectsErrorHelpers } from '../..';
|
||||
|
@ -31,6 +34,7 @@ type SetupServerReturn = UnwrapPromise<ReturnType<typeof setupServer>>;
|
|||
const { v4: uuidv4 } = jest.requireActual('uuid');
|
||||
const allowedTypes = ['index-pattern', 'visualization', 'dashboard'];
|
||||
const config = { maxImportPayloadBytes: 26214400, maxImportExportSize: 10000 } as SavedObjectConfig;
|
||||
let coreUsageStatsClient: jest.Mocked<CoreUsageStatsClient>;
|
||||
const URL = '/internal/saved_objects/_import';
|
||||
|
||||
describe(`POST ${URL}`, () => {
|
||||
|
@ -71,7 +75,10 @@ describe(`POST ${URL}`, () => {
|
|||
savedObjectsClient.checkConflicts.mockResolvedValue({ errors: [] });
|
||||
|
||||
const router = httpSetup.createRouter('/internal/saved_objects/');
|
||||
registerImportRoute(router, config);
|
||||
coreUsageStatsClient = coreUsageStatsClientMock.create();
|
||||
coreUsageStatsClient.incrementSavedObjectsImport.mockRejectedValue(new Error('Oh no!')); // this error is intentionally swallowed so the import does not fail
|
||||
const coreUsageData = coreUsageDataServiceMock.createSetupContract(coreUsageStatsClient);
|
||||
registerImportRoute(router, { config, coreUsageData });
|
||||
|
||||
await server.start();
|
||||
});
|
||||
|
@ -80,7 +87,7 @@ describe(`POST ${URL}`, () => {
|
|||
await server.stop();
|
||||
});
|
||||
|
||||
it('formats successful response', async () => {
|
||||
it('formats successful response and records usage stats', async () => {
|
||||
const result = await supertest(httpSetup.server.listener)
|
||||
.post(URL)
|
||||
.set('content-Type', 'multipart/form-data; boundary=BOUNDARY')
|
||||
|
@ -98,6 +105,11 @@ describe(`POST ${URL}`, () => {
|
|||
|
||||
expect(result.body).toEqual({ success: true, successCount: 0 });
|
||||
expect(savedObjectsClient.bulkCreate).not.toHaveBeenCalled(); // no objects were created
|
||||
expect(coreUsageStatsClient.incrementSavedObjectsImport).toHaveBeenCalledWith({
|
||||
headers: expect.anything(),
|
||||
createNewCopies: false,
|
||||
overwrite: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('defaults migrationVersion to empty object', async () => {
|
||||
|
|
|
@ -22,6 +22,9 @@ import supertest from 'supertest';
|
|||
import { UnwrapPromise } from '@kbn/utility-types';
|
||||
import { registerResolveImportErrorsRoute } from '../resolve_import_errors';
|
||||
import { savedObjectsClientMock } from '../../../../../core/server/mocks';
|
||||
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 { setupServer, createExportableType } from '../test_utils';
|
||||
import { SavedObjectConfig } from '../../saved_objects_config';
|
||||
|
||||
|
@ -30,6 +33,7 @@ type SetupServerReturn = UnwrapPromise<ReturnType<typeof setupServer>>;
|
|||
const { v4: uuidv4 } = jest.requireActual('uuid');
|
||||
const allowedTypes = ['index-pattern', 'visualization', 'dashboard'];
|
||||
const config = { maxImportPayloadBytes: 26214400, maxImportExportSize: 10000 } as SavedObjectConfig;
|
||||
let coreUsageStatsClient: jest.Mocked<CoreUsageStatsClient>;
|
||||
const URL = '/api/saved_objects/_resolve_import_errors';
|
||||
|
||||
describe(`POST ${URL}`, () => {
|
||||
|
@ -76,7 +80,12 @@ describe(`POST ${URL}`, () => {
|
|||
savedObjectsClient.checkConflicts.mockResolvedValue({ errors: [] });
|
||||
|
||||
const router = httpSetup.createRouter('/api/saved_objects/');
|
||||
registerResolveImportErrorsRoute(router, config);
|
||||
coreUsageStatsClient = coreUsageStatsClientMock.create();
|
||||
coreUsageStatsClient.incrementSavedObjectsResolveImportErrors.mockRejectedValue(
|
||||
new Error('Oh no!') // this error is intentionally swallowed so the export does not fail
|
||||
);
|
||||
const coreUsageData = coreUsageDataServiceMock.createSetupContract(coreUsageStatsClient);
|
||||
registerResolveImportErrorsRoute(router, { config, coreUsageData });
|
||||
|
||||
await server.start();
|
||||
});
|
||||
|
@ -85,7 +94,7 @@ describe(`POST ${URL}`, () => {
|
|||
await server.stop();
|
||||
});
|
||||
|
||||
it('formats successful response', async () => {
|
||||
it('formats successful response and records usage stats', async () => {
|
||||
const result = await supertest(httpSetup.server.listener)
|
||||
.post(URL)
|
||||
.set('content-Type', 'multipart/form-data; boundary=BOUNDARY')
|
||||
|
@ -107,6 +116,10 @@ describe(`POST ${URL}`, () => {
|
|||
|
||||
expect(result.body).toEqual({ success: true, successCount: 0 });
|
||||
expect(savedObjectsClient.bulkCreate).not.toHaveBeenCalled(); // no objects were created
|
||||
expect(coreUsageStatsClient.incrementSavedObjectsResolveImportErrors).toHaveBeenCalledWith({
|
||||
headers: expect.anything(),
|
||||
createNewCopies: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('defaults migrationVersion to empty object', async () => {
|
||||
|
|
|
@ -21,17 +21,26 @@ import { extname } from 'path';
|
|||
import { Readable } from 'stream';
|
||||
import { schema } from '@kbn/config-schema';
|
||||
import { IRouter } from '../../http';
|
||||
import { CoreUsageDataSetup } from '../../core_usage_data';
|
||||
import { resolveSavedObjectsImportErrors } from '../import';
|
||||
import { SavedObjectConfig } from '../saved_objects_config';
|
||||
import { createSavedObjectsStreamFromNdJson } from './utils';
|
||||
|
||||
interface RouteDependencies {
|
||||
config: SavedObjectConfig;
|
||||
coreUsageData: CoreUsageDataSetup;
|
||||
}
|
||||
|
||||
interface FileStream extends Readable {
|
||||
hapi: {
|
||||
filename: string;
|
||||
};
|
||||
}
|
||||
|
||||
export const registerResolveImportErrorsRoute = (router: IRouter, config: SavedObjectConfig) => {
|
||||
export const registerResolveImportErrorsRoute = (
|
||||
router: IRouter,
|
||||
{ config, coreUsageData }: RouteDependencies
|
||||
) => {
|
||||
const { maxImportExportSize, maxImportPayloadBytes } = config;
|
||||
|
||||
router.post(
|
||||
|
@ -72,6 +81,14 @@ export const registerResolveImportErrorsRoute = (router: IRouter, config: SavedO
|
|||
},
|
||||
},
|
||||
router.handleLegacyErrors(async (context, req, res) => {
|
||||
const { createNewCopies } = req.query;
|
||||
|
||||
const { headers } = req;
|
||||
const usageStatsClient = coreUsageData.getClient();
|
||||
usageStatsClient
|
||||
.incrementSavedObjectsResolveImportErrors({ headers, createNewCopies })
|
||||
.catch(() => {});
|
||||
|
||||
const file = req.body.file as FileStream;
|
||||
const fileExtension = extname(file.hapi.filename).toLowerCase();
|
||||
if (fileExtension !== '.ndjson') {
|
||||
|
@ -93,7 +110,7 @@ export const registerResolveImportErrorsRoute = (router: IRouter, config: SavedO
|
|||
readStream,
|
||||
retries: req.body.retries,
|
||||
objectLimit: maxImportExportSize,
|
||||
createNewCopies: req.query.createNewCopies,
|
||||
createNewCopies,
|
||||
});
|
||||
|
||||
return res.ok({ body: result });
|
||||
|
|
|
@ -33,6 +33,7 @@ import { Env } from '../config';
|
|||
import { configServiceMock } from '../mocks';
|
||||
import { elasticsearchServiceMock } from '../elasticsearch/elasticsearch_service.mock';
|
||||
import { elasticsearchClientMock } from '../elasticsearch/client/mocks';
|
||||
import { coreUsageDataServiceMock } from '../core_usage_data/core_usage_data_service.mock';
|
||||
import { httpServiceMock } from '../http/http_service.mock';
|
||||
import { httpServerMock } from '../http/http_server.mocks';
|
||||
import { SavedObjectsClientFactoryProvider } from './service/lib';
|
||||
|
@ -64,6 +65,7 @@ describe('SavedObjectsService', () => {
|
|||
return {
|
||||
http: httpServiceMock.createInternalSetupContract(),
|
||||
elasticsearch: elasticsearchMock,
|
||||
coreUsageData: coreUsageDataServiceMock.createSetupContract(),
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -27,6 +27,7 @@ import {
|
|||
} from './';
|
||||
import { KibanaMigrator, IKibanaMigrator } from './migrations';
|
||||
import { CoreContext } from '../core_context';
|
||||
import { CoreUsageDataSetup } from '../core_usage_data';
|
||||
import {
|
||||
ElasticsearchClient,
|
||||
IClusterClient,
|
||||
|
@ -253,6 +254,7 @@ export interface SavedObjectsRepositoryFactory {
|
|||
export interface SavedObjectsSetupDeps {
|
||||
http: InternalHttpServiceSetup;
|
||||
elasticsearch: InternalElasticsearchServiceSetup;
|
||||
coreUsageData: CoreUsageDataSetup;
|
||||
}
|
||||
|
||||
interface WrappedClientFactoryWrapper {
|
||||
|
@ -288,6 +290,7 @@ export class SavedObjectsService
|
|||
this.logger.debug('Setting up SavedObjects service');
|
||||
|
||||
this.setupDeps = setupDeps;
|
||||
const { http, elasticsearch, coreUsageData } = setupDeps;
|
||||
|
||||
const savedObjectsConfig = await this.coreContext.configService
|
||||
.atPath<SavedObjectsConfigType>('savedObjects')
|
||||
|
@ -299,8 +302,11 @@ export class SavedObjectsService
|
|||
.toPromise();
|
||||
this.config = new SavedObjectConfig(savedObjectsConfig, savedObjectsMigrationConfig);
|
||||
|
||||
coreUsageData.registerType(this.typeRegistry);
|
||||
|
||||
registerRoutes({
|
||||
http: setupDeps.http,
|
||||
http,
|
||||
coreUsageData,
|
||||
logger: this.logger,
|
||||
config: this.config,
|
||||
migratorPromise: this.migrator$.pipe(first()).toPromise(),
|
||||
|
@ -309,7 +315,7 @@ export class SavedObjectsService
|
|||
return {
|
||||
status$: calculateStatus$(
|
||||
this.migrator$.pipe(switchMap((migrator) => migrator.getStatus$())),
|
||||
setupDeps.elasticsearch.status$
|
||||
elasticsearch.status$
|
||||
),
|
||||
setClientFactoryProvider: (provider) => {
|
||||
if (this.started) {
|
||||
|
|
|
@ -1562,12 +1562,12 @@ export class SavedObjectsRepository {
|
|||
* @param options - {@link SavedObjectsIncrementCounterOptions}
|
||||
* @returns The saved object after the specified fields were incremented
|
||||
*/
|
||||
async incrementCounter(
|
||||
async incrementCounter<T = unknown>(
|
||||
type: string,
|
||||
id: string,
|
||||
counterFieldNames: string[],
|
||||
options: SavedObjectsIncrementCounterOptions = {}
|
||||
): Promise<SavedObject> {
|
||||
): Promise<SavedObject<T>> {
|
||||
if (typeof type !== 'string') {
|
||||
throw new Error('"type" argument must be a string');
|
||||
}
|
||||
|
|
|
@ -521,7 +521,7 @@ export interface CoreStatus {
|
|||
}
|
||||
|
||||
// @internal
|
||||
export interface CoreUsageData {
|
||||
export interface CoreUsageData extends CoreUsageStats {
|
||||
// (undocumented)
|
||||
config: CoreConfigUsageData;
|
||||
// (undocumented)
|
||||
|
@ -535,6 +535,44 @@ export interface CoreUsageDataStart {
|
|||
getCoreUsageData(): Promise<CoreUsageData>;
|
||||
}
|
||||
|
||||
// @internal
|
||||
export interface CoreUsageStats {
|
||||
// (undocumented)
|
||||
'apiCalls.savedObjectsExport.allTypesSelected.no'?: number;
|
||||
// (undocumented)
|
||||
'apiCalls.savedObjectsExport.allTypesSelected.yes'?: number;
|
||||
// (undocumented)
|
||||
'apiCalls.savedObjectsExport.kibanaRequest.no'?: number;
|
||||
// (undocumented)
|
||||
'apiCalls.savedObjectsExport.kibanaRequest.yes'?: number;
|
||||
// (undocumented)
|
||||
'apiCalls.savedObjectsExport.total'?: number;
|
||||
// (undocumented)
|
||||
'apiCalls.savedObjectsImport.createNewCopiesEnabled.no'?: number;
|
||||
// (undocumented)
|
||||
'apiCalls.savedObjectsImport.createNewCopiesEnabled.yes'?: number;
|
||||
// (undocumented)
|
||||
'apiCalls.savedObjectsImport.kibanaRequest.no'?: number;
|
||||
// (undocumented)
|
||||
'apiCalls.savedObjectsImport.kibanaRequest.yes'?: number;
|
||||
// (undocumented)
|
||||
'apiCalls.savedObjectsImport.overwriteEnabled.no'?: number;
|
||||
// (undocumented)
|
||||
'apiCalls.savedObjectsImport.overwriteEnabled.yes'?: number;
|
||||
// (undocumented)
|
||||
'apiCalls.savedObjectsImport.total'?: number;
|
||||
// (undocumented)
|
||||
'apiCalls.savedObjectsResolveImportErrors.createNewCopiesEnabled.no'?: number;
|
||||
// (undocumented)
|
||||
'apiCalls.savedObjectsResolveImportErrors.createNewCopiesEnabled.yes'?: number;
|
||||
// (undocumented)
|
||||
'apiCalls.savedObjectsResolveImportErrors.kibanaRequest.no'?: number;
|
||||
// (undocumented)
|
||||
'apiCalls.savedObjectsResolveImportErrors.kibanaRequest.yes'?: number;
|
||||
// (undocumented)
|
||||
'apiCalls.savedObjectsResolveImportErrors.total'?: number;
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
export interface CountResponse {
|
||||
// (undocumented)
|
||||
|
@ -2448,7 +2486,7 @@ export class SavedObjectsRepository {
|
|||
// (undocumented)
|
||||
find<T = unknown>(options: SavedObjectsFindOptions): Promise<SavedObjectsFindResponse<T>>;
|
||||
get<T = unknown>(type: string, id: string, options?: SavedObjectsBaseOptions): Promise<SavedObject<T>>;
|
||||
incrementCounter(type: string, id: string, counterFieldNames: string[], options?: SavedObjectsIncrementCounterOptions): Promise<SavedObject>;
|
||||
incrementCounter<T = unknown>(type: string, id: string, counterFieldNames: string[], options?: SavedObjectsIncrementCounterOptions): Promise<SavedObject<T>>;
|
||||
removeReferencesTo(type: string, id: string, options?: SavedObjectsRemoveReferencesToOptions): Promise<SavedObjectsRemoveReferencesToResponse>;
|
||||
update<T = unknown>(type: string, id: string, attributes: Partial<T>, options?: SavedObjectsUpdateOptions): Promise<SavedObjectsUpdateResponse<T>>;
|
||||
}
|
||||
|
|
|
@ -185,6 +185,7 @@ test(`doesn't setup core services if config validation fails`, async () => {
|
|||
expect(mockElasticsearchService.setup).not.toHaveBeenCalled();
|
||||
expect(mockPluginsService.setup).not.toHaveBeenCalled();
|
||||
expect(mockLegacyService.setup).not.toHaveBeenCalled();
|
||||
expect(mockSavedObjectsService.stop).not.toHaveBeenCalled();
|
||||
expect(mockUiSettingsService.setup).not.toHaveBeenCalled();
|
||||
expect(mockRenderingService.setup).not.toHaveBeenCalled();
|
||||
expect(mockMetricsService.setup).not.toHaveBeenCalled();
|
||||
|
|
|
@ -31,7 +31,7 @@ import { LegacyService, ensureValidConfiguration } from './legacy';
|
|||
import { Logger, LoggerFactory, LoggingService, ILoggingSystem } from './logging';
|
||||
import { UiSettingsService } from './ui_settings';
|
||||
import { PluginsService, config as pluginsConfig } from './plugins';
|
||||
import { SavedObjectsService } from './saved_objects';
|
||||
import { SavedObjectsService, SavedObjectsServiceStart } from './saved_objects';
|
||||
import { MetricsService, opsConfig } from './metrics';
|
||||
import { CapabilitiesService } from './capabilities';
|
||||
import { EnvironmentService, config as pidConfig } from './environment';
|
||||
|
@ -78,6 +78,9 @@ export class Server {
|
|||
private readonly coreUsageData: CoreUsageDataService;
|
||||
private readonly i18n: I18nService;
|
||||
|
||||
private readonly savedObjectsStartPromise: Promise<SavedObjectsServiceStart>;
|
||||
private resolveSavedObjectsStartPromise?: (value: SavedObjectsServiceStart) => void;
|
||||
|
||||
#pluginsInitialized?: boolean;
|
||||
private coreStart?: InternalCoreStart;
|
||||
private readonly logger: LoggerFactory;
|
||||
|
@ -109,6 +112,10 @@ export class Server {
|
|||
this.logging = new LoggingService(core);
|
||||
this.coreUsageData = new CoreUsageDataService(core);
|
||||
this.i18n = new I18nService(core);
|
||||
|
||||
this.savedObjectsStartPromise = new Promise((resolve) => {
|
||||
this.resolveSavedObjectsStartPromise = resolve;
|
||||
});
|
||||
}
|
||||
|
||||
public async setup() {
|
||||
|
@ -155,9 +162,17 @@ export class Server {
|
|||
http: httpSetup,
|
||||
});
|
||||
|
||||
const metricsSetup = await this.metrics.setup({ http: httpSetup });
|
||||
|
||||
const coreUsageDataSetup = this.coreUsageData.setup({
|
||||
metrics: metricsSetup,
|
||||
savedObjectsStartPromise: this.savedObjectsStartPromise,
|
||||
});
|
||||
|
||||
const savedObjectsSetup = await this.savedObjects.setup({
|
||||
http: httpSetup,
|
||||
elasticsearch: elasticsearchServiceSetup,
|
||||
coreUsageData: coreUsageDataSetup,
|
||||
});
|
||||
|
||||
const uiSettingsSetup = await this.uiSettings.setup({
|
||||
|
@ -165,8 +180,6 @@ export class Server {
|
|||
savedObjects: savedObjectsSetup,
|
||||
});
|
||||
|
||||
const metricsSetup = await this.metrics.setup({ http: httpSetup });
|
||||
|
||||
const statusSetup = await this.status.setup({
|
||||
elasticsearch: elasticsearchServiceSetup,
|
||||
pluginDependencies: pluginTree.asNames,
|
||||
|
@ -191,8 +204,6 @@ export class Server {
|
|||
loggingSystem: this.loggingSystem,
|
||||
});
|
||||
|
||||
this.coreUsageData.setup({ metrics: metricsSetup });
|
||||
|
||||
const coreSetup: InternalCoreSetup = {
|
||||
capabilities: capabilitiesSetup,
|
||||
context: contextServiceSetup,
|
||||
|
@ -235,6 +246,8 @@ export class Server {
|
|||
elasticsearch: elasticsearchStart,
|
||||
pluginsInitialized: this.#pluginsInitialized,
|
||||
});
|
||||
await this.resolveSavedObjectsStartPromise!(savedObjectsStart);
|
||||
|
||||
soStartSpan?.end();
|
||||
const capabilitiesStart = this.capabilities.start();
|
||||
const uiSettingsStart = await this.uiSettings.start();
|
||||
|
|
|
@ -115,6 +115,23 @@ export function getCoreUsageCollector(
|
|||
},
|
||||
},
|
||||
},
|
||||
'apiCalls.savedObjectsImport.total': { type: 'long' },
|
||||
'apiCalls.savedObjectsImport.kibanaRequest.yes': { type: 'long' },
|
||||
'apiCalls.savedObjectsImport.kibanaRequest.no': { type: 'long' },
|
||||
'apiCalls.savedObjectsImport.createNewCopiesEnabled.yes': { type: 'long' },
|
||||
'apiCalls.savedObjectsImport.createNewCopiesEnabled.no': { type: 'long' },
|
||||
'apiCalls.savedObjectsImport.overwriteEnabled.yes': { type: 'long' },
|
||||
'apiCalls.savedObjectsImport.overwriteEnabled.no': { type: 'long' },
|
||||
'apiCalls.savedObjectsResolveImportErrors.total': { type: 'long' },
|
||||
'apiCalls.savedObjectsResolveImportErrors.kibanaRequest.yes': { type: 'long' },
|
||||
'apiCalls.savedObjectsResolveImportErrors.kibanaRequest.no': { type: 'long' },
|
||||
'apiCalls.savedObjectsResolveImportErrors.createNewCopiesEnabled.yes': { type: 'long' },
|
||||
'apiCalls.savedObjectsResolveImportErrors.createNewCopiesEnabled.no': { type: 'long' },
|
||||
'apiCalls.savedObjectsExport.total': { type: 'long' },
|
||||
'apiCalls.savedObjectsExport.kibanaRequest.yes': { type: 'long' },
|
||||
'apiCalls.savedObjectsExport.kibanaRequest.no': { type: 'long' },
|
||||
'apiCalls.savedObjectsExport.allTypesSelected.yes': { type: 'long' },
|
||||
'apiCalls.savedObjectsExport.allTypesSelected.no': { type: 'long' },
|
||||
},
|
||||
fetch() {
|
||||
return getCoreUsageDataService().getCoreUsageData();
|
||||
|
|
|
@ -570,11 +570,17 @@ exports[`Flyout should render import step 1`] = `
|
|||
hasChildLabel={true}
|
||||
hasEmptyLabelSpace={false}
|
||||
label={
|
||||
<FormattedMessage
|
||||
defaultMessage="Select a file to import"
|
||||
id="savedObjectsManagement.objectsTable.flyout.selectFileToImportFormRowLabel"
|
||||
values={Object {}}
|
||||
/>
|
||||
<EuiTitle
|
||||
size="xs"
|
||||
>
|
||||
<span>
|
||||
<FormattedMessage
|
||||
defaultMessage="Select a file to import"
|
||||
id="savedObjectsManagement.objectsTable.flyout.selectFileToImportFormRowLabel"
|
||||
values={Object {}}
|
||||
/>
|
||||
</span>
|
||||
</EuiTitle>
|
||||
}
|
||||
labelType="label"
|
||||
>
|
||||
|
|
|
@ -758,10 +758,14 @@ export class Flyout extends Component<FlyoutProps, FlyoutState> {
|
|||
<EuiFormRow
|
||||
fullWidth
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="savedObjectsManagement.objectsTable.flyout.selectFileToImportFormRowLabel"
|
||||
defaultMessage="Select a file to import"
|
||||
/>
|
||||
<EuiTitle size="xs">
|
||||
<span>
|
||||
<FormattedMessage
|
||||
id="savedObjectsManagement.objectsTable.flyout.selectFileToImportFormRowLabel"
|
||||
defaultMessage="Select a file to import"
|
||||
/>
|
||||
</span>
|
||||
</EuiTitle>
|
||||
}
|
||||
>
|
||||
<EuiFilePicker
|
||||
|
|
|
@ -120,7 +120,7 @@ export const ImportModeControl = ({
|
|||
options={[overwriteEnabled, overwriteDisabled]}
|
||||
idSelected={overwrite ? overwriteEnabled.id : overwriteDisabled.id}
|
||||
onChange={(id: string) => onChange({ overwrite: id === overwriteEnabled.id })}
|
||||
disabled={createNewCopies}
|
||||
disabled={createNewCopies && !isLegacyFile}
|
||||
data-test-subj={'savedObjectsManagement-importModeControl-overwriteRadioGroup'}
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -1516,6 +1516,57 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"apiCalls.savedObjectsImport.total": {
|
||||
"type": "long"
|
||||
},
|
||||
"apiCalls.savedObjectsImport.kibanaRequest.yes": {
|
||||
"type": "long"
|
||||
},
|
||||
"apiCalls.savedObjectsImport.kibanaRequest.no": {
|
||||
"type": "long"
|
||||
},
|
||||
"apiCalls.savedObjectsImport.createNewCopiesEnabled.yes": {
|
||||
"type": "long"
|
||||
},
|
||||
"apiCalls.savedObjectsImport.createNewCopiesEnabled.no": {
|
||||
"type": "long"
|
||||
},
|
||||
"apiCalls.savedObjectsImport.overwriteEnabled.yes": {
|
||||
"type": "long"
|
||||
},
|
||||
"apiCalls.savedObjectsImport.overwriteEnabled.no": {
|
||||
"type": "long"
|
||||
},
|
||||
"apiCalls.savedObjectsResolveImportErrors.total": {
|
||||
"type": "long"
|
||||
},
|
||||
"apiCalls.savedObjectsResolveImportErrors.kibanaRequest.yes": {
|
||||
"type": "long"
|
||||
},
|
||||
"apiCalls.savedObjectsResolveImportErrors.kibanaRequest.no": {
|
||||
"type": "long"
|
||||
},
|
||||
"apiCalls.savedObjectsResolveImportErrors.createNewCopiesEnabled.yes": {
|
||||
"type": "long"
|
||||
},
|
||||
"apiCalls.savedObjectsResolveImportErrors.createNewCopiesEnabled.no": {
|
||||
"type": "long"
|
||||
},
|
||||
"apiCalls.savedObjectsExport.total": {
|
||||
"type": "long"
|
||||
},
|
||||
"apiCalls.savedObjectsExport.kibanaRequest.yes": {
|
||||
"type": "long"
|
||||
},
|
||||
"apiCalls.savedObjectsExport.kibanaRequest.no": {
|
||||
"type": "long"
|
||||
},
|
||||
"apiCalls.savedObjectsExport.allTypesSelected.yes": {
|
||||
"type": "long"
|
||||
},
|
||||
"apiCalls.savedObjectsExport.allTypesSelected.no": {
|
||||
"type": "long"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
@ -10,7 +10,7 @@ import { mountWithIntl } from '@kbn/test/jest';
|
|||
import { CopyModeControl, CopyModeControlProps } from './copy_mode_control';
|
||||
|
||||
describe('CopyModeControl', () => {
|
||||
const initialValues = { createNewCopies: false, overwrite: true }; // some test cases below make assumptions based on these initial values
|
||||
const initialValues = { createNewCopies: true, overwrite: true }; // some test cases below make assumptions based on these initial values
|
||||
const updateSelection = jest.fn();
|
||||
|
||||
const getOverwriteRadio = (wrapper: ReactWrapper) =>
|
||||
|
@ -34,21 +34,23 @@ describe('CopyModeControl', () => {
|
|||
const wrapper = mountWithIntl(<CopyModeControl {...props} />);
|
||||
|
||||
expect(updateSelection).not.toHaveBeenCalled();
|
||||
const { createNewCopies } = initialValues;
|
||||
// need to disable `createNewCopies` first
|
||||
getCreateNewCopiesDisabled(wrapper).simulate('change');
|
||||
const createNewCopies = false;
|
||||
|
||||
getOverwriteDisabled(wrapper).simulate('change');
|
||||
expect(updateSelection).toHaveBeenNthCalledWith(1, { createNewCopies, overwrite: false });
|
||||
expect(updateSelection).toHaveBeenNthCalledWith(2, { createNewCopies, overwrite: false });
|
||||
|
||||
getOverwriteEnabled(wrapper).simulate('change');
|
||||
expect(updateSelection).toHaveBeenNthCalledWith(2, { createNewCopies, overwrite: true });
|
||||
expect(updateSelection).toHaveBeenNthCalledWith(3, { createNewCopies, overwrite: true });
|
||||
});
|
||||
|
||||
it('should disable the Overwrite switch when `createNewCopies` is enabled', async () => {
|
||||
it('should enable the Overwrite switch when `createNewCopies` is disabled', async () => {
|
||||
const wrapper = mountWithIntl(<CopyModeControl {...props} />);
|
||||
|
||||
expect(getOverwriteRadio(wrapper).prop('disabled')).toBe(false);
|
||||
getCreateNewCopiesEnabled(wrapper).simulate('change');
|
||||
expect(getOverwriteRadio(wrapper).prop('disabled')).toBe(true);
|
||||
getCreateNewCopiesDisabled(wrapper).simulate('change');
|
||||
expect(getOverwriteRadio(wrapper).prop('disabled')).toBe(false);
|
||||
});
|
||||
|
||||
it('should allow the user to toggle `createNewCopies`', async () => {
|
||||
|
@ -57,10 +59,10 @@ describe('CopyModeControl', () => {
|
|||
expect(updateSelection).not.toHaveBeenCalled();
|
||||
const { overwrite } = initialValues;
|
||||
|
||||
getCreateNewCopiesEnabled(wrapper).simulate('change');
|
||||
expect(updateSelection).toHaveBeenNthCalledWith(1, { createNewCopies: true, overwrite });
|
||||
|
||||
getCreateNewCopiesDisabled(wrapper).simulate('change');
|
||||
expect(updateSelection).toHaveBeenNthCalledWith(2, { createNewCopies: false, overwrite });
|
||||
expect(updateSelection).toHaveBeenNthCalledWith(1, { createNewCopies: false, overwrite });
|
||||
|
||||
getCreateNewCopiesEnabled(wrapper).simulate('change');
|
||||
expect(updateSelection).toHaveBeenNthCalledWith(2, { createNewCopies: true, overwrite });
|
||||
});
|
||||
});
|
||||
|
|
|
@ -126,6 +126,15 @@ export const CopyModeControl = ({ initialValues, updateSelection }: CopyModeCont
|
|||
),
|
||||
}}
|
||||
>
|
||||
<EuiCheckableCard
|
||||
id={createNewCopiesEnabled.id}
|
||||
label={createLabel(createNewCopiesEnabled)}
|
||||
checked={createNewCopies}
|
||||
onChange={() => onChange({ createNewCopies: true })}
|
||||
/>
|
||||
|
||||
<EuiSpacer size="s" />
|
||||
|
||||
<EuiCheckableCard
|
||||
id={createNewCopiesDisabled.id}
|
||||
label={createLabel(createNewCopiesDisabled)}
|
||||
|
@ -140,15 +149,6 @@ export const CopyModeControl = ({ initialValues, updateSelection }: CopyModeCont
|
|||
data-test-subj={'cts-copyModeControl-overwriteRadioGroup'}
|
||||
/>
|
||||
</EuiCheckableCard>
|
||||
|
||||
<EuiSpacer size="s" />
|
||||
|
||||
<EuiCheckableCard
|
||||
id={createNewCopiesEnabled.id}
|
||||
label={createLabel(createNewCopiesEnabled)}
|
||||
checked={createNewCopies}
|
||||
onChange={() => onChange({ createNewCopies: true })}
|
||||
/>
|
||||
</EuiFormFieldset>
|
||||
|
||||
<EuiSpacer size="m" />
|
||||
|
|
|
@ -12,6 +12,7 @@ import { EuiLoadingSpinner, EuiEmptyPrompt } from '@elastic/eui';
|
|||
import { Space } from '../../../common/model/space';
|
||||
import { findTestSubject } from '@kbn/test/jest';
|
||||
import { SelectableSpacesControl } from './selectable_spaces_control';
|
||||
import { CopyModeControl } from './copy_mode_control';
|
||||
import { act } from '@testing-library/react';
|
||||
import { ProcessingCopyToSpace } from './processing_copy_to_space';
|
||||
import { spacesManagerMock } from '../../spaces_manager/mocks';
|
||||
|
@ -289,7 +290,7 @@ describe('CopyToSpaceFlyout', () => {
|
|||
[{ type: savedObjectToCopy.type, id: savedObjectToCopy.id }],
|
||||
['space-1', 'space-2'],
|
||||
true,
|
||||
false,
|
||||
true, // `createNewCopies` is enabled by default
|
||||
true
|
||||
);
|
||||
|
||||
|
@ -376,14 +377,25 @@ describe('CopyToSpaceFlyout', () => {
|
|||
spaceSelector.props().onChange(['space-1', 'space-2']);
|
||||
});
|
||||
|
||||
const startButton = findTestSubject(wrapper, 'cts-initiate-button');
|
||||
// Change copy mode to check for conflicts
|
||||
const copyModeControl = wrapper.find(CopyModeControl);
|
||||
copyModeControl.find('input[id="createNewCopiesDisabled"]').simulate('change');
|
||||
|
||||
await act(async () => {
|
||||
const startButton = findTestSubject(wrapper, 'cts-initiate-button');
|
||||
startButton.simulate('click');
|
||||
await nextTick();
|
||||
wrapper.update();
|
||||
});
|
||||
|
||||
expect(mockSpacesManager.copySavedObjects).toHaveBeenCalledWith(
|
||||
[{ type: savedObjectToCopy.type, id: savedObjectToCopy.id }],
|
||||
['space-1', 'space-2'],
|
||||
true,
|
||||
false, // `createNewCopies` is disabled
|
||||
true
|
||||
);
|
||||
|
||||
expect(wrapper.find(CopyToSpaceForm)).toHaveLength(0);
|
||||
expect(wrapper.find(ProcessingCopyToSpace)).toHaveLength(1);
|
||||
|
||||
|
@ -429,7 +441,7 @@ describe('CopyToSpaceFlyout', () => {
|
|||
],
|
||||
},
|
||||
true,
|
||||
false
|
||||
false // `createNewCopies` is disabled
|
||||
);
|
||||
|
||||
expect(onClose).toHaveBeenCalledTimes(1);
|
||||
|
@ -545,7 +557,7 @@ describe('CopyToSpaceFlyout', () => {
|
|||
],
|
||||
},
|
||||
true,
|
||||
false
|
||||
true // `createNewCopies` is enabled by default
|
||||
);
|
||||
|
||||
expect(onClose).toHaveBeenCalledTimes(1);
|
||||
|
|
|
@ -42,7 +42,7 @@ interface Props {
|
|||
}
|
||||
|
||||
const INCLUDE_RELATED_DEFAULT = true;
|
||||
const CREATE_NEW_COPIES_DEFAULT = false;
|
||||
const CREATE_NEW_COPIES_DEFAULT = true;
|
||||
const OVERWRITE_ALL_DEFAULT = true;
|
||||
|
||||
export const CopySavedObjectsToSpaceFlyout = (props: Props) => {
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { EuiSpacer, EuiFormRow } from '@elastic/eui';
|
||||
import { EuiSpacer, EuiTitle, EuiFormRow } from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { CopyOptions } from '../types';
|
||||
import { SavedObjectsManagementRecord } from '../../../../../../src/plugins/saved_objects_management/public';
|
||||
|
@ -45,14 +45,18 @@ export const CopyToSpaceForm = (props: Props) => {
|
|||
updateSelection={(newValues: CopyMode) => changeCopyMode(newValues)}
|
||||
/>
|
||||
|
||||
<EuiSpacer />
|
||||
<EuiSpacer size="m" />
|
||||
|
||||
<EuiFormRow
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="xpack.spaces.management.copyToSpace.selectSpacesLabel"
|
||||
defaultMessage="Select spaces"
|
||||
/>
|
||||
<EuiTitle size="xs">
|
||||
<span>
|
||||
<FormattedMessage
|
||||
id="xpack.spaces.management.copyToSpace.selectSpacesLabel"
|
||||
defaultMessage="Select spaces"
|
||||
/>
|
||||
</span>
|
||||
</EuiTitle>
|
||||
}
|
||||
fullWidth
|
||||
>
|
||||
|
|
|
@ -72,7 +72,7 @@ export const SelectableSpacesControl = (props: Props) => {
|
|||
className: 'spcCopyToSpace__spacesList',
|
||||
'data-test-subj': 'cts-form-space-selector',
|
||||
}}
|
||||
searchable
|
||||
searchable={options.length > 6}
|
||||
>
|
||||
{(list, search) => {
|
||||
return (
|
||||
|
|
|
@ -175,7 +175,7 @@ export const SelectableSpacesControl = (props: Props) => {
|
|||
'data-test-subj': 'sts-form-space-selector',
|
||||
}}
|
||||
height={ROW_HEIGHT * 3.5}
|
||||
searchable
|
||||
searchable={options.length > 6}
|
||||
>
|
||||
{(list, search) => {
|
||||
return (
|
||||
|
|
|
@ -38,7 +38,7 @@ export const ShareToSpaceForm = (props: Props) => {
|
|||
title={
|
||||
<FormattedMessage
|
||||
id="xpack.spaces.management.shareToSpace.shareWarningTitle"
|
||||
defaultMessage="Editing a shared object applies the changes in all spaces"
|
||||
defaultMessage="Editing a shared object applies the changes in every space"
|
||||
/>
|
||||
}
|
||||
color="warning"
|
||||
|
|
|
@ -91,7 +91,8 @@ export class SpacesManager {
|
|||
objects,
|
||||
spaces,
|
||||
includeReferences,
|
||||
...(createNewCopies ? { createNewCopies } : { overwrite }),
|
||||
createNewCopies,
|
||||
...(createNewCopies ? { overwrite: false } : { overwrite }), // ignore the overwrite option if createNewCopies is enabled
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
|
|
@ -20,8 +20,8 @@ import {
|
|||
import { LicensingPluginSetup } from '../../licensing/server';
|
||||
import { createSpacesTutorialContextFactory } from './lib/spaces_tutorial_context_factory';
|
||||
import { registerSpacesUsageCollector } from './usage_collection';
|
||||
import { SpacesService, SpacesServiceStart } from './spaces_service';
|
||||
import { SpacesServiceSetup } from './spaces_service';
|
||||
import { SpacesService, SpacesServiceSetup, SpacesServiceStart } from './spaces_service';
|
||||
import { UsageStatsService } from './usage_stats';
|
||||
import { ConfigType } from './config';
|
||||
import { initSpacesRequestInterceptors } from './lib/request_interceptors';
|
||||
import { initExternalSpacesApi } from './routes/api/external';
|
||||
|
@ -99,6 +99,10 @@ export class Plugin {
|
|||
return this.spacesServiceStart;
|
||||
};
|
||||
|
||||
const usageStatsServicePromise = new UsageStatsService(this.log).setup({
|
||||
getStartServices: core.getStartServices,
|
||||
});
|
||||
|
||||
const savedObjectsService = new SpacesSavedObjectsService();
|
||||
savedObjectsService.setup({ core, getSpacesService });
|
||||
|
||||
|
@ -126,6 +130,7 @@ export class Plugin {
|
|||
getStartServices: core.getStartServices,
|
||||
getImportExportObjectLimit: core.savedObjects.getImportExportObjectLimit,
|
||||
getSpacesService,
|
||||
usageStatsServicePromise,
|
||||
});
|
||||
|
||||
const internalRouter = core.http.createRouter();
|
||||
|
@ -148,6 +153,7 @@ export class Plugin {
|
|||
kibanaIndexConfig$: this.kibanaIndexConfig$,
|
||||
features: plugins.features,
|
||||
licensing: plugins.licensing,
|
||||
usageStatsServicePromise,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -22,6 +22,8 @@ import {
|
|||
coreMock,
|
||||
} from 'src/core/server/mocks';
|
||||
import { SpacesService } from '../../../spaces_service';
|
||||
import { usageStatsClientMock } from '../../../usage_stats/usage_stats_client.mock';
|
||||
import { usageStatsServiceMock } from '../../../usage_stats/usage_stats_service.mock';
|
||||
import { initCopyToSpacesApi } from './copy_to_space';
|
||||
import { spacesConfig } from '../../../lib/__fixtures__';
|
||||
import { ObjectType } from '@kbn/config-schema';
|
||||
|
@ -82,6 +84,11 @@ describe('copy to space', () => {
|
|||
basePath: httpService.basePath,
|
||||
});
|
||||
|
||||
const usageStatsClient = usageStatsClientMock.create();
|
||||
const usageStatsServicePromise = Promise.resolve(
|
||||
usageStatsServiceMock.createSetupContract(usageStatsClient)
|
||||
);
|
||||
|
||||
const clientServiceStart = clientService.start(coreStart);
|
||||
|
||||
const spacesServiceStart = service.start({
|
||||
|
@ -95,6 +102,7 @@ describe('copy to space', () => {
|
|||
getImportExportObjectLimit: () => 1000,
|
||||
log,
|
||||
getSpacesService: () => spacesServiceStart,
|
||||
usageStatsServicePromise,
|
||||
});
|
||||
|
||||
const [
|
||||
|
@ -113,6 +121,7 @@ describe('copy to space', () => {
|
|||
routeHandler: resolveRouteHandler,
|
||||
},
|
||||
savedObjectsRepositoryMock,
|
||||
usageStatsClient,
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -136,6 +145,27 @@ describe('copy to space', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it(`records usageStats data`, async () => {
|
||||
const createNewCopies = Symbol();
|
||||
const overwrite = Symbol();
|
||||
const payload = { spaces: ['a-space'], objects: [], createNewCopies, overwrite };
|
||||
|
||||
const { copyToSpace, usageStatsClient } = await setup();
|
||||
|
||||
const request = httpServerMock.createKibanaRequest({
|
||||
body: payload,
|
||||
method: 'post',
|
||||
});
|
||||
|
||||
await copyToSpace.routeHandler(mockRouteContext, request, kibanaResponseFactory);
|
||||
|
||||
expect(usageStatsClient.incrementCopySavedObjects).toHaveBeenCalledWith({
|
||||
headers: request.headers,
|
||||
createNewCopies,
|
||||
overwrite,
|
||||
});
|
||||
});
|
||||
|
||||
it(`uses a Saved Objects Client instance without the spaces wrapper`, async () => {
|
||||
const payload = {
|
||||
spaces: ['a-space'],
|
||||
|
@ -272,6 +302,25 @@ describe('copy to space', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it(`records usageStats data`, async () => {
|
||||
const createNewCopies = Symbol();
|
||||
const payload = { retries: {}, objects: [], createNewCopies };
|
||||
|
||||
const { resolveConflicts, usageStatsClient } = await setup();
|
||||
|
||||
const request = httpServerMock.createKibanaRequest({
|
||||
body: payload,
|
||||
method: 'post',
|
||||
});
|
||||
|
||||
await resolveConflicts.routeHandler(mockRouteContext, request, kibanaResponseFactory);
|
||||
|
||||
expect(usageStatsClient.incrementResolveCopySavedObjectsErrors).toHaveBeenCalledWith({
|
||||
headers: request.headers,
|
||||
createNewCopies,
|
||||
});
|
||||
});
|
||||
|
||||
it(`uses a Saved Objects Client instance without the spaces wrapper`, async () => {
|
||||
const payload = {
|
||||
retries: {
|
||||
|
|
|
@ -21,7 +21,14 @@ const areObjectsUnique = (objects: SavedObjectIdentifier[]) =>
|
|||
_.uniqBy(objects, (o: SavedObjectIdentifier) => `${o.type}:${o.id}`).length === objects.length;
|
||||
|
||||
export function initCopyToSpacesApi(deps: ExternalRouteDeps) {
|
||||
const { externalRouter, getSpacesService, getImportExportObjectLimit, getStartServices } = deps;
|
||||
const {
|
||||
externalRouter,
|
||||
getSpacesService,
|
||||
usageStatsServicePromise,
|
||||
getImportExportObjectLimit,
|
||||
getStartServices,
|
||||
} = deps;
|
||||
const usageStatsClientPromise = usageStatsServicePromise.then(({ getClient }) => getClient());
|
||||
|
||||
externalRouter.post(
|
||||
{
|
||||
|
@ -63,7 +70,7 @@ export function initCopyToSpacesApi(deps: ExternalRouteDeps) {
|
|||
),
|
||||
includeReferences: schema.boolean({ defaultValue: false }),
|
||||
overwrite: schema.boolean({ defaultValue: false }),
|
||||
createNewCopies: schema.boolean({ defaultValue: false }),
|
||||
createNewCopies: schema.boolean({ defaultValue: true }),
|
||||
},
|
||||
{
|
||||
validate: (object) => {
|
||||
|
@ -77,12 +84,6 @@ export function initCopyToSpacesApi(deps: ExternalRouteDeps) {
|
|||
},
|
||||
createLicensedRouteHandler(async (context, request, response) => {
|
||||
const [startServices] = await getStartServices();
|
||||
|
||||
const copySavedObjectsToSpaces = copySavedObjectsToSpacesFactory(
|
||||
startServices.savedObjects,
|
||||
getImportExportObjectLimit,
|
||||
request
|
||||
);
|
||||
const {
|
||||
spaces: destinationSpaceIds,
|
||||
objects,
|
||||
|
@ -90,6 +91,17 @@ export function initCopyToSpacesApi(deps: ExternalRouteDeps) {
|
|||
overwrite,
|
||||
createNewCopies,
|
||||
} = request.body;
|
||||
|
||||
const { headers } = request;
|
||||
usageStatsClientPromise.then((usageStatsClient) =>
|
||||
usageStatsClient.incrementCopySavedObjects({ headers, createNewCopies, overwrite })
|
||||
);
|
||||
|
||||
const copySavedObjectsToSpaces = copySavedObjectsToSpacesFactory(
|
||||
startServices.savedObjects,
|
||||
getImportExportObjectLimit,
|
||||
request
|
||||
);
|
||||
const sourceSpaceId = getSpacesService().getSpaceId(request);
|
||||
const copyResponse = await copySavedObjectsToSpaces(sourceSpaceId, destinationSpaceIds, {
|
||||
objects,
|
||||
|
@ -142,19 +154,24 @@ export function initCopyToSpacesApi(deps: ExternalRouteDeps) {
|
|||
}
|
||||
),
|
||||
includeReferences: schema.boolean({ defaultValue: false }),
|
||||
createNewCopies: schema.boolean({ defaultValue: false }),
|
||||
createNewCopies: schema.boolean({ defaultValue: true }),
|
||||
}),
|
||||
},
|
||||
},
|
||||
createLicensedRouteHandler(async (context, request, response) => {
|
||||
const [startServices] = await getStartServices();
|
||||
const { objects, includeReferences, retries, createNewCopies } = request.body;
|
||||
|
||||
const { headers } = request;
|
||||
usageStatsClientPromise.then((usageStatsClient) =>
|
||||
usageStatsClient.incrementResolveCopySavedObjectsErrors({ headers, createNewCopies })
|
||||
);
|
||||
|
||||
const resolveCopySavedObjectsToSpacesConflicts = resolveCopySavedObjectsToSpacesConflictsFactory(
|
||||
startServices.savedObjects,
|
||||
getImportExportObjectLimit,
|
||||
request
|
||||
);
|
||||
const { objects, includeReferences, retries, createNewCopies } = request.body;
|
||||
const sourceSpaceId = getSpacesService().getSpaceId(request);
|
||||
const resolveConflictsResponse = await resolveCopySavedObjectsToSpacesConflicts(
|
||||
sourceSpaceId,
|
||||
|
|
|
@ -27,6 +27,7 @@ import { initDeleteSpacesApi } from './delete';
|
|||
import { spacesConfig } from '../../../lib/__fixtures__';
|
||||
import { ObjectType } from '@kbn/config-schema';
|
||||
import { SpacesClientService } from '../../../spaces_client';
|
||||
import { usageStatsServiceMock } from '../../../usage_stats/usage_stats_service.mock';
|
||||
|
||||
describe('Spaces Public API', () => {
|
||||
const spacesSavedObjects = createSpaces();
|
||||
|
@ -51,6 +52,8 @@ describe('Spaces Public API', () => {
|
|||
basePath: httpService.basePath,
|
||||
});
|
||||
|
||||
const usageStatsServicePromise = Promise.resolve(usageStatsServiceMock.createSetupContract());
|
||||
|
||||
const clientServiceStart = clientService.start(coreStart);
|
||||
|
||||
const spacesServiceStart = service.start({
|
||||
|
@ -64,6 +67,7 @@ describe('Spaces Public API', () => {
|
|||
getImportExportObjectLimit: () => 1000,
|
||||
log,
|
||||
getSpacesService: () => spacesServiceStart,
|
||||
usageStatsServicePromise,
|
||||
});
|
||||
|
||||
const [routeDefinition, routeHandler] = router.delete.mock.calls[0];
|
||||
|
|
|
@ -21,6 +21,7 @@ import {
|
|||
import { SpacesService } from '../../../spaces_service';
|
||||
import { spacesConfig } from '../../../lib/__fixtures__';
|
||||
import { SpacesClientService } from '../../../spaces_client';
|
||||
import { usageStatsServiceMock } from '../../../usage_stats/usage_stats_service.mock';
|
||||
|
||||
describe('GET space', () => {
|
||||
const spacesSavedObjects = createSpaces();
|
||||
|
@ -46,6 +47,8 @@ describe('GET space', () => {
|
|||
basePath: httpService.basePath,
|
||||
});
|
||||
|
||||
const usageStatsServicePromise = Promise.resolve(usageStatsServiceMock.createSetupContract());
|
||||
|
||||
const clientServiceStart = clientService.start(coreStart);
|
||||
|
||||
const spacesServiceStart = service.start({
|
||||
|
@ -59,6 +62,7 @@ describe('GET space', () => {
|
|||
getImportExportObjectLimit: () => 1000,
|
||||
log,
|
||||
getSpacesService: () => spacesServiceStart,
|
||||
usageStatsServicePromise,
|
||||
});
|
||||
|
||||
return {
|
||||
|
|
|
@ -22,6 +22,7 @@ import { initGetAllSpacesApi } from './get_all';
|
|||
import { spacesConfig } from '../../../lib/__fixtures__';
|
||||
import { ObjectType } from '@kbn/config-schema';
|
||||
import { SpacesClientService } from '../../../spaces_client';
|
||||
import { usageStatsServiceMock } from '../../../usage_stats/usage_stats_service.mock';
|
||||
|
||||
describe('GET /spaces/space', () => {
|
||||
const spacesSavedObjects = createSpaces();
|
||||
|
@ -47,6 +48,8 @@ describe('GET /spaces/space', () => {
|
|||
basePath: httpService.basePath,
|
||||
});
|
||||
|
||||
const usageStatsServicePromise = Promise.resolve(usageStatsServiceMock.createSetupContract());
|
||||
|
||||
const clientServiceStart = clientService.start(coreStart);
|
||||
|
||||
const spacesServiceStart = service.start({
|
||||
|
@ -60,6 +63,7 @@ describe('GET /spaces/space', () => {
|
|||
getImportExportObjectLimit: () => 1000,
|
||||
log,
|
||||
getSpacesService: () => spacesServiceStart,
|
||||
usageStatsServicePromise,
|
||||
});
|
||||
|
||||
return {
|
||||
|
|
|
@ -10,7 +10,8 @@ import { initGetSpaceApi } from './get';
|
|||
import { initGetAllSpacesApi } from './get_all';
|
||||
import { initPostSpacesApi } from './post';
|
||||
import { initPutSpacesApi } from './put';
|
||||
import { SpacesServiceStart } from '../../../spaces_service/spaces_service';
|
||||
import { SpacesServiceStart } from '../../../spaces_service';
|
||||
import { UsageStatsServiceSetup } from '../../../usage_stats';
|
||||
import { initCopyToSpacesApi } from './copy_to_space';
|
||||
import { initShareToSpacesApi } from './share_to_space';
|
||||
|
||||
|
@ -19,6 +20,7 @@ export interface ExternalRouteDeps {
|
|||
getStartServices: CoreSetup['getStartServices'];
|
||||
getImportExportObjectLimit: () => number;
|
||||
getSpacesService: () => SpacesServiceStart;
|
||||
usageStatsServicePromise: Promise<UsageStatsServiceSetup>;
|
||||
log: Logger;
|
||||
}
|
||||
|
||||
|
|
|
@ -22,6 +22,7 @@ import { initPostSpacesApi } from './post';
|
|||
import { spacesConfig } from '../../../lib/__fixtures__';
|
||||
import { ObjectType } from '@kbn/config-schema';
|
||||
import { SpacesClientService } from '../../../spaces_client';
|
||||
import { usageStatsServiceMock } from '../../../usage_stats/usage_stats_service.mock';
|
||||
|
||||
describe('Spaces Public API', () => {
|
||||
const spacesSavedObjects = createSpaces();
|
||||
|
@ -46,6 +47,8 @@ describe('Spaces Public API', () => {
|
|||
basePath: httpService.basePath,
|
||||
});
|
||||
|
||||
const usageStatsServicePromise = Promise.resolve(usageStatsServiceMock.createSetupContract());
|
||||
|
||||
const clientServiceStart = clientService.start(coreStart);
|
||||
|
||||
const spacesServiceStart = service.start({
|
||||
|
@ -59,6 +62,7 @@ describe('Spaces Public API', () => {
|
|||
getImportExportObjectLimit: () => 1000,
|
||||
log,
|
||||
getSpacesService: () => spacesServiceStart,
|
||||
usageStatsServicePromise,
|
||||
});
|
||||
|
||||
const [routeDefinition, routeHandler] = router.post.mock.calls[0];
|
||||
|
|
|
@ -23,6 +23,7 @@ import { initPutSpacesApi } from './put';
|
|||
import { spacesConfig } from '../../../lib/__fixtures__';
|
||||
import { ObjectType } from '@kbn/config-schema';
|
||||
import { SpacesClientService } from '../../../spaces_client';
|
||||
import { usageStatsServiceMock } from '../../../usage_stats/usage_stats_service.mock';
|
||||
|
||||
describe('PUT /api/spaces/space', () => {
|
||||
const spacesSavedObjects = createSpaces();
|
||||
|
@ -47,6 +48,8 @@ describe('PUT /api/spaces/space', () => {
|
|||
basePath: httpService.basePath,
|
||||
});
|
||||
|
||||
const usageStatsServicePromise = Promise.resolve(usageStatsServiceMock.createSetupContract());
|
||||
|
||||
const clientServiceStart = clientService.start(coreStart);
|
||||
|
||||
const spacesServiceStart = service.start({
|
||||
|
@ -60,6 +63,7 @@ describe('PUT /api/spaces/space', () => {
|
|||
getImportExportObjectLimit: () => 1000,
|
||||
log,
|
||||
getSpacesService: () => spacesServiceStart,
|
||||
usageStatsServicePromise,
|
||||
});
|
||||
|
||||
const [routeDefinition, routeHandler] = router.put.mock.calls[0];
|
||||
|
|
|
@ -23,6 +23,7 @@ import { initShareToSpacesApi } from './share_to_space';
|
|||
import { spacesConfig } from '../../../lib/__fixtures__';
|
||||
import { ObjectType } from '@kbn/config-schema';
|
||||
import { SpacesClientService } from '../../../spaces_client';
|
||||
import { usageStatsServiceMock } from '../../../usage_stats/usage_stats_service.mock';
|
||||
|
||||
describe('share to space', () => {
|
||||
const spacesSavedObjects = createSpaces();
|
||||
|
@ -47,6 +48,8 @@ describe('share to space', () => {
|
|||
basePath: httpService.basePath,
|
||||
});
|
||||
|
||||
const usageStatsServicePromise = Promise.resolve(usageStatsServiceMock.createSetupContract());
|
||||
|
||||
const clientServiceStart = clientService.start(coreStart);
|
||||
|
||||
const spacesServiceStart = service.start({
|
||||
|
@ -59,6 +62,7 @@ describe('share to space', () => {
|
|||
getImportExportObjectLimit: () => 1000,
|
||||
log,
|
||||
getSpacesService: () => spacesServiceStart,
|
||||
usageStatsServicePromise,
|
||||
});
|
||||
|
||||
const [
|
||||
|
|
|
@ -38,3 +38,8 @@ export const SpacesSavedObjectMappings = deepFreeze({
|
|||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const UsageStatsMappings = deepFreeze({
|
||||
dynamic: false as false, // we aren't querying or aggregating over this data, so we don't need to specify any fields
|
||||
properties: {},
|
||||
});
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
*/
|
||||
|
||||
import { coreMock } from 'src/core/server/mocks';
|
||||
import { SPACES_USAGE_STATS_TYPE } from '../usage_stats';
|
||||
import { spacesServiceMock } from '../spaces_service/spaces_service.mock';
|
||||
import { SpacesSavedObjectsService } from './saved_objects_service';
|
||||
|
||||
|
@ -17,51 +18,15 @@ describe('SpacesSavedObjectsService', () => {
|
|||
const service = new SpacesSavedObjectsService();
|
||||
service.setup({ core, getSpacesService: () => spacesService });
|
||||
|
||||
expect(core.savedObjects.registerType).toHaveBeenCalledTimes(1);
|
||||
expect(core.savedObjects.registerType.mock.calls[0]).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"hidden": true,
|
||||
"mappings": Object {
|
||||
"properties": Object {
|
||||
"_reserved": Object {
|
||||
"type": "boolean",
|
||||
},
|
||||
"color": Object {
|
||||
"type": "keyword",
|
||||
},
|
||||
"description": Object {
|
||||
"type": "text",
|
||||
},
|
||||
"disabledFeatures": Object {
|
||||
"type": "keyword",
|
||||
},
|
||||
"imageUrl": Object {
|
||||
"index": false,
|
||||
"type": "text",
|
||||
},
|
||||
"initials": Object {
|
||||
"type": "keyword",
|
||||
},
|
||||
"name": Object {
|
||||
"fields": Object {
|
||||
"keyword": Object {
|
||||
"ignore_above": 2048,
|
||||
"type": "keyword",
|
||||
},
|
||||
},
|
||||
"type": "text",
|
||||
},
|
||||
},
|
||||
},
|
||||
"migrations": Object {
|
||||
"6.6.0": [Function],
|
||||
},
|
||||
"name": "space",
|
||||
"namespaceType": "agnostic",
|
||||
},
|
||||
]
|
||||
`);
|
||||
expect(core.savedObjects.registerType).toHaveBeenCalledTimes(2);
|
||||
expect(core.savedObjects.registerType).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
expect.objectContaining({ name: 'space' })
|
||||
);
|
||||
expect(core.savedObjects.registerType).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
expect.objectContaining({ name: SPACES_USAGE_STATS_TYPE })
|
||||
);
|
||||
});
|
||||
|
||||
it('registers the client wrapper', () => {
|
||||
|
|
|
@ -5,10 +5,11 @@
|
|||
*/
|
||||
|
||||
import { CoreSetup } from 'src/core/server';
|
||||
import { SpacesSavedObjectMappings } from './mappings';
|
||||
import { SpacesSavedObjectMappings, UsageStatsMappings } from './mappings';
|
||||
import { migrateToKibana660 } from './migrations';
|
||||
import { spacesSavedObjectsClientWrapperFactory } from './saved_objects_client_wrapper_factory';
|
||||
import { SpacesServiceStart } from '../spaces_service';
|
||||
import { SPACES_USAGE_STATS_TYPE } from '../usage_stats';
|
||||
|
||||
interface SetupDeps {
|
||||
core: Pick<CoreSetup, 'savedObjects' | 'getStartServices'>;
|
||||
|
@ -27,6 +28,13 @@ export class SpacesSavedObjectsService {
|
|||
},
|
||||
});
|
||||
|
||||
core.savedObjects.registerType({
|
||||
name: SPACES_USAGE_STATS_TYPE,
|
||||
hidden: true,
|
||||
namespaceType: 'agnostic',
|
||||
mappings: UsageStatsMappings,
|
||||
});
|
||||
|
||||
core.savedObjects.addClientWrapper(
|
||||
Number.MIN_SAFE_INTEGER,
|
||||
'spaces',
|
||||
|
|
|
@ -4,11 +4,14 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { getSpacesUsageCollector, UsageStats } from './spaces_usage_collector';
|
||||
import { getSpacesUsageCollector, UsageData } from './spaces_usage_collector';
|
||||
import * as Rx from 'rxjs';
|
||||
import { PluginsSetup } from '../plugin';
|
||||
import { KibanaFeature } from '../../../features/server';
|
||||
import { ILicense, LicensingPluginSetup } from '../../../licensing/server';
|
||||
import { UsageStats } from '../usage_stats';
|
||||
import { usageStatsClientMock } from '../usage_stats/usage_stats_client.mock';
|
||||
import { usageStatsServiceMock } from '../usage_stats/usage_stats_service.mock';
|
||||
import { pluginInitializerContextConfigMock } from 'src/core/server/mocks';
|
||||
import { createCollectorFetchContextMock } from 'src/plugins/usage_collection/server/mocks';
|
||||
|
||||
|
@ -17,6 +20,21 @@ interface SetupOpts {
|
|||
features?: KibanaFeature[];
|
||||
}
|
||||
|
||||
const MOCK_USAGE_STATS: UsageStats = {
|
||||
'apiCalls.copySavedObjects.total': 5,
|
||||
'apiCalls.copySavedObjects.kibanaRequest.yes': 5,
|
||||
'apiCalls.copySavedObjects.kibanaRequest.no': 0,
|
||||
'apiCalls.copySavedObjects.createNewCopiesEnabled.yes': 2,
|
||||
'apiCalls.copySavedObjects.createNewCopiesEnabled.no': 3,
|
||||
'apiCalls.copySavedObjects.overwriteEnabled.yes': 1,
|
||||
'apiCalls.copySavedObjects.overwriteEnabled.no': 4,
|
||||
'apiCalls.resolveCopySavedObjectsErrors.total': 13,
|
||||
'apiCalls.resolveCopySavedObjectsErrors.kibanaRequest.yes': 13,
|
||||
'apiCalls.resolveCopySavedObjectsErrors.kibanaRequest.no': 0,
|
||||
'apiCalls.resolveCopySavedObjectsErrors.createNewCopiesEnabled.yes': 6,
|
||||
'apiCalls.resolveCopySavedObjectsErrors.createNewCopiesEnabled.no': 7,
|
||||
};
|
||||
|
||||
function setup({
|
||||
license = { isAvailable: true },
|
||||
features = [{ id: 'feature1' } as KibanaFeature, { id: 'feature2' } as KibanaFeature],
|
||||
|
@ -41,12 +59,18 @@ function setup({
|
|||
getKibanaFeatures: jest.fn().mockReturnValue(features),
|
||||
} as unknown) as PluginsSetup['features'];
|
||||
|
||||
const usageStatsClient = usageStatsClientMock.create();
|
||||
usageStatsClient.getUsageStats.mockResolvedValue(MOCK_USAGE_STATS);
|
||||
const usageStatsService = usageStatsServiceMock.createSetupContract(usageStatsClient);
|
||||
|
||||
return {
|
||||
licensing,
|
||||
features: featuresSetup,
|
||||
usageCollection: {
|
||||
makeUsageCollector: (options: any) => new MockUsageCollector(options),
|
||||
},
|
||||
usageStatsService,
|
||||
usageStatsClient,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -77,26 +101,28 @@ const getMockFetchContext = (mockedCallCluster: jest.Mock) => {
|
|||
|
||||
describe('error handling', () => {
|
||||
it('handles a 404 when searching for space usage', async () => {
|
||||
const { features, licensing, usageCollection } = setup({
|
||||
const { features, licensing, usageCollection, usageStatsService } = setup({
|
||||
license: { isAvailable: true, type: 'basic' },
|
||||
});
|
||||
const collector = getSpacesUsageCollector(usageCollection as any, {
|
||||
kibanaIndexConfig$: Rx.of({ kibana: { index: '.kibana' } }),
|
||||
features,
|
||||
licensing,
|
||||
usageStatsServicePromise: Promise.resolve(usageStatsService),
|
||||
});
|
||||
|
||||
await collector.fetch(getMockFetchContext(jest.fn().mockRejectedValue({ status: 404 })));
|
||||
});
|
||||
|
||||
it('throws error for a non-404', async () => {
|
||||
const { features, licensing, usageCollection } = setup({
|
||||
const { features, licensing, usageCollection, usageStatsService } = setup({
|
||||
license: { isAvailable: true, type: 'basic' },
|
||||
});
|
||||
const collector = getSpacesUsageCollector(usageCollection as any, {
|
||||
kibanaIndexConfig$: Rx.of({ kibana: { index: '.kibana' } }),
|
||||
features,
|
||||
licensing,
|
||||
usageStatsServicePromise: Promise.resolve(usageStatsService),
|
||||
});
|
||||
|
||||
const statusCodes = [401, 402, 403, 500];
|
||||
|
@ -110,17 +136,19 @@ describe('error handling', () => {
|
|||
});
|
||||
|
||||
describe('with a basic license', () => {
|
||||
let usageStats: UsageStats;
|
||||
let usageData: UsageData;
|
||||
const { features, licensing, usageCollection, usageStatsService, usageStatsClient } = setup({
|
||||
license: { isAvailable: true, type: 'basic' },
|
||||
});
|
||||
|
||||
beforeAll(async () => {
|
||||
const { features, licensing, usageCollection } = setup({
|
||||
license: { isAvailable: true, type: 'basic' },
|
||||
});
|
||||
const collector = getSpacesUsageCollector(usageCollection as any, {
|
||||
kibanaIndexConfig$: pluginInitializerContextConfigMock({}).legacy.globalConfig$,
|
||||
features,
|
||||
licensing,
|
||||
usageStatsServicePromise: Promise.resolve(usageStatsService),
|
||||
});
|
||||
usageStats = await collector.fetch(getMockFetchContext(defaultCallClusterMock));
|
||||
usageData = await collector.fetch(getMockFetchContext(defaultCallClusterMock));
|
||||
|
||||
expect(defaultCallClusterMock).toHaveBeenCalledWith('search', {
|
||||
body: {
|
||||
|
@ -138,87 +166,111 @@ describe('with a basic license', () => {
|
|||
});
|
||||
|
||||
test('sets enabled to true', () => {
|
||||
expect(usageStats.enabled).toBe(true);
|
||||
expect(usageData.enabled).toBe(true);
|
||||
});
|
||||
|
||||
test('sets available to true', () => {
|
||||
expect(usageStats.available).toBe(true);
|
||||
expect(usageData.available).toBe(true);
|
||||
});
|
||||
|
||||
test('sets the number of spaces', () => {
|
||||
expect(usageStats.count).toBe(2);
|
||||
expect(usageData.count).toBe(2);
|
||||
});
|
||||
|
||||
test('calculates feature control usage', () => {
|
||||
expect(usageStats.usesFeatureControls).toBe(true);
|
||||
expect(usageStats).toHaveProperty('disabledFeatures');
|
||||
expect(usageStats.disabledFeatures).toEqual({
|
||||
expect(usageData.usesFeatureControls).toBe(true);
|
||||
expect(usageData).toHaveProperty('disabledFeatures');
|
||||
expect(usageData.disabledFeatures).toEqual({
|
||||
feature1: 1,
|
||||
feature2: 0,
|
||||
});
|
||||
});
|
||||
|
||||
test('fetches usageStats data', () => {
|
||||
expect(usageStatsService.getClient).toHaveBeenCalledTimes(1);
|
||||
expect(usageStatsClient.getUsageStats).toHaveBeenCalledTimes(1);
|
||||
expect(usageData).toEqual(expect.objectContaining(MOCK_USAGE_STATS));
|
||||
});
|
||||
});
|
||||
|
||||
describe('with no license', () => {
|
||||
let usageStats: UsageStats;
|
||||
let usageData: UsageData;
|
||||
const { features, licensing, usageCollection, usageStatsService, usageStatsClient } = setup({
|
||||
license: { isAvailable: false },
|
||||
});
|
||||
|
||||
beforeAll(async () => {
|
||||
const { features, licensing, usageCollection } = setup({ license: { isAvailable: false } });
|
||||
const collector = getSpacesUsageCollector(usageCollection as any, {
|
||||
kibanaIndexConfig$: pluginInitializerContextConfigMock({}).legacy.globalConfig$,
|
||||
features,
|
||||
licensing,
|
||||
usageStatsServicePromise: Promise.resolve(usageStatsService),
|
||||
});
|
||||
usageStats = await collector.fetch(getMockFetchContext(defaultCallClusterMock));
|
||||
usageData = await collector.fetch(getMockFetchContext(defaultCallClusterMock));
|
||||
});
|
||||
|
||||
test('sets enabled to false', () => {
|
||||
expect(usageStats.enabled).toBe(false);
|
||||
expect(usageData.enabled).toBe(false);
|
||||
});
|
||||
|
||||
test('sets available to false', () => {
|
||||
expect(usageStats.available).toBe(false);
|
||||
expect(usageData.available).toBe(false);
|
||||
});
|
||||
|
||||
test('does not set the number of spaces', () => {
|
||||
expect(usageStats.count).toBeUndefined();
|
||||
expect(usageData.count).toBeUndefined();
|
||||
});
|
||||
|
||||
test('does not set feature control usage', () => {
|
||||
expect(usageStats.usesFeatureControls).toBeUndefined();
|
||||
expect(usageData.usesFeatureControls).toBeUndefined();
|
||||
});
|
||||
|
||||
test('does not fetch usageStats data', () => {
|
||||
expect(usageStatsService.getClient).not.toHaveBeenCalled();
|
||||
expect(usageStatsClient.getUsageStats).not.toHaveBeenCalled();
|
||||
expect(usageData).not.toEqual(expect.objectContaining(MOCK_USAGE_STATS));
|
||||
});
|
||||
});
|
||||
|
||||
describe('with platinum license', () => {
|
||||
let usageStats: UsageStats;
|
||||
let usageData: UsageData;
|
||||
const { features, licensing, usageCollection, usageStatsService, usageStatsClient } = setup({
|
||||
license: { isAvailable: true, type: 'platinum' },
|
||||
});
|
||||
|
||||
beforeAll(async () => {
|
||||
const { features, licensing, usageCollection } = setup({
|
||||
license: { isAvailable: true, type: 'platinum' },
|
||||
});
|
||||
const collector = getSpacesUsageCollector(usageCollection as any, {
|
||||
kibanaIndexConfig$: pluginInitializerContextConfigMock({}).legacy.globalConfig$,
|
||||
features,
|
||||
licensing,
|
||||
usageStatsServicePromise: Promise.resolve(usageStatsService),
|
||||
});
|
||||
usageStats = await collector.fetch(getMockFetchContext(defaultCallClusterMock));
|
||||
usageData = await collector.fetch(getMockFetchContext(defaultCallClusterMock));
|
||||
});
|
||||
|
||||
test('sets enabled to true', () => {
|
||||
expect(usageStats.enabled).toBe(true);
|
||||
expect(usageData.enabled).toBe(true);
|
||||
});
|
||||
|
||||
test('sets available to true', () => {
|
||||
expect(usageStats.available).toBe(true);
|
||||
expect(usageData.available).toBe(true);
|
||||
});
|
||||
|
||||
test('sets the number of spaces', () => {
|
||||
expect(usageStats.count).toBe(2);
|
||||
expect(usageData.count).toBe(2);
|
||||
});
|
||||
|
||||
test('calculates feature control usage', () => {
|
||||
expect(usageStats.usesFeatureControls).toBe(true);
|
||||
expect(usageStats.disabledFeatures).toEqual({
|
||||
expect(usageData.usesFeatureControls).toBe(true);
|
||||
expect(usageData.disabledFeatures).toEqual({
|
||||
feature1: 1,
|
||||
feature2: 0,
|
||||
});
|
||||
});
|
||||
|
||||
test('fetches usageStats data', () => {
|
||||
expect(usageStatsService.getClient).toHaveBeenCalledTimes(1);
|
||||
expect(usageStatsClient.getUsageStats).toHaveBeenCalledTimes(1);
|
||||
expect(usageData).toEqual(expect.objectContaining(MOCK_USAGE_STATS));
|
||||
});
|
||||
});
|
||||
|
|
|
@ -9,6 +9,7 @@ import { take } from 'rxjs/operators';
|
|||
import { CollectorFetchContext, UsageCollectionSetup } from 'src/plugins/usage_collection/server';
|
||||
import { Observable } from 'rxjs';
|
||||
import { PluginsSetup } from '../plugin';
|
||||
import { UsageStats, UsageStatsServiceSetup } from '../usage_stats';
|
||||
|
||||
type CallCluster = <T = unknown>(
|
||||
endpoint: string,
|
||||
|
@ -33,7 +34,7 @@ interface SpacesAggregationResponse {
|
|||
* @param {string} kibanaIndex
|
||||
* @param {PluginsSetup['features']} features
|
||||
* @param {boolean} spacesAvailable
|
||||
* @return {UsageStats}
|
||||
* @return {UsageData}
|
||||
*/
|
||||
async function getSpacesUsage(
|
||||
callCluster: CallCluster,
|
||||
|
@ -109,10 +110,22 @@ async function getSpacesUsage(
|
|||
count,
|
||||
usesFeatureControls,
|
||||
disabledFeatures,
|
||||
} as UsageStats;
|
||||
} as UsageData;
|
||||
}
|
||||
|
||||
export interface UsageStats {
|
||||
async function getUsageStats(
|
||||
usageStatsServicePromise: Promise<UsageStatsServiceSetup>,
|
||||
spacesAvailable: boolean
|
||||
) {
|
||||
if (!spacesAvailable) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const usageStatsClient = await usageStatsServicePromise.then(({ getClient }) => getClient());
|
||||
return usageStatsClient.getUsageStats();
|
||||
}
|
||||
|
||||
export interface UsageData extends UsageStats {
|
||||
available: boolean;
|
||||
enabled: boolean;
|
||||
count?: number;
|
||||
|
@ -143,6 +156,7 @@ interface CollectorDeps {
|
|||
kibanaIndexConfig$: Observable<{ kibana: { index: string } }>;
|
||||
features: PluginsSetup['features'];
|
||||
licensing: PluginsSetup['licensing'];
|
||||
usageStatsServicePromise: Promise<UsageStatsServiceSetup>;
|
||||
}
|
||||
|
||||
/*
|
||||
|
@ -153,7 +167,7 @@ export function getSpacesUsageCollector(
|
|||
usageCollection: UsageCollectionSetup,
|
||||
deps: CollectorDeps
|
||||
) {
|
||||
return usageCollection.makeUsageCollector<UsageStats>({
|
||||
return usageCollection.makeUsageCollector<UsageData>({
|
||||
type: 'spaces',
|
||||
isReady: () => true,
|
||||
schema: {
|
||||
|
@ -181,20 +195,35 @@ export function getSpacesUsageCollector(
|
|||
available: { type: 'boolean' },
|
||||
enabled: { type: 'boolean' },
|
||||
count: { type: 'long' },
|
||||
'apiCalls.copySavedObjects.total': { type: 'long' },
|
||||
'apiCalls.copySavedObjects.kibanaRequest.yes': { type: 'long' },
|
||||
'apiCalls.copySavedObjects.kibanaRequest.no': { type: 'long' },
|
||||
'apiCalls.copySavedObjects.createNewCopiesEnabled.yes': { type: 'long' },
|
||||
'apiCalls.copySavedObjects.createNewCopiesEnabled.no': { type: 'long' },
|
||||
'apiCalls.copySavedObjects.overwriteEnabled.yes': { type: 'long' },
|
||||
'apiCalls.copySavedObjects.overwriteEnabled.no': { type: 'long' },
|
||||
'apiCalls.resolveCopySavedObjectsErrors.total': { type: 'long' },
|
||||
'apiCalls.resolveCopySavedObjectsErrors.kibanaRequest.yes': { type: 'long' },
|
||||
'apiCalls.resolveCopySavedObjectsErrors.kibanaRequest.no': { type: 'long' },
|
||||
'apiCalls.resolveCopySavedObjectsErrors.createNewCopiesEnabled.yes': { type: 'long' },
|
||||
'apiCalls.resolveCopySavedObjectsErrors.createNewCopiesEnabled.no': { type: 'long' },
|
||||
},
|
||||
fetch: async ({ callCluster }: CollectorFetchContext) => {
|
||||
const license = await deps.licensing.license$.pipe(take(1)).toPromise();
|
||||
const { licensing, kibanaIndexConfig$, features, usageStatsServicePromise } = deps;
|
||||
const license = await licensing.license$.pipe(take(1)).toPromise();
|
||||
const available = license.isAvailable; // some form of spaces is available for all valid licenses
|
||||
|
||||
const kibanaIndex = (await deps.kibanaIndexConfig$.pipe(take(1)).toPromise()).kibana.index;
|
||||
const kibanaIndex = (await kibanaIndexConfig$.pipe(take(1)).toPromise()).kibana.index;
|
||||
|
||||
const usageStats = await getSpacesUsage(callCluster, kibanaIndex, deps.features, available);
|
||||
const usageData = await getSpacesUsage(callCluster, kibanaIndex, features, available);
|
||||
const usageStats = await getUsageStats(usageStatsServicePromise, available);
|
||||
|
||||
return {
|
||||
available,
|
||||
enabled: available,
|
||||
...usageData,
|
||||
...usageStats,
|
||||
} as UsageStats;
|
||||
} as UsageData;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
8
x-pack/plugins/spaces/server/usage_stats/constants.ts
Normal file
8
x-pack/plugins/spaces/server/usage_stats/constants.ts
Normal file
|
@ -0,0 +1,8 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export const SPACES_USAGE_STATS_TYPE = 'spaces-usage-stats';
|
||||
export const SPACES_USAGE_STATS_ID = 'spaces-usage-stats';
|
9
x-pack/plugins/spaces/server/usage_stats/index.ts
Normal file
9
x-pack/plugins/spaces/server/usage_stats/index.ts
Normal file
|
@ -0,0 +1,9 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export { SPACES_USAGE_STATS_TYPE } from './constants';
|
||||
export { UsageStatsService, UsageStatsServiceSetup } from './usage_stats_service';
|
||||
export { UsageStats } from './types';
|
20
x-pack/plugins/spaces/server/usage_stats/types.ts
Normal file
20
x-pack/plugins/spaces/server/usage_stats/types.ts
Normal file
|
@ -0,0 +1,20 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export interface UsageStats {
|
||||
'apiCalls.copySavedObjects.total'?: number;
|
||||
'apiCalls.copySavedObjects.kibanaRequest.yes'?: number;
|
||||
'apiCalls.copySavedObjects.kibanaRequest.no'?: number;
|
||||
'apiCalls.copySavedObjects.createNewCopiesEnabled.yes'?: number;
|
||||
'apiCalls.copySavedObjects.createNewCopiesEnabled.no'?: number;
|
||||
'apiCalls.copySavedObjects.overwriteEnabled.yes'?: number;
|
||||
'apiCalls.copySavedObjects.overwriteEnabled.no'?: number;
|
||||
'apiCalls.resolveCopySavedObjectsErrors.total'?: number;
|
||||
'apiCalls.resolveCopySavedObjectsErrors.kibanaRequest.yes'?: number;
|
||||
'apiCalls.resolveCopySavedObjectsErrors.kibanaRequest.no'?: number;
|
||||
'apiCalls.resolveCopySavedObjectsErrors.createNewCopiesEnabled.yes'?: number;
|
||||
'apiCalls.resolveCopySavedObjectsErrors.createNewCopiesEnabled.no'?: number;
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { UsageStatsClient } from './usage_stats_client';
|
||||
|
||||
const createUsageStatsClientMock = () =>
|
||||
(({
|
||||
getUsageStats: jest.fn().mockResolvedValue({}),
|
||||
incrementCopySavedObjects: jest.fn().mockResolvedValue(null),
|
||||
incrementResolveCopySavedObjectsErrors: jest.fn().mockResolvedValue(null),
|
||||
} as unknown) as jest.Mocked<UsageStatsClient>);
|
||||
|
||||
export const usageStatsClientMock = {
|
||||
create: createUsageStatsClientMock,
|
||||
};
|
|
@ -0,0 +1,181 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { savedObjectsRepositoryMock } from 'src/core/server/mocks';
|
||||
import { SPACES_USAGE_STATS_TYPE, SPACES_USAGE_STATS_ID } from './constants';
|
||||
import {
|
||||
UsageStatsClient,
|
||||
IncrementCopySavedObjectsOptions,
|
||||
IncrementResolveCopySavedObjectsErrorsOptions,
|
||||
COPY_STATS_PREFIX,
|
||||
RESOLVE_COPY_STATS_PREFIX,
|
||||
} from './usage_stats_client';
|
||||
|
||||
describe('UsageStatsClient', () => {
|
||||
const setup = () => {
|
||||
const debugLoggerMock = jest.fn();
|
||||
const repositoryMock = savedObjectsRepositoryMock.create();
|
||||
const usageStatsClient = new UsageStatsClient(debugLoggerMock, Promise.resolve(repositoryMock));
|
||||
return { usageStatsClient, debugLoggerMock, repositoryMock };
|
||||
};
|
||||
|
||||
const firstPartyRequestHeaders = { 'kbn-version': 'a', origin: 'b', referer: 'c' }; // as long as these three header fields are truthy, this will be treated like a first-party request
|
||||
const incrementOptions = { refresh: false };
|
||||
|
||||
describe('#getUsageStats', () => {
|
||||
it('calls repository.incrementCounter and initializes fields', async () => {
|
||||
const { usageStatsClient, repositoryMock } = setup();
|
||||
await usageStatsClient.getUsageStats();
|
||||
expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(1);
|
||||
expect(repositoryMock.incrementCounter).toHaveBeenCalledWith(
|
||||
SPACES_USAGE_STATS_TYPE,
|
||||
SPACES_USAGE_STATS_ID,
|
||||
[
|
||||
`${COPY_STATS_PREFIX}.total`,
|
||||
`${COPY_STATS_PREFIX}.kibanaRequest.yes`,
|
||||
`${COPY_STATS_PREFIX}.kibanaRequest.no`,
|
||||
`${COPY_STATS_PREFIX}.createNewCopiesEnabled.yes`,
|
||||
`${COPY_STATS_PREFIX}.createNewCopiesEnabled.no`,
|
||||
`${COPY_STATS_PREFIX}.overwriteEnabled.yes`,
|
||||
`${COPY_STATS_PREFIX}.overwriteEnabled.no`,
|
||||
`${RESOLVE_COPY_STATS_PREFIX}.total`,
|
||||
`${RESOLVE_COPY_STATS_PREFIX}.kibanaRequest.yes`,
|
||||
`${RESOLVE_COPY_STATS_PREFIX}.kibanaRequest.no`,
|
||||
`${RESOLVE_COPY_STATS_PREFIX}.createNewCopiesEnabled.yes`,
|
||||
`${RESOLVE_COPY_STATS_PREFIX}.createNewCopiesEnabled.no`,
|
||||
],
|
||||
{ initialize: true }
|
||||
);
|
||||
});
|
||||
|
||||
it('returns empty object when encountering a repository error', async () => {
|
||||
const { usageStatsClient, repositoryMock } = setup();
|
||||
repositoryMock.incrementCounter.mockRejectedValue(new Error('Oh no!'));
|
||||
|
||||
const result = await usageStatsClient.getUsageStats();
|
||||
expect(result).toEqual({});
|
||||
});
|
||||
|
||||
it('returns object attributes when usageStats data exists', async () => {
|
||||
const { usageStatsClient, repositoryMock } = setup();
|
||||
const usageStats = { foo: 'bar' };
|
||||
repositoryMock.incrementCounter.mockResolvedValue({
|
||||
type: SPACES_USAGE_STATS_TYPE,
|
||||
id: SPACES_USAGE_STATS_ID,
|
||||
attributes: usageStats,
|
||||
references: [],
|
||||
});
|
||||
|
||||
const result = await usageStatsClient.getUsageStats();
|
||||
expect(result).toEqual(usageStats);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#incrementCopySavedObjects', () => {
|
||||
it('does not throw an error if repository incrementCounter operation fails', async () => {
|
||||
const { usageStatsClient, repositoryMock } = setup();
|
||||
repositoryMock.incrementCounter.mockRejectedValue(new Error('Oh no!'));
|
||||
|
||||
await expect(
|
||||
usageStatsClient.incrementCopySavedObjects({} as IncrementCopySavedObjectsOptions)
|
||||
).resolves.toBeUndefined();
|
||||
expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('handles falsy options appropriately', async () => {
|
||||
const { usageStatsClient, repositoryMock } = setup();
|
||||
|
||||
await usageStatsClient.incrementCopySavedObjects({} as IncrementCopySavedObjectsOptions);
|
||||
expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(1);
|
||||
expect(repositoryMock.incrementCounter).toHaveBeenCalledWith(
|
||||
SPACES_USAGE_STATS_TYPE,
|
||||
SPACES_USAGE_STATS_ID,
|
||||
[
|
||||
`${COPY_STATS_PREFIX}.total`,
|
||||
`${COPY_STATS_PREFIX}.kibanaRequest.no`,
|
||||
`${COPY_STATS_PREFIX}.createNewCopiesEnabled.no`,
|
||||
`${COPY_STATS_PREFIX}.overwriteEnabled.no`,
|
||||
],
|
||||
incrementOptions
|
||||
);
|
||||
});
|
||||
|
||||
it('handles truthy options appropriately', async () => {
|
||||
const { usageStatsClient, repositoryMock } = setup();
|
||||
|
||||
await usageStatsClient.incrementCopySavedObjects({
|
||||
headers: firstPartyRequestHeaders,
|
||||
createNewCopies: true,
|
||||
overwrite: true,
|
||||
} as IncrementCopySavedObjectsOptions);
|
||||
expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(1);
|
||||
expect(repositoryMock.incrementCounter).toHaveBeenCalledWith(
|
||||
SPACES_USAGE_STATS_TYPE,
|
||||
SPACES_USAGE_STATS_ID,
|
||||
[
|
||||
`${COPY_STATS_PREFIX}.total`,
|
||||
`${COPY_STATS_PREFIX}.kibanaRequest.yes`,
|
||||
`${COPY_STATS_PREFIX}.createNewCopiesEnabled.yes`,
|
||||
`${COPY_STATS_PREFIX}.overwriteEnabled.yes`,
|
||||
],
|
||||
incrementOptions
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#incrementResolveCopySavedObjectsErrors', () => {
|
||||
it('does not throw an error if repository create operation fails', async () => {
|
||||
const { usageStatsClient, repositoryMock } = setup();
|
||||
repositoryMock.incrementCounter.mockRejectedValue(new Error('Oh no!'));
|
||||
|
||||
await expect(
|
||||
usageStatsClient.incrementResolveCopySavedObjectsErrors(
|
||||
{} as IncrementResolveCopySavedObjectsErrorsOptions
|
||||
)
|
||||
).resolves.toBeUndefined();
|
||||
expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('handles falsy options appropriately', async () => {
|
||||
const { usageStatsClient, repositoryMock } = setup();
|
||||
|
||||
await usageStatsClient.incrementResolveCopySavedObjectsErrors(
|
||||
{} as IncrementResolveCopySavedObjectsErrorsOptions
|
||||
);
|
||||
expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(1);
|
||||
expect(repositoryMock.incrementCounter).toHaveBeenCalledWith(
|
||||
SPACES_USAGE_STATS_TYPE,
|
||||
SPACES_USAGE_STATS_ID,
|
||||
[
|
||||
`${RESOLVE_COPY_STATS_PREFIX}.total`,
|
||||
`${RESOLVE_COPY_STATS_PREFIX}.kibanaRequest.no`,
|
||||
`${RESOLVE_COPY_STATS_PREFIX}.createNewCopiesEnabled.no`,
|
||||
],
|
||||
incrementOptions
|
||||
);
|
||||
});
|
||||
|
||||
it('handles truthy options appropriately', async () => {
|
||||
const { usageStatsClient, repositoryMock } = setup();
|
||||
|
||||
await usageStatsClient.incrementResolveCopySavedObjectsErrors({
|
||||
headers: firstPartyRequestHeaders,
|
||||
createNewCopies: true,
|
||||
} as IncrementResolveCopySavedObjectsErrorsOptions);
|
||||
expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(1);
|
||||
expect(repositoryMock.incrementCounter).toHaveBeenCalledWith(
|
||||
SPACES_USAGE_STATS_TYPE,
|
||||
SPACES_USAGE_STATS_ID,
|
||||
[
|
||||
`${RESOLVE_COPY_STATS_PREFIX}.total`,
|
||||
`${RESOLVE_COPY_STATS_PREFIX}.kibanaRequest.yes`,
|
||||
`${RESOLVE_COPY_STATS_PREFIX}.createNewCopiesEnabled.yes`,
|
||||
],
|
||||
incrementOptions
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
108
x-pack/plugins/spaces/server/usage_stats/usage_stats_client.ts
Normal file
108
x-pack/plugins/spaces/server/usage_stats/usage_stats_client.ts
Normal file
|
@ -0,0 +1,108 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { ISavedObjectsRepository, Headers } from 'src/core/server';
|
||||
import { SPACES_USAGE_STATS_TYPE, SPACES_USAGE_STATS_ID } from './constants';
|
||||
import { CopyOptions, ResolveConflictsOptions } from '../lib/copy_to_spaces/types';
|
||||
import { UsageStats } from './types';
|
||||
|
||||
interface BaseIncrementOptions {
|
||||
headers?: Headers;
|
||||
}
|
||||
export type IncrementCopySavedObjectsOptions = BaseIncrementOptions &
|
||||
Pick<CopyOptions, 'createNewCopies' | 'overwrite'>;
|
||||
export type IncrementResolveCopySavedObjectsErrorsOptions = BaseIncrementOptions &
|
||||
Pick<ResolveConflictsOptions, 'createNewCopies'>;
|
||||
|
||||
export const COPY_STATS_PREFIX = 'apiCalls.copySavedObjects';
|
||||
export const RESOLVE_COPY_STATS_PREFIX = 'apiCalls.resolveCopySavedObjectsErrors';
|
||||
const ALL_COUNTER_FIELDS = [
|
||||
`${COPY_STATS_PREFIX}.total`,
|
||||
`${COPY_STATS_PREFIX}.kibanaRequest.yes`,
|
||||
`${COPY_STATS_PREFIX}.kibanaRequest.no`,
|
||||
`${COPY_STATS_PREFIX}.createNewCopiesEnabled.yes`,
|
||||
`${COPY_STATS_PREFIX}.createNewCopiesEnabled.no`,
|
||||
`${COPY_STATS_PREFIX}.overwriteEnabled.yes`,
|
||||
`${COPY_STATS_PREFIX}.overwriteEnabled.no`,
|
||||
`${RESOLVE_COPY_STATS_PREFIX}.total`,
|
||||
`${RESOLVE_COPY_STATS_PREFIX}.kibanaRequest.yes`,
|
||||
`${RESOLVE_COPY_STATS_PREFIX}.kibanaRequest.no`,
|
||||
`${RESOLVE_COPY_STATS_PREFIX}.createNewCopiesEnabled.yes`,
|
||||
`${RESOLVE_COPY_STATS_PREFIX}.createNewCopiesEnabled.no`,
|
||||
];
|
||||
export class UsageStatsClient {
|
||||
constructor(
|
||||
private readonly debugLogger: (message: string) => void,
|
||||
private readonly repositoryPromise: Promise<ISavedObjectsRepository>
|
||||
) {}
|
||||
|
||||
public async getUsageStats() {
|
||||
this.debugLogger('getUsageStats() called');
|
||||
let usageStats: UsageStats = {};
|
||||
try {
|
||||
const repository = await this.repositoryPromise;
|
||||
const result = await repository.incrementCounter<UsageStats>(
|
||||
SPACES_USAGE_STATS_TYPE,
|
||||
SPACES_USAGE_STATS_ID,
|
||||
ALL_COUNTER_FIELDS,
|
||||
{ initialize: true }
|
||||
);
|
||||
usageStats = result.attributes;
|
||||
} catch (err) {
|
||||
// do nothing
|
||||
}
|
||||
return usageStats;
|
||||
}
|
||||
|
||||
public async incrementCopySavedObjects({
|
||||
headers,
|
||||
createNewCopies,
|
||||
overwrite,
|
||||
}: IncrementCopySavedObjectsOptions) {
|
||||
const isKibanaRequest = getIsKibanaRequest(headers);
|
||||
const counterFieldNames = [
|
||||
'total',
|
||||
`kibanaRequest.${isKibanaRequest ? 'yes' : 'no'}`,
|
||||
`createNewCopiesEnabled.${createNewCopies ? 'yes' : 'no'}`,
|
||||
`overwriteEnabled.${overwrite ? 'yes' : 'no'}`,
|
||||
];
|
||||
await this.updateUsageStats(counterFieldNames, COPY_STATS_PREFIX);
|
||||
}
|
||||
|
||||
public async incrementResolveCopySavedObjectsErrors({
|
||||
headers,
|
||||
createNewCopies,
|
||||
}: IncrementResolveCopySavedObjectsErrorsOptions) {
|
||||
const isKibanaRequest = getIsKibanaRequest(headers);
|
||||
const counterFieldNames = [
|
||||
'total',
|
||||
`kibanaRequest.${isKibanaRequest ? 'yes' : 'no'}`,
|
||||
`createNewCopiesEnabled.${createNewCopies ? 'yes' : 'no'}`,
|
||||
];
|
||||
await this.updateUsageStats(counterFieldNames, RESOLVE_COPY_STATS_PREFIX);
|
||||
}
|
||||
|
||||
private async updateUsageStats(counterFieldNames: string[], prefix: string) {
|
||||
const options = { refresh: false };
|
||||
try {
|
||||
const repository = await this.repositoryPromise;
|
||||
await repository.incrementCounter(
|
||||
SPACES_USAGE_STATS_TYPE,
|
||||
SPACES_USAGE_STATS_ID,
|
||||
counterFieldNames.map((x) => `${prefix}.${x}`),
|
||||
options
|
||||
);
|
||||
} catch (err) {
|
||||
// do nothing
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getIsKibanaRequest(headers?: Headers) {
|
||||
// The presence of these three request headers gives us a good indication that this is a first-party request from the Kibana client.
|
||||
// We can't be 100% certain, but this is a reasonable attempt.
|
||||
return headers && headers['kbn-version'] && headers.origin && headers.referer;
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { usageStatsClientMock } from './usage_stats_client.mock';
|
||||
import { UsageStatsServiceSetup } from './usage_stats_service';
|
||||
|
||||
const createSetupContractMock = (usageStatsClient = usageStatsClientMock.create()) => {
|
||||
const setupContract: jest.Mocked<UsageStatsServiceSetup> = {
|
||||
getClient: jest.fn().mockReturnValue(usageStatsClient),
|
||||
};
|
||||
return setupContract;
|
||||
};
|
||||
|
||||
export const usageStatsServiceMock = {
|
||||
createSetupContract: createSetupContractMock,
|
||||
};
|
|
@ -0,0 +1,39 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { coreMock, loggingSystemMock } from 'src/core/server/mocks';
|
||||
import { UsageStatsService } from '.';
|
||||
import { UsageStatsClient } from './usage_stats_client';
|
||||
import { SPACES_USAGE_STATS_TYPE } from './constants';
|
||||
|
||||
describe('UsageStatsService', () => {
|
||||
const mockLogger = loggingSystemMock.createLogger();
|
||||
|
||||
describe('#setup', () => {
|
||||
const setup = async () => {
|
||||
const core = coreMock.createSetup();
|
||||
const usageStatsService = await new UsageStatsService(mockLogger).setup(core);
|
||||
return { core, usageStatsService };
|
||||
};
|
||||
|
||||
it('creates internal repository', async () => {
|
||||
const { core } = await setup();
|
||||
|
||||
const [{ savedObjects }] = await core.getStartServices();
|
||||
expect(savedObjects.createInternalRepository).toHaveBeenCalledTimes(1);
|
||||
expect(savedObjects.createInternalRepository).toHaveBeenCalledWith([SPACES_USAGE_STATS_TYPE]);
|
||||
});
|
||||
|
||||
describe('#getClient', () => {
|
||||
it('returns client', async () => {
|
||||
const { usageStatsService } = await setup();
|
||||
|
||||
const usageStatsClient = usageStatsService.getClient();
|
||||
expect(usageStatsClient).toBeInstanceOf(UsageStatsClient);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,36 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { Logger, CoreSetup } from '../../../../../src/core/server';
|
||||
import { UsageStatsClient } from './usage_stats_client';
|
||||
import { SPACES_USAGE_STATS_TYPE } from './constants';
|
||||
|
||||
export interface UsageStatsServiceSetup {
|
||||
getClient(): UsageStatsClient;
|
||||
}
|
||||
|
||||
interface UsageStatsServiceDeps {
|
||||
getStartServices: CoreSetup['getStartServices'];
|
||||
}
|
||||
|
||||
export class UsageStatsService {
|
||||
constructor(private readonly log: Logger) {}
|
||||
|
||||
public async setup({ getStartServices }: UsageStatsServiceDeps): Promise<UsageStatsServiceSetup> {
|
||||
const internalRepositoryPromise = getStartServices().then(([coreStart]) =>
|
||||
coreStart.savedObjects.createInternalRepository([SPACES_USAGE_STATS_TYPE])
|
||||
);
|
||||
|
||||
const getClient = () => {
|
||||
const debugLogger = (message: string) => this.log.debug(message);
|
||||
return new UsageStatsClient(debugLogger, internalRepositoryPromise);
|
||||
};
|
||||
|
||||
return { getClient };
|
||||
}
|
||||
|
||||
public async stop() {}
|
||||
}
|
|
@ -3381,6 +3381,42 @@
|
|||
},
|
||||
"count": {
|
||||
"type": "long"
|
||||
},
|
||||
"apiCalls.copySavedObjects.total": {
|
||||
"type": "long"
|
||||
},
|
||||
"apiCalls.copySavedObjects.kibanaRequest.yes": {
|
||||
"type": "long"
|
||||
},
|
||||
"apiCalls.copySavedObjects.kibanaRequest.no": {
|
||||
"type": "long"
|
||||
},
|
||||
"apiCalls.copySavedObjects.createNewCopiesEnabled.yes": {
|
||||
"type": "long"
|
||||
},
|
||||
"apiCalls.copySavedObjects.createNewCopiesEnabled.no": {
|
||||
"type": "long"
|
||||
},
|
||||
"apiCalls.copySavedObjects.overwriteEnabled.yes": {
|
||||
"type": "long"
|
||||
},
|
||||
"apiCalls.copySavedObjects.overwriteEnabled.no": {
|
||||
"type": "long"
|
||||
},
|
||||
"apiCalls.resolveCopySavedObjectsErrors.total": {
|
||||
"type": "long"
|
||||
},
|
||||
"apiCalls.resolveCopySavedObjectsErrors.kibanaRequest.yes": {
|
||||
"type": "long"
|
||||
},
|
||||
"apiCalls.resolveCopySavedObjectsErrors.kibanaRequest.no": {
|
||||
"type": "long"
|
||||
},
|
||||
"apiCalls.resolveCopySavedObjectsErrors.createNewCopiesEnabled.yes": {
|
||||
"type": "long"
|
||||
},
|
||||
"apiCalls.resolveCopySavedObjectsErrors.createNewCopiesEnabled.no": {
|
||||
"type": "long"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
@ -19330,7 +19330,6 @@
|
|||
"xpack.spaces.management.shareToSpace.shareToSpacesButton": "保存して閉じる",
|
||||
"xpack.spaces.management.shareToSpace.shareWarningBody": "1つのスペースでのみ編集するには、{makeACopyLink}してください。",
|
||||
"xpack.spaces.management.shareToSpace.shareWarningLink": "コピーを作成",
|
||||
"xpack.spaces.management.shareToSpace.shareWarningTitle": "共有オブジェクトの編集は、すべてのスペースで変更を適用します。",
|
||||
"xpack.spaces.management.shareToSpace.showLessSpacesLink": "縮小表示",
|
||||
"xpack.spaces.management.shareToSpace.showMoreSpacesLink": "他{count}件",
|
||||
"xpack.spaces.management.shareToSpace.spacesLoadErrorTitle": "利用可能なスペースを読み込み中にエラーが発生",
|
||||
|
|
|
@ -19349,7 +19349,6 @@
|
|||
"xpack.spaces.management.shareToSpace.shareToSpacesButton": "保存并关闭",
|
||||
"xpack.spaces.management.shareToSpace.shareWarningBody": "要仅在一个工作区中编辑,请改为{makeACopyLink}。",
|
||||
"xpack.spaces.management.shareToSpace.shareWarningLink": "创建副本",
|
||||
"xpack.spaces.management.shareToSpace.shareWarningTitle": "编辑共享对象会在所有工作区中应用更改",
|
||||
"xpack.spaces.management.shareToSpace.showLessSpacesLink": "显示更少",
|
||||
"xpack.spaces.management.shareToSpace.showMoreSpacesLink": "另外 {count} 个",
|
||||
"xpack.spaces.management.shareToSpace.spacesLoadErrorTitle": "加载可用工作区时出错",
|
||||
|
|
|
@ -52,6 +52,7 @@ export default function spaceSelectorFunctonalTests({
|
|||
await PageObjects.copySavedObjectsToSpace.openCopyToSpaceFlyoutForObject('A Dashboard');
|
||||
|
||||
await PageObjects.copySavedObjectsToSpace.setupForm({
|
||||
createNewCopies: false,
|
||||
overwrite: true,
|
||||
destinationSpaceId,
|
||||
});
|
||||
|
@ -80,6 +81,7 @@ export default function spaceSelectorFunctonalTests({
|
|||
await PageObjects.copySavedObjectsToSpace.openCopyToSpaceFlyoutForObject('A Dashboard');
|
||||
|
||||
await PageObjects.copySavedObjectsToSpace.setupForm({
|
||||
createNewCopies: false,
|
||||
overwrite: false,
|
||||
destinationSpaceId,
|
||||
});
|
||||
|
@ -116,12 +118,42 @@ export default function spaceSelectorFunctonalTests({
|
|||
await PageObjects.copySavedObjectsToSpace.finishCopy();
|
||||
});
|
||||
|
||||
it('avoids conflicts when createNewCopies is enabled', async () => {
|
||||
const destinationSpaceId = 'sales';
|
||||
|
||||
await PageObjects.copySavedObjectsToSpace.openCopyToSpaceFlyoutForObject('A Dashboard');
|
||||
|
||||
await PageObjects.copySavedObjectsToSpace.setupForm({
|
||||
createNewCopies: true,
|
||||
overwrite: false,
|
||||
destinationSpaceId,
|
||||
});
|
||||
|
||||
await PageObjects.copySavedObjectsToSpace.startCopy();
|
||||
|
||||
// Wait for successful copy
|
||||
await testSubjects.waitForDeleted(`cts-summary-indicator-loading-${destinationSpaceId}`);
|
||||
await testSubjects.existOrFail(`cts-summary-indicator-success-${destinationSpaceId}`);
|
||||
|
||||
const summaryCounts = await PageObjects.copySavedObjectsToSpace.getSummaryCounts();
|
||||
|
||||
expect(summaryCounts).to.eql({
|
||||
success: 3,
|
||||
pending: 0,
|
||||
skipped: 0,
|
||||
errors: 0,
|
||||
});
|
||||
|
||||
await PageObjects.copySavedObjectsToSpace.finishCopy();
|
||||
});
|
||||
|
||||
it('allows a dashboard to be copied to the marketing space, with circular references', async () => {
|
||||
const destinationSpaceId = 'marketing';
|
||||
|
||||
await PageObjects.copySavedObjectsToSpace.openCopyToSpaceFlyoutForObject('Dashboard Foo');
|
||||
|
||||
await PageObjects.copySavedObjectsToSpace.setupForm({
|
||||
createNewCopies: false,
|
||||
overwrite: true,
|
||||
destinationSpaceId,
|
||||
});
|
||||
|
|
|
@ -28,12 +28,24 @@ export function CopySavedObjectsToSpacePageProvider({
|
|||
},
|
||||
|
||||
async setupForm({
|
||||
createNewCopies,
|
||||
overwrite,
|
||||
destinationSpaceId,
|
||||
}: {
|
||||
createNewCopies?: boolean;
|
||||
overwrite?: boolean;
|
||||
destinationSpaceId: string;
|
||||
}) {
|
||||
if (createNewCopies && overwrite) {
|
||||
throw new Error('createNewCopies and overwrite options cannot be used together');
|
||||
}
|
||||
if (!createNewCopies) {
|
||||
const form = await testSubjects.find('copy-to-space-form');
|
||||
// a radio button consists of a div tag that contains an input, a div, and a label
|
||||
// we can't click the input directly, need to click the label
|
||||
const label = await form.findByCssSelector('label[for="createNewCopiesDisabled"]');
|
||||
await label.click();
|
||||
}
|
||||
if (!overwrite) {
|
||||
const radio = await testSubjects.find('cts-copyModeControl-overwriteRadioGroup');
|
||||
// a radio button consists of a div tag that contains an input, a div, and a label
|
||||
|
|
|
@ -607,6 +607,7 @@ export function copyToSpaceTestSuiteFactory(
|
|||
objects: [dashboardObject],
|
||||
spaces: [destination],
|
||||
includeReferences: false,
|
||||
createNewCopies: false,
|
||||
overwrite: false,
|
||||
})
|
||||
.expect(tests.noConflictsWithoutReferences.statusCode)
|
||||
|
@ -625,6 +626,7 @@ export function copyToSpaceTestSuiteFactory(
|
|||
objects: [dashboardObject],
|
||||
spaces: [destination],
|
||||
includeReferences: true,
|
||||
createNewCopies: false,
|
||||
overwrite: false,
|
||||
})
|
||||
.expect(tests.noConflictsWithReferences.statusCode)
|
||||
|
@ -643,6 +645,7 @@ export function copyToSpaceTestSuiteFactory(
|
|||
objects: [dashboardObject],
|
||||
spaces: [destination],
|
||||
includeReferences: true,
|
||||
createNewCopies: false,
|
||||
overwrite: true,
|
||||
})
|
||||
.expect(tests.withConflictsOverwriting.statusCode)
|
||||
|
@ -661,6 +664,7 @@ export function copyToSpaceTestSuiteFactory(
|
|||
objects: [dashboardObject],
|
||||
spaces: [destination],
|
||||
includeReferences: true,
|
||||
createNewCopies: false,
|
||||
overwrite: false,
|
||||
})
|
||||
.expect(tests.withConflictsWithoutOverwriting.statusCode)
|
||||
|
@ -678,6 +682,7 @@ export function copyToSpaceTestSuiteFactory(
|
|||
objects: [dashboardObject],
|
||||
spaces: [conflictDestination, noConflictDestination],
|
||||
includeReferences: true,
|
||||
createNewCopies: false,
|
||||
overwrite: true,
|
||||
})
|
||||
.expect(tests.multipleSpaces.statusCode)
|
||||
|
@ -710,6 +715,7 @@ export function copyToSpaceTestSuiteFactory(
|
|||
objects: [dashboardObject],
|
||||
spaces: ['non_existent_space'],
|
||||
includeReferences: false,
|
||||
createNewCopies: false,
|
||||
overwrite: true,
|
||||
})
|
||||
.expect(tests.nonExistentSpace.statusCode)
|
||||
|
@ -720,6 +726,7 @@ export function copyToSpaceTestSuiteFactory(
|
|||
[false, true].forEach((overwrite) => {
|
||||
const spaces = ['space_2'];
|
||||
const includeReferences = false;
|
||||
const createNewCopies = false;
|
||||
describe(`multi-namespace types with overwrite=${overwrite}`, () => {
|
||||
before(() => esArchiver.load('saved_objects/spaces'));
|
||||
after(() => esArchiver.unload('saved_objects/spaces'));
|
||||
|
@ -730,7 +737,7 @@ export function copyToSpaceTestSuiteFactory(
|
|||
return supertest
|
||||
.post(`${getUrlPrefix(spaceId)}/api/spaces/_copy_saved_objects`)
|
||||
.auth(user.username, user.password)
|
||||
.send({ objects, spaces, includeReferences, overwrite })
|
||||
.send({ objects, spaces, includeReferences, createNewCopies, overwrite })
|
||||
.expect(statusCode)
|
||||
.then(response);
|
||||
});
|
||||
|
|
|
@ -442,6 +442,7 @@ export function resolveCopyToSpaceConflictsSuite(
|
|||
.send({
|
||||
objects: [dashboardObject],
|
||||
includeReferences: true,
|
||||
createNewCopies: false,
|
||||
retries: { [destination]: [{ ...visualizationObject, overwrite: false }] },
|
||||
})
|
||||
.expect(tests.withReferencesNotOverwriting.statusCode)
|
||||
|
@ -457,6 +458,7 @@ export function resolveCopyToSpaceConflictsSuite(
|
|||
.send({
|
||||
objects: [dashboardObject],
|
||||
includeReferences: true,
|
||||
createNewCopies: false,
|
||||
retries: { [destination]: [{ ...visualizationObject, overwrite: true }] },
|
||||
})
|
||||
.expect(tests.withReferencesOverwriting.statusCode)
|
||||
|
@ -472,6 +474,7 @@ export function resolveCopyToSpaceConflictsSuite(
|
|||
.send({
|
||||
objects: [dashboardObject],
|
||||
includeReferences: false,
|
||||
createNewCopies: false,
|
||||
retries: { [destination]: [{ ...dashboardObject, overwrite: true }] },
|
||||
})
|
||||
.expect(tests.withoutReferencesOverwriting.statusCode)
|
||||
|
@ -487,6 +490,7 @@ export function resolveCopyToSpaceConflictsSuite(
|
|||
.send({
|
||||
objects: [dashboardObject],
|
||||
includeReferences: false,
|
||||
createNewCopies: false,
|
||||
retries: { [destination]: [{ ...dashboardObject, overwrite: false }] },
|
||||
})
|
||||
.expect(tests.withoutReferencesNotOverwriting.statusCode)
|
||||
|
@ -502,6 +506,7 @@ export function resolveCopyToSpaceConflictsSuite(
|
|||
.send({
|
||||
objects: [dashboardObject],
|
||||
includeReferences: false,
|
||||
createNewCopies: false,
|
||||
retries: { [destination]: [{ ...dashboardObject, overwrite: true }] },
|
||||
})
|
||||
.expect(tests.nonExistentSpace.statusCode)
|
||||
|
@ -510,6 +515,7 @@ export function resolveCopyToSpaceConflictsSuite(
|
|||
});
|
||||
|
||||
const includeReferences = false;
|
||||
const createNewCopies = false;
|
||||
describe(`multi-namespace types with "overwrite" retry`, () => {
|
||||
before(() => esArchiver.load('saved_objects/spaces'));
|
||||
after(() => esArchiver.unload('saved_objects/spaces'));
|
||||
|
@ -520,7 +526,7 @@ export function resolveCopyToSpaceConflictsSuite(
|
|||
return supertestWithoutAuth
|
||||
.post(`${getUrlPrefix(spaceId)}/api/spaces/_resolve_copy_saved_objects_errors`)
|
||||
.auth(user.username, user.password)
|
||||
.send({ objects, includeReferences, retries })
|
||||
.send({ objects, includeReferences, createNewCopies, retries })
|
||||
.expect(statusCode)
|
||||
.then(response);
|
||||
});
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue