From fa7b414905f2371c3313b4f3431a7c24a1d7d715 Mon Sep 17 00:00:00 2001 From: James Gowdy Date: Mon, 16 Aug 2021 20:36:53 +0100 Subject: [PATCH 01/31] [ML] Add support for model_prune_window in job wizard (#108734) --- .../plugins/ml/server/routes/schemas/anomaly_detectors_schema.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/x-pack/plugins/ml/server/routes/schemas/anomaly_detectors_schema.ts b/x-pack/plugins/ml/server/routes/schemas/anomaly_detectors_schema.ts index ec39d08ee357d..158e522ddf9b3 100644 --- a/x-pack/plugins/ml/server/routes/schemas/anomaly_detectors_schema.ts +++ b/x-pack/plugins/ml/server/routes/schemas/anomaly_detectors_schema.ts @@ -101,6 +101,7 @@ export const analysisConfigSchema = schema.object({ stop_on_warn: schema.maybe(schema.boolean()), }) ), + model_prune_window: schema.maybe(schema.string()), }); export const anomalyDetectionJobSchema = { From ea3fcbd8e92ad1e9c5a05951b6f5892f4046febc Mon Sep 17 00:00:00 2001 From: Mikhail Shustov Date: Mon, 16 Aug 2021 22:38:52 +0300 Subject: [PATCH 02/31] provide execution context information for task and alerting (#108197) * provide execution context information for task.run and alert.execute * log default space * expose setRequestId to plugins for cases when a runtime scope exists outside of requestHandler * fix typescript errors in jest test files Unfortunately, some of the tests are still failing. Not quite sure what's going on, but it looks like the calls to `withContext()` are not returning the result of the function passed in, but only in the tests. Because the code seems to run fine when I run Kibana - but perhaps we're just lucky and don't need the results the tests are looking for, for my simple smoke tests. But seems unlikely to me - guessing the mock is not being set up correctly, or there's some weird interaction with jest mocks and async hooks. * fix alerting unit tests * add tests that withContext is called even when exection_context service is disabled * update docs * add fakerequestid registration * do not attach request id when undefined (FakeRequest, for example) * Revert "add fakerequestid registration" This reverts commit ca5a396dcc3980c988081c6679c3861cc2e832d0. * not to expose setRequestId for time being * cleanup alerting code * update docs * add a unit test for alerting execution context * add a test for execution context of task runner * improve description readability * update type definitons * fall back to unknownId if no requestId is provided Co-authored-by: Patrick Mueller --- .../execution_context_service.mock.ts | 8 ++ .../execution_context_service.test.ts | 37 ++++++++- .../execution_context_service.ts | 11 ++- x-pack/plugins/alerting/server/plugin.ts | 1 + .../server/task_runner/task_runner.test.ts | 17 ++++ .../server/task_runner/task_runner.ts | 79 +++++++++++-------- .../task_runner/task_runner_factory.test.ts | 4 + .../server/task_runner/task_runner_factory.ts | 4 +- .../server/ephemeral_task_lifecycle.test.ts | 4 + .../server/ephemeral_task_lifecycle.ts | 7 +- x-pack/plugins/task_manager/server/plugin.ts | 8 +- .../server/polling_lifecycle.test.ts | 3 + .../task_manager/server/polling_lifecycle.ts | 7 +- .../task_running/ephemeral_task_runner.ts | 16 +++- .../server/task_running/task_runner.test.ts | 53 +++++++++++++ .../server/task_running/task_runner.ts | 20 ++++- 16 files changed, 231 insertions(+), 48 deletions(-) diff --git a/src/core/server/execution_context/execution_context_service.mock.ts b/src/core/server/execution_context/execution_context_service.mock.ts index 2e31145f6c0ee..68aab7a5b84f8 100644 --- a/src/core/server/execution_context/execution_context_service.mock.ts +++ b/src/core/server/execution_context/execution_context_service.mock.ts @@ -6,12 +6,18 @@ * Side Public License, v 1. */ +import type { KibanaExecutionContext } from '../../types'; import type { IExecutionContext, InternalExecutionContextSetup, ExecutionContextSetup, } from './execution_context_service'; +// attempted removal of any: unsuccessful! In theory, replaceable with /R +function withContextMock(context: KibanaExecutionContext | undefined, fn: () => any): any { + return fn(); +} + const createExecutionContextMock = () => { const mock: jest.Mocked = { set: jest.fn(), @@ -21,6 +27,7 @@ const createExecutionContextMock = () => { getParentContextFrom: jest.fn(), getAsHeader: jest.fn(), }; + mock.withContext.mockImplementation(withContextMock); return mock; }; const createInternalSetupContractMock = () => { @@ -32,6 +39,7 @@ const createSetupContractMock = () => { const mock: jest.Mocked = { withContext: jest.fn(), }; + mock.withContext.mockImplementation(withContextMock); return mock; }; diff --git a/src/core/server/execution_context/execution_context_service.test.ts b/src/core/server/execution_context/execution_context_service.test.ts index 3abaa13d11103..3fa4de34ebda0 100644 --- a/src/core/server/execution_context/execution_context_service.test.ts +++ b/src/core/server/execution_context/execution_context_service.test.ts @@ -346,7 +346,7 @@ describe('ExecutionContextService', () => { id: 'id-a', description: 'description-a', }, - (i) => i + () => null ); expect(loggingSystemMock.collect(core.logger).debug).toMatchInlineSnapshot(` Array [ @@ -378,6 +378,26 @@ describe('ExecutionContextService', () => { expect(result).toBeUndefined(); }); + it('executes provided function when disabled', async () => { + const coreWithDisabledService = mockCoreContext.create(); + coreWithDisabledService.configService.atPath.mockReturnValue( + new BehaviorSubject({ enabled: false }) + ); + const disabledService = new ExecutionContextService(coreWithDisabledService).setup(); + const fn = jest.fn(); + + disabledService.withContext( + { + type: 'type-b', + name: 'name-b', + id: 'id-b', + description: 'description-b', + }, + fn + ); + + expect(fn).toHaveBeenCalledTimes(1); + }); }); describe('getAsHeader', () => { @@ -387,6 +407,21 @@ describe('ExecutionContextService', () => { expect(service.getAsHeader()).toBe('1234'); }); + it('falls back to "unknownId" if no id provided', async () => { + expect(service.getAsHeader()).toBe('unknownId'); + }); + + it('falls back to "unknownId" and context if no id provided', async () => { + service.set({ + type: 'type-a', + name: 'name-a', + id: 'id-a', + description: 'description-a', + }); + + expect(service.getAsHeader()).toBe('unknownId;kibana:type-a:name-a:id-a'); + }); + it('returns request id and registered context', async () => { service.setRequestId('1234'); service.set({ diff --git a/src/core/server/execution_context/execution_context_service.ts b/src/core/server/execution_context/execution_context_service.ts index 5a8d104cebcd9..41b225cf1d0f3 100644 --- a/src/core/server/execution_context/execution_context_service.ts +++ b/src/core/server/execution_context/execution_context_service.ts @@ -34,7 +34,7 @@ export interface IExecutionContext { * https://nodejs.org/api/async_context.html#async_context_asynclocalstorage_enterwith_store */ get(): IExecutionContextContainer | undefined; - withContext(context: KibanaExecutionContext | undefined, fn: (...args: any[]) => R): R; + withContext(context: KibanaExecutionContext | undefined, fn: () => R): R; /** * returns serialized representation to send as a header **/ @@ -153,8 +153,11 @@ export class ExecutionContextService private getAsHeader(): string | undefined { if (!this.enabled) return; - const stringifiedCtx = this.contextStore.getStore()?.toString(); - const requestId = this.requestIdStore.getStore()?.requestId; - return stringifiedCtx ? `${requestId};kibana:${stringifiedCtx}` : requestId; + // requestId may not be present in the case of FakeRequest + const requestId = this.requestIdStore.getStore()?.requestId ?? 'unknownId'; + const executionContext = this.contextStore.getStore()?.toString(); + const executionContextStr = executionContext ? `;kibana:${executionContext}` : ''; + + return `${requestId}${executionContextStr}`; } } diff --git a/x-pack/plugins/alerting/server/plugin.ts b/x-pack/plugins/alerting/server/plugin.ts index 197cf49edc147..6f257fa1e0e68 100644 --- a/x-pack/plugins/alerting/server/plugin.ts +++ b/x-pack/plugins/alerting/server/plugin.ts @@ -381,6 +381,7 @@ export class AlertingPlugin { basePathService: core.http.basePath, eventLogger: this.eventLogger!, internalSavedObjectsRepository: core.savedObjects.createInternalRepository(['alert']), + executionContext: core.executionContext, ruleTypeRegistry: this.ruleTypeRegistry!, kibanaBaseUrl: this.kibanaBaseUrl, supportsEphemeralTasks: plugins.taskManager.supportsEphemeralTasks(), diff --git a/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts b/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts index 4d876a1d6ecb5..26b17d76553fd 100644 --- a/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts +++ b/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts @@ -27,6 +27,7 @@ import { loggingSystemMock, savedObjectsRepositoryMock, httpServiceMock, + executionContextServiceMock, } from '../../../../../src/core/server/mocks'; import { PluginStartContract as ActionsPluginStart } from '../../../actions/server'; import { actionsMock, actionsClientMock } from '../../../actions/server/mocks'; @@ -89,6 +90,7 @@ describe('Task Runner', () => { type TaskRunnerFactoryInitializerParamsType = jest.Mocked & { actionsPlugin: jest.Mocked; eventLogger: jest.Mocked; + executionContext: ReturnType; }; const taskRunnerFactoryInitializerParams: TaskRunnerFactoryInitializerParamsType = { @@ -97,6 +99,7 @@ describe('Task Runner', () => { getRulesClientWithRequest: jest.fn().mockReturnValue(rulesClient), encryptedSavedObjectsClient, logger: loggingSystemMock.create().get(), + executionContext: executionContextServiceMock.createInternalStartContract(), spaceIdToNamespace: jest.fn().mockReturnValue(undefined), basePathService: httpServiceMock.createBasePath(), eventLogger: eventLoggerMock.create(), @@ -185,6 +188,9 @@ describe('Task Runner', () => { (actionTypeId, actionId, params) => params ); ruleTypeRegistry.get.mockReturnValue(alertType); + taskRunnerFactoryInitializerParams.executionContext.withContext.mockImplementation((ctx, fn) => + fn() + ); }); test('successfully executes the task', async () => { @@ -338,6 +344,17 @@ describe('Task Runner', () => { }, { refresh: false, namespace: undefined } ); + + expect(taskRunnerFactoryInitializerParams.executionContext.withContext).toBeCalledTimes(1); + expect(taskRunnerFactoryInitializerParams.executionContext.withContext).toHaveBeenCalledWith( + { + id: '1', + name: 'execute test', + type: 'alert', + description: 'execute [test] with name [alert-name] in [default] namespace', + }, + expect.any(Function) + ); }); testAgainstEphemeralSupport( diff --git a/x-pack/plugins/alerting/server/task_runner/task_runner.ts b/x-pack/plugins/alerting/server/task_runner/task_runner.ts index 63748eafe037c..6694146c3e2b9 100644 --- a/x-pack/plugins/alerting/server/task_runner/task_runner.ts +++ b/x-pack/plugins/alerting/server/task_runner/task_runner.ts @@ -262,44 +262,55 @@ export class TaskRunner< let updatedAlertTypeState: void | Record; try { - updatedAlertTypeState = await this.alertType.executor({ - alertId, - services: { - ...services, - alertInstanceFactory: createAlertInstanceFactory< - InstanceState, - InstanceContext, - WithoutReservedActionGroups - >(alertInstances), - }, - params, - state: alertTypeState as State, - startedAt: this.taskInstance.startedAt!, - previousStartedAt: previousStartedAt ? new Date(previousStartedAt) : null, - spaceId, - namespace, - name, - tags, - createdBy, - updatedBy, - rule: { + const ctx = { + type: 'alert', + name: `execute ${alert.alertTypeId}`, + id: alertId, + description: `execute [${alert.alertTypeId}] with name [${name}] in [${ + namespace ?? 'default' + }] namespace`, + }; + + updatedAlertTypeState = await this.context.executionContext.withContext(ctx, () => + this.alertType.executor({ + alertId, + services: { + ...services, + alertInstanceFactory: createAlertInstanceFactory< + InstanceState, + InstanceContext, + WithoutReservedActionGroups + >(alertInstances), + }, + params, + state: alertTypeState as State, + startedAt: this.taskInstance.startedAt!, + previousStartedAt: previousStartedAt ? new Date(previousStartedAt) : null, + spaceId, + namespace, name, tags, - consumer, - producer: alertType.producer, - ruleTypeId: alert.alertTypeId, - ruleTypeName: alertType.name, - enabled, - schedule, - actions, createdBy, updatedBy, - createdAt, - updatedAt, - throttle, - notifyWhen, - }, - }); + rule: { + name, + tags, + consumer, + producer: alertType.producer, + ruleTypeId: alert.alertTypeId, + ruleTypeName: alertType.name, + enabled, + schedule, + actions, + createdBy, + updatedBy, + createdAt, + updatedAt, + throttle, + notifyWhen, + }, + }) + ); } catch (err) { event.message = `alert execution failure: ${alertLabel}`; event.error = event.error || {}; diff --git a/x-pack/plugins/alerting/server/task_runner/task_runner_factory.test.ts b/x-pack/plugins/alerting/server/task_runner/task_runner_factory.test.ts index 61aa168acf617..d262607958347 100644 --- a/x-pack/plugins/alerting/server/task_runner/task_runner_factory.test.ts +++ b/x-pack/plugins/alerting/server/task_runner/task_runner_factory.test.ts @@ -19,6 +19,9 @@ import { alertsMock, rulesClientMock } from '../mocks'; import { eventLoggerMock } from '../../../event_log/server/event_logger.mock'; import { UntypedNormalizedAlertType } from '../rule_type_registry'; import { ruleTypeRegistryMock } from '../rule_type_registry.mock'; +import { executionContextServiceMock } from '../../../../../src/core/server/mocks'; + +const executionContext = executionContextServiceMock.createSetupContract(); const alertType: UntypedNormalizedAlertType = { id: 'test', @@ -81,6 +84,7 @@ describe('Task Runner Factory', () => { kibanaBaseUrl: 'https://localhost:5601', supportsEphemeralTasks: true, maxEphemeralActionsPerAlert: new Promise((resolve) => resolve(10)), + executionContext, }; beforeEach(() => { diff --git a/x-pack/plugins/alerting/server/task_runner/task_runner_factory.ts b/x-pack/plugins/alerting/server/task_runner/task_runner_factory.ts index 4d9c7b37ed645..524b779a0d9ac 100644 --- a/x-pack/plugins/alerting/server/task_runner/task_runner_factory.ts +++ b/x-pack/plugins/alerting/server/task_runner/task_runner_factory.ts @@ -6,11 +6,12 @@ */ import type { PublicMethodsOf } from '@kbn/utility-types'; -import { +import type { Logger, KibanaRequest, ISavedObjectsRepository, IBasePath, + ExecutionContextStart, } from '../../../../../src/core/server'; import { RunContext } from '../../../task_manager/server'; import { EncryptedSavedObjectsClient } from '../../../encrypted_saved_objects/server'; @@ -36,6 +37,7 @@ export interface TaskRunnerContext { actionsPlugin: ActionsPluginStartContract; eventLogger: IEventLogger; encryptedSavedObjectsClient: EncryptedSavedObjectsClient; + executionContext: ExecutionContextStart; spaceIdToNamespace: SpaceIdToNamespaceFunction; basePathService: IBasePath; internalSavedObjectsRepository: ISavedObjectsRepository; diff --git a/x-pack/plugins/task_manager/server/ephemeral_task_lifecycle.test.ts b/x-pack/plugins/task_manager/server/ephemeral_task_lifecycle.test.ts index 182e7cd5bcabf..5dbea8e2f4dee 100644 --- a/x-pack/plugins/task_manager/server/ephemeral_task_lifecycle.test.ts +++ b/x-pack/plugins/task_manager/server/ephemeral_task_lifecycle.test.ts @@ -21,6 +21,9 @@ import { asTaskPollingCycleEvent, asTaskRunEvent, TaskPersistence } from './task import { TaskRunResult } from './task_running'; import { TaskPoolRunResult } from './task_pool'; import { TaskPoolMock } from './task_pool.mock'; +import { executionContextServiceMock } from '../../../../src/core/server/mocks'; + +const executionContext = executionContextServiceMock.createSetupContract(); describe('EphemeralTaskLifecycle', () => { function initTaskLifecycleParams({ @@ -37,6 +40,7 @@ describe('EphemeralTaskLifecycle', () => { const opts: EphemeralTaskLifecycleOpts = { logger: taskManagerLogger, definitions: new TaskTypeDictionary(taskManagerLogger), + executionContext, config: { enabled: true, max_workers: 10, diff --git a/x-pack/plugins/task_manager/server/ephemeral_task_lifecycle.ts b/x-pack/plugins/task_manager/server/ephemeral_task_lifecycle.ts index ce719ebed36e4..ded9091b4f226 100644 --- a/x-pack/plugins/task_manager/server/ephemeral_task_lifecycle.ts +++ b/x-pack/plugins/task_manager/server/ephemeral_task_lifecycle.ts @@ -7,7 +7,7 @@ import { Subject, Observable, Subscription } from 'rxjs'; import { filter } from 'rxjs/operators'; -import { Logger } from '../../../../src/core/server'; +import { Logger, ExecutionContextStart } from '../../../../src/core/server'; import { Result, asErr, asOk } from './lib/result_type'; import { TaskManagerConfig } from './config'; @@ -28,6 +28,7 @@ export interface EphemeralTaskLifecycleOpts { elasticsearchAndSOAvailability$: Observable; pool: TaskPool; lifecycleEvent: Observable; + executionContext: ExecutionContextStart; } export type EphemeralTaskInstanceRequest = Omit; @@ -46,6 +47,7 @@ export class EphemeralTaskLifecycle { private config: TaskManagerConfig; private middleware: Middleware; private lifecycleSubscription: Subscription = Subscription.EMPTY; + private readonly executionContext: ExecutionContextStart; constructor({ logger, @@ -54,6 +56,7 @@ export class EphemeralTaskLifecycle { pool, lifecycleEvent, config, + executionContext, }: EphemeralTaskLifecycleOpts) { this.logger = logger; this.middleware = middleware; @@ -61,6 +64,7 @@ export class EphemeralTaskLifecycle { this.pool = pool; this.lifecycleEvent = lifecycleEvent; this.config = config; + this.executionContext = executionContext; if (this.enabled) { this.lifecycleSubscription = this.lifecycleEvent @@ -179,6 +183,7 @@ export class EphemeralTaskLifecycle { beforeRun: this.middleware.beforeRun, beforeMarkRunning: this.middleware.beforeMarkRunning, onTaskEvent: this.emitEvent, + executionContext: this.executionContext, }); }; } diff --git a/x-pack/plugins/task_manager/server/plugin.ts b/x-pack/plugins/task_manager/server/plugin.ts index 3d3d180fc0665..11746e2da2847 100644 --- a/x-pack/plugins/task_manager/server/plugin.ts +++ b/x-pack/plugins/task_manager/server/plugin.ts @@ -126,7 +126,11 @@ export class TaskManagerPlugin }; } - public start({ savedObjects, elasticsearch }: CoreStart): TaskManagerStartContract { + public start({ + savedObjects, + elasticsearch, + executionContext, + }: CoreStart): TaskManagerStartContract { const savedObjectsRepository = savedObjects.createInternalRepository(['task']); const serializer = savedObjects.createSerializer(); @@ -150,6 +154,7 @@ export class TaskManagerPlugin config: this.config!, definitions: this.definitions, logger: this.logger, + executionContext, taskStore, middleware: this.middleware, elasticsearchAndSOAvailability$: this.elasticsearchAndSOAvailability$!, @@ -160,6 +165,7 @@ export class TaskManagerPlugin config: this.config!, definitions: this.definitions, logger: this.logger, + executionContext, middleware: this.middleware, elasticsearchAndSOAvailability$: this.elasticsearchAndSOAvailability$!, pool: this.taskPollingLifecycle.pool, diff --git a/x-pack/plugins/task_manager/server/polling_lifecycle.test.ts b/x-pack/plugins/task_manager/server/polling_lifecycle.test.ts index aad03951bbb9b..42bccbcbaba89 100644 --- a/x-pack/plugins/task_manager/server/polling_lifecycle.test.ts +++ b/x-pack/plugins/task_manager/server/polling_lifecycle.test.ts @@ -20,7 +20,9 @@ import type { TaskClaiming as TaskClaimingClass } from './queries/task_claiming' import { asOk, Err, isErr, isOk, Result } from './lib/result_type'; import { FillPoolResult } from './lib/fill_pool'; import { ElasticsearchResponseError } from './lib/identify_es_error'; +import { executionContextServiceMock } from '../../../../src/core/server/mocks'; +const executionContext = executionContextServiceMock.createSetupContract(); let mockTaskClaiming = taskClaimingMock.create({}); jest.mock('./queries/task_claiming', () => { return { @@ -69,6 +71,7 @@ describe('TaskPollingLifecycle', () => { middleware: createInitialMiddleware(), maxWorkersConfiguration$: of(100), pollIntervalConfiguration$: of(100), + executionContext, }; beforeEach(() => { diff --git a/x-pack/plugins/task_manager/server/polling_lifecycle.ts b/x-pack/plugins/task_manager/server/polling_lifecycle.ts index 16b15d0c46e3e..847e1f32e8f2a 100644 --- a/x-pack/plugins/task_manager/server/polling_lifecycle.ts +++ b/x-pack/plugins/task_manager/server/polling_lifecycle.ts @@ -9,7 +9,7 @@ import { Subject, Observable, Subscription } from 'rxjs'; import { pipe } from 'fp-ts/lib/pipeable'; import { Option, some, map as mapOptional } from 'fp-ts/lib/Option'; import { tap } from 'rxjs/operators'; -import { Logger } from '../../../../src/core/server'; +import type { Logger, ExecutionContextStart } from '../../../../src/core/server'; import { Result, asErr, mapErr, asOk, map, mapOk } from './lib/result_type'; import { ManagedConfiguration } from './lib/create_managed_configuration'; @@ -53,6 +53,7 @@ export type TaskPollingLifecycleOpts = { config: TaskManagerConfig; middleware: Middleware; elasticsearchAndSOAvailability$: Observable; + executionContext: ExecutionContextStart; } & ManagedConfiguration; export type TaskLifecycleEvent = @@ -73,6 +74,7 @@ export class TaskPollingLifecycle { private store: TaskStore; private taskClaiming: TaskClaiming; private bufferedStore: BufferedTaskStore; + private readonly executionContext: ExecutionContextStart; private logger: Logger; public pool: TaskPool; @@ -100,11 +102,13 @@ export class TaskPollingLifecycle { config, taskStore, definitions, + executionContext, }: TaskPollingLifecycleOpts) { this.logger = logger; this.middleware = middleware; this.definitions = definitions; this.store = taskStore; + this.executionContext = executionContext; const emitEvent = (event: TaskLifecycleEvent) => this.events$.next(event); @@ -227,6 +231,7 @@ export class TaskPollingLifecycle { beforeMarkRunning: this.middleware.beforeMarkRunning, onTaskEvent: this.emitEvent, defaultMaxAttempts: this.taskClaiming.maxAttempts, + executionContext: this.executionContext, }); }; diff --git a/x-pack/plugins/task_manager/server/task_running/ephemeral_task_runner.ts b/x-pack/plugins/task_manager/server/task_running/ephemeral_task_runner.ts index bc1ff0541fdff..358a8003382b0 100644 --- a/x-pack/plugins/task_manager/server/task_running/ephemeral_task_runner.ts +++ b/x-pack/plugins/task_manager/server/task_running/ephemeral_task_runner.ts @@ -14,7 +14,7 @@ import apm from 'elastic-apm-node'; import { withSpan } from '@kbn/apm-utils'; import { identity } from 'lodash'; -import { Logger } from '../../../../../src/core/server'; +import { Logger, ExecutionContextStart } from '../../../../../src/core/server'; import { Middleware } from '../lib/middleware'; import { asOk, asErr, eitherAsync, Result } from '../lib/result_type'; @@ -54,6 +54,7 @@ type Opts = { definitions: TaskTypeDictionary; instance: EphemeralTaskInstance; onTaskEvent?: (event: TaskRun | TaskMarkRunning) => void; + executionContext: ExecutionContextStart; } & Pick; // ephemeral tasks cannot be rescheduled or scheduled to run again in the future @@ -74,6 +75,7 @@ export class EphemeralTaskManagerRunner implements TaskRunner { private beforeRun: Middleware['beforeRun']; private beforeMarkRunning: Middleware['beforeMarkRunning']; private onTaskEvent: (event: TaskRun | TaskMarkRunning) => void; + private readonly executionContext: ExecutionContextStart; /** * Creates an instance of EphemeralTaskManagerRunner. @@ -91,6 +93,7 @@ export class EphemeralTaskManagerRunner implements TaskRunner { beforeRun, beforeMarkRunning, onTaskEvent = identity, + executionContext, }: Opts) { this.instance = asPending(asConcreteInstance(sanitizeInstance(instance))); this.definitions = definitions; @@ -98,6 +101,7 @@ export class EphemeralTaskManagerRunner implements TaskRunner { this.beforeRun = beforeRun; this.beforeMarkRunning = beforeMarkRunning; this.onTaskEvent = onTaskEvent; + this.executionContext = executionContext; } /** @@ -193,8 +197,14 @@ export class EphemeralTaskManagerRunner implements TaskRunner { const stopTaskTimer = startTaskTimer(); try { this.task = this.definition.createTaskRunner(modifiedContext); - const result = await withSpan({ name: 'ephemeral run', type: 'task manager' }, () => - this.task!.run() + const ctx = { + type: 'task manager', + name: `run ephemeral ${this.instance.task.taskType}`, + id: this.instance.task.id, + description: 'run ephemeral task', + }; + const result = await this.executionContext.withContext(ctx, () => + withSpan({ name: 'ephemeral run', type: 'task manager' }, () => this.task!.run()) ); const validatedResult = this.validateResult(result); const processedResult = await withSpan( diff --git a/x-pack/plugins/task_manager/server/task_running/task_runner.test.ts b/x-pack/plugins/task_manager/server/task_running/task_runner.test.ts index e54962c7c8857..b78232d59803e 100644 --- a/x-pack/plugins/task_manager/server/task_running/task_runner.test.ts +++ b/x-pack/plugins/task_manager/server/task_running/task_runner.test.ts @@ -25,7 +25,9 @@ import { mockLogger } from '../test_utils'; import { throwUnrecoverableError } from './errors'; import { taskStoreMock } from '../task_store.mock'; import apm from 'elastic-apm-node'; +import { executionContextServiceMock } from '../../../../../src/core/server/mocks'; +const executionContext = executionContextServiceMock.createSetupContract(); const minutesFromNow = (mins: number): Date => secondsFromNow(mins * 60); let fakeTimer: sinon.SinonFakeTimers; @@ -103,6 +105,31 @@ describe('TaskManagerRunner', () => { ); expect(mockApmTrans.end).toHaveBeenCalledWith('failure'); }); + test('provides execution context on run', async () => { + const { runner } = await readyToRunStageSetup({ + definitions: { + bar: { + title: 'Bar!', + createTaskRunner: () => ({ + async run() { + return { state: {} }; + }, + }), + }, + }, + }); + await runner.run(); + expect(executionContext.withContext).toHaveBeenCalledTimes(1); + expect(executionContext.withContext).toHaveBeenCalledWith( + { + description: 'run task', + id: 'foo', + name: 'run bar', + type: 'task manager', + }, + expect.any(Function) + ); + }); test('provides details about the task that is running', async () => { const { runner } = await pendingStageSetup({ instance: { @@ -690,6 +717,31 @@ describe('TaskManagerRunner', () => { }); expect(mockApmTrans.end).toHaveBeenCalledWith('failure'); }); + test('provides execution context on run', async () => { + const { runner } = await readyToRunStageSetup({ + definitions: { + bar: { + title: 'Bar!', + createTaskRunner: () => ({ + async run() { + return { state: {} }; + }, + }), + }, + }, + }); + await runner.run(); + expect(executionContext.withContext).toHaveBeenCalledTimes(1); + expect(executionContext.withContext).toHaveBeenCalledWith( + { + description: 'run task', + id: 'foo', + name: 'run bar', + type: 'task manager', + }, + expect.any(Function) + ); + }); test('queues a reattempt if the task fails', async () => { const initialAttempts = _.random(0, 2); const id = Date.now().toString(); @@ -1465,6 +1517,7 @@ describe('TaskManagerRunner', () => { instance, definitions, onTaskEvent: opts.onTaskEvent, + executionContext, }); if (stage === TaskRunningStage.READY_TO_RUN) { diff --git a/x-pack/plugins/task_manager/server/task_running/task_runner.ts b/x-pack/plugins/task_manager/server/task_running/task_runner.ts index 97b40a75a59c4..30aa0cd0c522e 100644 --- a/x-pack/plugins/task_manager/server/task_running/task_runner.ts +++ b/x-pack/plugins/task_manager/server/task_running/task_runner.ts @@ -15,7 +15,11 @@ import apm from 'elastic-apm-node'; import { withSpan } from '@kbn/apm-utils'; import { performance } from 'perf_hooks'; import { identity, defaults, flow } from 'lodash'; -import { Logger, SavedObjectsErrorHelpers } from '../../../../../src/core/server'; +import { + Logger, + SavedObjectsErrorHelpers, + ExecutionContextStart, +} from '../../../../../src/core/server'; import { Middleware } from '../lib/middleware'; import { @@ -93,6 +97,7 @@ type Opts = { store: Updatable; onTaskEvent?: (event: TaskRun | TaskMarkRunning) => void; defaultMaxAttempts: number; + executionContext: ExecutionContextStart; } & Pick; export enum TaskRunResult { @@ -137,6 +142,7 @@ export class TaskManagerRunner implements TaskRunner { private beforeMarkRunning: Middleware['beforeMarkRunning']; private onTaskEvent: (event: TaskRun | TaskMarkRunning) => void; private defaultMaxAttempts: number; + private readonly executionContext: ExecutionContextStart; /** * Creates an instance of TaskManagerRunner. @@ -157,6 +163,7 @@ export class TaskManagerRunner implements TaskRunner { beforeMarkRunning, defaultMaxAttempts, onTaskEvent = identity, + executionContext, }: Opts) { this.instance = asPending(sanitizeInstance(instance)); this.definitions = definitions; @@ -166,6 +173,7 @@ export class TaskManagerRunner implements TaskRunner { this.beforeMarkRunning = beforeMarkRunning; this.onTaskEvent = onTaskEvent; this.defaultMaxAttempts = defaultMaxAttempts; + this.executionContext = executionContext; } /** @@ -261,7 +269,15 @@ export class TaskManagerRunner implements TaskRunner { try { this.task = this.definition.createTaskRunner(modifiedContext); - const result = await withSpan({ name: 'run', type: 'task manager' }, () => this.task!.run()); + const ctx = { + type: 'task manager', + name: `run ${this.instance.task.taskType}`, + id: this.instance.task.id, + description: 'run task', + }; + const result = await this.executionContext.withContext(ctx, () => + withSpan({ name: 'run', type: 'task manager' }, () => this.task!.run()) + ); const validatedResult = this.validateResult(result); const processedResult = await withSpan({ name: 'process result', type: 'task manager' }, () => this.processResult(validatedResult, stopTaskTimer()) From fa64146244912204974265cb3a2171ee6daba872 Mon Sep 17 00:00:00 2001 From: Aaron Caldwell Date: Mon, 16 Aug 2021 13:51:07 -0600 Subject: [PATCH 03/31] [Maps] Add edit tools defaults for user and timestamp (#103588) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- x-pack/plugins/maps/common/constants.ts | 3 +- x-pack/plugins/maps/kibana.json | 3 +- .../create_new_index_pattern.ts | 16 ++++-- .../layers/new_vector_layer_wizard/wizard.tsx | 18 ++++++- .../layers/vector_layer/vector_layer.tsx | 4 +- .../es_search_source/es_search_source.tsx | 44 ++++++++++++++-- .../es_search_source/util/feature_edit.ts | 19 +++++-- .../mvt_single_layer_vector_source.tsx | 4 ++ .../sources/vector_source/vector_source.tsx | 15 +++++- x-pack/plugins/maps/public/kibana_services.ts | 1 + x-pack/plugins/maps/public/plugin.ts | 2 + .../server/data_indexing/create_doc_source.ts | 8 +-- .../server/data_indexing/indexing_routes.ts | 41 +++++++++++++++ .../test/api_integration/apis/maps/index.js | 1 + .../apis/maps/validate_drawing_index.js | 51 +++++++++++++++++++ 15 files changed, 209 insertions(+), 21 deletions(-) create mode 100644 x-pack/test/api_integration/apis/maps/validate_drawing_index.js diff --git a/x-pack/plugins/maps/common/constants.ts b/x-pack/plugins/maps/common/constants.ts index e2de2c412e828..1a3065cb4518d 100644 --- a/x-pack/plugins/maps/common/constants.ts +++ b/x-pack/plugins/maps/common/constants.ts @@ -44,6 +44,7 @@ export const INDEX_SOURCE_API_PATH = `${GIS_API_PATH}/docSource`; export const API_ROOT_PATH = `/${GIS_API_PATH}`; export const INDEX_FEATURE_PATH = `/${GIS_API_PATH}/feature`; export const GET_MATCHING_INDEXES_PATH = `/${GIS_API_PATH}/getMatchingIndexes`; +export const CHECK_IS_DRAWING_INDEX = `/${GIS_API_PATH}/checkIsDrawingIndex`; export const MVT_GETTILE_API_PATH = 'mvt/getTile'; export const MVT_GETGRIDTILE_API_PATH = 'mvt/getGridTile'; @@ -310,7 +311,7 @@ export type RawValue = string | string[] | number | boolean | undefined | null; export type FieldFormatter = (value: RawValue) => string | number; -export const INDEX_META_DATA_CREATED_BY = 'maps-drawing-data-ingest'; +export const MAPS_NEW_VECTOR_LAYER_META_CREATED_BY = 'maps-new-vector-layer'; export const MAX_DRAWING_SIZE_BYTES = 10485760; // 10MB diff --git a/x-pack/plugins/maps/kibana.json b/x-pack/plugins/maps/kibana.json index aa643b431721c..1cccfaa7748b1 100644 --- a/x-pack/plugins/maps/kibana.json +++ b/x-pack/plugins/maps/kibana.json @@ -26,7 +26,8 @@ "optionalPlugins": [ "home", "savedObjectsTagging", - "charts" + "charts", + "security" ], "ui": true, "server": true, diff --git a/x-pack/plugins/maps/public/classes/layers/new_vector_layer_wizard/create_new_index_pattern.ts b/x-pack/plugins/maps/public/classes/layers/new_vector_layer_wizard/create_new_index_pattern.ts index d612c25157095..2e57014824a3c 100644 --- a/x-pack/plugins/maps/public/classes/layers/new_vector_layer_wizard/create_new_index_pattern.ts +++ b/x-pack/plugins/maps/public/classes/layers/new_vector_layer_wizard/create_new_index_pattern.ts @@ -6,20 +6,30 @@ */ import { getHttp } from '../../../kibana_services'; -import { CreateDocSourceResp, INDEX_SOURCE_API_PATH } from '../../../../common'; +import { + CreateDocSourceResp, + INDEX_SOURCE_API_PATH, + IndexSourceMappings, +} from '../../../../common'; -export const createNewIndexAndPattern = async (indexName: string) => { +export const createNewIndexAndPattern = async ({ + indexName, + defaultMappings = {}, +}: { + indexName: string; + defaultMappings: IndexSourceMappings | {}; +}) => { return await getHttp().fetch({ path: `/${INDEX_SOURCE_API_PATH}`, method: 'POST', body: JSON.stringify({ index: indexName, - // Initially set to static mappings mappings: { properties: { coordinates: { type: 'geo_shape', }, + ...defaultMappings, }, }, }), diff --git a/x-pack/plugins/maps/public/classes/layers/new_vector_layer_wizard/wizard.tsx b/x-pack/plugins/maps/public/classes/layers/new_vector_layer_wizard/wizard.tsx index 4f9e9c39ce466..6dac22581efdb 100644 --- a/x-pack/plugins/maps/public/classes/layers/new_vector_layer_wizard/wizard.tsx +++ b/x-pack/plugins/maps/public/classes/layers/new_vector_layer_wizard/wizard.tsx @@ -22,6 +22,19 @@ interface State { createIndexError: string; } +const DEFAULT_MAPPINGS = { + created: { + properties: { + '@timestamp': { + type: 'date', + }, + user: { + type: 'keyword', + }, + }, + }, +}; + export class NewVectorLayerEditor extends Component { private _isMounted: boolean = false; @@ -59,7 +72,10 @@ export class NewVectorLayerEditor extends Component { let indexPatternId: string | undefined; try { - const response = await createNewIndexAndPattern(this.state.indexName); + const response = await createNewIndexAndPattern({ + indexName: this.state.indexName, + defaultMappings: DEFAULT_MAPPINGS, + }); indexPatternId = response.indexPatternId; } catch (e) { this._setCreateIndexError(e.message); diff --git a/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.tsx b/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.tsx index 74a0ea1e63882..54e0c00141cd3 100644 --- a/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.tsx +++ b/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.tsx @@ -184,7 +184,6 @@ export class VectorLayer extends AbstractLayer implements IVectorLayer { supportsFeatureEditing(): boolean { const dataRequest = this.getDataRequest(SUPPORTS_FEATURE_EDITING_REQUEST_ID); const data = dataRequest?.getData() as { supportsFeatureEditing: boolean } | undefined; - return data ? data.supportsFeatureEditing : false; } @@ -1116,7 +1115,8 @@ export class VectorLayer extends AbstractLayer implements IVectorLayer { async addFeature(geometry: Geometry | Position[]) { const layerSource = this.getSource(); - await layerSource.addFeature(geometry); + const defaultFields = await layerSource.getDefaultFields(); + await layerSource.addFeature(geometry, defaultFields); } async deleteFeature(featureId: string) { diff --git a/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.tsx b/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.tsx index b63a821cdb0c7..464ba024663ec 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.tsx +++ b/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.tsx @@ -13,7 +13,12 @@ import type { Filter, IFieldType, IndexPattern } from 'src/plugins/data/public'; import { GeoJsonProperties, Geometry, Position } from 'geojson'; import { esFilters } from '../../../../../../../src/plugins/data/public'; import { AbstractESSource } from '../es_source'; -import { getHttp, getSearchService, getTimeFilter } from '../../../kibana_services'; +import { + getHttp, + getSearchService, + getSecurityService, + getTimeFilter, +} from '../../../kibana_services'; import { addFieldToDSL, getField, @@ -62,7 +67,12 @@ import { isValidStringConfig } from '../../util/valid_string_config'; import { TopHitsUpdateSourceEditor } from './top_hits'; import { getDocValueAndSourceFields, ScriptField } from './util/get_docvalue_source_fields'; import { ITiledSingleLayerMvtParams } from '../tiled_single_layer_vector_source/tiled_single_layer_vector_source'; -import { addFeatureToIndex, deleteFeatureFromIndex, getMatchingIndexes } from './util/feature_edit'; +import { + addFeatureToIndex, + deleteFeatureFromIndex, + getIsDrawLayer, + getMatchingIndexes, +} from './util/feature_edit'; export function timerangeToTimeextent(timerange: TimeRange): Timeslice | undefined { const timeRangeBounds = getTimeFilter().calculateBounds(timerange); @@ -444,6 +454,29 @@ export class ESSearchSource extends AbstractESSource implements ITiledSingleLaye return matchingIndexes.length === 1; } + async getDefaultFields(): Promise>> { + if (!(await this._isDrawingIndex())) { + return {}; + } + const user = await getSecurityService()?.authc.getCurrentUser(); + const timestamp = new Date().toISOString(); + return { + created: { + ...(user ? { user: user.username } : {}), + '@timestamp': timestamp, + }, + }; + } + + async _isDrawingIndex(): Promise { + await this.getIndexPattern(); + if (!(this.indexPattern && this.indexPattern.title)) { + return false; + } + const { success, isDrawingIndex } = await getIsDrawLayer(this.indexPattern.title); + return success && isDrawingIndex; + } + _hasSort(): boolean { const { sortField, sortOrder } = this._descriptor; return !!sortField && !!sortOrder; @@ -716,9 +749,12 @@ export class ESSearchSource extends AbstractESSource implements ITiledSingleLaye return MVT_SOURCE_LAYER_NAME; } - async addFeature(geometry: Geometry | Position[]) { + async addFeature( + geometry: Geometry | Position[], + defaultFields: Record> + ) { const indexPattern = await this.getIndexPattern(); - await addFeatureToIndex(indexPattern.title, geometry, this.getGeoFieldName()); + await addFeatureToIndex(indexPattern.title, geometry, this.getGeoFieldName(), defaultFields); } async deleteFeature(featureId: string) { diff --git a/x-pack/plugins/maps/public/classes/sources/es_search_source/util/feature_edit.ts b/x-pack/plugins/maps/public/classes/sources/es_search_source/util/feature_edit.ts index f306a225df69a..c9a967bea3e2c 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_search_source/util/feature_edit.ts +++ b/x-pack/plugins/maps/public/classes/sources/es_search_source/util/feature_edit.ts @@ -7,15 +7,20 @@ import { Geometry, Position } from 'geojson'; import { set } from '@elastic/safer-lodash-set'; -import { GET_MATCHING_INDEXES_PATH, INDEX_FEATURE_PATH } from '../../../../../common'; +import { + CHECK_IS_DRAWING_INDEX, + GET_MATCHING_INDEXES_PATH, + INDEX_FEATURE_PATH, +} from '../../../../../common'; import { getHttp } from '../../../../kibana_services'; export const addFeatureToIndex = async ( indexName: string, geometry: Geometry | Position[], - path: string + path: string, + defaultFields: Record> ) => { - const data = set({}, path, geometry); + const data = set({ ...defaultFields }, path, geometry); return await getHttp().fetch({ path: `${INDEX_FEATURE_PATH}`, method: 'POST', @@ -42,3 +47,11 @@ export const getMatchingIndexes = async (indexPattern: string) => { method: 'GET', }); }; + +export const getIsDrawLayer = async (index: string) => { + return await getHttp().fetch({ + path: CHECK_IS_DRAWING_INDEX, + method: 'GET', + query: { index }, + }); +}; diff --git a/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/mvt_single_layer_vector_source.tsx b/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/mvt_single_layer_vector_source.tsx index f825a85f50bbd..6911cbabdf971 100644 --- a/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/mvt_single_layer_vector_source.tsx +++ b/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/mvt_single_layer_vector_source.tsx @@ -239,6 +239,10 @@ export class MVTSingleLayerVectorSource async supportsFeatureEditing(): Promise { return false; } + + async getDefaultFields(): Promise>> { + return {}; + } } registerSource({ diff --git a/x-pack/plugins/maps/public/classes/sources/vector_source/vector_source.tsx b/x-pack/plugins/maps/public/classes/sources/vector_source/vector_source.tsx index 4dbf5f16b0673..9d02c21d00aab 100644 --- a/x-pack/plugins/maps/public/classes/sources/vector_source/vector_source.tsx +++ b/x-pack/plugins/maps/public/classes/sources/vector_source/vector_source.tsx @@ -69,7 +69,11 @@ export interface IVectorSource extends ISource { getSourceTooltipContent(sourceDataRequest?: DataRequest): SourceTooltipConfig; getTimesliceMaskFieldName(): Promise; supportsFeatureEditing(): Promise; - addFeature(geometry: Geometry | Position[]): Promise; + getDefaultFields(): Promise>>; + addFeature( + geometry: Geometry | Position[], + defaultFields: Record> + ): Promise; deleteFeature(featureId: string): Promise; isFilterByMapBounds(): boolean; } @@ -164,7 +168,10 @@ export class AbstractVectorSource extends AbstractSource implements IVectorSourc return null; } - async addFeature(geometry: Geometry | Position[]) { + async addFeature( + geometry: Geometry | Position[], + defaultFields: Record> + ) { throw new Error('Should implement VectorSource#addFeature'); } @@ -175,4 +182,8 @@ export class AbstractVectorSource extends AbstractSource implements IVectorSourc async supportsFeatureEditing(): Promise { return false; } + + async getDefaultFields(): Promise>> { + return {}; + } } diff --git a/x-pack/plugins/maps/public/kibana_services.ts b/x-pack/plugins/maps/public/kibana_services.ts index 4fce4c276c336..c4340a051a07d 100644 --- a/x-pack/plugins/maps/public/kibana_services.ts +++ b/x-pack/plugins/maps/public/kibana_services.ts @@ -52,6 +52,7 @@ export const getEmbeddableService = () => pluginsStart.embeddable; export const getNavigateToApp = () => coreStart.application.navigateToApp; export const getSavedObjectsTagging = () => pluginsStart.savedObjectsTagging; export const getPresentationUtilContext = () => pluginsStart.presentationUtil.ContextProvider; +export const getSecurityService = () => pluginsStart.security; // xpack.maps.* kibana.yml settings from this plugin let mapAppConfig: MapsConfigType; diff --git a/x-pack/plugins/maps/public/plugin.ts b/x-pack/plugins/maps/public/plugin.ts index b526e7b24d90f..7ee84eb8b67e2 100644 --- a/x-pack/plugins/maps/public/plugin.ts +++ b/x-pack/plugins/maps/public/plugin.ts @@ -71,6 +71,7 @@ import { MapsAppRegionMapLocatorDefinition, MapsAppTileMapLocatorDefinition, } from './locators'; +import { SecurityPluginStart } from '../../security/public'; export interface MapsPluginSetupDependencies { inspector: InspectorSetupContract; @@ -97,6 +98,7 @@ export interface MapsPluginStartDependencies { dashboard: DashboardStart; savedObjectsTagging?: SavedObjectTaggingPluginStart; presentationUtil: PresentationUtilPluginStart; + security: SecurityPluginStart; } /** diff --git a/x-pack/plugins/maps/server/data_indexing/create_doc_source.ts b/x-pack/plugins/maps/server/data_indexing/create_doc_source.ts index 22c3da61244af..cb2e144dc0cf2 100644 --- a/x-pack/plugins/maps/server/data_indexing/create_doc_source.ts +++ b/x-pack/plugins/maps/server/data_indexing/create_doc_source.ts @@ -7,7 +7,7 @@ import { ElasticsearchClient, IScopedClusterClient } from 'kibana/server'; import { - INDEX_META_DATA_CREATED_BY, + MAPS_NEW_VECTOR_LAYER_META_CREATED_BY, CreateDocSourceResp, IndexSourceMappings, BodySettings, @@ -15,9 +15,9 @@ import { import { IndexPatternsCommonService } from '../../../../../src/plugins/data/server'; const DEFAULT_SETTINGS = { number_of_shards: 1 }; -const DEFAULT_MAPPINGS = { +const DEFAULT_META = { _meta: { - created_by: INDEX_META_DATA_CREATED_BY, + created_by: MAPS_NEW_VECTOR_LAYER_META_CREATED_BY, }, }; @@ -50,7 +50,7 @@ async function createIndex( ) { const body: { mappings: IndexSourceMappings; settings: BodySettings } = { mappings: { - ...DEFAULT_MAPPINGS, + ...DEFAULT_META, ...mappings, }, settings: DEFAULT_SETTINGS, diff --git a/x-pack/plugins/maps/server/data_indexing/indexing_routes.ts b/x-pack/plugins/maps/server/data_indexing/indexing_routes.ts index 6cae0d0630f90..52dd1c56d2435 100644 --- a/x-pack/plugins/maps/server/data_indexing/indexing_routes.ts +++ b/x-pack/plugins/maps/server/data_indexing/indexing_routes.ts @@ -14,11 +14,14 @@ import { MAX_DRAWING_SIZE_BYTES, GET_MATCHING_INDEXES_PATH, INDEX_FEATURE_PATH, + CHECK_IS_DRAWING_INDEX, + MAPS_NEW_VECTOR_LAYER_META_CREATED_BY, } from '../../common/constants'; import { createDocSource } from './create_doc_source'; import { writeDataToIndex } from './index_data'; import { PluginStart as DataPluginStart } from '../../../../../src/plugins/data/server'; import { getMatchingIndexes } from './get_indexes_matching_pattern'; +import { SecurityPluginStart } from '../../../security/server'; export function initIndexingRoutes({ router, @@ -28,6 +31,7 @@ export function initIndexingRoutes({ router: IRouter; logger: Logger; dataPlugin: DataPluginStart; + securityPlugin?: SecurityPluginStart; }) { router.post( { @@ -174,4 +178,41 @@ export function initIndexingRoutes({ return response.ok({ body: result }); } ); + + router.get( + { + path: CHECK_IS_DRAWING_INDEX, + validate: { + query: schema.object({ + index: schema.string(), + }), + }, + }, + async (context, request, response) => { + const { index } = request.query; + try { + const { + body: mappingsResp, + } = await context.core.elasticsearch.client.asCurrentUser.indices.getMapping({ + index: request.query.index, + }); + const isDrawingIndex = + mappingsResp[index].mappings?._meta?.created_by === MAPS_NEW_VECTOR_LAYER_META_CREATED_BY; + return response.ok({ + body: { + success: true, + isDrawingIndex, + }, + }); + } catch (error) { + // Index likely doesn't exist + return response.ok({ + body: { + success: false, + error, + }, + }); + } + } + ); } diff --git a/x-pack/test/api_integration/apis/maps/index.js b/x-pack/test/api_integration/apis/maps/index.js index 6d4122163d66a..bd2505905c395 100644 --- a/x-pack/test/api_integration/apis/maps/index.js +++ b/x-pack/test/api_integration/apis/maps/index.js @@ -22,6 +22,7 @@ export default function ({ loadTestFile, getService }) { describe('', () => { loadTestFile(require.resolve('./get_indexes_matching_pattern')); loadTestFile(require.resolve('./create_doc_source')); + loadTestFile(require.resolve('./validate_drawing_index')); loadTestFile(require.resolve('./delete_feature')); loadTestFile(require.resolve('./index_data')); loadTestFile(require.resolve('./fonts_api')); diff --git a/x-pack/test/api_integration/apis/maps/validate_drawing_index.js b/x-pack/test/api_integration/apis/maps/validate_drawing_index.js new file mode 100644 index 0000000000000..491847c37be6d --- /dev/null +++ b/x-pack/test/api_integration/apis/maps/validate_drawing_index.js @@ -0,0 +1,51 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; + +export default function ({ getService }) { + const supertest = getService('supertest'); + + describe('validate drawing index', () => { + it('confirm valid drawing index', async () => { + await supertest + .post(`/api/maps/docSource`) + .set('kbn-xsrf', 'kibana') + .send({ + index: 'valid-drawing-index', + mappings: { properties: { coordinates: { type: 'geo_point' } } }, + }); + + const resp = await supertest + .get(`/api/maps/checkIsDrawingIndex?index=valid-drawing-index`) + .set('kbn-xsrf', 'kibana') + .expect(200); + + expect(resp.body.success).to.be(true); + expect(resp.body.isDrawingIndex).to.be(true); + }); + + it('confirm valid index that is not a drawing index', async () => { + const resp = await supertest + .get(`/api/maps/checkIsDrawingIndex?index=geo_shapes`) + .set('kbn-xsrf', 'kibana') + .expect(200); + + expect(resp.body.success).to.be(true); + expect(resp.body.isDrawingIndex).to.be(false); + }); + + it('confirm invalid index', async () => { + const resp = await supertest + .get(`/api/maps/checkIsDrawingIndex?index=not-an-index`) + .set('kbn-xsrf', 'kibana') + .expect(200); + + expect(resp.body.success).to.be(false); + }); + }); +} From 36bba6ffe0aa4ab532ed857946205ee98ba2d5a4 Mon Sep 17 00:00:00 2001 From: Ryan Keairns Date: Mon, 16 Aug 2021 14:54:22 -0500 Subject: [PATCH 04/31] Update Analytics overview page to new empty state template (#108532) * Use empty state page template * Remove unused translations * Fixed snaps * Use docLinks service * Fix test * Revert "Use docLinks service" Use exisiting docLinks.ELASTIC_WEBSITE_URL instead * Update learn more link and test * fix test Co-authored-by: cchaos Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../getting_started.test.tsx.snap | 391 ------------------ .../getting_started/getting_started.test.tsx | 107 ----- .../getting_started/getting_started.tsx | 115 ------ .../components/getting_started/index.ts | 9 - .../__snapshots__/overview.test.tsx.snap | 70 ++++ .../components/overview/overview.test.tsx | 5 + .../public/components/overview/overview.tsx | 223 +++++----- .../translations/translations/ja-JP.json | 3 - .../translations/translations/zh-CN.json | 3 - 9 files changed, 190 insertions(+), 736 deletions(-) delete mode 100644 src/plugins/kibana_overview/public/components/getting_started/__snapshots__/getting_started.test.tsx.snap delete mode 100644 src/plugins/kibana_overview/public/components/getting_started/getting_started.test.tsx delete mode 100644 src/plugins/kibana_overview/public/components/getting_started/getting_started.tsx delete mode 100644 src/plugins/kibana_overview/public/components/getting_started/index.ts diff --git a/src/plugins/kibana_overview/public/components/getting_started/__snapshots__/getting_started.test.tsx.snap b/src/plugins/kibana_overview/public/components/getting_started/__snapshots__/getting_started.test.tsx.snap deleted file mode 100644 index b5f501eb88a01..0000000000000 --- a/src/plugins/kibana_overview/public/components/getting_started/__snapshots__/getting_started.test.tsx.snap +++ /dev/null @@ -1,391 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`GettingStarted dark mode on 1`] = ` -
- - -
- -

- -

-
- - -

- -

-
- - - - - } - layout="horizontal" - paddingSize="none" - title="Dashboard" - titleElement="h3" - titleSize="xs" - /> - - - - } - layout="horizontal" - paddingSize="none" - title="Discover" - titleElement="h3" - titleSize="xs" - /> - - - - } - layout="horizontal" - paddingSize="none" - title="Canvas" - titleElement="h3" - titleSize="xs" - /> - - - - } - layout="horizontal" - paddingSize="none" - title="Maps" - titleElement="h3" - titleSize="xs" - /> - - - - } - layout="horizontal" - paddingSize="none" - title="Machine Learning" - titleElement="h3" - titleSize="xs" - /> - - - - } - layout="horizontal" - paddingSize="none" - title="Graph" - titleElement="h3" - titleSize="xs" - /> - - - - - - - - -
-
- - - -
-
-`; - -exports[`GettingStarted render 1`] = ` -
- - -
- -

- -

-
- - -

- -

-
- - - - - } - layout="horizontal" - paddingSize="none" - title="Dashboard" - titleElement="h3" - titleSize="xs" - /> - - - - } - layout="horizontal" - paddingSize="none" - title="Discover" - titleElement="h3" - titleSize="xs" - /> - - - - } - layout="horizontal" - paddingSize="none" - title="Canvas" - titleElement="h3" - titleSize="xs" - /> - - - - } - layout="horizontal" - paddingSize="none" - title="Maps" - titleElement="h3" - titleSize="xs" - /> - - - - } - layout="horizontal" - paddingSize="none" - title="Machine Learning" - titleElement="h3" - titleSize="xs" - /> - - - - } - layout="horizontal" - paddingSize="none" - title="Graph" - titleElement="h3" - titleSize="xs" - /> - - - - - - - - -
-
- - - -
-
-`; diff --git a/src/plugins/kibana_overview/public/components/getting_started/getting_started.test.tsx b/src/plugins/kibana_overview/public/components/getting_started/getting_started.test.tsx deleted file mode 100644 index a2ce059f913f4..0000000000000 --- a/src/plugins/kibana_overview/public/components/getting_started/getting_started.test.tsx +++ /dev/null @@ -1,107 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import React from 'react'; -import { GettingStarted } from './getting_started'; -import { shallowWithIntl } from '@kbn/test/jest'; -import { FeatureCatalogueCategory } from 'src/plugins/home/public'; - -const addBasePathMock = jest.fn((path: string) => (path ? path : 'path')); - -const mockApps = [ - { - category: FeatureCatalogueCategory.DATA, - description: 'Display and share a collection of visualizations and saved searches.', - icon: 'dashboardApp', - id: 'dashboard', - order: 100, - path: 'path-to-dashboard', - showOnHomePage: false, - solutionId: 'kibana', - subtitle: 'Analyze data in dashboards.', - title: 'Dashboard', - }, - { - category: FeatureCatalogueCategory.DATA, - description: 'Interactively explore your data by querying and filtering raw documents.', - icon: 'discoverApp', - id: 'discover', - order: 200, - path: 'path-to-discover', - - showOnHomePage: false, - solutionId: 'kibana', - subtitle: 'Search and find insights.', - title: 'Discover', - }, - { - category: FeatureCatalogueCategory.DATA, - description: 'Showcase your data in a pixel-perfect way.', - icon: 'canvasApp', - id: 'canvas', - order: 300, - path: 'path-to-canvas', - - showOnHomePage: false, - solutionId: 'kibana', - subtitle: 'Design pixel-perfect reports.', - title: 'Canvas', - }, - { - category: FeatureCatalogueCategory.DATA, - description: 'Explore geospatial data from Elasticsearch and the Elastic Maps Service.', - icon: 'gisApp', - id: 'maps', - order: 400, - path: 'path-to-maps', - showOnHomePage: false, - solutionId: 'kibana', - subtitle: 'Plot geographic data.', - title: 'Maps', - }, - { - category: FeatureCatalogueCategory.DATA, - description: - 'Automatically model the normal behavior of your time series data to detect anomalies.', - icon: 'machineLearningApp', - id: 'ml', - order: 500, - path: 'path-to-ml', - showOnHomePage: false, - solutionId: 'kibana', - subtitle: 'Model, predict, and detect.', - title: 'Machine Learning', - }, - { - category: FeatureCatalogueCategory.DATA, - description: 'Surface and analyze relevant relationships in your Elasticsearch data.', - icon: 'graphApp', - id: 'graph', - order: 600, - path: 'path-to-graph', - showOnHomePage: false, - solutionId: 'kibana', - subtitle: 'Reveal patterns and relationships.', - title: 'Graph', - }, -]; - -describe('GettingStarted', () => { - test('render', () => { - const component = shallowWithIntl( - - ); - expect(component).toMatchSnapshot(); - }); - test('dark mode on', () => { - const component = shallowWithIntl( - - ); - expect(component).toMatchSnapshot(); - }); -}); diff --git a/src/plugins/kibana_overview/public/components/getting_started/getting_started.tsx b/src/plugins/kibana_overview/public/components/getting_started/getting_started.tsx deleted file mode 100644 index 56247c4e91fa1..0000000000000 --- a/src/plugins/kibana_overview/public/components/getting_started/getting_started.tsx +++ /dev/null @@ -1,115 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import React, { FC } from 'react'; -import { - EuiButton, - EuiCard, - EuiFlexGrid, - EuiFlexGroup, - EuiFlexItem, - EuiIcon, - EuiImage, - EuiSpacer, - EuiText, - EuiTitle, -} from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { CoreStart } from 'kibana/public'; -import { RedirectAppLinks, useKibana } from '../../../../../../src/plugins/kibana_react/public'; -import { FeatureCatalogueEntry } from '../../../../../../src/plugins/home/public'; -import { PLUGIN_ID } from '../../../common'; - -interface Props { - addBasePath: (path: string) => string; - isDarkTheme: boolean; - apps: FeatureCatalogueEntry[]; -} - -export const GettingStarted: FC = ({ addBasePath, isDarkTheme, apps }) => { - const { - services: { application }, - } = useKibana(); - const gettingStartedGraphicURL = `/plugins/${PLUGIN_ID}/assets/kibana_montage_${ - isDarkTheme ? 'dark' : 'light' - }.svg`; - - return ( -
- - -
- -

- -

-
- - - - -

- -

-
- - - - - {apps.map(({ subtitle = '', icon, title }) => ( - - } - layout="horizontal" - paddingSize="none" - title={title} - titleElement="h3" - titleSize="xs" - /> - - ))} - - - - - - - - - -
-
- - - - -
-
- ); -}; diff --git a/src/plugins/kibana_overview/public/components/getting_started/index.ts b/src/plugins/kibana_overview/public/components/getting_started/index.ts deleted file mode 100644 index 10977941afeee..0000000000000 --- a/src/plugins/kibana_overview/public/components/getting_started/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -export * from './getting_started'; diff --git a/src/plugins/kibana_overview/public/components/overview/__snapshots__/overview.test.tsx.snap b/src/plugins/kibana_overview/public/components/overview/__snapshots__/overview.test.tsx.snap index a32e27050fad1..6581fcd88cb77 100644 --- a/src/plugins/kibana_overview/public/components/overview/__snapshots__/overview.test.tsx.snap +++ b/src/plugins/kibana_overview/public/components/overview/__snapshots__/overview.test.tsx.snap @@ -233,6 +233,9 @@ exports[`Overview render 1`] = ` addBasePath={ [MockFunction] { "calls": Array [ + Array [ + "home#/tutorial_directory", + ], Array [ "kibana_landing_page", ], @@ -259,6 +262,10 @@ exports[`Overview render 1`] = ` ], ], "results": Array [ + Object { + "type": "return", + "value": "home#/tutorial_directory", + }, Object { "type": "return", "value": "kibana_landing_page", @@ -534,6 +541,9 @@ exports[`Overview without features 1`] = ` addBasePath={ [MockFunction] { "calls": Array [ + Array [ + "home#/tutorial_directory", + ], Array [ "kibana_landing_page", ], @@ -558,6 +568,12 @@ exports[`Overview without features 1`] = ` Array [ "/plugins/kibanaOverview/assets/solutions_solution_4_2x.png", ], + Array [ + "home#/tutorial_directory", + ], + Array [ + "home#/tutorial_directory", + ], Array [ "kibana_landing_page", ], @@ -584,6 +600,10 @@ exports[`Overview without features 1`] = ` ], ], "results": Array [ + Object { + "type": "return", + "value": "home#/tutorial_directory", + }, Object { "type": "return", "value": "kibana_landing_page", @@ -616,6 +636,14 @@ exports[`Overview without features 1`] = ` "type": "return", "value": "/plugins/kibanaOverview/assets/solutions_solution_4_2x.png", }, + Object { + "type": "return", + "value": "home#/tutorial_directory", + }, + Object { + "type": "return", + "value": "home#/tutorial_directory", + }, Object { "type": "return", "value": "kibana_landing_page", @@ -760,6 +788,9 @@ exports[`Overview without solutions 1`] = ` addBasePath={ [MockFunction] { "calls": Array [ + Array [ + "home#/tutorial_directory", + ], Array [ "kibana_landing_page", ], @@ -784,8 +815,15 @@ exports[`Overview without solutions 1`] = ` Array [ "/plugins/kibanaOverview/assets/solutions_solution_4_2x.png", ], + Array [ + "home#/tutorial_directory", + ], ], "results": Array [ + Object { + "type": "return", + "value": "home#/tutorial_directory", + }, Object { "type": "return", "value": "kibana_landing_page", @@ -818,6 +856,10 @@ exports[`Overview without solutions 1`] = ` "type": "return", "value": "/plugins/kibanaOverview/assets/solutions_solution_4_2x.png", }, + Object { + "type": "return", + "value": "home#/tutorial_directory", + }, ], } } @@ -829,6 +871,9 @@ exports[`Overview without solutions 1`] = ` addBasePath={ [MockFunction] { "calls": Array [ + Array [ + "home#/tutorial_directory", + ], Array [ "kibana_landing_page", ], @@ -853,8 +898,15 @@ exports[`Overview without solutions 1`] = ` Array [ "/plugins/kibanaOverview/assets/solutions_solution_4_2x.png", ], + Array [ + "home#/tutorial_directory", + ], ], "results": Array [ + Object { + "type": "return", + "value": "home#/tutorial_directory", + }, Object { "type": "return", "value": "kibana_landing_page", @@ -887,6 +939,10 @@ exports[`Overview without solutions 1`] = ` "type": "return", "value": "/plugins/kibanaOverview/assets/solutions_solution_4_2x.png", }, + Object { + "type": "return", + "value": "home#/tutorial_directory", + }, ], } } @@ -904,6 +960,9 @@ exports[`Overview without solutions 1`] = ` addBasePath={ [MockFunction] { "calls": Array [ + Array [ + "home#/tutorial_directory", + ], Array [ "kibana_landing_page", ], @@ -928,8 +987,15 @@ exports[`Overview without solutions 1`] = ` Array [ "/plugins/kibanaOverview/assets/solutions_solution_4_2x.png", ], + Array [ + "home#/tutorial_directory", + ], ], "results": Array [ + Object { + "type": "return", + "value": "home#/tutorial_directory", + }, Object { "type": "return", "value": "kibana_landing_page", @@ -962,6 +1028,10 @@ exports[`Overview without solutions 1`] = ` "type": "return", "value": "/plugins/kibanaOverview/assets/solutions_solution_4_2x.png", }, + Object { + "type": "return", + "value": "home#/tutorial_directory", + }, ], } } diff --git a/src/plugins/kibana_overview/public/components/overview/overview.test.tsx b/src/plugins/kibana_overview/public/components/overview/overview.test.tsx index 9d260469625ad..6320b5070a8a6 100644 --- a/src/plugins/kibana_overview/public/components/overview/overview.test.tsx +++ b/src/plugins/kibana_overview/public/components/overview/overview.test.tsx @@ -18,6 +18,11 @@ jest.mock('../../../../../../src/plugins/kibana_react/public', () => ({ http: { basePath: { prepend: jest.fn((path: string) => (path ? path : 'path')) } }, data: { indexPatterns: {} }, uiSettings: { get: jest.fn() }, + docLinks: { + links: { + kibana: 'kibana_docs_url', + }, + }, }, }), RedirectAppLinks: jest.fn((element: JSX.Element) => element), diff --git a/src/plugins/kibana_overview/public/components/overview/overview.tsx b/src/plugins/kibana_overview/public/components/overview/overview.tsx index 9bba923371f64..a037cbc214266 100644 --- a/src/plugins/kibana_overview/public/components/overview/overview.tsx +++ b/src/plugins/kibana_overview/public/components/overview/overview.tsx @@ -24,6 +24,7 @@ import { RedirectAppLinks, useKibana, KibanaPageTemplate, + KibanaPageTemplateProps, overviewPageActions, OverviewPageFooter, } from '../../../../../../src/plugins/kibana_react/public'; @@ -36,7 +37,6 @@ import { import { PLUGIN_ID, PLUGIN_PATH } from '../../../common'; import { AppPluginStartDependencies } from '../../types'; import { AddData } from '../add_data'; -import { GettingStarted } from '../getting_started'; import { ManageData } from '../manage_data'; import { NewsFeed } from '../news_feed'; import { METRIC_TYPE, trackUiMetric } from '../../lib/ui_metric'; @@ -53,7 +53,7 @@ interface Props { export const Overview: FC = ({ newsFetchResult, solutions, features }) => { const [isNewKibanaInstance, setNewKibanaInstance] = useState(false); const { - services: { http, data, uiSettings, application }, + services: { http, docLinks, data, uiSettings, application }, } = useKibana(); const addBasePath = http.basePath.prepend; const indexPatternService = data.indexPatterns; @@ -72,6 +72,16 @@ export const Overview: FC = ({ newsFetchResult, solutions, features }) => const addDataFeatures = getFeaturesByCategory(FeatureCatalogueCategory.DATA); const manageDataFeatures = getFeaturesByCategory(FeatureCatalogueCategory.ADMIN); const devTools = findFeatureById('console'); + const noDataConfig: KibanaPageTemplateProps['noDataConfig'] = { + solution: 'Analytics', + logo: 'logoKibana', + actions: { + beats: { + href: addBasePath(`home#/tutorial_directory`), + }, + }, + docsLink: docLinks.links.kibana, + }; // Show card for console if none of the manage data plugins are available, most likely in OSS if (manageDataFeatures.length < 1 && devTools) { @@ -127,126 +137,123 @@ export const Overview: FC = ({ newsFetchResult, solutions, features }) => hidden: isNewKibanaInstance, }), }} + noDataConfig={isNewKibanaInstance ? noDataConfig : undefined} template="empty" > - {isNewKibanaInstance ? ( - - ) : ( - <> -
- -

- -

-
- - {mainApps.length ? ( - <> - - {mainApps.map(renderAppCard)} - + <> +
+ +

+ +

+
- - - ) : null} - - {remainingApps.length ? ( + {mainApps.length ? ( + <> - {remainingApps.map(renderAppCard)} + {mainApps.map(renderAppCard)} - ) : null} -
-
- - {newsFetchResult && newsFetchResult.feedItems.length ? ( - - - - ) : null} + +