;
export interface ExternalServiceCommentResponse {
commentId: string;
@@ -29,18 +27,6 @@ export interface ExternalServiceCommentResponse {
externalCommentId?: string;
}
-export type ExecutorSubActionGetIncidentParams = TypeOf<
- typeof ExecutorSubActionGetIncidentParamsSchema
->;
-
-export type ExecutorSubActionHandshakeParams = TypeOf<
- typeof ExecutorSubActionHandshakeParamsSchema
->;
-
-export interface PushToServiceResponse extends ExternalServiceIncidentResponse {
- comments?: ExternalServiceCommentResponse[];
-}
-
export interface PipedField {
key: string;
value: string;
@@ -48,10 +34,10 @@ export interface PipedField {
pipes: string[];
}
-export interface TransformFieldsArgs {
- params: PushToServiceApiParams;
+export interface TransformFieldsArgs {
+ params: P;
fields: PipedField[];
- currentIncident?: ExternalServiceParams;
+ currentIncident?: S;
}
export interface TransformerArgs {
diff --git a/x-pack/plugins/actions/server/builtin_action_types/case/utils.ts b/x-pack/plugins/actions/server/builtin_action_types/case/utils.ts
index d895bf386a367..701bbea14fde8 100644
--- a/x-pack/plugins/actions/server/builtin_action_types/case/utils.ts
+++ b/x-pack/plugins/actions/server/builtin_action_types/case/utils.ts
@@ -51,10 +51,7 @@ export const buildMap = (mapping: MapRecord[]): Map => {
}, new Map());
};
-export const mapParams = (
- params: Partial,
- mapping: Map
-): AnyParams => {
+export const mapParams = (params: T, mapping: Map): AnyParams => {
return Object.keys(params).reduce((prev: AnyParams, curr: string): AnyParams => {
const field = mapping.get(curr);
if (field) {
@@ -106,7 +103,10 @@ export const createConnectorExecutor = ({
const { comments, externalId, ...restParams } = pushToServiceParams;
const mapping = buildMap(config.casesConfiguration.mapping);
- const externalCase = mapParams(restParams, mapping);
+ const externalCase = mapParams(
+ restParams as ExecutorSubActionPushParams,
+ mapping
+ );
data = await api.pushToService({
externalService,
diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/api.test.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/api.test.ts
index bcfb82077d286..4495c37f758ee 100644
--- a/x-pack/plugins/actions/server/builtin_action_types/jira/api.test.ts
+++ b/x-pack/plugins/actions/server/builtin_action_types/jira/api.test.ts
@@ -4,15 +4,18 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { api } from '../case/api';
+import { Logger } from '../../../../../../src/core/server';
import { externalServiceMock, mapping, apiParams } from './mocks';
-import { ExternalService } from '../case/types';
+import { ExternalService } from './types';
+import { api } from './api';
+let mockedLogger: jest.Mocked;
describe('api', () => {
let externalService: jest.Mocked;
beforeEach(() => {
externalService = externalServiceMock.create();
+ jest.clearAllMocks();
});
afterEach(() => {
@@ -20,10 +23,15 @@ describe('api', () => {
});
describe('pushToService', () => {
- describe('create incident', () => {
+ describe('create incident - cases', () => {
test('it creates an incident', async () => {
const params = { ...apiParams, externalId: null };
- const res = await api.pushToService({ externalService, mapping, params });
+ const res = await api.pushToService({
+ externalService,
+ mapping,
+ params,
+ logger: mockedLogger,
+ });
expect(res).toEqual({
id: 'incident-1',
@@ -45,7 +53,12 @@ describe('api', () => {
test('it creates an incident without comments', async () => {
const params = { ...apiParams, externalId: null, comments: [] };
- const res = await api.pushToService({ externalService, mapping, params });
+ const res = await api.pushToService({
+ externalService,
+ mapping,
+ params,
+ logger: mockedLogger,
+ });
expect(res).toEqual({
id: 'incident-1',
@@ -57,7 +70,7 @@ describe('api', () => {
test('it calls createIncident correctly', async () => {
const params = { ...apiParams, externalId: null };
- await api.pushToService({ externalService, mapping, params });
+ await api.pushToService({ externalService, mapping, params, logger: mockedLogger });
expect(externalService.createIncident).toHaveBeenCalledWith({
incident: {
@@ -69,9 +82,25 @@ describe('api', () => {
expect(externalService.updateIncident).not.toHaveBeenCalled();
});
+ test('it calls createIncident correctly without mapping', async () => {
+ const params = { ...apiParams, externalId: null };
+ await api.pushToService({ externalService, mapping: null, params, logger: mockedLogger });
+
+ expect(externalService.createIncident).toHaveBeenCalledWith({
+ incident: {
+ description: 'Incident description',
+ summary: 'Incident title',
+ issueType: '10006',
+ labels: ['kibana', 'elastic'],
+ priority: 'High',
+ },
+ });
+ expect(externalService.updateIncident).not.toHaveBeenCalled();
+ });
+
test('it calls createComment correctly', async () => {
const params = { ...apiParams, externalId: null };
- await api.pushToService({ externalService, mapping, params });
+ await api.pushToService({ externalService, mapping, params, logger: mockedLogger });
expect(externalService.createComment).toHaveBeenCalledTimes(2);
expect(externalService.createComment).toHaveBeenNthCalledWith(1, {
incidentId: 'incident-1',
@@ -89,7 +118,6 @@ describe('api', () => {
username: 'elastic',
},
},
- field: 'comments',
});
expect(externalService.createComment).toHaveBeenNthCalledWith(2, {
@@ -108,14 +136,59 @@ describe('api', () => {
username: 'elastic',
},
},
- field: 'comments',
+ });
+ });
+
+ test('it calls createComment correctly without mapping', async () => {
+ const params = { ...apiParams, externalId: null };
+ await api.pushToService({ externalService, mapping: null, params, logger: mockedLogger });
+ expect(externalService.createComment).toHaveBeenCalledTimes(2);
+ expect(externalService.createComment).toHaveBeenNthCalledWith(1, {
+ incidentId: 'incident-1',
+ comment: {
+ commentId: 'case-comment-1',
+ comment: 'A comment',
+ createdAt: '2020-04-27T10:59:46.202Z',
+ createdBy: {
+ fullName: 'Elastic User',
+ username: 'elastic',
+ },
+ updatedAt: '2020-04-27T10:59:46.202Z',
+ updatedBy: {
+ fullName: 'Elastic User',
+ username: 'elastic',
+ },
+ },
+ });
+
+ expect(externalService.createComment).toHaveBeenNthCalledWith(2, {
+ incidentId: 'incident-1',
+ comment: {
+ commentId: 'case-comment-2',
+ comment: 'Another comment',
+ createdAt: '2020-04-27T10:59:46.202Z',
+ createdBy: {
+ fullName: 'Elastic User',
+ username: 'elastic',
+ },
+ updatedAt: '2020-04-27T10:59:46.202Z',
+ updatedBy: {
+ fullName: 'Elastic User',
+ username: 'elastic',
+ },
+ },
});
});
});
describe('update incident', () => {
test('it updates an incident', async () => {
- const res = await api.pushToService({ externalService, mapping, params: apiParams });
+ const res = await api.pushToService({
+ externalService,
+ mapping,
+ params: apiParams,
+ logger: mockedLogger,
+ });
expect(res).toEqual({
id: 'incident-1',
@@ -137,7 +210,12 @@ describe('api', () => {
test('it updates an incident without comments', async () => {
const params = { ...apiParams, comments: [] };
- const res = await api.pushToService({ externalService, mapping, params });
+ const res = await api.pushToService({
+ externalService,
+ mapping,
+ params,
+ logger: mockedLogger,
+ });
expect(res).toEqual({
id: 'incident-1',
@@ -149,7 +227,7 @@ describe('api', () => {
test('it calls updateIncident correctly', async () => {
const params = { ...apiParams };
- await api.pushToService({ externalService, mapping, params });
+ await api.pushToService({ externalService, mapping, params, logger: mockedLogger });
expect(externalService.updateIncident).toHaveBeenCalledWith({
incidentId: 'incident-3',
@@ -162,9 +240,26 @@ describe('api', () => {
expect(externalService.createIncident).not.toHaveBeenCalled();
});
+ test('it calls updateIncident correctly without mapping', async () => {
+ const params = { ...apiParams };
+ await api.pushToService({ externalService, mapping: null, params, logger: mockedLogger });
+
+ expect(externalService.updateIncident).toHaveBeenCalledWith({
+ incidentId: 'incident-3',
+ incident: {
+ description: 'Incident description',
+ summary: 'Incident title',
+ issueType: '10006',
+ labels: ['kibana', 'elastic'],
+ priority: 'High',
+ },
+ });
+ expect(externalService.createIncident).not.toHaveBeenCalled();
+ });
+
test('it calls createComment correctly', async () => {
const params = { ...apiParams };
- await api.pushToService({ externalService, mapping, params });
+ await api.pushToService({ externalService, mapping, params, logger: mockedLogger });
expect(externalService.createComment).toHaveBeenCalledTimes(2);
expect(externalService.createComment).toHaveBeenNthCalledWith(1, {
incidentId: 'incident-1',
@@ -182,7 +277,6 @@ describe('api', () => {
username: 'elastic',
},
},
- field: 'comments',
});
expect(externalService.createComment).toHaveBeenNthCalledWith(2, {
@@ -201,7 +295,87 @@ describe('api', () => {
username: 'elastic',
},
},
- field: 'comments',
+ });
+ });
+
+ test('it calls createComment correctly without mapping', async () => {
+ const params = { ...apiParams };
+ await api.pushToService({ externalService, mapping: null, params, logger: mockedLogger });
+ expect(externalService.createComment).toHaveBeenCalledTimes(2);
+ expect(externalService.createComment).toHaveBeenNthCalledWith(1, {
+ incidentId: 'incident-1',
+ comment: {
+ commentId: 'case-comment-1',
+ comment: 'A comment',
+ createdAt: '2020-04-27T10:59:46.202Z',
+ createdBy: {
+ fullName: 'Elastic User',
+ username: 'elastic',
+ },
+ updatedAt: '2020-04-27T10:59:46.202Z',
+ updatedBy: {
+ fullName: 'Elastic User',
+ username: 'elastic',
+ },
+ },
+ });
+
+ expect(externalService.createComment).toHaveBeenNthCalledWith(2, {
+ incidentId: 'incident-1',
+ comment: {
+ commentId: 'case-comment-2',
+ comment: 'Another comment',
+ createdAt: '2020-04-27T10:59:46.202Z',
+ createdBy: {
+ fullName: 'Elastic User',
+ username: 'elastic',
+ },
+ updatedAt: '2020-04-27T10:59:46.202Z',
+ updatedBy: {
+ fullName: 'Elastic User',
+ username: 'elastic',
+ },
+ },
+ });
+ });
+ });
+
+ describe('issueTypes', () => {
+ test('it returns the issue types correctly', async () => {
+ const res = await api.issueTypes({
+ externalService,
+ params: {},
+ });
+ expect(res).toEqual([
+ {
+ id: '10006',
+ name: 'Task',
+ },
+ {
+ id: '10007',
+ name: 'Bug',
+ },
+ ]);
+ });
+ });
+
+ describe('fieldsByIssueType', () => {
+ test('it returns the fields correctly', async () => {
+ const res = await api.fieldsByIssueType({
+ externalService,
+ params: { id: '10006' },
+ });
+ expect(res).toEqual({
+ summary: { allowedValues: [], defaultValue: {} },
+ priority: {
+ allowedValues: [
+ {
+ name: 'Medium',
+ id: '3',
+ },
+ ],
+ defaultValue: { name: 'Medium', id: '3' },
+ },
});
});
});
@@ -228,7 +402,12 @@ describe('api', () => {
actionType: 'overwrite',
});
- await api.pushToService({ externalService, mapping, params: apiParams });
+ await api.pushToService({
+ externalService,
+ mapping,
+ params: apiParams,
+ logger: mockedLogger,
+ });
expect(externalService.updateIncident).toHaveBeenCalledWith({
incidentId: 'incident-3',
incident: {
@@ -260,7 +439,12 @@ describe('api', () => {
actionType: 'nothing',
});
- await api.pushToService({ externalService, mapping, params: apiParams });
+ await api.pushToService({
+ externalService,
+ mapping,
+ params: apiParams,
+ logger: mockedLogger,
+ });
expect(externalService.updateIncident).toHaveBeenCalledWith({
incidentId: 'incident-3',
incident: {
@@ -291,7 +475,12 @@ describe('api', () => {
actionType: 'append',
});
- await api.pushToService({ externalService, mapping, params: apiParams });
+ await api.pushToService({
+ externalService,
+ mapping,
+ params: apiParams,
+ logger: mockedLogger,
+ });
expect(externalService.updateIncident).toHaveBeenCalledWith({
incidentId: 'incident-3',
incident: {
@@ -324,7 +513,12 @@ describe('api', () => {
actionType: 'nothing',
});
- await api.pushToService({ externalService, mapping, params: apiParams });
+ await api.pushToService({
+ externalService,
+ mapping,
+ params: apiParams,
+ logger: mockedLogger,
+ });
expect(externalService.updateIncident).toHaveBeenCalledWith({
incidentId: 'incident-3',
incident: {},
@@ -352,7 +546,12 @@ describe('api', () => {
actionType: 'overwrite',
});
- await api.pushToService({ externalService, mapping, params: apiParams });
+ await api.pushToService({
+ externalService,
+ mapping,
+ params: apiParams,
+ logger: mockedLogger,
+ });
expect(externalService.updateIncident).toHaveBeenCalledWith({
incidentId: 'incident-3',
incident: {
@@ -382,7 +581,12 @@ describe('api', () => {
actionType: 'overwrite',
});
- await api.pushToService({ externalService, mapping, params: apiParams });
+ await api.pushToService({
+ externalService,
+ mapping,
+ params: apiParams,
+ logger: mockedLogger,
+ });
expect(externalService.updateIncident).toHaveBeenCalledWith({
incidentId: 'incident-3',
incident: {
@@ -414,7 +618,12 @@ describe('api', () => {
actionType: 'nothing',
});
- await api.pushToService({ externalService, mapping, params: apiParams });
+ await api.pushToService({
+ externalService,
+ mapping,
+ params: apiParams,
+ logger: mockedLogger,
+ });
expect(externalService.updateIncident).toHaveBeenCalledWith({
incidentId: 'incident-3',
incident: {
@@ -445,7 +654,12 @@ describe('api', () => {
actionType: 'append',
});
- await api.pushToService({ externalService, mapping, params: apiParams });
+ await api.pushToService({
+ externalService,
+ mapping,
+ params: apiParams,
+ logger: mockedLogger,
+ });
expect(externalService.updateIncident).toHaveBeenCalledWith({
incidentId: 'incident-3',
incident: {
@@ -478,7 +692,12 @@ describe('api', () => {
actionType: 'append',
});
- await api.pushToService({ externalService, mapping, params: apiParams });
+ await api.pushToService({
+ externalService,
+ mapping,
+ params: apiParams,
+ logger: mockedLogger,
+ });
expect(externalService.updateIncident).toHaveBeenCalledWith({
incidentId: 'incident-3',
incident: {
@@ -509,7 +728,12 @@ describe('api', () => {
actionType: 'overwrite',
});
- await api.pushToService({ externalService, mapping, params: apiParams });
+ await api.pushToService({
+ externalService,
+ mapping,
+ params: apiParams,
+ logger: mockedLogger,
+ });
expect(externalService.createComment).not.toHaveBeenCalled();
});
});
diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/api.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/api.ts
index 3db66e5884af4..da47a4bfb839b 100644
--- a/x-pack/plugins/actions/server/builtin_action_types/jira/api.ts
+++ b/x-pack/plugins/actions/server/builtin_action_types/jira/api.ts
@@ -4,4 +4,179 @@
* you may not use this file except in compliance with the Elastic License.
*/
-export { api } from '../case/api';
+import { flow } from 'lodash';
+import {
+ ExternalServiceParams,
+ PushToServiceApiHandlerArgs,
+ HandshakeApiHandlerArgs,
+ GetIncidentApiHandlerArgs,
+ ExternalServiceApi,
+ Incident,
+ GetFieldsByIssueTypeHandlerArgs,
+ GetIssueTypesHandlerArgs,
+ PushToServiceApiParams,
+} from './types';
+
+// TODO: to remove, need to support Case
+import { transformers } from '../case/transformers';
+import { TransformFieldsArgs, Comment, EntityInformation } from '../case/common_types';
+
+import { PushToServiceResponse } from './types';
+import { prepareFieldsForTransformation } from '../case/utils';
+
+const handshakeHandler = async ({
+ externalService,
+ mapping,
+ params,
+}: HandshakeApiHandlerArgs) => {};
+
+const getIncidentHandler = async ({
+ externalService,
+ mapping,
+ params,
+}: GetIncidentApiHandlerArgs) => {};
+
+const getIssueTypesHandler = async ({ externalService }: GetIssueTypesHandlerArgs) => {
+ const res = await externalService.getIssueTypes();
+ return res;
+};
+
+const getFieldsByIssueTypeHandler = async ({
+ externalService,
+ params,
+}: GetFieldsByIssueTypeHandlerArgs) => {
+ const { id } = params;
+ const res = await externalService.getFieldsByIssueType(id);
+ return res;
+};
+
+const pushToServiceHandler = async ({
+ externalService,
+ mapping,
+ params,
+ logger,
+}: PushToServiceApiHandlerArgs): Promise => {
+ const { externalId, comments } = params;
+ const updateIncident = externalId ? true : false;
+ const defaultPipes = updateIncident ? ['informationUpdated'] : ['informationCreated'];
+ let currentIncident: ExternalServiceParams | undefined;
+ let res: PushToServiceResponse;
+
+ if (externalId) {
+ try {
+ currentIncident = await externalService.getIncident(externalId);
+ } catch (ex) {
+ logger.debug(
+ `Retrieving Incident by id ${externalId} from Jira failed with exception: ${ex}`
+ );
+ }
+ }
+
+ let incident: Incident;
+ // TODO: should be removed later but currently keep it for the Case implementation support
+ if (mapping) {
+ const fields = prepareFieldsForTransformation({
+ externalCase: params.externalObject,
+ mapping,
+ defaultPipes,
+ });
+
+ incident = transformFields({
+ params,
+ fields,
+ currentIncident,
+ });
+ } else {
+ const { title, description, priority, labels, issueType } = params;
+ incident = { summary: title, description, priority, labels, issueType };
+ }
+
+ if (externalId != null) {
+ res = await externalService.updateIncident({
+ incidentId: externalId,
+ incident,
+ });
+ } else {
+ res = await externalService.createIncident({
+ incident: {
+ ...incident,
+ },
+ });
+ }
+
+ if (comments && Array.isArray(comments) && comments.length > 0) {
+ if (mapping && mapping.get('comments')?.actionType === 'nothing') {
+ return res;
+ }
+
+ const commentsTransformed = mapping
+ ? transformComments(comments, ['informationAdded'])
+ : comments;
+
+ res.comments = [];
+ for (const currentComment of commentsTransformed) {
+ const comment = await externalService.createComment({
+ incidentId: res.id,
+ comment: currentComment,
+ });
+ res.comments = [
+ ...(res.comments ?? []),
+ {
+ commentId: comment.commentId,
+ pushedDate: comment.pushedDate,
+ },
+ ];
+ }
+ }
+
+ return res;
+};
+
+export const transformFields = ({
+ params,
+ fields,
+ currentIncident,
+}: TransformFieldsArgs): Incident => {
+ return fields.reduce((prev, cur) => {
+ const transform = flow(...cur.pipes.map((p) => transformers[p]));
+ return {
+ ...prev,
+ [cur.key]: transform({
+ value: cur.value,
+ date: params.updatedAt ?? params.createdAt,
+ user: getEntity(params),
+ previousValue: currentIncident ? currentIncident[cur.key] : '',
+ }).value,
+ };
+ }, {} as Incident);
+};
+
+export const transformComments = (comments: Comment[], pipes: string[]): Comment[] => {
+ return comments.map((c) => ({
+ ...c,
+ comment: flow(...pipes.map((p) => transformers[p]))({
+ value: c.comment,
+ date: c.updatedAt ?? c.createdAt,
+ user: getEntity(c),
+ }).value,
+ }));
+};
+
+export const getEntity = (entity: EntityInformation): string =>
+ (entity.updatedBy != null
+ ? entity.updatedBy.fullName
+ ? entity.updatedBy.fullName
+ : entity.updatedBy.username
+ : entity.createdBy != null
+ ? entity.createdBy.fullName
+ ? entity.createdBy.fullName
+ : entity.createdBy.username
+ : '') ?? '';
+
+export const api: ExternalServiceApi = {
+ handshake: handshakeHandler,
+ pushToService: pushToServiceHandler,
+ getIncident: getIncidentHandler,
+ issueTypes: getIssueTypesHandler,
+ fieldsByIssueType: getFieldsByIssueTypeHandler,
+};
diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/index.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/index.ts
index 66be0bad02d7b..d3346557f3684 100644
--- a/x-pack/plugins/actions/server/builtin_action_types/jira/index.ts
+++ b/x-pack/plugins/actions/server/builtin_action_types/jira/index.ts
@@ -4,33 +4,138 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { Logger } from '../../../../../../src/core/server';
-import { createConnector } from '../case/utils';
-import { ActionType } from '../../types';
+import { curry } from 'lodash';
+import { schema } from '@kbn/config-schema';
-import { api } from './api';
-import { config } from './config';
import { validate } from './validators';
-import { createExternalService } from './service';
-import { JiraSecretConfiguration, JiraPublicConfiguration } from './schema';
+import {
+ ExternalIncidentServiceConfiguration,
+ ExternalIncidentServiceSecretConfiguration,
+ ExecutorParamsSchema,
+} from './schema';
import { ActionsConfigurationUtilities } from '../../actions_config';
+import { ActionType, ActionTypeExecutorOptions, ActionTypeExecutorResult } from '../../types';
+import { createExternalService } from './service';
+import { api } from './api';
+import {
+ ExecutorParams,
+ ExecutorSubActionPushParams,
+ JiraPublicConfigurationType,
+ JiraSecretConfigurationType,
+ JiraExecutorResultData,
+ ExecutorSubActionGetFieldsByIssueTypeParams,
+ ExecutorSubActionGetIssueTypesParams,
+} from './types';
+import * as i18n from './translations';
+import { Logger } from '../../../../../../src/core/server';
-export function getActionType({
- logger,
- configurationUtilities,
-}: {
+// TODO: to remove, need to support Case
+import { buildMap, mapParams } from '../case/utils';
+
+interface GetActionTypeParams {
logger: Logger;
configurationUtilities: ActionsConfigurationUtilities;
-}): ActionType {
- return createConnector({
- api,
- config,
- validate,
- createExternalService,
- validationSchema: {
- config: JiraPublicConfiguration,
- secrets: JiraSecretConfiguration,
+}
+
+const supportedSubActions: string[] = ['pushToService', 'issueTypes', 'fieldsByIssueType'];
+
+// action type definition
+export function getActionType(
+ params: GetActionTypeParams
+): ActionType<
+ JiraPublicConfigurationType,
+ JiraSecretConfigurationType,
+ ExecutorParams,
+ JiraExecutorResultData | {}
+> {
+ const { logger, configurationUtilities } = params;
+ return {
+ id: '.jira',
+ minimumLicenseRequired: 'gold',
+ name: i18n.NAME,
+ validate: {
+ config: schema.object(ExternalIncidentServiceConfiguration, {
+ validate: curry(validate.config)(configurationUtilities),
+ }),
+ secrets: schema.object(ExternalIncidentServiceSecretConfiguration, {
+ validate: curry(validate.secrets)(configurationUtilities),
+ }),
+ params: ExecutorParamsSchema,
+ },
+ executor: curry(executor)({ logger }),
+ };
+}
+
+// action executor
+async function executor(
+ { logger }: { logger: Logger },
+ execOptions: ActionTypeExecutorOptions<
+ JiraPublicConfigurationType,
+ JiraSecretConfigurationType,
+ ExecutorParams
+ >
+): Promise> {
+ const { actionId, config, params, secrets } = execOptions;
+ const { subAction, subActionParams } = params as ExecutorParams;
+ let data: JiraExecutorResultData | null = null;
+
+ const externalService = createExternalService(
+ {
+ config,
+ secrets,
},
logger,
- })({ configurationUtilities });
+ execOptions.proxySettings
+ );
+
+ if (!api[subAction]) {
+ const errorMessage = `[Action][ExternalService] Unsupported subAction type ${subAction}.`;
+ logger.error(errorMessage);
+ throw new Error(errorMessage);
+ }
+
+ if (!supportedSubActions.includes(subAction)) {
+ const errorMessage = `[Action][ExternalService] subAction ${subAction} not implemented.`;
+ logger.error(errorMessage);
+ throw new Error(errorMessage);
+ }
+
+ if (subAction === 'pushToService') {
+ const pushToServiceParams = subActionParams as ExecutorSubActionPushParams;
+
+ const { comments, externalId, ...restParams } = pushToServiceParams;
+ const incidentConfiguration = config.incidentConfiguration;
+ const mapping = incidentConfiguration ? buildMap(incidentConfiguration.mapping) : null;
+ const externalObject =
+ config.incidentConfiguration && mapping
+ ? mapParams(restParams as ExecutorSubActionPushParams, mapping)
+ : {};
+
+ data = await api.pushToService({
+ externalService,
+ mapping,
+ params: { ...pushToServiceParams, externalObject },
+ logger,
+ });
+
+ logger.debug(`response push to service for incident id: ${data.id}`);
+ }
+
+ if (subAction === 'issueTypes') {
+ const getIssueTypesParams = subActionParams as ExecutorSubActionGetIssueTypesParams;
+ data = await api.issueTypes({
+ externalService,
+ params: getIssueTypesParams,
+ });
+ }
+
+ if (subAction === 'fieldsByIssueType') {
+ const getFieldsByIssueTypeParams = subActionParams as ExecutorSubActionGetFieldsByIssueTypeParams;
+ data = await api.fieldsByIssueType({
+ externalService,
+ params: getFieldsByIssueTypeParams,
+ });
+ }
+
+ return { status: 'ok', data: data ?? {}, actionId };
}
diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/mocks.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/mocks.ts
index 709d490a5227f..e7841996fedef 100644
--- a/x-pack/plugins/actions/server/builtin_action_types/jira/mocks.ts
+++ b/x-pack/plugins/actions/server/builtin_action_types/jira/mocks.ts
@@ -4,12 +4,9 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import {
- ExternalService,
- PushToServiceApiParams,
- ExecutorSubActionPushParams,
- MapRecord,
-} from '../case/types';
+import { ExternalService, PushToServiceApiParams, ExecutorSubActionPushParams } from './types';
+
+import { MapRecord } from '../case/common_types';
const createMock = (): jest.Mocked => {
const service = {
@@ -40,6 +37,30 @@ const createMock = (): jest.Mocked => {
})
),
createComment: jest.fn(),
+ findIncidents: jest.fn(),
+ getCapabilities: jest.fn(),
+ getIssueTypes: jest.fn().mockImplementation(() => [
+ {
+ id: '10006',
+ name: 'Task',
+ },
+ {
+ id: '10007',
+ name: 'Bug',
+ },
+ ]),
+ getFieldsByIssueType: jest.fn().mockImplementation(() => ({
+ summary: { allowedValues: [], defaultValue: {} },
+ priority: {
+ allowedValues: [
+ {
+ name: 'Medium',
+ id: '3',
+ },
+ ],
+ defaultValue: { name: 'Medium', id: '3' },
+ },
+ })),
};
service.createComment.mockImplementationOnce(() =>
@@ -96,6 +117,9 @@ const executorParams: ExecutorSubActionPushParams = {
updatedBy: { fullName: 'Elastic User', username: 'elastic' },
title: 'Incident title',
description: 'Incident description',
+ labels: ['kibana', 'elastic'],
+ priority: 'High',
+ issueType: '10006',
comments: [
{
commentId: 'case-comment-1',
@@ -118,7 +142,7 @@ const executorParams: ExecutorSubActionPushParams = {
const apiParams: PushToServiceApiParams = {
...executorParams,
- externalCase: { summary: 'Incident title', description: 'Incident description' },
+ externalObject: { summary: 'Incident title', description: 'Incident description' },
};
export { externalServiceMock, mapping, executorParams, apiParams };
diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/schema.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/schema.ts
index 9c831e75d91c1..07c8e22812b27 100644
--- a/x-pack/plugins/actions/server/builtin_action_types/jira/schema.ts
+++ b/x-pack/plugins/actions/server/builtin_action_types/jira/schema.ts
@@ -5,18 +5,85 @@
*/
import { schema } from '@kbn/config-schema';
-import { ExternalIncidentServiceConfiguration } from '../case/schema';
+import {
+ CommentSchema,
+ EntityInformation,
+ IncidentConfigurationSchema,
+} from '../case/common_schema';
-export const JiraPublicConfiguration = {
+export const ExternalIncidentServiceConfiguration = {
+ apiUrl: schema.string(),
projectKey: schema.string(),
- ...ExternalIncidentServiceConfiguration,
+ // TODO: to remove - set it optional for the current stage to support Case Jira implementation
+ incidentConfiguration: schema.nullable(IncidentConfigurationSchema),
+ isCaseOwned: schema.nullable(schema.boolean()),
};
-export const JiraPublicConfigurationSchema = schema.object(JiraPublicConfiguration);
+export const ExternalIncidentServiceConfigurationSchema = schema.object(
+ ExternalIncidentServiceConfiguration
+);
-export const JiraSecretConfiguration = {
+export const ExternalIncidentServiceSecretConfiguration = {
email: schema.string(),
apiToken: schema.string(),
};
-export const JiraSecretConfigurationSchema = schema.object(JiraSecretConfiguration);
+export const ExternalIncidentServiceSecretConfigurationSchema = schema.object(
+ ExternalIncidentServiceSecretConfiguration
+);
+
+export const ExecutorSubActionSchema = schema.oneOf([
+ schema.literal('getIncident'),
+ schema.literal('pushToService'),
+ schema.literal('handshake'),
+ schema.literal('issueTypes'),
+ schema.literal('fieldsByIssueType'),
+]);
+
+export const ExecutorSubActionPushParamsSchema = schema.object({
+ savedObjectId: schema.string(),
+ title: schema.string(),
+ description: schema.nullable(schema.string()),
+ externalId: schema.nullable(schema.string()),
+ issueType: schema.nullable(schema.string()),
+ priority: schema.nullable(schema.string()),
+ labels: schema.nullable(schema.arrayOf(schema.string())),
+ // TODO: modify later to string[] - need for support Case schema
+ comments: schema.nullable(schema.arrayOf(CommentSchema)),
+ ...EntityInformation,
+});
+
+export const ExecutorSubActionGetIncidentParamsSchema = schema.object({
+ externalId: schema.string(),
+});
+
+// Reserved for future implementation
+export const ExecutorSubActionHandshakeParamsSchema = schema.object({});
+export const ExecutorSubActionGetCapabilitiesParamsSchema = schema.object({});
+export const ExecutorSubActionGetIssueTypesParamsSchema = schema.object({});
+export const ExecutorSubActionGetFieldsByIssueTypeParamsSchema = schema.object({
+ id: schema.string(),
+});
+
+export const ExecutorParamsSchema = schema.oneOf([
+ schema.object({
+ subAction: schema.literal('getIncident'),
+ subActionParams: ExecutorSubActionGetIncidentParamsSchema,
+ }),
+ schema.object({
+ subAction: schema.literal('handshake'),
+ subActionParams: ExecutorSubActionHandshakeParamsSchema,
+ }),
+ schema.object({
+ subAction: schema.literal('pushToService'),
+ subActionParams: ExecutorSubActionPushParamsSchema,
+ }),
+ schema.object({
+ subAction: schema.literal('issueTypes'),
+ subActionParams: ExecutorSubActionGetIssueTypesParamsSchema,
+ }),
+ schema.object({
+ subAction: schema.literal('fieldsByIssueType'),
+ subActionParams: ExecutorSubActionGetFieldsByIssueTypeParamsSchema,
+ }),
+]);
diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/service.test.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/service.test.ts
index 547595b4c183f..2439c507c3328 100644
--- a/x-pack/plugins/actions/server/builtin_action_types/jira/service.test.ts
+++ b/x-pack/plugins/actions/server/builtin_action_types/jira/service.test.ts
@@ -8,11 +8,15 @@ import axios from 'axios';
import { createExternalService } from './service';
import * as utils from '../lib/axios_utils';
-import { ExternalService } from '../case/types';
+import { ExternalService } from './types';
import { Logger } from '../../../../../../src/core/server';
import { loggingSystemMock } from '../../../../../../src/core/server/mocks';
const logger = loggingSystemMock.create().get() as jest.Mocked;
+interface ResponseError extends Error {
+ response?: { data: { errors: Record } };
+}
+
jest.mock('axios');
jest.mock('../lib/axios_utils', () => {
const originalUtils = jest.requireActual('../lib/axios_utils');
@@ -25,6 +29,72 @@ jest.mock('../lib/axios_utils', () => {
axios.create = jest.fn(() => axios);
const requestMock = utils.request as jest.Mock;
+const issueTypesResponse = {
+ data: {
+ projects: [
+ {
+ issuetypes: [
+ {
+ id: '10006',
+ name: 'Task',
+ },
+ {
+ id: '10007',
+ name: 'Bug',
+ },
+ ],
+ },
+ ],
+ },
+};
+
+const fieldsResponse = {
+ data: {
+ projects: [
+ {
+ issuetypes: [
+ {
+ id: '10006',
+ name: 'Task',
+ fields: {
+ summary: { fieldId: 'summary' },
+ priority: {
+ fieldId: 'priority',
+ allowedValues: [
+ {
+ name: 'Highest',
+ id: '1',
+ },
+ {
+ name: 'High',
+ id: '2',
+ },
+ {
+ name: 'Medium',
+ id: '3',
+ },
+ {
+ name: 'Low',
+ id: '4',
+ },
+ {
+ name: 'Lowest',
+ id: '5',
+ },
+ ],
+ defaultValue: {
+ name: 'Medium',
+ id: '3',
+ },
+ },
+ },
+ },
+ ],
+ },
+ ],
+ },
+};
+
describe('Jira service', () => {
let service: ExternalService;
@@ -116,19 +186,24 @@ describe('Jira service', () => {
test('it should throw an error', async () => {
requestMock.mockImplementation(() => {
- throw new Error('An error has occurred');
+ const error: ResponseError = new Error('An error has occurred');
+ error.response = { data: { errors: { summary: 'Required field' } } };
+ throw error;
});
expect(service.getIncident('1')).rejects.toThrow(
- 'Unable to get incident with id 1. Error: An error has occurred'
+ '[Action][Jira]: Unable to get incident with id 1. Error: An error has occurred Reason: Required field'
);
});
});
describe('createIncident', () => {
test('it creates the incident correctly', async () => {
- // The response from Jira when creating an issue contains only the key and the id.
- // The service makes two calls when creating an issue. One to create and one to get
- // the created incident with all the necessary fields.
+ /* The response from Jira when creating an issue contains only the key and the id.
+ The function makes the following calls when creating an issue:
+ 1. Get issueTypes to set a default ONLY when incident.issueType is missing
+ 2. Create the issue.
+ 3. Get the created issue with all the necessary fields.
+ */
requestMock.mockImplementationOnce(() => ({
data: { id: '1', key: 'CK-1', fields: { summary: 'title', description: 'description' } },
}));
@@ -138,7 +213,13 @@ describe('Jira service', () => {
}));
const res = await service.createIncident({
- incident: { summary: 'title', description: 'desc' },
+ incident: {
+ summary: 'title',
+ description: 'desc',
+ labels: [],
+ issueType: '10006',
+ priority: 'High',
+ },
});
expect(res).toEqual({
@@ -149,6 +230,68 @@ describe('Jira service', () => {
});
});
+ test('it creates the incident correctly without issue type', async () => {
+ /* The response from Jira when creating an issue contains only the key and the id.
+ The function makes the following calls when creating an issue:
+ 1. Get issueTypes to set a default ONLY when incident.issueType is missing
+ 2. Create the issue.
+ 3. Get the created issue with all the necessary fields.
+ */
+ // getIssueType mocks
+ requestMock.mockImplementationOnce(() => ({
+ data: {
+ capabilities: {
+ navigation: 'https://siem-kibana.atlassian.net/rest/capabilities/navigation',
+ },
+ },
+ }));
+
+ // getIssueType mocks
+ requestMock.mockImplementationOnce(() => issueTypesResponse);
+
+ requestMock.mockImplementationOnce(() => ({
+ data: { id: '1', key: 'CK-1', fields: { summary: 'title', description: 'description' } },
+ }));
+
+ requestMock.mockImplementationOnce(() => ({
+ data: { id: '1', key: 'CK-1', fields: { created: '2020-04-27T10:59:46.202Z' } },
+ }));
+
+ const res = await service.createIncident({
+ incident: {
+ summary: 'title',
+ description: 'desc',
+ labels: [],
+ priority: 'High',
+ issueType: null,
+ },
+ });
+
+ expect(res).toEqual({
+ title: 'CK-1',
+ id: '1',
+ pushedDate: '2020-04-27T10:59:46.202Z',
+ url: 'https://siem-kibana.atlassian.net/browse/CK-1',
+ });
+
+ expect(requestMock).toHaveBeenCalledWith({
+ axios,
+ url: 'https://siem-kibana.atlassian.net/rest/api/2/issue',
+ logger,
+ method: 'post',
+ data: {
+ fields: {
+ summary: 'title',
+ description: 'desc',
+ project: { key: 'CK' },
+ issuetype: { id: '10006' },
+ labels: [],
+ priority: { name: 'High' },
+ },
+ },
+ });
+ });
+
test('it should call request with correct arguments', async () => {
requestMock.mockImplementation(() => ({
data: {
@@ -159,7 +302,13 @@ describe('Jira service', () => {
}));
await service.createIncident({
- incident: { summary: 'title', description: 'desc' },
+ incident: {
+ summary: 'title',
+ description: 'desc',
+ labels: [],
+ issueType: '10006',
+ priority: 'High',
+ },
});
expect(requestMock).toHaveBeenCalledWith({
@@ -172,7 +321,9 @@ describe('Jira service', () => {
summary: 'title',
description: 'desc',
project: { key: 'CK' },
- issuetype: { name: 'Task' },
+ issuetype: { id: '10006' },
+ labels: [],
+ priority: { name: 'High' },
},
},
});
@@ -180,14 +331,24 @@ describe('Jira service', () => {
test('it should throw an error', async () => {
requestMock.mockImplementation(() => {
- throw new Error('An error has occurred');
+ const error: ResponseError = new Error('An error has occurred');
+ error.response = { data: { errors: { summary: 'Required field' } } };
+ throw error;
});
expect(
service.createIncident({
- incident: { summary: 'title', description: 'desc' },
+ incident: {
+ summary: 'title',
+ description: 'desc',
+ labels: [],
+ issueType: '10006',
+ priority: 'High',
+ },
})
- ).rejects.toThrow('[Action][Jira]: Unable to create incident. Error: An error has occurred');
+ ).rejects.toThrow(
+ '[Action][Jira]: Unable to create incident. Error: An error has occurred. Reason: Required field'
+ );
});
});
@@ -203,7 +364,13 @@ describe('Jira service', () => {
const res = await service.updateIncident({
incidentId: '1',
- incident: { summary: 'title', description: 'desc' },
+ incident: {
+ summary: 'title',
+ description: 'desc',
+ labels: [],
+ issueType: '10006',
+ priority: 'High',
+ },
});
expect(res).toEqual({
@@ -225,7 +392,13 @@ describe('Jira service', () => {
await service.updateIncident({
incidentId: '1',
- incident: { summary: 'title', description: 'desc' },
+ incident: {
+ summary: 'title',
+ description: 'desc',
+ labels: [],
+ issueType: '10006',
+ priority: 'High',
+ },
});
expect(requestMock).toHaveBeenCalledWith({
@@ -233,22 +406,39 @@ describe('Jira service', () => {
logger,
method: 'put',
url: 'https://siem-kibana.atlassian.net/rest/api/2/issue/1',
- data: { fields: { summary: 'title', description: 'desc' } },
+ data: {
+ fields: {
+ summary: 'title',
+ description: 'desc',
+ labels: [],
+ priority: { name: 'High' },
+ issuetype: { id: '10006' },
+ project: { key: 'CK' },
+ },
+ },
});
});
test('it should throw an error', async () => {
requestMock.mockImplementation(() => {
- throw new Error('An error has occurred');
+ const error: ResponseError = new Error('An error has occurred');
+ error.response = { data: { errors: { summary: 'Required field' } } };
+ throw error;
});
expect(
service.updateIncident({
incidentId: '1',
- incident: { summary: 'title', description: 'desc' },
+ incident: {
+ summary: 'title',
+ description: 'desc',
+ labels: [],
+ issueType: '10006',
+ priority: 'High',
+ },
})
).rejects.toThrow(
- '[Action][Jira]: Unable to update incident with id 1. Error: An error has occurred'
+ '[Action][Jira]: Unable to update incident with id 1. Error: An error has occurred. Reason: Required field'
);
});
});
@@ -265,8 +455,14 @@ describe('Jira service', () => {
const res = await service.createComment({
incidentId: '1',
- comment: { comment: 'comment', commentId: 'comment-1' },
- field: 'comments',
+ comment: {
+ comment: 'comment',
+ commentId: 'comment-1',
+ createdBy: null,
+ createdAt: null,
+ updatedAt: null,
+ updatedBy: null,
+ },
});
expect(res).toEqual({
@@ -287,8 +483,14 @@ describe('Jira service', () => {
await service.createComment({
incidentId: '1',
- comment: { comment: 'comment', commentId: 'comment-1' },
- field: 'my_field',
+ comment: {
+ comment: 'comment',
+ commentId: 'comment-1',
+ createdBy: null,
+ createdAt: null,
+ updatedAt: null,
+ updatedBy: null,
+ },
});
expect(requestMock).toHaveBeenCalledWith({
@@ -302,18 +504,416 @@ describe('Jira service', () => {
test('it should throw an error', async () => {
requestMock.mockImplementation(() => {
- throw new Error('An error has occurred');
+ const error: ResponseError = new Error('An error has occurred');
+ error.response = { data: { errors: { summary: 'Required field' } } };
+ throw error;
});
expect(
service.createComment({
incidentId: '1',
- comment: { comment: 'comment', commentId: 'comment-1' },
- field: 'comments',
+ comment: {
+ comment: 'comment',
+ commentId: 'comment-1',
+ createdBy: null,
+ createdAt: null,
+ updatedAt: null,
+ updatedBy: null,
+ },
})
).rejects.toThrow(
- '[Action][Jira]: Unable to create comment at incident with id 1. Error: An error has occurred'
+ '[Action][Jira]: Unable to create comment at incident with id 1. Error: An error has occurred. Reason: Required field'
+ );
+ });
+ });
+
+ describe('getCapabilities', () => {
+ test('it should return the capabilities', async () => {
+ requestMock.mockImplementation(() => ({
+ data: {
+ capabilities: {
+ navigation: 'https://siem-kibana.atlassian.net/rest/capabilities/navigation',
+ },
+ },
+ }));
+ const res = await service.getCapabilities();
+ expect(res).toEqual({
+ capabilities: {
+ navigation: 'https://siem-kibana.atlassian.net/rest/capabilities/navigation',
+ },
+ });
+ });
+
+ test('it should call request with correct arguments', async () => {
+ requestMock.mockImplementation(() => ({
+ data: {
+ capabilities: {
+ navigation: 'https://siem-kibana.atlassian.net/rest/capabilities/navigation',
+ },
+ },
+ }));
+
+ await service.getCapabilities();
+
+ expect(requestMock).toHaveBeenCalledWith({
+ axios,
+ logger,
+ method: 'get',
+ url: 'https://siem-kibana.atlassian.net/rest/capabilities',
+ });
+ });
+
+ test('it should throw an error', async () => {
+ requestMock.mockImplementation(() => {
+ const error: ResponseError = new Error('An error has occurred');
+ error.response = { data: { errors: { capabilities: 'Could not get capabilities' } } };
+ throw error;
+ });
+
+ expect(service.getCapabilities()).rejects.toThrow(
+ '[Action][Jira]: Unable to get capabilities. Error: An error has occurred. Reason: Could not get capabilities'
);
});
});
+
+ describe('getIssueTypes', () => {
+ describe('Old API', () => {
+ test('it should return the issue types', async () => {
+ requestMock.mockImplementationOnce(() => ({
+ data: {
+ capabilities: {
+ navigation: 'https://siem-kibana.atlassian.net/rest/capabilities/navigation',
+ },
+ },
+ }));
+
+ requestMock.mockImplementationOnce(() => issueTypesResponse);
+
+ const res = await service.getIssueTypes();
+
+ expect(res).toEqual([
+ {
+ id: '10006',
+ name: 'Task',
+ },
+ {
+ id: '10007',
+ name: 'Bug',
+ },
+ ]);
+ });
+
+ test('it should call request with correct arguments', async () => {
+ requestMock.mockImplementationOnce(() => ({
+ data: {
+ capabilities: {
+ navigation: 'https://siem-kibana.atlassian.net/rest/capabilities/navigation',
+ },
+ },
+ }));
+
+ requestMock.mockImplementationOnce(() => issueTypesResponse);
+
+ await service.getIssueTypes();
+
+ expect(requestMock).toHaveBeenLastCalledWith({
+ axios,
+ logger,
+ method: 'get',
+ url:
+ 'https://siem-kibana.atlassian.net/rest/api/2/issue/createmeta?projectKeys=CK&expand=projects.issuetypes.fields',
+ });
+ });
+
+ test('it should throw an error', async () => {
+ requestMock.mockImplementationOnce(() => ({
+ data: {
+ capabilities: {
+ navigation: 'https://siem-kibana.atlassian.net/rest/capabilities/navigation',
+ },
+ },
+ }));
+
+ requestMock.mockImplementation(() => {
+ const error: ResponseError = new Error('An error has occurred');
+ error.response = { data: { errors: { issuetypes: 'Could not get issue types' } } };
+ throw error;
+ });
+
+ expect(service.getIssueTypes()).rejects.toThrow(
+ '[Action][Jira]: Unable to get issue types. Error: An error has occurred. Reason: Could not get issue types'
+ );
+ });
+ });
+ describe('New API', () => {
+ test('it should return the issue types', async () => {
+ requestMock.mockImplementationOnce(() => ({
+ data: {
+ capabilities: {
+ 'list-project-issuetypes':
+ 'https://siem-kibana.atlassian.net/rest/capabilities/list-project-issuetypes',
+ 'list-issuetype-fields':
+ 'https://siem-kibana.atlassian.net/rest/capabilities/list-issuetype-fields',
+ },
+ },
+ }));
+
+ requestMock.mockImplementationOnce(() => ({
+ data: {
+ values: issueTypesResponse.data.projects[0].issuetypes,
+ },
+ }));
+
+ const res = await service.getIssueTypes();
+
+ expect(res).toEqual([
+ {
+ id: '10006',
+ name: 'Task',
+ },
+ {
+ id: '10007',
+ name: 'Bug',
+ },
+ ]);
+ });
+
+ test('it should call request with correct arguments', async () => {
+ requestMock.mockImplementationOnce(() => ({
+ data: {
+ capabilities: {
+ 'list-project-issuetypes':
+ 'https://siem-kibana.atlassian.net/rest/capabilities/list-project-issuetypes',
+ 'list-issuetype-fields':
+ 'https://siem-kibana.atlassian.net/rest/capabilities/list-issuetype-fields',
+ },
+ },
+ }));
+
+ requestMock.mockImplementationOnce(() => ({
+ data: {
+ values: issueTypesResponse.data.projects[0].issuetypes,
+ },
+ }));
+
+ await service.getIssueTypes();
+
+ expect(requestMock).toHaveBeenLastCalledWith({
+ axios,
+ logger,
+ method: 'get',
+ url: 'https://siem-kibana.atlassian.net/rest/api/2/issue/createmeta/CK/issuetypes',
+ });
+ });
+
+ test('it should throw an error', async () => {
+ requestMock.mockImplementationOnce(() => ({
+ data: {
+ capabilities: {
+ 'list-project-issuetypes':
+ 'https://siem-kibana.atlassian.net/rest/capabilities/list-project-issuetypes',
+ 'list-issuetype-fields':
+ 'https://siem-kibana.atlassian.net/rest/capabilities/list-issuetype-fields',
+ },
+ },
+ }));
+
+ requestMock.mockImplementation(() => {
+ const error: ResponseError = new Error('An error has occurred');
+ error.response = { data: { errors: { issuetypes: 'Could not get issue types' } } };
+ throw error;
+ });
+
+ expect(service.getIssueTypes()).rejects.toThrow(
+ '[Action][Jira]: Unable to get issue types. Error: An error has occurred. Reason: Could not get issue types'
+ );
+ });
+ });
+ });
+
+ describe('getFieldsByIssueType', () => {
+ describe('Old API', () => {
+ test('it should return the fields', async () => {
+ requestMock.mockImplementationOnce(() => ({
+ data: {
+ capabilities: {
+ navigation: 'https://siem-kibana.atlassian.net/rest/capabilities/navigation',
+ },
+ },
+ }));
+
+ requestMock.mockImplementationOnce(() => fieldsResponse);
+
+ const res = await service.getFieldsByIssueType('10006');
+
+ expect(res).toEqual({
+ priority: {
+ allowedValues: [
+ { id: '1', name: 'Highest' },
+ { id: '2', name: 'High' },
+ { id: '3', name: 'Medium' },
+ { id: '4', name: 'Low' },
+ { id: '5', name: 'Lowest' },
+ ],
+ defaultValue: { id: '3', name: 'Medium' },
+ },
+ summary: { allowedValues: [], defaultValue: {} },
+ });
+ });
+
+ test('it should call request with correct arguments', async () => {
+ requestMock.mockImplementationOnce(() => ({
+ data: {
+ capabilities: {
+ navigation: 'https://siem-kibana.atlassian.net/rest/capabilities/navigation',
+ },
+ },
+ }));
+
+ requestMock.mockImplementationOnce(() => fieldsResponse);
+
+ await service.getFieldsByIssueType('10006');
+
+ expect(requestMock).toHaveBeenLastCalledWith({
+ axios,
+ logger,
+ method: 'get',
+ url:
+ 'https://siem-kibana.atlassian.net/rest/api/2/issue/createmeta?projectKeys=CK&issuetypeIds=10006&expand=projects.issuetypes.fields',
+ });
+ });
+
+ test('it should throw an error', async () => {
+ requestMock.mockImplementationOnce(() => ({
+ data: {
+ capabilities: {
+ navigation: 'https://siem-kibana.atlassian.net/rest/capabilities/navigation',
+ },
+ },
+ }));
+
+ requestMock.mockImplementation(() => {
+ const error: ResponseError = new Error('An error has occurred');
+ error.response = { data: { errors: { fields: 'Could not get fields' } } };
+ throw error;
+ });
+
+ expect(service.getFieldsByIssueType('10006')).rejects.toThrow(
+ '[Action][Jira]: Unable to get fields. Error: An error has occurred. Reason: Could not get fields'
+ );
+ });
+ });
+
+ describe('New API', () => {
+ test('it should return the fields', async () => {
+ requestMock.mockImplementationOnce(() => ({
+ data: {
+ capabilities: {
+ 'list-project-issuetypes':
+ 'https://siem-kibana.atlassian.net/rest/capabilities/list-project-issuetypes',
+ 'list-issuetype-fields':
+ 'https://siem-kibana.atlassian.net/rest/capabilities/list-issuetype-fields',
+ },
+ },
+ }));
+
+ requestMock.mockImplementationOnce(() => ({
+ data: {
+ values: [
+ { fieldId: 'summary' },
+ {
+ fieldId: 'priority',
+ allowedValues: [
+ {
+ name: 'Medium',
+ id: '3',
+ },
+ ],
+ defaultValue: {
+ name: 'Medium',
+ id: '3',
+ },
+ },
+ ],
+ },
+ }));
+
+ const res = await service.getFieldsByIssueType('10006');
+
+ expect(res).toEqual({
+ priority: {
+ allowedValues: [{ id: '3', name: 'Medium' }],
+ defaultValue: { id: '3', name: 'Medium' },
+ },
+ summary: { allowedValues: [], defaultValue: {} },
+ });
+ });
+
+ test('it should call request with correct arguments', async () => {
+ requestMock.mockImplementationOnce(() => ({
+ data: {
+ capabilities: {
+ 'list-project-issuetypes':
+ 'https://siem-kibana.atlassian.net/rest/capabilities/list-project-issuetypes',
+ 'list-issuetype-fields':
+ 'https://siem-kibana.atlassian.net/rest/capabilities/list-issuetype-fields',
+ },
+ },
+ }));
+
+ requestMock.mockImplementationOnce(() => ({
+ data: {
+ values: [
+ { fieldId: 'summary' },
+ {
+ fieldId: 'priority',
+ allowedValues: [
+ {
+ name: 'Medium',
+ id: '3',
+ },
+ ],
+ defaultValue: {
+ name: 'Medium',
+ id: '3',
+ },
+ },
+ ],
+ },
+ }));
+
+ await service.getFieldsByIssueType('10006');
+
+ expect(requestMock).toHaveBeenLastCalledWith({
+ axios,
+ logger,
+ method: 'get',
+ url: 'https://siem-kibana.atlassian.net/rest/api/2/issue/createmeta/CK/issuetypes/10006',
+ });
+ });
+
+ test('it should throw an error', async () => {
+ requestMock.mockImplementationOnce(() => ({
+ data: {
+ capabilities: {
+ 'list-project-issuetypes':
+ 'https://siem-kibana.atlassian.net/rest/capabilities/list-project-issuetypes',
+ 'list-issuetype-fields':
+ 'https://siem-kibana.atlassian.net/rest/capabilities/list-issuetype-fields',
+ },
+ },
+ }));
+
+ requestMock.mockImplementation(() => {
+ const error: ResponseError = new Error('An error has occurred');
+ error.response = { data: { errors: { issuetypes: 'Could not get issue types' } } };
+ throw error;
+ });
+
+ expect(service.getFieldsByIssueType('10006')).rejects.toThrow(
+ '[Action][Jira]: Unable to get fields. Error: An error has occurred. Reason: Could not get issue types'
+ );
+ });
+ });
+ });
});
diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/service.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/service.ts
index aec73cfb375ed..84b6e70d2a100 100644
--- a/x-pack/plugins/actions/server/builtin_action_types/jira/service.ts
+++ b/x-pack/plugins/actions/server/builtin_action_types/jira/service.ts
@@ -6,14 +6,20 @@
import axios from 'axios';
-import { ExternalServiceCredentials, ExternalService, ExternalServiceParams } from '../case/types';
import { Logger } from '../../../../../../src/core/server';
import {
+ ExternalServiceCredentials,
+ ExternalService,
+ CreateIncidentParams,
+ UpdateIncidentParams,
JiraPublicConfigurationType,
JiraSecretConfigurationType,
- CreateIncidentRequest,
- UpdateIncidentRequest,
- CreateCommentRequest,
+ Fields,
+ CreateCommentParams,
+ Incident,
+ ResponseError,
+ ExternalServiceCommentResponse,
+ ExternalServiceIncidentResponse,
} from './types';
import * as i18n from './translations';
@@ -22,11 +28,12 @@ import { ProxySettings } from '../../types';
const VERSION = '2';
const BASE_URL = `rest/api/${VERSION}`;
-const INCIDENT_URL = `issue`;
-const COMMENT_URL = `comment`;
+const CAPABILITIES_URL = `rest/capabilities`;
const VIEW_INCIDENT_URL = `browse`;
+const createMetaCapabilities = ['list-project-issuetypes', 'list-issuetype-fields'];
+
export const createExternalService = (
{ config, secrets }: ExternalServiceCredentials,
logger: Logger,
@@ -39,8 +46,13 @@ export const createExternalService = (
throw Error(`[Action]${i18n.NAME}: Wrong configuration.`);
}
- const incidentUrl = `${url}/${BASE_URL}/${INCIDENT_URL}`;
- const commentUrl = `${incidentUrl}/{issueId}/${COMMENT_URL}`;
+ const incidentUrl = `${url}/${BASE_URL}/issue`;
+ const capabilitiesUrl = `${url}/${CAPABILITIES_URL}`;
+ const commentUrl = `${incidentUrl}/{issueId}/comment`;
+ const getIssueTypesOldAPIURL = `${url}/${BASE_URL}/issue/createmeta?projectKeys=${projectKey}&expand=projects.issuetypes.fields`;
+ const getIssueTypeFieldsOldAPIURL = `${url}/${BASE_URL}/issue/createmeta?projectKeys=${projectKey}&issuetypeIds={issueTypeId}&expand=projects.issuetypes.fields`;
+ const getIssueTypesUrl = `${url}/${BASE_URL}/issue/createmeta/${projectKey}/issuetypes`;
+ const getIssueTypeFieldsUrl = `${url}/${BASE_URL}/issue/createmeta/${projectKey}/issuetypes/{issueTypeId}`;
const axiosInstance = axios.create({
auth: { username: email, password: apiToken },
});
@@ -52,6 +64,60 @@ export const createExternalService = (
const getCommentsURL = (issueId: string) => {
return commentUrl.replace('{issueId}', issueId);
};
+ const createGetIssueTypeFieldsUrl = (uri: string, issueTypeId: string) => {
+ return uri.replace('{issueTypeId}', issueTypeId);
+ };
+
+ const createFields = (key: string, incident: Incident): Fields => {
+ let fields: Fields = {
+ summary: incident.summary,
+ project: { key },
+ };
+
+ if (incident.issueType) {
+ fields = { ...fields, issuetype: { id: incident.issueType } };
+ }
+
+ if (incident.description) {
+ fields = { ...fields, description: incident.description };
+ }
+
+ if (incident.labels) {
+ fields = { ...fields, labels: incident.labels };
+ }
+
+ if (incident.priority) {
+ fields = { ...fields, priority: { name: incident.priority } };
+ }
+
+ return fields;
+ };
+
+ const createErrorMessage = (errors: ResponseError) => {
+ return Object.entries(errors).reduce((errorMessage, [, value]) => {
+ const msg = errorMessage.length > 0 ? `${errorMessage} ${value}` : value;
+ return msg;
+ }, '');
+ };
+
+ const hasSupportForNewAPI = (capabilities: { capabilities?: {} }) =>
+ createMetaCapabilities.every((c) => Object.keys(capabilities?.capabilities ?? {}).includes(c));
+
+ const normalizeIssueTypes = (issueTypes: Array<{ id: string; name: string }>) =>
+ issueTypes.map((type) => ({ id: type.id, name: type.name }));
+
+ const normalizeFields = (fields: {
+ [key: string]: { allowedValues?: Array<{}>; defaultValue?: {} };
+ }) =>
+ Object.keys(fields ?? {}).reduce((fieldsAcc, fieldKey) => {
+ return {
+ ...fieldsAcc,
+ [fieldKey]: {
+ allowedValues: fields[fieldKey]?.allowedValues ?? [],
+ defaultValue: fields[fieldKey]?.defaultValue ?? {},
+ },
+ };
+ }, {});
const getIncident = async (id: string) => {
try {
@@ -67,23 +133,46 @@ export const createExternalService = (
return { ...rest, ...fields };
} catch (error) {
throw new Error(
- getErrorMessage(i18n.NAME, `Unable to get incident with id ${id}. Error: ${error.message}`)
+ getErrorMessage(
+ i18n.NAME,
+ `Unable to get incident with id ${id}. Error: ${
+ error.message
+ } Reason: ${createErrorMessage(error.response?.data?.errors ?? {})}`
+ )
);
}
};
- const createIncident = async ({ incident }: ExternalServiceParams) => {
- // The response from Jira when creating an issue contains only the key and the id.
- // The function makes two calls when creating an issue. One to create the issue and one to get
- // the created issue with all the necessary fields.
+ const createIncident = async ({
+ incident,
+ }: CreateIncidentParams): Promise => {
+ /* The response from Jira when creating an issue contains only the key and the id.
+ The function makes the following calls when creating an issue:
+ 1. Get issueTypes to set a default ONLY when incident.issueType is missing
+ 2. Create the issue.
+ 3. Get the created issue with all the necessary fields.
+ */
+
+ let issueType = incident.issueType;
+
+ if (!incident.issueType) {
+ const issueTypes = await getIssueTypes();
+ issueType = issueTypes[0]?.id ?? '';
+ }
+
+ const fields = createFields(projectKey, {
+ ...incident,
+ issueType,
+ });
+
try {
- const res = await request({
+ const res = await request({
axios: axiosInstance,
url: `${incidentUrl}`,
logger,
method: 'post',
data: {
- fields: { ...incident, project: { key: projectKey }, issuetype: { name: 'Task' } },
+ fields,
},
proxySettings,
});
@@ -98,23 +187,38 @@ export const createExternalService = (
};
} catch (error) {
throw new Error(
- getErrorMessage(i18n.NAME, `Unable to create incident. Error: ${error.message}`)
+ getErrorMessage(
+ i18n.NAME,
+ `Unable to create incident. Error: ${error.message}. Reason: ${createErrorMessage(
+ error.response?.data?.errors ?? {}
+ )}`
+ )
);
}
};
- const updateIncident = async ({ incidentId, incident }: ExternalServiceParams) => {
+ const updateIncident = async ({
+ incidentId,
+ incident,
+ }: UpdateIncidentParams): Promise => {
+ const incidentWithoutNullValues = Object.entries(incident).reduce(
+ (obj, [key, value]) => (value != null ? { ...obj, [key]: value } : obj),
+ {} as Incident
+ );
+
+ const fields = createFields(projectKey, incidentWithoutNullValues);
+
try {
- await request({
+ await request({
axios: axiosInstance,
method: 'put',
url: `${incidentUrl}/${incidentId}`,
logger,
- data: { fields: { ...incident } },
+ data: { fields },
proxySettings,
});
- const updatedIncident = await getIncident(incidentId);
+ const updatedIncident = await getIncident(incidentId as string);
return {
title: updatedIncident.key,
@@ -126,15 +230,20 @@ export const createExternalService = (
throw new Error(
getErrorMessage(
i18n.NAME,
- `Unable to update incident with id ${incidentId}. Error: ${error.message}`
+ `Unable to update incident with id ${incidentId}. Error: ${
+ error.message
+ }. Reason: ${createErrorMessage(error.response?.data?.errors ?? {})}`
)
);
}
};
- const createComment = async ({ incidentId, comment, field }: ExternalServiceParams) => {
+ const createComment = async ({
+ incidentId,
+ comment,
+ }: CreateCommentParams): Promise => {
try {
- const res = await request({
+ const res = await request({
axios: axiosInstance,
method: 'post',
url: getCommentsURL(incidentId),
@@ -152,7 +261,118 @@ export const createExternalService = (
throw new Error(
getErrorMessage(
i18n.NAME,
- `Unable to create comment at incident with id ${incidentId}. Error: ${error.message}`
+ `Unable to create comment at incident with id ${incidentId}. Error: ${
+ error.message
+ }. Reason: ${createErrorMessage(error.response?.data?.errors ?? {})}`
+ )
+ );
+ }
+ };
+
+ const getCapabilities = async () => {
+ try {
+ const res = await request({
+ axios: axiosInstance,
+ method: 'get',
+ url: capabilitiesUrl,
+ logger,
+ proxySettings,
+ });
+
+ return { ...res.data };
+ } catch (error) {
+ throw new Error(
+ getErrorMessage(
+ i18n.NAME,
+ `Unable to get capabilities. Error: ${error.message}. Reason: ${createErrorMessage(
+ error.response?.data?.errors ?? {}
+ )}`
+ )
+ );
+ }
+ };
+
+ const getIssueTypes = async () => {
+ const capabilitiesResponse = await getCapabilities();
+ const supportsNewAPI = hasSupportForNewAPI(capabilitiesResponse);
+
+ try {
+ if (!supportsNewAPI) {
+ const res = await request({
+ axios: axiosInstance,
+ method: 'get',
+ url: getIssueTypesOldAPIURL,
+ logger,
+ proxySettings,
+ });
+
+ const issueTypes = res.data.projects[0]?.issuetypes ?? [];
+ return normalizeIssueTypes(issueTypes);
+ } else {
+ const res = await request({
+ axios: axiosInstance,
+ method: 'get',
+ url: getIssueTypesUrl,
+ logger,
+ proxySettings,
+ });
+
+ const issueTypes = res.data.values;
+ return normalizeIssueTypes(issueTypes);
+ }
+ } catch (error) {
+ throw new Error(
+ getErrorMessage(
+ i18n.NAME,
+ `Unable to get issue types. Error: ${error.message}. Reason: ${createErrorMessage(
+ error.response?.data?.errors ?? {}
+ )}`
+ )
+ );
+ }
+ };
+
+ const getFieldsByIssueType = async (issueTypeId: string) => {
+ const capabilitiesResponse = await getCapabilities();
+ const supportsNewAPI = hasSupportForNewAPI(capabilitiesResponse);
+
+ try {
+ if (!supportsNewAPI) {
+ const res = await request({
+ axios: axiosInstance,
+ method: 'get',
+ url: createGetIssueTypeFieldsUrl(getIssueTypeFieldsOldAPIURL, issueTypeId),
+ logger,
+ proxySettings,
+ });
+
+ const fields = res.data.projects[0]?.issuetypes[0]?.fields || {};
+ return normalizeFields(fields);
+ } else {
+ const res = await request({
+ axios: axiosInstance,
+ method: 'get',
+ url: createGetIssueTypeFieldsUrl(getIssueTypeFieldsUrl, issueTypeId),
+ logger,
+ proxySettings,
+ });
+
+ const fields = res.data.values.reduce(
+ (acc: { [x: string]: {} }, value: { fieldId: string }) => ({
+ ...acc,
+ [value.fieldId]: { ...value },
+ }),
+ {}
+ );
+ return normalizeFields(fields);
+ }
+ } catch (error) {
+ throw new Error(
+ getErrorMessage(
+ i18n.NAME,
+ `Unable to get fields. Error: ${error.message}. Reason: ${createErrorMessage(
+ error.response?.data?.errors ?? {}
+ )}`
)
);
}
@@ -163,5 +383,8 @@ export const createExternalService = (
createIncident,
updateIncident,
createComment,
+ getCapabilities,
+ getIssueTypes,
+ getFieldsByIssueType,
};
};
diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/translations.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/translations.ts
index dae0d75952e11..0e71de813eb5d 100644
--- a/x-pack/plugins/actions/server/builtin_action_types/jira/translations.ts
+++ b/x-pack/plugins/actions/server/builtin_action_types/jira/translations.ts
@@ -9,3 +9,19 @@ import { i18n } from '@kbn/i18n';
export const NAME = i18n.translate('xpack.actions.builtin.case.jiraTitle', {
defaultMessage: 'Jira',
});
+
+export const ALLOWED_HOSTS_ERROR = (message: string) =>
+ i18n.translate('xpack.actions.builtin.jira.configuration.apiAllowedHostsError', {
+ defaultMessage: 'error configuring connector action: {message}',
+ values: {
+ message,
+ },
+ });
+
+// TODO: remove when Case mappings will be removed
+export const MAPPING_EMPTY = i18n.translate(
+ 'xpack.actions.builtin.jira.configuration.emptyMapping',
+ {
+ defaultMessage: '[incidentConfiguration.mapping]: expected non-empty but got empty',
+ }
+);
diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/types.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/types.ts
index 8d9c6b92abb3b..5e97f5309f8ee 100644
--- a/x-pack/plugins/actions/server/builtin_action_types/jira/types.ts
+++ b/x-pack/plugins/actions/server/builtin_action_types/jira/types.ts
@@ -4,29 +4,169 @@
* you may not use this file except in compliance with the Elastic License.
*/
+/* eslint-disable @typescript-eslint/no-explicit-any */
+
import { TypeOf } from '@kbn/config-schema';
-import { JiraPublicConfigurationSchema, JiraSecretConfigurationSchema } from './schema';
+import {
+ ExternalIncidentServiceConfigurationSchema,
+ ExternalIncidentServiceSecretConfigurationSchema,
+ ExecutorParamsSchema,
+ ExecutorSubActionPushParamsSchema,
+ ExecutorSubActionGetIncidentParamsSchema,
+ ExecutorSubActionHandshakeParamsSchema,
+ ExecutorSubActionGetCapabilitiesParamsSchema,
+ ExecutorSubActionGetIssueTypesParamsSchema,
+ ExecutorSubActionGetFieldsByIssueTypeParamsSchema,
+} from './schema';
+import { ActionsConfigurationUtilities } from '../../actions_config';
+import { IncidentConfigurationSchema } from '../case/common_schema';
+import { Comment } from '../case/common_types';
+import { Logger } from '../../../../../../src/core/server';
+
+export type JiraPublicConfigurationType = TypeOf;
+export type JiraSecretConfigurationType = TypeOf<
+ typeof ExternalIncidentServiceSecretConfigurationSchema
+>;
+
+export type ExecutorParams = TypeOf;
+export type ExecutorSubActionPushParams = TypeOf;
+
+export type IncidentConfiguration = TypeOf;
+
+export interface ExternalServiceCredentials {
+ config: Record;
+ secrets: Record;
+}
+
+export interface ExternalServiceValidation {
+ config: (configurationUtilities: ActionsConfigurationUtilities, configObject: any) => void;
+ secrets: (configurationUtilities: ActionsConfigurationUtilities, secrets: any) => void;
+}
+
+export interface ExternalServiceIncidentResponse {
+ id: string;
+ title: string;
+ url: string;
+ pushedDate: string;
+}
+
+export interface ExternalServiceCommentResponse {
+ commentId: string;
+ pushedDate: string;
+ externalCommentId?: string;
+}
+
+export type ExternalServiceParams = Record;
+
+export type Incident = Pick<
+ ExecutorSubActionPushParams,
+ 'description' | 'priority' | 'labels' | 'issueType'
+> & { summary: string };
+
+export interface CreateIncidentParams {
+ incident: Incident;
+}
+
+export interface UpdateIncidentParams {
+ incidentId: string;
+ incident: Incident;
+}
+
+export interface CreateCommentParams {
+ incidentId: string;
+ comment: Comment;
+}
-export type JiraPublicConfigurationType = TypeOf;
-export type JiraSecretConfigurationType = TypeOf;
+export type GetIssueTypesResponse = Array<{ id: string; name: string }>;
+export type GetFieldsByIssueTypeResponse = Record<
+ string,
+ { allowedValues: Array<{}>; defaultValue: {} }
+>;
-interface CreateIncidentBasicRequestArgs {
- summary: string;
- description: string;
+export interface ExternalService {
+ getIncident: (id: string) => Promise;
+ createIncident: (params: CreateIncidentParams) => Promise;
+ updateIncident: (params: UpdateIncidentParams) => Promise;
+ createComment: (params: CreateCommentParams) => Promise;
+ getCapabilities: () => Promise;
+ getIssueTypes: () => Promise;
+ getFieldsByIssueType: (issueTypeId: string) => Promise;
}
-interface CreateIncidentRequestArgs extends CreateIncidentBasicRequestArgs {
- project: { key: string };
- issuetype: { name: string };
+
+export interface PushToServiceApiParams extends ExecutorSubActionPushParams {
+ externalObject: Record;
+}
+
+export type ExecutorSubActionGetIncidentParams = TypeOf<
+ typeof ExecutorSubActionGetIncidentParamsSchema
+>;
+
+export type ExecutorSubActionHandshakeParams = TypeOf<
+ typeof ExecutorSubActionHandshakeParamsSchema
+>;
+
+export type ExecutorSubActionGetCapabilitiesParams = TypeOf<
+ typeof ExecutorSubActionGetCapabilitiesParamsSchema
+>;
+
+export type ExecutorSubActionGetIssueTypesParams = TypeOf<
+ typeof ExecutorSubActionGetIssueTypesParamsSchema
+>;
+
+export type ExecutorSubActionGetFieldsByIssueTypeParams = TypeOf<
+ typeof ExecutorSubActionGetFieldsByIssueTypeParamsSchema
+>;
+
+export interface ExternalServiceApiHandlerArgs {
+ externalService: ExternalService;
+ mapping: Map | null;
+}
+
+export interface PushToServiceApiHandlerArgs extends ExternalServiceApiHandlerArgs {
+ params: PushToServiceApiParams;
+ logger: Logger;
}
-export interface CreateIncidentRequest {
- fields: CreateIncidentRequestArgs;
+export interface GetIncidentApiHandlerArgs extends ExternalServiceApiHandlerArgs {
+ params: ExecutorSubActionGetIncidentParams;
}
-export interface UpdateIncidentRequest {
- fields: Partial;
+export interface HandshakeApiHandlerArgs extends ExternalServiceApiHandlerArgs {
+ params: ExecutorSubActionHandshakeParams;
}
-export interface CreateCommentRequest {
- body: string;
+export interface GetIssueTypesHandlerArgs {
+ externalService: ExternalService;
+ params: ExecutorSubActionGetIssueTypesParams;
+}
+
+export interface GetFieldsByIssueTypeHandlerArgs {
+ externalService: ExternalService;
+ params: ExecutorSubActionGetFieldsByIssueTypeParams;
+}
+
+export interface PushToServiceResponse extends ExternalServiceIncidentResponse {
+ comments?: ExternalServiceCommentResponse[];
+}
+
+export interface ExternalServiceApi {
+ handshake: (args: HandshakeApiHandlerArgs) => Promise;
+ pushToService: (args: PushToServiceApiHandlerArgs) => Promise;
+ getIncident: (args: GetIncidentApiHandlerArgs) => Promise;
+ issueTypes: (args: GetIssueTypesHandlerArgs) => Promise;
+ fieldsByIssueType: (
+ args: GetFieldsByIssueTypeHandlerArgs
+ ) => Promise;
+}
+
+export type JiraExecutorResultData =
+ | PushToServiceResponse
+ | GetIssueTypesResponse
+ | GetFieldsByIssueTypeResponse;
+
+export interface Fields {
+ [key: string]: string | string[] | { name: string } | { key: string } | { id: string };
+}
+export interface ResponseError {
+ [k: string]: string;
}
diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/validators.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/validators.ts
index 7226071392bc6..58a3e27247fae 100644
--- a/x-pack/plugins/actions/server/builtin_action_types/jira/validators.ts
+++ b/x-pack/plugins/actions/server/builtin_action_types/jira/validators.ts
@@ -4,8 +4,38 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { validateCommonConfig, validateCommonSecrets } from '../case/validators';
-import { ExternalServiceValidation } from '../case/types';
+import { isEmpty } from 'lodash';
+import { ActionsConfigurationUtilities } from '../../actions_config';
+import {
+ JiraPublicConfigurationType,
+ JiraSecretConfigurationType,
+ ExternalServiceValidation,
+} from './types';
+
+import * as i18n from './translations';
+
+export const validateCommonConfig = (
+ configurationUtilities: ActionsConfigurationUtilities,
+ configObject: JiraPublicConfigurationType
+) => {
+ if (
+ configObject.incidentConfiguration !== null &&
+ isEmpty(configObject.incidentConfiguration.mapping)
+ ) {
+ return i18n.MAPPING_EMPTY;
+ }
+
+ try {
+ configurationUtilities.ensureUriAllowed(configObject.apiUrl);
+ } catch (allowedListError) {
+ return i18n.ALLOWED_HOSTS_ERROR(allowedListError.message);
+ }
+};
+
+export const validateCommonSecrets = (
+ configurationUtilities: ActionsConfigurationUtilities,
+ secrets: JiraSecretConfigurationType
+) => {};
export const validate: ExternalServiceValidation = {
config: validateCommonConfig,
diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/api.test.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/api.test.ts
index 0bb096ecd0f62..7a68781bb9a75 100644
--- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/api.test.ts
+++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/api.test.ts
@@ -91,7 +91,7 @@ describe('api', () => {
expect(externalService.updateIncident).not.toHaveBeenCalled();
});
- test('it calls updateIncident correctly', async () => {
+ test('it calls updateIncident correctly when creating an incident and having comments', async () => {
const params = { ...apiParams, externalId: null };
await api.pushToService({
externalService,
@@ -103,7 +103,7 @@ describe('api', () => {
expect(externalService.updateIncident).toHaveBeenCalledTimes(2);
expect(externalService.updateIncident).toHaveBeenNthCalledWith(1, {
incident: {
- comments: 'A comment',
+ comments: 'A comment (added at 2020-03-13T08:34:53.450Z by Elastic User)',
description:
'Incident description (created at 2020-03-13T08:34:53.450Z by Elastic User)',
short_description:
@@ -114,7 +114,7 @@ describe('api', () => {
expect(externalService.updateIncident).toHaveBeenNthCalledWith(2, {
incident: {
- comments: 'Another comment',
+ comments: 'Another comment (added at 2020-03-13T08:34:53.450Z by Elastic User)',
description:
'Incident description (created at 2020-03-13T08:34:53.450Z by Elastic User)',
short_description:
@@ -215,7 +215,7 @@ describe('api', () => {
expect(externalService.updateIncident).toHaveBeenNthCalledWith(2, {
incident: {
- comments: 'A comment',
+ comments: 'A comment (added at 2020-03-13T08:34:53.450Z by Elastic User)',
description:
'Incident description (updated at 2020-03-13T08:34:53.450Z by Elastic User)',
short_description:
diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/api.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/api.ts
index 3281832941558..c8e6147ecef46 100644
--- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/api.ts
+++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/api.ts
@@ -10,11 +10,13 @@ import {
HandshakeApiHandlerArgs,
GetIncidentApiHandlerArgs,
ExternalServiceApi,
+ PushToServiceApiParams,
+ PushToServiceResponse,
} from './types';
// TODO: to remove, need to support Case
import { transformers } from '../case/transformers';
-import { PushToServiceResponse, TransformFieldsArgs } from './case_types';
+import { TransformFieldsArgs, Comment, EntityInformation } from '../case/common_types';
import { prepareFieldsForTransformation } from '../case/utils';
const handshakeHandler = async ({
@@ -92,9 +94,10 @@ const pushToServiceHandler = async ({
mapping.get('comments')?.actionType !== 'nothing'
) {
res.comments = [];
+ const commentsTransformed = transformComments(comments, ['informationAdded']);
const fieldsKey = mapping.get('comments')?.target ?? 'comments';
- for (const currentComment of comments) {
+ for (const currentComment of commentsTransformed) {
await externalService.updateIncident({
incidentId: res.id,
incident: {
@@ -118,7 +121,7 @@ export const transformFields = ({
params,
fields,
currentIncident,
-}: TransformFieldsArgs): Record => {
+}: TransformFieldsArgs): Record => {
return fields.reduce((prev, cur) => {
const transform = flow(...cur.pipes.map((p) => transformers[p]));
return {
@@ -126,20 +129,35 @@ export const transformFields = ({
[cur.key]: transform({
value: cur.value,
date: params.updatedAt ?? params.createdAt,
- user:
- (params.updatedBy != null
- ? params.updatedBy.fullName
- ? params.updatedBy.fullName
- : params.updatedBy.username
- : params.createdBy.fullName
- ? params.createdBy.fullName
- : params.createdBy.username) ?? '',
+ user: getEntity(params),
previousValue: currentIncident ? currentIncident[cur.key] : '',
}).value,
};
}, {});
};
+export const transformComments = (comments: Comment[], pipes: string[]): Comment[] => {
+ return comments.map((c) => ({
+ ...c,
+ comment: flow(...pipes.map((p) => transformers[p]))({
+ value: c.comment,
+ date: c.updatedAt ?? c.createdAt,
+ user: getEntity(c),
+ }).value,
+ }));
+};
+
+export const getEntity = (entity: EntityInformation): string =>
+ (entity.updatedBy != null
+ ? entity.updatedBy.fullName
+ ? entity.updatedBy.fullName
+ : entity.updatedBy.username
+ : entity.createdBy != null
+ ? entity.createdBy.fullName
+ ? entity.createdBy.fullName
+ : entity.createdBy.username
+ : '') ?? '';
+
export const api: ExternalServiceApi = {
handshake: handshakeHandler,
pushToService: pushToServiceHandler,
diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.ts
index 3addbe7c54dac..41a577918b18e 100644
--- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.ts
+++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.ts
@@ -24,11 +24,11 @@ import {
ExecutorSubActionPushParams,
ServiceNowPublicConfigurationType,
ServiceNowSecretConfigurationType,
+ PushToServiceResponse,
} from './types';
// TODO: to remove, need to support Case
import { buildMap, mapParams } from '../case/utils';
-import { PushToServiceResponse } from './case_types';
interface GetActionTypeParams {
logger: Logger;
diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/mocks.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/mocks.ts
index 5f22fcd4fdc85..55a14e4528acf 100644
--- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/mocks.ts
+++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/mocks.ts
@@ -5,7 +5,7 @@
*/
import { ExternalService, PushToServiceApiParams, ExecutorSubActionPushParams } from './types';
-import { MapRecord } from './case_types';
+import { MapRecord } from '../case/common_types';
const createMock = (): jest.Mocked => {
const service = {
diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/schema.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/schema.ts
index 82afebaaee445..921de42adfcaf 100644
--- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/schema.ts
+++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/schema.ts
@@ -5,7 +5,11 @@
*/
import { schema } from '@kbn/config-schema';
-import { CommentSchema, EntityInformation, IncidentConfigurationSchema } from './case_shema';
+import {
+ CommentSchema,
+ EntityInformation,
+ IncidentConfigurationSchema,
+} from '../case/common_schema';
export const ExternalIncidentServiceConfiguration = {
apiUrl: schema.string(),
diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/translations.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/translations.ts
index 05c7d805a1852..7cc97a241c4bc 100644
--- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/translations.ts
+++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/translations.ts
@@ -10,8 +10,8 @@ export const NAME = i18n.translate('xpack.actions.builtin.servicenowTitle', {
defaultMessage: 'ServiceNow',
});
-export const WHITE_LISTED_ERROR = (message: string) =>
- i18n.translate('xpack.actions.builtin.configuration.apiWhitelistError', {
+export const ALLOWED_HOSTS_ERROR = (message: string) =>
+ i18n.translate('xpack.actions.builtin.configuration.apiAllowedHostsError', {
defaultMessage: 'error configuring connector action: {message}',
values: {
message,
diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/types.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/types.ts
index 0db9b6642ea5c..e8fcfac45d789 100644
--- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/types.ts
+++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/types.ts
@@ -16,8 +16,8 @@ import {
ExecutorSubActionHandshakeParamsSchema,
} from './schema';
import { ActionsConfigurationUtilities } from '../../actions_config';
-import { IncidentConfigurationSchema } from './case_shema';
-import { PushToServiceResponse } from './case_types';
+import { ExternalServiceCommentResponse } from '../case/common_types';
+import { IncidentConfigurationSchema } from '../case/common_schema';
import { Logger } from '../../../../../../src/core/server';
export type ServiceNowPublicConfigurationType = TypeOf<
@@ -52,6 +52,9 @@ export interface ExternalServiceIncidentResponse {
url: string;
pushedDate: string;
}
+export interface PushToServiceResponse extends ExternalServiceIncidentResponse {
+ comments?: ExternalServiceCommentResponse[];
+}
export type ExternalServiceParams = Record;
diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/validators.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/validators.ts
index 6eec3b8d63b86..87bbfd9c7ea95 100644
--- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/validators.ts
+++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/validators.ts
@@ -27,8 +27,8 @@ export const validateCommonConfig = (
try {
configurationUtilities.ensureUriAllowed(configObject.apiUrl);
- } catch (allowListError) {
- return i18n.WHITE_LISTED_ERROR(allowListError.message);
+ } catch (allowedListError) {
+ return i18n.ALLOWED_HOSTS_ERROR(allowedListError.message);
}
};
diff --git a/x-pack/plugins/alerts/README.md b/x-pack/plugins/alerts/README.md
index aab05cb0a7cfd..6307e463af853 100644
--- a/x-pack/plugins/alerts/README.md
+++ b/x-pack/plugins/alerts/README.md
@@ -26,7 +26,7 @@ Table of Contents
- [`GET /api/alerts/_find`: Find alerts](#get-apialertfind-find-alerts)
- [`GET /api/alerts/alert/{id}`: Get alert](#get-apialertid-get-alert)
- [`GET /api/alerts/alert/{id}/state`: Get alert state](#get-apialertidstate-get-alert-state)
- - [`GET /api/alerts/alert/{id}/status`: Get alert status](#get-apialertidstate-get-alert-status)
+ - [`GET /api/alerts/alert/{id}/_instance_summary`: Get alert instance summary](#get-apialertidstate-get-alert-instance-summary)
- [`GET /api/alerts/list_alert_types`: List alert types](#get-apialerttypes-list-alert-types)
- [`PUT /api/alerts/alert/{id}`: Update alert](#put-apialertid-update-alert)
- [`POST /api/alerts/alert/{id}/_enable`: Enable an alert](#post-apialertidenable-enable-an-alert)
@@ -505,7 +505,7 @@ Params:
|---|---|---|
|id|The id of the alert whose state you're trying to get.|string|
-### `GET /api/alerts/alert/{id}/status`: Get alert status
+### `GET /api/alerts/alert/{id}/_instance_summary`: Get alert instance summary
Similar to the `GET state` call, but collects additional information from
the event log.
@@ -514,7 +514,7 @@ Params:
|Property|Description|Type|
|---|---|---|
-|id|The id of the alert whose status you're trying to get.|string|
+|id|The id of the alert whose instance summary you're trying to get.|string|
Query:
diff --git a/x-pack/plugins/alerts/common/alert_status.ts b/x-pack/plugins/alerts/common/alert_instance_summary.ts
similarity index 95%
rename from x-pack/plugins/alerts/common/alert_status.ts
rename to x-pack/plugins/alerts/common/alert_instance_summary.ts
index 517db6d6cb243..333db3ccda963 100644
--- a/x-pack/plugins/alerts/common/alert_status.ts
+++ b/x-pack/plugins/alerts/common/alert_instance_summary.ts
@@ -7,7 +7,7 @@
type AlertStatusValues = 'OK' | 'Active' | 'Error';
type AlertInstanceStatusValues = 'OK' | 'Active';
-export interface AlertStatus {
+export interface AlertInstanceSummary {
id: string;
name: string;
tags: string[];
diff --git a/x-pack/plugins/alerts/common/index.ts b/x-pack/plugins/alerts/common/index.ts
index 0922e164a3aa3..ab71f77a049f6 100644
--- a/x-pack/plugins/alerts/common/index.ts
+++ b/x-pack/plugins/alerts/common/index.ts
@@ -9,7 +9,7 @@ export * from './alert_type';
export * from './alert_instance';
export * from './alert_task_instance';
export * from './alert_navigation';
-export * from './alert_status';
+export * from './alert_instance_summary';
export interface ActionGroup {
id: string;
diff --git a/x-pack/plugins/alerts/server/alerts_client.mock.ts b/x-pack/plugins/alerts/server/alerts_client.mock.ts
index b61139ae72c99..b28e9f805f725 100644
--- a/x-pack/plugins/alerts/server/alerts_client.mock.ts
+++ b/x-pack/plugins/alerts/server/alerts_client.mock.ts
@@ -25,7 +25,7 @@ const createAlertsClientMock = () => {
muteInstance: jest.fn(),
unmuteInstance: jest.fn(),
listAlertTypes: jest.fn(),
- getAlertStatus: jest.fn(),
+ getAlertInstanceSummary: jest.fn(),
};
return mocked;
};
diff --git a/x-pack/plugins/alerts/server/alerts_client.test.ts b/x-pack/plugins/alerts/server/alerts_client.test.ts
index f4aef62657abc..801c2c8775361 100644
--- a/x-pack/plugins/alerts/server/alerts_client.test.ts
+++ b/x-pack/plugins/alerts/server/alerts_client.test.ts
@@ -20,7 +20,7 @@ import { ActionsAuthorization } from '../../actions/server';
import { eventLogClientMock } from '../../event_log/server/mocks';
import { QueryEventsBySavedObjectResult } from '../../event_log/server';
import { SavedObject } from 'kibana/server';
-import { EventsFactory } from './lib/alert_status_from_event_log.test';
+import { EventsFactory } from './lib/alert_instance_summary_from_event_log.test';
const taskManager = taskManagerMock.start();
const alertTypeRegistry = alertTypeRegistryMock.create();
@@ -2382,16 +2382,16 @@ describe('getAlertState()', () => {
});
});
-const AlertStatusFindEventsResult: QueryEventsBySavedObjectResult = {
+const AlertInstanceSummaryFindEventsResult: QueryEventsBySavedObjectResult = {
page: 1,
per_page: 10000,
total: 0,
data: [],
};
-const AlertStatusIntervalSeconds = 1;
+const AlertInstanceSummaryIntervalSeconds = 1;
-const BaseAlertStatusSavedObject: SavedObject = {
+const BaseAlertInstanceSummarySavedObject: SavedObject = {
id: '1',
type: 'alert',
attributes: {
@@ -2400,7 +2400,7 @@ const BaseAlertStatusSavedObject: SavedObject = {
tags: ['tag-1', 'tag-2'],
alertTypeId: '123',
consumer: 'alert-consumer',
- schedule: { interval: `${AlertStatusIntervalSeconds}s` },
+ schedule: { interval: `${AlertInstanceSummaryIntervalSeconds}s` },
actions: [],
params: {},
createdBy: null,
@@ -2415,14 +2415,16 @@ const BaseAlertStatusSavedObject: SavedObject = {
references: [],
};
-function getAlertStatusSavedObject(attributes: Partial = {}): SavedObject {
+function getAlertInstanceSummarySavedObject(
+ attributes: Partial = {}
+): SavedObject {
return {
- ...BaseAlertStatusSavedObject,
- attributes: { ...BaseAlertStatusSavedObject.attributes, ...attributes },
+ ...BaseAlertInstanceSummarySavedObject,
+ attributes: { ...BaseAlertInstanceSummarySavedObject.attributes, ...attributes },
};
}
-describe('getAlertStatus()', () => {
+describe('getAlertInstanceSummary()', () => {
let alertsClient: AlertsClient;
beforeEach(() => {
@@ -2430,7 +2432,9 @@ describe('getAlertStatus()', () => {
});
test('runs as expected with some event log data', async () => {
- const alertSO = getAlertStatusSavedObject({ mutedInstanceIds: ['instance-muted-no-activity'] });
+ const alertSO = getAlertInstanceSummarySavedObject({
+ mutedInstanceIds: ['instance-muted-no-activity'],
+ });
unsecuredSavedObjectsClient.get.mockResolvedValueOnce(alertSO);
const eventsFactory = new EventsFactory(mockedDateString);
@@ -2446,7 +2450,7 @@ describe('getAlertStatus()', () => {
.addActiveInstance('instance-currently-active')
.getEvents();
const eventsResult = {
- ...AlertStatusFindEventsResult,
+ ...AlertInstanceSummaryFindEventsResult,
total: events.length,
data: events,
};
@@ -2454,7 +2458,7 @@ describe('getAlertStatus()', () => {
const dateStart = new Date(Date.now() - 60 * 1000).toISOString();
- const result = await alertsClient.getAlertStatus({ id: '1', dateStart });
+ const result = await alertsClient.getAlertInstanceSummary({ id: '1', dateStart });
expect(result).toMatchInlineSnapshot(`
Object {
"alertTypeId": "123",
@@ -2494,16 +2498,18 @@ describe('getAlertStatus()', () => {
`);
});
- // Further tests don't check the result of `getAlertStatus()`, as the result
- // is just the result from the `alertStatusFromEventLog()`, which itself
+ // Further tests don't check the result of `getAlertInstanceSummary()`, as the result
+ // is just the result from the `alertInstanceSummaryFromEventLog()`, which itself
// has a complete set of tests. These tests just make sure the data gets
- // sent into `getAlertStatus()` as appropriate.
+ // sent into `getAlertInstanceSummary()` as appropriate.
test('calls saved objects and event log client with default params', async () => {
- unsecuredSavedObjectsClient.get.mockResolvedValueOnce(getAlertStatusSavedObject());
- eventLogClient.findEventsBySavedObject.mockResolvedValueOnce(AlertStatusFindEventsResult);
+ unsecuredSavedObjectsClient.get.mockResolvedValueOnce(getAlertInstanceSummarySavedObject());
+ eventLogClient.findEventsBySavedObject.mockResolvedValueOnce(
+ AlertInstanceSummaryFindEventsResult
+ );
- await alertsClient.getAlertStatus({ id: '1' });
+ await alertsClient.getAlertInstanceSummary({ id: '1' });
expect(unsecuredSavedObjectsClient.get).toHaveBeenCalledTimes(1);
expect(eventLogClient.findEventsBySavedObject).toHaveBeenCalledTimes(1);
@@ -2526,17 +2532,21 @@ describe('getAlertStatus()', () => {
const startMillis = Date.parse(start!);
const endMillis = Date.parse(end!);
- const expectedDuration = 60 * AlertStatusIntervalSeconds * 1000;
+ const expectedDuration = 60 * AlertInstanceSummaryIntervalSeconds * 1000;
expect(endMillis - startMillis).toBeGreaterThan(expectedDuration - 2);
expect(endMillis - startMillis).toBeLessThan(expectedDuration + 2);
});
test('calls event log client with start date', async () => {
- unsecuredSavedObjectsClient.get.mockResolvedValueOnce(getAlertStatusSavedObject());
- eventLogClient.findEventsBySavedObject.mockResolvedValueOnce(AlertStatusFindEventsResult);
+ unsecuredSavedObjectsClient.get.mockResolvedValueOnce(getAlertInstanceSummarySavedObject());
+ eventLogClient.findEventsBySavedObject.mockResolvedValueOnce(
+ AlertInstanceSummaryFindEventsResult
+ );
- const dateStart = new Date(Date.now() - 60 * AlertStatusIntervalSeconds * 1000).toISOString();
- await alertsClient.getAlertStatus({ id: '1', dateStart });
+ const dateStart = new Date(
+ Date.now() - 60 * AlertInstanceSummaryIntervalSeconds * 1000
+ ).toISOString();
+ await alertsClient.getAlertInstanceSummary({ id: '1', dateStart });
expect(unsecuredSavedObjectsClient.get).toHaveBeenCalledTimes(1);
expect(eventLogClient.findEventsBySavedObject).toHaveBeenCalledTimes(1);
@@ -2551,11 +2561,13 @@ describe('getAlertStatus()', () => {
});
test('calls event log client with relative start date', async () => {
- unsecuredSavedObjectsClient.get.mockResolvedValueOnce(getAlertStatusSavedObject());
- eventLogClient.findEventsBySavedObject.mockResolvedValueOnce(AlertStatusFindEventsResult);
+ unsecuredSavedObjectsClient.get.mockResolvedValueOnce(getAlertInstanceSummarySavedObject());
+ eventLogClient.findEventsBySavedObject.mockResolvedValueOnce(
+ AlertInstanceSummaryFindEventsResult
+ );
const dateStart = '2m';
- await alertsClient.getAlertStatus({ id: '1', dateStart });
+ await alertsClient.getAlertInstanceSummary({ id: '1', dateStart });
expect(unsecuredSavedObjectsClient.get).toHaveBeenCalledTimes(1);
expect(eventLogClient.findEventsBySavedObject).toHaveBeenCalledTimes(1);
@@ -2570,28 +2582,36 @@ describe('getAlertStatus()', () => {
});
test('invalid start date throws an error', async () => {
- unsecuredSavedObjectsClient.get.mockResolvedValueOnce(getAlertStatusSavedObject());
- eventLogClient.findEventsBySavedObject.mockResolvedValueOnce(AlertStatusFindEventsResult);
+ unsecuredSavedObjectsClient.get.mockResolvedValueOnce(getAlertInstanceSummarySavedObject());
+ eventLogClient.findEventsBySavedObject.mockResolvedValueOnce(
+ AlertInstanceSummaryFindEventsResult
+ );
const dateStart = 'ain"t no way this will get parsed as a date';
- expect(alertsClient.getAlertStatus({ id: '1', dateStart })).rejects.toMatchInlineSnapshot(
+ expect(
+ alertsClient.getAlertInstanceSummary({ id: '1', dateStart })
+ ).rejects.toMatchInlineSnapshot(
`[Error: Invalid date for parameter dateStart: "ain"t no way this will get parsed as a date"]`
);
});
test('saved object get throws an error', async () => {
unsecuredSavedObjectsClient.get.mockRejectedValueOnce(new Error('OMG!'));
- eventLogClient.findEventsBySavedObject.mockResolvedValueOnce(AlertStatusFindEventsResult);
+ eventLogClient.findEventsBySavedObject.mockResolvedValueOnce(
+ AlertInstanceSummaryFindEventsResult
+ );
- expect(alertsClient.getAlertStatus({ id: '1' })).rejects.toMatchInlineSnapshot(`[Error: OMG!]`);
+ expect(alertsClient.getAlertInstanceSummary({ id: '1' })).rejects.toMatchInlineSnapshot(
+ `[Error: OMG!]`
+ );
});
test('findEvents throws an error', async () => {
- unsecuredSavedObjectsClient.get.mockResolvedValueOnce(getAlertStatusSavedObject());
+ unsecuredSavedObjectsClient.get.mockResolvedValueOnce(getAlertInstanceSummarySavedObject());
eventLogClient.findEventsBySavedObject.mockRejectedValueOnce(new Error('OMG 2!'));
// error eaten but logged
- await alertsClient.getAlertStatus({ id: '1' });
+ await alertsClient.getAlertInstanceSummary({ id: '1' });
});
});
diff --git a/x-pack/plugins/alerts/server/alerts_client.ts b/x-pack/plugins/alerts/server/alerts_client.ts
index 74aef644d58ca..0703a1e13937c 100644
--- a/x-pack/plugins/alerts/server/alerts_client.ts
+++ b/x-pack/plugins/alerts/server/alerts_client.ts
@@ -24,7 +24,7 @@ import {
IntervalSchedule,
SanitizedAlert,
AlertTaskState,
- AlertStatus,
+ AlertInstanceSummary,
} from './types';
import { validateAlertTypeParams } from './lib';
import {
@@ -44,7 +44,7 @@ import {
} from './authorization/alerts_authorization';
import { IEventLogClient } from '../../../plugins/event_log/server';
import { parseIsoOrRelativeDate } from './lib/iso_or_relative_date';
-import { alertStatusFromEventLog } from './lib/alert_status_from_event_log';
+import { alertInstanceSummaryFromEventLog } from './lib/alert_instance_summary_from_event_log';
import { IEvent } from '../../event_log/server';
import { parseDuration } from '../common/parse_duration';
@@ -139,7 +139,7 @@ interface UpdateOptions {
};
}
-interface GetAlertStatusParams {
+interface GetAlertInstanceSummaryParams {
id: string;
dateStart?: string;
}
@@ -284,16 +284,19 @@ export class AlertsClient {
}
}
- public async getAlertStatus({ id, dateStart }: GetAlertStatusParams): Promise {
- this.logger.debug(`getAlertStatus(): getting alert ${id}`);
+ public async getAlertInstanceSummary({
+ id,
+ dateStart,
+ }: GetAlertInstanceSummaryParams): Promise {
+ this.logger.debug(`getAlertInstanceSummary(): getting alert ${id}`);
const alert = await this.get({ id });
await this.authorization.ensureAuthorized(
alert.alertTypeId,
alert.consumer,
- ReadOperations.GetAlertStatus
+ ReadOperations.GetAlertInstanceSummary
);
- // default duration of status is 60 * alert interval
+ // default duration of instance summary is 60 * alert interval
const dateNow = new Date();
const durationMillis = parseDuration(alert.schedule.interval) * 60;
const defaultDateStart = new Date(dateNow.valueOf() - durationMillis);
@@ -301,7 +304,7 @@ export class AlertsClient {
const eventLogClient = await this.getEventLogClient();
- this.logger.debug(`getAlertStatus(): search the event log for alert ${id}`);
+ this.logger.debug(`getAlertInstanceSummary(): search the event log for alert ${id}`);
let events: IEvent[];
try {
const queryResults = await eventLogClient.findEventsBySavedObject('alert', id, {
@@ -314,12 +317,12 @@ export class AlertsClient {
events = queryResults.data;
} catch (err) {
this.logger.debug(
- `alertsClient.getAlertStatus(): error searching event log for alert ${id}: ${err.message}`
+ `alertsClient.getAlertInstanceSummary(): error searching event log for alert ${id}: ${err.message}`
);
events = [];
}
- return alertStatusFromEventLog({
+ return alertInstanceSummaryFromEventLog({
alert,
events,
dateStart: parsedDateStart.toISOString(),
@@ -952,7 +955,7 @@ function parseDate(dateString: string | undefined, propertyName: string, default
const parsedDate = parseIsoOrRelativeDate(dateString);
if (parsedDate === undefined) {
throw Boom.badRequest(
- i18n.translate('xpack.alerts.alertsClient.getAlertStatus.invalidDate', {
+ i18n.translate('xpack.alerts.alertsClient.invalidDate', {
defaultMessage: 'Invalid date for parameter {field}: "{dateValue}"',
values: {
field: propertyName,
diff --git a/x-pack/plugins/alerts/server/authorization/alerts_authorization.ts b/x-pack/plugins/alerts/server/authorization/alerts_authorization.ts
index b2a214eae9316..b362a50c9f10b 100644
--- a/x-pack/plugins/alerts/server/authorization/alerts_authorization.ts
+++ b/x-pack/plugins/alerts/server/authorization/alerts_authorization.ts
@@ -18,7 +18,7 @@ import { Space } from '../../../spaces/server';
export enum ReadOperations {
Get = 'get',
GetAlertState = 'getAlertState',
- GetAlertStatus = 'getAlertStatus',
+ GetAlertInstanceSummary = 'getAlertInstanceSummary',
Find = 'find',
}
diff --git a/x-pack/plugins/alerts/server/lib/alert_status_from_event_log.test.ts b/x-pack/plugins/alerts/server/lib/alert_instance_summary_from_event_log.test.ts
similarity index 83%
rename from x-pack/plugins/alerts/server/lib/alert_status_from_event_log.test.ts
rename to x-pack/plugins/alerts/server/lib/alert_instance_summary_from_event_log.test.ts
index 15570d3032f24..b5936cf3577b3 100644
--- a/x-pack/plugins/alerts/server/lib/alert_status_from_event_log.test.ts
+++ b/x-pack/plugins/alerts/server/lib/alert_instance_summary_from_event_log.test.ts
@@ -4,22 +4,27 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { SanitizedAlert, AlertStatus } from '../types';
+import { SanitizedAlert, AlertInstanceSummary } from '../types';
import { IValidatedEvent } from '../../../event_log/server';
import { EVENT_LOG_ACTIONS, EVENT_LOG_PROVIDER } from '../plugin';
-import { alertStatusFromEventLog } from './alert_status_from_event_log';
+import { alertInstanceSummaryFromEventLog } from './alert_instance_summary_from_event_log';
const ONE_HOUR_IN_MILLIS = 60 * 60 * 1000;
const dateStart = '2020-06-18T00:00:00.000Z';
const dateEnd = dateString(dateStart, ONE_HOUR_IN_MILLIS);
-describe('alertStatusFromEventLog', () => {
+describe('alertInstanceSummaryFromEventLog', () => {
test('no events and muted ids', async () => {
const alert = createAlert({});
const events: IValidatedEvent[] = [];
- const status: AlertStatus = alertStatusFromEventLog({ alert, events, dateStart, dateEnd });
+ const summary: AlertInstanceSummary = alertInstanceSummaryFromEventLog({
+ alert,
+ events,
+ dateStart,
+ dateEnd,
+ });
- expect(status).toMatchInlineSnapshot(`
+ expect(summary).toMatchInlineSnapshot(`
Object {
"alertTypeId": "123",
"consumer": "alert-consumer",
@@ -52,14 +57,14 @@ describe('alertStatusFromEventLog', () => {
muteAll: true,
});
const events: IValidatedEvent[] = [];
- const status: AlertStatus = alertStatusFromEventLog({
+ const summary: AlertInstanceSummary = alertInstanceSummaryFromEventLog({
alert,
events,
dateStart: dateString(dateEnd, ONE_HOUR_IN_MILLIS),
dateEnd: dateString(dateEnd, ONE_HOUR_IN_MILLIS * 2),
});
- expect(status).toMatchInlineSnapshot(`
+ expect(summary).toMatchInlineSnapshot(`
Object {
"alertTypeId": "456",
"consumer": "alert-consumer-2",
@@ -87,9 +92,14 @@ describe('alertStatusFromEventLog', () => {
mutedInstanceIds: ['instance-1', 'instance-2'],
});
const events: IValidatedEvent[] = [];
- const alertStatus: AlertStatus = alertStatusFromEventLog({ alert, events, dateStart, dateEnd });
+ const summary: AlertInstanceSummary = alertInstanceSummaryFromEventLog({
+ alert,
+ events,
+ dateStart,
+ dateEnd,
+ });
- const { lastRun, status, instances } = alertStatus;
+ const { lastRun, status, instances } = summary;
expect({ lastRun, status, instances }).toMatchInlineSnapshot(`
Object {
"instances": Object {
@@ -115,9 +125,14 @@ describe('alertStatusFromEventLog', () => {
const eventsFactory = new EventsFactory();
const events = eventsFactory.addExecute().advanceTime(10000).addExecute().getEvents();
- const alertStatus: AlertStatus = alertStatusFromEventLog({ alert, events, dateStart, dateEnd });
+ const summary: AlertInstanceSummary = alertInstanceSummaryFromEventLog({
+ alert,
+ events,
+ dateStart,
+ dateEnd,
+ });
- const { lastRun, status, instances } = alertStatus;
+ const { lastRun, status, instances } = summary;
expect({ lastRun, status, instances }).toMatchInlineSnapshot(`
Object {
"instances": Object {},
@@ -136,9 +151,14 @@ describe('alertStatusFromEventLog', () => {
.addExecute('rut roh!')
.getEvents();
- const alertStatus: AlertStatus = alertStatusFromEventLog({ alert, events, dateStart, dateEnd });
+ const summary: AlertInstanceSummary = alertInstanceSummaryFromEventLog({
+ alert,
+ events,
+ dateStart,
+ dateEnd,
+ });
- const { lastRun, status, errorMessages, instances } = alertStatus;
+ const { lastRun, status, errorMessages, instances } = summary;
expect({ lastRun, status, errorMessages, instances }).toMatchInlineSnapshot(`
Object {
"errorMessages": Array [
@@ -170,9 +190,14 @@ describe('alertStatusFromEventLog', () => {
.addResolvedInstance('instance-1')
.getEvents();
- const alertStatus: AlertStatus = alertStatusFromEventLog({ alert, events, dateStart, dateEnd });
+ const summary: AlertInstanceSummary = alertInstanceSummaryFromEventLog({
+ alert,
+ events,
+ dateStart,
+ dateEnd,
+ });
- const { lastRun, status, instances } = alertStatus;
+ const { lastRun, status, instances } = summary;
expect({ lastRun, status, instances }).toMatchInlineSnapshot(`
Object {
"instances": Object {
@@ -199,9 +224,14 @@ describe('alertStatusFromEventLog', () => {
.addResolvedInstance('instance-1')
.getEvents();
- const alertStatus: AlertStatus = alertStatusFromEventLog({ alert, events, dateStart, dateEnd });
+ const summary: AlertInstanceSummary = alertInstanceSummaryFromEventLog({
+ alert,
+ events,
+ dateStart,
+ dateEnd,
+ });
- const { lastRun, status, instances } = alertStatus;
+ const { lastRun, status, instances } = summary;
expect({ lastRun, status, instances }).toMatchInlineSnapshot(`
Object {
"instances": Object {
@@ -229,9 +259,14 @@ describe('alertStatusFromEventLog', () => {
.addActiveInstance('instance-1')
.getEvents();
- const alertStatus: AlertStatus = alertStatusFromEventLog({ alert, events, dateStart, dateEnd });
+ const summary: AlertInstanceSummary = alertInstanceSummaryFromEventLog({
+ alert,
+ events,
+ dateStart,
+ dateEnd,
+ });
- const { lastRun, status, instances } = alertStatus;
+ const { lastRun, status, instances } = summary;
expect({ lastRun, status, instances }).toMatchInlineSnapshot(`
Object {
"instances": Object {
@@ -258,9 +293,14 @@ describe('alertStatusFromEventLog', () => {
.addActiveInstance('instance-1')
.getEvents();
- const alertStatus: AlertStatus = alertStatusFromEventLog({ alert, events, dateStart, dateEnd });
+ const summary: AlertInstanceSummary = alertInstanceSummaryFromEventLog({
+ alert,
+ events,
+ dateStart,
+ dateEnd,
+ });
- const { lastRun, status, instances } = alertStatus;
+ const { lastRun, status, instances } = summary;
expect({ lastRun, status, instances }).toMatchInlineSnapshot(`
Object {
"instances": Object {
@@ -291,9 +331,14 @@ describe('alertStatusFromEventLog', () => {
.addResolvedInstance('instance-2')
.getEvents();
- const alertStatus: AlertStatus = alertStatusFromEventLog({ alert, events, dateStart, dateEnd });
+ const summary: AlertInstanceSummary = alertInstanceSummaryFromEventLog({
+ alert,
+ events,
+ dateStart,
+ dateEnd,
+ });
- const { lastRun, status, instances } = alertStatus;
+ const { lastRun, status, instances } = summary;
expect({ lastRun, status, instances }).toMatchInlineSnapshot(`
Object {
"instances": Object {
@@ -335,9 +380,14 @@ describe('alertStatusFromEventLog', () => {
.addActiveInstance('instance-1')
.getEvents();
- const alertStatus: AlertStatus = alertStatusFromEventLog({ alert, events, dateStart, dateEnd });
+ const summary: AlertInstanceSummary = alertInstanceSummaryFromEventLog({
+ alert,
+ events,
+ dateStart,
+ dateEnd,
+ });
- const { lastRun, status, instances } = alertStatus;
+ const { lastRun, status, instances } = summary;
expect({ lastRun, status, instances }).toMatchInlineSnapshot(`
Object {
"instances": Object {
diff --git a/x-pack/plugins/alerts/server/lib/alert_status_from_event_log.ts b/x-pack/plugins/alerts/server/lib/alert_instance_summary_from_event_log.ts
similarity index 79%
rename from x-pack/plugins/alerts/server/lib/alert_status_from_event_log.ts
rename to x-pack/plugins/alerts/server/lib/alert_instance_summary_from_event_log.ts
index 606bd44c6990c..9a5e870c8199a 100644
--- a/x-pack/plugins/alerts/server/lib/alert_status_from_event_log.ts
+++ b/x-pack/plugins/alerts/server/lib/alert_instance_summary_from_event_log.ts
@@ -4,21 +4,23 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { SanitizedAlert, AlertStatus, AlertInstanceStatus } from '../types';
+import { SanitizedAlert, AlertInstanceSummary, AlertInstanceStatus } from '../types';
import { IEvent } from '../../../event_log/server';
import { EVENT_LOG_ACTIONS, EVENT_LOG_PROVIDER } from '../plugin';
-export interface AlertStatusFromEventLogParams {
+export interface AlertInstanceSummaryFromEventLogParams {
alert: SanitizedAlert;
events: IEvent[];
dateStart: string;
dateEnd: string;
}
-export function alertStatusFromEventLog(params: AlertStatusFromEventLogParams): AlertStatus {
+export function alertInstanceSummaryFromEventLog(
+ params: AlertInstanceSummaryFromEventLogParams
+): AlertInstanceSummary {
// initialize the result
const { alert, events, dateStart, dateEnd } = params;
- const alertStatus: AlertStatus = {
+ const alertInstanceSummary: AlertInstanceSummary = {
id: alert.id,
name: alert.name,
tags: alert.tags,
@@ -50,17 +52,17 @@ export function alertStatusFromEventLog(params: AlertStatusFromEventLogParams):
if (action === undefined) continue;
if (action === EVENT_LOG_ACTIONS.execute) {
- alertStatus.lastRun = timeStamp;
+ alertInstanceSummary.lastRun = timeStamp;
const errorMessage = event?.error?.message;
if (errorMessage !== undefined) {
- alertStatus.status = 'Error';
- alertStatus.errorMessages.push({
+ alertInstanceSummary.status = 'Error';
+ alertInstanceSummary.errorMessages.push({
date: timeStamp,
message: errorMessage,
});
} else {
- alertStatus.status = 'OK';
+ alertInstanceSummary.status = 'OK';
}
continue;
@@ -91,19 +93,19 @@ export function alertStatusFromEventLog(params: AlertStatusFromEventLogParams):
// convert the instances map to object form
const instanceIds = Array.from(instances.keys()).sort();
for (const instanceId of instanceIds) {
- alertStatus.instances[instanceId] = instances.get(instanceId)!;
+ alertInstanceSummary.instances[instanceId] = instances.get(instanceId)!;
}
// set the overall alert status to Active if appropriate
- if (alertStatus.status !== 'Error') {
+ if (alertInstanceSummary.status !== 'Error') {
if (Array.from(instances.values()).some((instance) => instance.status === 'Active')) {
- alertStatus.status = 'Active';
+ alertInstanceSummary.status = 'Active';
}
}
- alertStatus.errorMessages.sort((a, b) => a.date.localeCompare(b.date));
+ alertInstanceSummary.errorMessages.sort((a, b) => a.date.localeCompare(b.date));
- return alertStatus;
+ return alertInstanceSummary;
}
// return an instance status object, creating and adding to the map if needed
diff --git a/x-pack/plugins/alerts/server/plugin.ts b/x-pack/plugins/alerts/server/plugin.ts
index b16ded9fb5c91..4f9b1f7c22e6d 100644
--- a/x-pack/plugins/alerts/server/plugin.ts
+++ b/x-pack/plugins/alerts/server/plugin.ts
@@ -38,7 +38,7 @@ import {
findAlertRoute,
getAlertRoute,
getAlertStateRoute,
- getAlertStatusRoute,
+ getAlertInstanceSummaryRoute,
listAlertTypesRoute,
updateAlertRoute,
enableAlertRoute,
@@ -193,7 +193,7 @@ export class AlertingPlugin {
findAlertRoute(router, this.licenseState);
getAlertRoute(router, this.licenseState);
getAlertStateRoute(router, this.licenseState);
- getAlertStatusRoute(router, this.licenseState);
+ getAlertInstanceSummaryRoute(router, this.licenseState);
listAlertTypesRoute(router, this.licenseState);
updateAlertRoute(router, this.licenseState);
enableAlertRoute(router, this.licenseState);
diff --git a/x-pack/plugins/alerts/server/routes/get_alert_status.test.ts b/x-pack/plugins/alerts/server/routes/get_alert_instance_summary.test.ts
similarity index 75%
rename from x-pack/plugins/alerts/server/routes/get_alert_status.test.ts
rename to x-pack/plugins/alerts/server/routes/get_alert_instance_summary.test.ts
index 1b4cb1941018b..8957a3d7c091e 100644
--- a/x-pack/plugins/alerts/server/routes/get_alert_status.test.ts
+++ b/x-pack/plugins/alerts/server/routes/get_alert_instance_summary.test.ts
@@ -4,13 +4,13 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { getAlertStatusRoute } from './get_alert_status';
+import { getAlertInstanceSummaryRoute } from './get_alert_instance_summary';
import { httpServiceMock } from 'src/core/server/mocks';
import { mockLicenseState } from '../lib/license_state.mock';
import { mockHandlerArguments } from './_mock_handler_arguments';
import { SavedObjectsErrorHelpers } from 'src/core/server';
import { alertsClientMock } from '../alerts_client.mock';
-import { AlertStatus } from '../types';
+import { AlertInstanceSummary } from '../types';
const alertsClient = alertsClientMock.create();
jest.mock('../lib/license_api_access.ts', () => ({
@@ -21,9 +21,9 @@ beforeEach(() => {
jest.resetAllMocks();
});
-describe('getAlertStatusRoute', () => {
+describe('getAlertInstanceSummaryRoute', () => {
const dateString = new Date().toISOString();
- const mockedAlertStatus: AlertStatus = {
+ const mockedAlertInstanceSummary: AlertInstanceSummary = {
id: '',
name: '',
tags: [],
@@ -39,17 +39,17 @@ describe('getAlertStatusRoute', () => {
instances: {},
};
- it('gets alert status', async () => {
+ it('gets alert instance summary', async () => {
const licenseState = mockLicenseState();
const router = httpServiceMock.createRouter();
- getAlertStatusRoute(router, licenseState);
+ getAlertInstanceSummaryRoute(router, licenseState);
const [config, handler] = router.get.mock.calls[0];
- expect(config.path).toMatchInlineSnapshot(`"/api/alerts/alert/{id}/status"`);
+ expect(config.path).toMatchInlineSnapshot(`"/api/alerts/alert/{id}/_instance_summary"`);
- alertsClient.getAlertStatus.mockResolvedValueOnce(mockedAlertStatus);
+ alertsClient.getAlertInstanceSummary.mockResolvedValueOnce(mockedAlertInstanceSummary);
const [context, req, res] = mockHandlerArguments(
{ alertsClient },
@@ -64,8 +64,8 @@ describe('getAlertStatusRoute', () => {
await handler(context, req, res);
- expect(alertsClient.getAlertStatus).toHaveBeenCalledTimes(1);
- expect(alertsClient.getAlertStatus.mock.calls[0]).toMatchInlineSnapshot(`
+ expect(alertsClient.getAlertInstanceSummary).toHaveBeenCalledTimes(1);
+ expect(alertsClient.getAlertInstanceSummary.mock.calls[0]).toMatchInlineSnapshot(`
Array [
Object {
"dateStart": undefined,
@@ -81,11 +81,11 @@ describe('getAlertStatusRoute', () => {
const licenseState = mockLicenseState();
const router = httpServiceMock.createRouter();
- getAlertStatusRoute(router, licenseState);
+ getAlertInstanceSummaryRoute(router, licenseState);
const [, handler] = router.get.mock.calls[0];
- alertsClient.getAlertStatus = jest
+ alertsClient.getAlertInstanceSummary = jest
.fn()
.mockResolvedValueOnce(SavedObjectsErrorHelpers.createGenericNotFoundError('alert', '1'));
diff --git a/x-pack/plugins/alerts/server/routes/get_alert_status.ts b/x-pack/plugins/alerts/server/routes/get_alert_instance_summary.ts
similarity index 83%
rename from x-pack/plugins/alerts/server/routes/get_alert_status.ts
rename to x-pack/plugins/alerts/server/routes/get_alert_instance_summary.ts
index eab18c50189f4..11a10c2967a58 100644
--- a/x-pack/plugins/alerts/server/routes/get_alert_status.ts
+++ b/x-pack/plugins/alerts/server/routes/get_alert_instance_summary.ts
@@ -24,10 +24,10 @@ const querySchema = schema.object({
dateStart: schema.maybe(schema.string()),
});
-export const getAlertStatusRoute = (router: IRouter, licenseState: LicenseState) => {
+export const getAlertInstanceSummaryRoute = (router: IRouter, licenseState: LicenseState) => {
router.get(
{
- path: `${BASE_ALERT_API_PATH}/alert/{id}/status`,
+ path: `${BASE_ALERT_API_PATH}/alert/{id}/_instance_summary`,
validate: {
params: paramSchema,
query: querySchema,
@@ -45,8 +45,8 @@ export const getAlertStatusRoute = (router: IRouter, licenseState: LicenseState)
const alertsClient = context.alerting.getAlertsClient();
const { id } = req.params;
const { dateStart } = req.query;
- const status = await alertsClient.getAlertStatus({ id, dateStart });
- return res.ok({ body: status });
+ const summary = await alertsClient.getAlertInstanceSummary({ id, dateStart });
+ return res.ok({ body: summary });
})
);
};
diff --git a/x-pack/plugins/alerts/server/routes/index.ts b/x-pack/plugins/alerts/server/routes/index.ts
index 4c6b1eb8e9b58..aed66e82d11f8 100644
--- a/x-pack/plugins/alerts/server/routes/index.ts
+++ b/x-pack/plugins/alerts/server/routes/index.ts
@@ -9,7 +9,7 @@ export { deleteAlertRoute } from './delete';
export { findAlertRoute } from './find';
export { getAlertRoute } from './get';
export { getAlertStateRoute } from './get_alert_state';
-export { getAlertStatusRoute } from './get_alert_status';
+export { getAlertInstanceSummaryRoute } from './get_alert_instance_summary';
export { listAlertTypesRoute } from './list_alert_types';
export { updateAlertRoute } from './update';
export { enableAlertRoute } from './enable';
diff --git a/x-pack/plugins/apm/common/__snapshots__/elasticsearch_fieldnames.test.ts.snap b/x-pack/plugins/apm/common/__snapshots__/elasticsearch_fieldnames.test.ts.snap
index 6238fbfdaa1ab..8218eefe738f0 100644
--- a/x-pack/plugins/apm/common/__snapshots__/elasticsearch_fieldnames.test.ts.snap
+++ b/x-pack/plugins/apm/common/__snapshots__/elasticsearch_fieldnames.test.ts.snap
@@ -14,6 +14,8 @@ exports[`Error CLOUD_PROVIDER 1`] = `"gcp"`;
exports[`Error CLOUD_REGION 1`] = `"europe-west1"`;
+exports[`Error CLS_FIELD 1`] = `undefined`;
+
exports[`Error CONTAINER_ID 1`] = `undefined`;
exports[`Error DESTINATION_ADDRESS 1`] = `undefined`;
@@ -34,6 +36,10 @@ exports[`Error ERROR_LOG_MESSAGE 1`] = `undefined`;
exports[`Error ERROR_PAGE_URL 1`] = `undefined`;
+exports[`Error FCP_FIELD 1`] = `undefined`;
+
+exports[`Error FID_FIELD 1`] = `undefined`;
+
exports[`Error EVENT_OUTCOME 1`] = `undefined`;
exports[`Error HOST_NAME 1`] = `"my hostname"`;
@@ -44,6 +50,8 @@ exports[`Error HTTP_RESPONSE_STATUS_CODE 1`] = `undefined`;
exports[`Error LABEL_NAME 1`] = `undefined`;
+exports[`Error LCP_FIELD 1`] = `undefined`;
+
exports[`Error METRIC_JAVA_GC_COUNT 1`] = `undefined`;
exports[`Error METRIC_JAVA_GC_TIME 1`] = `undefined`;
@@ -118,6 +126,8 @@ exports[`Error SPAN_SUBTYPE 1`] = `undefined`;
exports[`Error SPAN_TYPE 1`] = `undefined`;
+exports[`Error TBT_FIELD 1`] = `undefined`;
+
exports[`Error TRACE_ID 1`] = `"trace id"`;
exports[`Error TRANSACTION_BREAKDOWN_COUNT 1`] = `undefined`;
@@ -168,6 +178,8 @@ exports[`Span CLOUD_PROVIDER 1`] = `"gcp"`;
exports[`Span CLOUD_REGION 1`] = `"europe-west1"`;
+exports[`Span CLS_FIELD 1`] = `undefined`;
+
exports[`Span CONTAINER_ID 1`] = `undefined`;
exports[`Span DESTINATION_ADDRESS 1`] = `undefined`;
@@ -188,6 +200,10 @@ exports[`Span ERROR_LOG_MESSAGE 1`] = `undefined`;
exports[`Span ERROR_PAGE_URL 1`] = `undefined`;
+exports[`Span FCP_FIELD 1`] = `undefined`;
+
+exports[`Span FID_FIELD 1`] = `undefined`;
+
exports[`Span EVENT_OUTCOME 1`] = `undefined`;
exports[`Span HOST_NAME 1`] = `undefined`;
@@ -198,6 +214,8 @@ exports[`Span HTTP_RESPONSE_STATUS_CODE 1`] = `undefined`;
exports[`Span LABEL_NAME 1`] = `undefined`;
+exports[`Span LCP_FIELD 1`] = `undefined`;
+
exports[`Span METRIC_JAVA_GC_COUNT 1`] = `undefined`;
exports[`Span METRIC_JAVA_GC_TIME 1`] = `undefined`;
@@ -272,6 +290,8 @@ exports[`Span SPAN_SUBTYPE 1`] = `"my subtype"`;
exports[`Span SPAN_TYPE 1`] = `"span type"`;
+exports[`Span TBT_FIELD 1`] = `undefined`;
+
exports[`Span TRACE_ID 1`] = `"trace id"`;
exports[`Span TRANSACTION_BREAKDOWN_COUNT 1`] = `undefined`;
@@ -322,6 +342,8 @@ exports[`Transaction CLOUD_PROVIDER 1`] = `"gcp"`;
exports[`Transaction CLOUD_REGION 1`] = `"europe-west1"`;
+exports[`Transaction CLS_FIELD 1`] = `undefined`;
+
exports[`Transaction CONTAINER_ID 1`] = `"container1234567890abcdef"`;
exports[`Transaction DESTINATION_ADDRESS 1`] = `undefined`;
@@ -342,6 +364,10 @@ exports[`Transaction ERROR_LOG_MESSAGE 1`] = `undefined`;
exports[`Transaction ERROR_PAGE_URL 1`] = `undefined`;
+exports[`Transaction FCP_FIELD 1`] = `undefined`;
+
+exports[`Transaction FID_FIELD 1`] = `undefined`;
+
exports[`Transaction EVENT_OUTCOME 1`] = `undefined`;
exports[`Transaction HOST_NAME 1`] = `"my hostname"`;
@@ -352,6 +378,8 @@ exports[`Transaction HTTP_RESPONSE_STATUS_CODE 1`] = `200`;
exports[`Transaction LABEL_NAME 1`] = `undefined`;
+exports[`Transaction LCP_FIELD 1`] = `undefined`;
+
exports[`Transaction METRIC_JAVA_GC_COUNT 1`] = `undefined`;
exports[`Transaction METRIC_JAVA_GC_TIME 1`] = `undefined`;
@@ -426,6 +454,8 @@ exports[`Transaction SPAN_SUBTYPE 1`] = `undefined`;
exports[`Transaction SPAN_TYPE 1`] = `undefined`;
+exports[`Transaction TBT_FIELD 1`] = `undefined`;
+
exports[`Transaction TRACE_ID 1`] = `"trace id"`;
exports[`Transaction TRANSACTION_BREAKDOWN_COUNT 1`] = `undefined`;
@@ -448,7 +478,7 @@ exports[`Transaction TRANSACTION_TIME_TO_FIRST_BYTE 1`] = `undefined`;
exports[`Transaction TRANSACTION_TYPE 1`] = `"transaction type"`;
-exports[`Transaction TRANSACTION_URL 1`] = `undefined`;
+exports[`Transaction TRANSACTION_URL 1`] = `"http://www.elastic.co"`;
exports[`Transaction URL_FULL 1`] = `"http://www.elastic.co"`;
diff --git a/x-pack/plugins/apm/common/elasticsearch_fieldnames.ts b/x-pack/plugins/apm/common/elasticsearch_fieldnames.ts
index c13169549a566..e1a279714d308 100644
--- a/x-pack/plugins/apm/common/elasticsearch_fieldnames.ts
+++ b/x-pack/plugins/apm/common/elasticsearch_fieldnames.ts
@@ -97,7 +97,7 @@ export const POD_NAME = 'kubernetes.pod.name';
export const CLIENT_GEO_COUNTRY_ISO_CODE = 'client.geo.country_iso_code';
// RUM Labels
-export const TRANSACTION_URL = 'transaction.page.url';
+export const TRANSACTION_URL = 'url.full';
export const CLIENT_GEO = 'client.geo';
export const USER_AGENT_DEVICE = 'user_agent.device.name';
export const USER_AGENT_OS = 'user_agent.os.name';
@@ -106,3 +106,9 @@ export const TRANSACTION_TIME_TO_FIRST_BYTE =
'transaction.marks.agent.timeToFirstByte';
export const TRANSACTION_DOM_INTERACTIVE =
'transaction.marks.agent.domInteractive';
+
+export const FCP_FIELD = 'transaction.marks.agent.firstContentfulPaint';
+export const LCP_FIELD = 'transaction.marks.agent.largestContentfulPaint';
+export const TBT_FIELD = 'transaction.experience.tbt';
+export const FID_FIELD = 'transaction.experience.fid';
+export const CLS_FIELD = 'transaction.experience.cls';
diff --git a/x-pack/plugins/apm/dev_docs/routing_and_linking.md b/x-pack/plugins/apm/dev_docs/routing_and_linking.md
new file mode 100644
index 0000000000000..d27513d44935f
--- /dev/null
+++ b/x-pack/plugins/apm/dev_docs/routing_and_linking.md
@@ -0,0 +1,38 @@
+# APM Plugin Routing and Linking
+
+## Routing
+
+This document describes routing in the APM plugin.
+
+### Server-side
+
+Route definitions for APM's server-side API are in the [server/routes directory](../server/routes). Routes are created with [the `createRoute` function](../server/routes/create_route.ts). Routes are added to the API in [the `createApmApi` function](../server/routes/create_apm_api.ts), which is initialized in the plugin `start` lifecycle method.
+
+The path and query string parameters are defined in the calls to `createRoute` with io-ts types, so that each route has its parameters type checked.
+
+### Client-side
+
+The client-side routing uses [React Router](https://reactrouter.com/), The [`ApmRoute` component from the Elastic RUM Agent](https://www.elastic.co/guide/en/apm/agent/rum-js/current/react-integration.html), and the `history` object provided by the Kibana Platform.
+
+Routes are defined in [public/components/app/Main/route_config/index.tsx](../public/components/app/Main/route_config/index.tsx). These contain route definitions as well as the breadcrumb text.
+
+#### Parameter handling
+
+Path parameters (like `serviceName` in '/services/:serviceName/transactions') are handled by the `match.params` props passed into
+routes by React Router. The types of these parameters are defined in the route definitions.
+
+If the parameters are not available as props you can use React Router's `useParams`, but their type definitions should be delcared inline and it's a good idea to make the properties optional if you don't know where a component will be used, since those parameters might not be available at that route.
+
+Query string parameters can be used in any component with `useUrlParams`. All of the available parameters are defined by this hook and its context.
+
+## Linking
+
+Raw URLs should almost never be used in the APM UI. Instead, we have mechanisms for creating links and URLs that ensure links are reliable.
+
+### In-app linking
+
+Links that stay inside APM should use the [`getAPMHref` function and `APMLink` component](../public/components/shared/Links/apm/APMLink.tsx). Other components inside that directory contain other functions and components that provide the same functionality for linking to more specific sections inside the APM plugin.
+
+### Cross-app linking
+
+Other helpers and components in [the Links directory](../public/components/shared/Links) allow linking to other Kibana apps.
diff --git a/x-pack/plugins/apm/dev_docs/updating_functional_tests_archives.md b/x-pack/plugins/apm/dev_docs/updating_functional_tests_archives.md
new file mode 100644
index 0000000000000..467090fb3c91b
--- /dev/null
+++ b/x-pack/plugins/apm/dev_docs/updating_functional_tests_archives.md
@@ -0,0 +1,8 @@
+### Updating functional tests archives
+
+Some of our API tests use an archive generated by the [`esarchiver`](https://www.elastic.co/guide/en/kibana/current/development-functional-tests.html) script. Updating the main archive (`apm_8.0.0`) is a scripted process, where a 30m snapshot is downloaded from a cluster running the [APM Integration Testing server](https://github.com/elastic/apm-integration-testing). The script will copy the generated archives into the `fixtures/es_archiver` folders of our test suites (currently `basic` and `trial`). It will also generate a file that contains metadata about the archive, that can be imported to get the time range of the snapshot.
+
+Usage:
+`node x-pack/plugins/apm/scripts/create-functional-tests-archive --es-url=https://admin:changeme@localhost:9200 --kibana-url=https://localhost:5601`
+
+
diff --git a/x-pack/plugins/apm/public/application/csmApp.tsx b/x-pack/plugins/apm/public/application/csmApp.tsx
index d76ed5c2100b2..cdfe42bd628cc 100644
--- a/x-pack/plugins/apm/public/application/csmApp.tsx
+++ b/x-pack/plugins/apm/public/application/csmApp.tsx
@@ -4,52 +4,51 @@
* you may not use this file except in compliance with the Elastic License.
*/
+import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json';
+import euiLightVars from '@elastic/eui/dist/eui_theme_light.json';
+import { AppMountParameters, CoreStart } from 'kibana/public';
import React from 'react';
import ReactDOM from 'react-dom';
import { Route, Router } from 'react-router-dom';
-import styled, { ThemeProvider, DefaultTheme } from 'styled-components';
-import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json';
-import euiLightVars from '@elastic/eui/dist/eui_theme_light.json';
-import { CoreStart, AppMountParameters } from 'kibana/public';
-import { ApmPluginSetupDeps } from '../plugin';
-
+import 'react-vis/dist/style.css';
+import styled, { DefaultTheme, ThemeProvider } from 'styled-components';
import {
KibanaContextProvider,
- useUiSetting$,
RedirectAppLinks,
+ useUiSetting$,
} from '../../../../../src/plugins/kibana_react/public';
-import { px, units } from '../style/variables';
-import { UpdateBreadcrumbs } from '../components/app/Main/UpdateBreadcrumbs';
+import { APMRouteDefinition } from '../application/routes';
+import { renderAsRedirectTo } from '../components/app/Main/route_config';
import { ScrollToTopOnPathChange } from '../components/app/Main/ScrollToTopOnPathChange';
-import 'react-vis/dist/style.css';
import { RumHome } from '../components/app/RumDashboard/RumHome';
-import { ConfigSchema } from '../index';
-import { BreadcrumbRoute } from '../components/app/Main/ProvideBreadcrumbs';
-import { RouteName } from '../components/app/Main/route_config/route_names';
-import { renderAsRedirectTo } from '../components/app/Main/route_config';
import { ApmPluginContext } from '../context/ApmPluginContext';
-import { UrlParamsProvider } from '../context/UrlParamsContext';
import { LoadingIndicatorProvider } from '../context/LoadingIndicatorContext';
+import { UrlParamsProvider } from '../context/UrlParamsContext';
+import { useBreadcrumbs } from '../hooks/use_breadcrumbs';
+import { ConfigSchema } from '../index';
+import { ApmPluginSetupDeps } from '../plugin';
import { createCallApmApi } from '../services/rest/createCallApmApi';
+import { px, units } from '../style/variables';
const CsmMainContainer = styled.div`
padding: ${px(units.plus)};
height: 100%;
`;
-export const rumRoutes: BreadcrumbRoute[] = [
+export const rumRoutes: APMRouteDefinition[] = [
{
exact: true,
path: '/',
render: renderAsRedirectTo('/csm'),
breadcrumb: 'Client Side Monitoring',
- name: RouteName.CSM,
},
];
function CsmApp() {
const [darkMode] = useUiSetting$('theme:darkMode');
+ useBreadcrumbs(rumRoutes);
+
return (
({
@@ -59,7 +58,6 @@ function CsmApp() {
})}
>
-
diff --git a/x-pack/plugins/apm/public/application/index.tsx b/x-pack/plugins/apm/public/application/index.tsx
index 3f4f3116152c4..536d70b053f76 100644
--- a/x-pack/plugins/apm/public/application/index.tsx
+++ b/x-pack/plugins/apm/public/application/index.tsx
@@ -22,13 +22,12 @@ import {
import { AlertsContextProvider } from '../../../triggers_actions_ui/public';
import { routes } from '../components/app/Main/route_config';
import { ScrollToTopOnPathChange } from '../components/app/Main/ScrollToTopOnPathChange';
-import { UpdateBreadcrumbs } from '../components/app/Main/UpdateBreadcrumbs';
import { ApmPluginContext } from '../context/ApmPluginContext';
import { LicenseProvider } from '../context/LicenseContext';
import { LoadingIndicatorProvider } from '../context/LoadingIndicatorContext';
import { LocationProvider } from '../context/LocationContext';
-import { MatchedRouteProvider } from '../context/MatchedRouteContext';
import { UrlParamsProvider } from '../context/UrlParamsContext';
+import { useBreadcrumbs } from '../hooks/use_breadcrumbs';
import { ApmPluginSetupDeps } from '../plugin';
import { createCallApmApi } from '../services/rest/createCallApmApi';
import { createStaticIndexPattern } from '../services/rest/index_pattern';
@@ -44,6 +43,8 @@ const MainContainer = styled.div`
function App() {
const [darkMode] = useUiSetting$('theme:darkMode');
+ useBreadcrumbs(routes);
+
return (
({
@@ -53,7 +54,6 @@ function App() {
})}
>
-
{routes.map((route, i) => (
@@ -100,15 +100,13 @@ export function ApmAppRoot({
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
diff --git a/x-pack/plugins/apm/public/application/routes/index.tsx b/x-pack/plugins/apm/public/application/routes/index.tsx
new file mode 100644
index 0000000000000..d1bb8ae8fc8a3
--- /dev/null
+++ b/x-pack/plugins/apm/public/application/routes/index.tsx
@@ -0,0 +1,16 @@
+/*
+ * 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 { RouteComponentProps, RouteProps } from 'react-router-dom';
+
+export type BreadcrumbTitle =
+ | string
+ | ((props: RouteComponentProps) => string)
+ | null;
+
+export interface APMRouteDefinition extends RouteProps {
+ breadcrumb: BreadcrumbTitle;
+}
diff --git a/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/index.tsx b/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/index.tsx
index 31f299f94bc26..e95d35142684d 100644
--- a/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/index.tsx
+++ b/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/index.tsx
@@ -15,11 +15,11 @@ import {
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import React, { Fragment } from 'react';
+import { RouteComponentProps } from 'react-router-dom';
import styled from 'styled-components';
import { useTrackPageview } from '../../../../../observability/public';
import { NOT_AVAILABLE_LABEL } from '../../../../common/i18n';
import { useFetcher } from '../../../hooks/useFetcher';
-import { useLocation } from '../../../hooks/useLocation';
import { useUrlParams } from '../../../hooks/useUrlParams';
import { callApmApi } from '../../../services/rest/createCallApmApi';
import { fontFamilyCode, fontSizes, px, units } from '../../../style/variables';
@@ -56,19 +56,24 @@ function getShortGroupId(errorGroupId?: string) {
return errorGroupId.slice(0, 5);
}
-export function ErrorGroupDetails() {
- const location = useLocation();
+type ErrorGroupDetailsProps = RouteComponentProps<{
+ groupId: string;
+ serviceName: string;
+}>;
+
+export function ErrorGroupDetails({ location, match }: ErrorGroupDetailsProps) {
+ const { serviceName, groupId } = match.params;
const { urlParams, uiFilters } = useUrlParams();
- const { serviceName, start, end, errorGroupId } = urlParams;
+ const { start, end } = urlParams;
const { data: errorGroupData } = useFetcher(() => {
- if (serviceName && start && end && errorGroupId) {
+ if (start && end) {
return callApmApi({
pathname: '/api/apm/services/{serviceName}/errors/{groupId}',
params: {
path: {
serviceName,
- groupId: errorGroupId,
+ groupId,
},
query: {
start,
@@ -78,10 +83,10 @@ export function ErrorGroupDetails() {
},
});
}
- }, [serviceName, start, end, errorGroupId, uiFilters]);
+ }, [serviceName, start, end, groupId, uiFilters]);
const { data: errorDistributionData } = useFetcher(() => {
- if (serviceName && start && end && errorGroupId) {
+ if (start && end) {
return callApmApi({
pathname: '/api/apm/services/{serviceName}/errors/distribution',
params: {
@@ -91,13 +96,13 @@ export function ErrorGroupDetails() {
query: {
start,
end,
- groupId: errorGroupId,
+ groupId,
uiFilters: JSON.stringify(uiFilters),
},
},
});
}
- }, [serviceName, start, end, errorGroupId, uiFilters]);
+ }, [serviceName, start, end, groupId, uiFilters]);
useTrackPageview({ app: 'apm', path: 'error_group_details' });
useTrackPageview({ app: 'apm', path: 'error_group_details', delay: 15000 });
@@ -124,7 +129,7 @@ export function ErrorGroupDetails() {
{i18n.translate('xpack.apm.errorGroupDetails.errorGroupTitle', {
defaultMessage: 'Error group {errorGroupId}',
values: {
- errorGroupId: getShortGroupId(urlParams.errorGroupId),
+ errorGroupId: getShortGroupId(groupId),
},
})}
diff --git a/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/List/__test__/List.test.tsx b/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/List/__test__/List.test.tsx
index 5798deaf19c9c..1acfc5c49245d 100644
--- a/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/List/__test__/List.test.tsx
+++ b/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/List/__test__/List.test.tsx
@@ -27,7 +27,7 @@ describe('ErrorGroupOverview -> List', () => {
const storeState = {};
const wrapper = mount(
-
+
,
storeState
);
@@ -39,7 +39,7 @@ describe('ErrorGroupOverview -> List', () => {
const wrapper = mount(
-
+
);
diff --git a/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/List/index.tsx b/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/List/index.tsx
index 5c16bf0f324be..33105189f9c3e 100644
--- a/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/List/index.tsx
+++ b/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/List/index.tsx
@@ -51,16 +51,12 @@ const Culprit = styled.div`
interface Props {
items: ErrorGroupListAPIResponse;
+ serviceName: string;
}
-function ErrorGroupList(props: Props) {
- const { items } = props;
+function ErrorGroupList({ items, serviceName }: Props) {
const { urlParams } = useUrlParams();
- const { serviceName } = urlParams;
- if (!serviceName) {
- throw new Error('Service name is required');
- }
const columns = useMemo(
() => [
{
diff --git a/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/index.tsx b/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/index.tsx
index 92ea044720531..42b0016ca8cfe 100644
--- a/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/index.tsx
+++ b/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/index.tsx
@@ -22,13 +22,17 @@ import { LocalUIFilters } from '../../shared/LocalUIFilters';
import { ErrorDistribution } from '../ErrorGroupDetails/Distribution';
import { ErrorGroupList } from './List';
-function ErrorGroupOverview() {
+interface ErrorGroupOverviewProps {
+ serviceName: string;
+}
+
+function ErrorGroupOverview({ serviceName }: ErrorGroupOverviewProps) {
const { urlParams, uiFilters } = useUrlParams();
- const { serviceName, start, end, sortField, sortDirection } = urlParams;
+ const { start, end, sortField, sortDirection } = urlParams;
const { data: errorDistributionData } = useFetcher(() => {
- if (serviceName && start && end) {
+ if (start && end) {
return callApmApi({
pathname: '/api/apm/services/{serviceName}/errors/distribution',
params: {
@@ -48,7 +52,7 @@ function ErrorGroupOverview() {
const { data: errorGroupListData } = useFetcher(() => {
const normalizedSortDirection = sortDirection === 'asc' ? 'asc' : 'desc';
- if (serviceName && start && end) {
+ if (start && end) {
return callApmApi({
pathname: '/api/apm/services/{serviceName}/errors',
params: {
@@ -117,7 +121,10 @@ function ErrorGroupOverview() {
-
+
diff --git a/x-pack/plugins/apm/public/components/app/Home/__snapshots__/Home.test.tsx.snap b/x-pack/plugins/apm/public/components/app/Home/__snapshots__/Home.test.tsx.snap
index 24b51e3fba917..9706895b164a6 100644
--- a/x-pack/plugins/apm/public/components/app/Home/__snapshots__/Home.test.tsx.snap
+++ b/x-pack/plugins/apm/public/components/app/Home/__snapshots__/Home.test.tsx.snap
@@ -18,6 +18,7 @@ exports[`Home component should render services 1`] = `
"currentAppId$": Observable {
"_isScalar": false,
},
+ "navigateToUrl": [Function],
},
"chrome": Object {
"docTitle": Object {
@@ -78,6 +79,7 @@ exports[`Home component should render traces 1`] = `
"currentAppId$": Observable {
"_isScalar": false,
},
+ "navigateToUrl": [Function],
},
"chrome": Object {
"docTitle": Object {
diff --git a/x-pack/plugins/apm/public/components/app/Main/ProvideBreadcrumbs.test.tsx b/x-pack/plugins/apm/public/components/app/Main/ProvideBreadcrumbs.test.tsx
deleted file mode 100644
index bf1cd75432ff5..0000000000000
--- a/x-pack/plugins/apm/public/components/app/Main/ProvideBreadcrumbs.test.tsx
+++ /dev/null
@@ -1,109 +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;
- * you may not use this file except in compliance with the Elastic License.
- */
-
-import { Location } from 'history';
-import { BreadcrumbRoute, getBreadcrumbs } from './ProvideBreadcrumbs';
-import { RouteName } from './route_config/route_names';
-
-describe('getBreadcrumbs', () => {
- const getTestRoutes = (): BreadcrumbRoute[] => [
- { path: '/a', exact: true, breadcrumb: 'A', name: RouteName.HOME },
- {
- path: '/a/ignored',
- exact: true,
- breadcrumb: 'Ignored Route',
- name: RouteName.METRICS,
- },
- {
- path: '/a/:letter',
- exact: true,
- name: RouteName.SERVICE,
- breadcrumb: ({ match }) => `Second level: ${match.params.letter}`,
- },
- {
- path: '/a/:letter/c',
- exact: true,
- name: RouteName.ERRORS,
- breadcrumb: ({ match }) => `Third level: ${match.params.letter}`,
- },
- ];
-
- const getLocation = () =>
- ({
- pathname: '/a/b/c/',
- } as Location);
-
- it('should return a set of matching breadcrumbs for a given path', () => {
- const breadcrumbs = getBreadcrumbs({
- location: getLocation(),
- routes: getTestRoutes(),
- });
-
- expect(breadcrumbs.map((b) => b.value)).toMatchInlineSnapshot(`
-Array [
- "A",
- "Second level: b",
- "Third level: b",
-]
-`);
- });
-
- it('should skip breadcrumbs if breadcrumb is null', () => {
- const location = getLocation();
- const routes = getTestRoutes();
-
- routes[2].breadcrumb = null;
-
- const breadcrumbs = getBreadcrumbs({
- location,
- routes,
- });
-
- expect(breadcrumbs.map((b) => b.value)).toMatchInlineSnapshot(`
-Array [
- "A",
- "Third level: b",
-]
-`);
- });
-
- it('should skip breadcrumbs if breadcrumb key is missing', () => {
- const location = getLocation();
- const routes = getTestRoutes();
-
- // @ts-expect-error
- delete routes[2].breadcrumb;
-
- const breadcrumbs = getBreadcrumbs({ location, routes });
-
- expect(breadcrumbs.map((b) => b.value)).toMatchInlineSnapshot(`
-Array [
- "A",
- "Third level: b",
-]
-`);
- });
-
- it('should produce matching breadcrumbs even if the pathname has a query string appended', () => {
- const location = getLocation();
- const routes = getTestRoutes();
-
- location.pathname += '?some=thing';
-
- const breadcrumbs = getBreadcrumbs({
- location,
- routes,
- });
-
- expect(breadcrumbs.map((b) => b.value)).toMatchInlineSnapshot(`
-Array [
- "A",
- "Second level: b",
- "Third level: b",
-]
-`);
- });
-});
diff --git a/x-pack/plugins/apm/public/components/app/Main/ProvideBreadcrumbs.tsx b/x-pack/plugins/apm/public/components/app/Main/ProvideBreadcrumbs.tsx
deleted file mode 100644
index f2505b64fb1e3..0000000000000
--- a/x-pack/plugins/apm/public/components/app/Main/ProvideBreadcrumbs.tsx
+++ /dev/null
@@ -1,135 +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;
- * you may not use this file except in compliance with the Elastic License.
- */
-
-import { Location } from 'history';
-import React from 'react';
-import {
- matchPath,
- RouteComponentProps,
- RouteProps,
- withRouter,
-} from 'react-router-dom';
-import { RouteName } from './route_config/route_names';
-
-type LocationMatch = Pick<
- RouteComponentProps>,
- 'location' | 'match'
->;
-
-type BreadcrumbFunction = (props: LocationMatch) => string;
-
-export interface BreadcrumbRoute extends RouteProps {
- breadcrumb: string | BreadcrumbFunction | null;
- name: RouteName;
-}
-
-export interface Breadcrumb extends LocationMatch {
- value: string;
-}
-
-interface RenderProps extends RouteComponentProps {
- breadcrumbs: Breadcrumb[];
-}
-
-interface ProvideBreadcrumbsProps extends RouteComponentProps {
- routes: BreadcrumbRoute[];
- render: (props: RenderProps) => React.ReactElement | null;
-}
-
-interface ParseOptions extends LocationMatch {
- breadcrumb: string | BreadcrumbFunction;
-}
-
-const parse = (options: ParseOptions) => {
- const { breadcrumb, match, location } = options;
- let value;
-
- if (typeof breadcrumb === 'function') {
- value = breadcrumb({ match, location });
- } else {
- value = breadcrumb;
- }
-
- return { value, match, location };
-};
-
-export function getBreadcrumb({
- location,
- currentPath,
- routes,
-}: {
- location: Location;
- currentPath: string;
- routes: BreadcrumbRoute[];
-}) {
- return routes.reduce((found, { breadcrumb, ...route }) => {
- if (found) {
- return found;
- }
-
- if (!breadcrumb) {
- return null;
- }
-
- const match = matchPath>(currentPath, route);
-
- if (match) {
- return parse({
- breadcrumb,
- match,
- location,
- });
- }
-
- return null;
- }, null);
-}
-
-export function getBreadcrumbs({
- routes,
- location,
-}: {
- routes: BreadcrumbRoute[];
- location: Location;
-}) {
- const breadcrumbs: Breadcrumb[] = [];
- const { pathname } = location;
-
- pathname
- .split('?')[0]
- .replace(/\/$/, '')
- .split('/')
- .reduce((acc, next) => {
- // `/1/2/3` results in match checks for `/1`, `/1/2`, `/1/2/3`.
- const currentPath = !next ? '/' : `${acc}/${next}`;
- const breadcrumb = getBreadcrumb({
- location,
- currentPath,
- routes,
- });
-
- if (breadcrumb) {
- breadcrumbs.push(breadcrumb);
- }
-
- return currentPath === '/' ? '' : currentPath;
- }, '');
-
- return breadcrumbs;
-}
-
-function ProvideBreadcrumbsComponent({
- routes = [],
- render,
- location,
- match,
- history,
-}: ProvideBreadcrumbsProps) {
- const breadcrumbs = getBreadcrumbs({ routes, location });
- return render({ breadcrumbs, location, match, history });
-}
-
-export const ProvideBreadcrumbs = withRouter(ProvideBreadcrumbsComponent);
diff --git a/x-pack/plugins/apm/public/components/app/Main/UpdateBreadcrumbs.tsx b/x-pack/plugins/apm/public/components/app/Main/UpdateBreadcrumbs.tsx
deleted file mode 100644
index 5bf5cea587f93..0000000000000
--- a/x-pack/plugins/apm/public/components/app/Main/UpdateBreadcrumbs.tsx
+++ /dev/null
@@ -1,90 +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;
- * you may not use this file except in compliance with the Elastic License.
- */
-
-import { Location } from 'history';
-import React, { MouseEvent } from 'react';
-import { CoreStart } from 'src/core/public';
-import { useApmPluginContext } from '../../../hooks/useApmPluginContext';
-import { getAPMHref } from '../../shared/Links/apm/APMLink';
-import {
- Breadcrumb,
- BreadcrumbRoute,
- ProvideBreadcrumbs,
-} from './ProvideBreadcrumbs';
-
-interface Props {
- location: Location;
- breadcrumbs: Breadcrumb[];
- core: CoreStart;
-}
-
-function getTitleFromBreadCrumbs(breadcrumbs: Breadcrumb[]) {
- return breadcrumbs.map(({ value }) => value).reverse();
-}
-
-class UpdateBreadcrumbsComponent extends React.Component {
- public updateHeaderBreadcrumbs() {
- const { basePath } = this.props.core.http;
- const breadcrumbs = this.props.breadcrumbs.map(
- ({ value, match }, index) => {
- const { search } = this.props.location;
- const isLastBreadcrumbItem =
- index === this.props.breadcrumbs.length - 1;
- const href = isLastBreadcrumbItem
- ? undefined // makes the breadcrumb item not clickable
- : getAPMHref({ basePath, path: match.url, search });
- return {
- text: value,
- href,
- onClick: (event: MouseEvent) => {
- if (href) {
- event.preventDefault();
- this.props.core.application.navigateToUrl(href);
- }
- },
- };
- }
- );
-
- this.props.core.chrome.docTitle.change(
- getTitleFromBreadCrumbs(this.props.breadcrumbs)
- );
- this.props.core.chrome.setBreadcrumbs(breadcrumbs);
- }
-
- public componentDidMount() {
- this.updateHeaderBreadcrumbs();
- }
-
- public componentDidUpdate() {
- this.updateHeaderBreadcrumbs();
- }
-
- public render() {
- return null;
- }
-}
-
-interface UpdateBreadcrumbsProps {
- routes: BreadcrumbRoute[];
-}
-
-export function UpdateBreadcrumbs({ routes }: UpdateBreadcrumbsProps) {
- const { core } = useApmPluginContext();
-
- return (
- (
-
- )}
- />
- );
-}
diff --git a/x-pack/plugins/apm/public/components/app/Main/route_config/index.tsx b/x-pack/plugins/apm/public/components/app/Main/route_config/index.tsx
index 56026dcf477ec..0cefcbdc54228 100644
--- a/x-pack/plugins/apm/public/components/app/Main/route_config/index.tsx
+++ b/x-pack/plugins/apm/public/components/app/Main/route_config/index.tsx
@@ -7,38 +7,32 @@
import { i18n } from '@kbn/i18n';
import React from 'react';
import { Redirect, RouteComponentProps } from 'react-router-dom';
+import { UNIDENTIFIED_SERVICE_NODES_LABEL } from '../../../../../common/i18n';
import { SERVICE_NODE_NAME_MISSING } from '../../../../../common/service_nodes';
+import { APMRouteDefinition } from '../../../../application/routes';
+import { toQuery } from '../../../shared/Links/url_helpers';
import { ErrorGroupDetails } from '../../ErrorGroupDetails';
-import { ServiceDetails } from '../../ServiceDetails';
-import { TransactionDetails } from '../../TransactionDetails';
import { Home } from '../../Home';
-import { BreadcrumbRoute } from '../ProvideBreadcrumbs';
-import { RouteName } from './route_names';
+import { ServiceDetails } from '../../ServiceDetails';
+import { ServiceNodeMetrics } from '../../ServiceNodeMetrics';
import { Settings } from '../../Settings';
import { AgentConfigurations } from '../../Settings/AgentConfigurations';
+import { AnomalyDetection } from '../../Settings/anomaly_detection';
import { ApmIndices } from '../../Settings/ApmIndices';
-import { toQuery } from '../../../shared/Links/url_helpers';
-import { ServiceNodeMetrics } from '../../ServiceNodeMetrics';
-import { resolveUrlParams } from '../../../../context/UrlParamsContext/resolveUrlParams';
-import { UNIDENTIFIED_SERVICE_NODES_LABEL } from '../../../../../common/i18n';
-import { TraceLink } from '../../TraceLink';
import { CustomizeUI } from '../../Settings/CustomizeUI';
-import { AnomalyDetection } from '../../Settings/anomaly_detection';
+import { TraceLink } from '../../TraceLink';
+import { TransactionDetails } from '../../TransactionDetails';
import {
CreateAgentConfigurationRouteHandler,
EditAgentConfigurationRouteHandler,
} from './route_handlers/agent_configuration';
-const metricsBreadcrumb = i18n.translate('xpack.apm.breadcrumb.metricsTitle', {
- defaultMessage: 'Metrics',
-});
-
-interface RouteParams {
- serviceName: string;
-}
-
-export const renderAsRedirectTo = (to: string) => {
- return ({ location }: RouteComponentProps) => {
+/**
+ * Given a path, redirect to that location, preserving the search and maintaining
+ * backward-compatibilty with legacy (pre-7.9) hash-based URLs.
+ */
+export function renderAsRedirectTo(to: string) {
+ return ({ location }: RouteComponentProps<{}>) => {
let resolvedUrl: URL | undefined;
// Redirect root URLs with a hash to support backward compatibility with URLs
@@ -60,71 +54,149 @@ export const renderAsRedirectTo = (to: string) => {
/>
);
};
-};
+}
+
+// These component function definitions are used below with the `component`
+// property of the route definitions.
+//
+// If you provide an inline function to the component prop, you would create a
+// new component every render. This results in the existing component unmounting
+// and the new component mounting instead of just updating the existing component.
+//
+// This means you should use `render` if you're providing an inline function.
+// However, the `ApmRoute` component from @elastic/apm-rum-react, only supports
+// `component`, and will give you a large console warning if you use `render`.
+//
+// This warning cannot be turned off
+// (see https://github.com/elastic/apm-agent-rum-js/issues/881) so while this is
+// slightly more code, it provides better performance without causing console
+// warnings to appear.
+function HomeServices() {
+ return ;
+}
+
+function HomeServiceMap() {
+ return ;
+}
+
+function HomeTraces() {
+ return ;
+}
+
+function ServiceDetailsErrors(
+ props: RouteComponentProps<{ serviceName: string }>
+) {
+ return ;
+}
-export const routes: BreadcrumbRoute[] = [
+function ServiceDetailsMetrics(
+ props: RouteComponentProps<{ serviceName: string }>
+) {
+ return ;
+}
+
+function ServiceDetailsNodes(
+ props: RouteComponentProps<{ serviceName: string }>
+) {
+ return ;
+}
+
+function ServiceDetailsServiceMap(
+ props: RouteComponentProps<{ serviceName: string }>
+) {
+ return ;
+}
+
+function ServiceDetailsTransactions(
+ props: RouteComponentProps<{ serviceName: string }>
+) {
+ return ;
+}
+
+function SettingsAgentConfiguration() {
+ return (
+
+
+
+ );
+}
+
+function SettingsAnomalyDetection() {
+ return (
+
+
+
+ );
+}
+
+function SettingsApmIndices() {
+ return (
+
+
+
+ );
+}
+
+function SettingsCustomizeUI() {
+ return (
+
+
+
+ );
+}
+
+/**
+ * The array of route definitions to be used when the application
+ * creates the routes.
+ */
+export const routes: APMRouteDefinition[] = [
{
exact: true,
path: '/',
- render: renderAsRedirectTo('/services'),
+ component: renderAsRedirectTo('/services'),
breadcrumb: 'APM',
- name: RouteName.HOME,
},
{
exact: true,
path: '/services',
- component: () => ,
+ component: HomeServices,
breadcrumb: i18n.translate('xpack.apm.breadcrumb.servicesTitle', {
defaultMessage: 'Services',
}),
- name: RouteName.SERVICES,
},
{
exact: true,
path: '/traces',
- component: () => ,
+ component: HomeTraces,
breadcrumb: i18n.translate('xpack.apm.breadcrumb.tracesTitle', {
defaultMessage: 'Traces',
}),
- name: RouteName.TRACES,
},
{
exact: true,
path: '/settings',
- render: renderAsRedirectTo('/settings/agent-configuration'),
+ component: renderAsRedirectTo('/settings/agent-configuration'),
breadcrumb: i18n.translate('xpack.apm.breadcrumb.listSettingsTitle', {
defaultMessage: 'Settings',
}),
- name: RouteName.SETTINGS,
},
{
exact: true,
path: '/settings/apm-indices',
- component: () => (
-
-
-
- ),
+ component: SettingsApmIndices,
breadcrumb: i18n.translate('xpack.apm.breadcrumb.settings.indicesTitle', {
defaultMessage: 'Indices',
}),
- name: RouteName.INDICES,
},
{
exact: true,
path: '/settings/agent-configuration',
- component: () => (
-
-
-
- ),
+ component: SettingsAgentConfiguration,
breadcrumb: i18n.translate(
'xpack.apm.breadcrumb.settings.agentConfigurationTitle',
{ defaultMessage: 'Agent Configuration' }
),
- name: RouteName.AGENT_CONFIGURATION,
},
-
{
exact: true,
path: '/settings/agent-configuration/create',
@@ -132,8 +204,7 @@ export const routes: BreadcrumbRoute[] = [
'xpack.apm.breadcrumb.settings.createAgentConfigurationTitle',
{ defaultMessage: 'Create Agent Configuration' }
),
- name: RouteName.AGENT_CONFIGURATION_CREATE,
- component: () => ,
+ component: CreateAgentConfigurationRouteHandler,
},
{
exact: true,
@@ -142,71 +213,66 @@ export const routes: BreadcrumbRoute[] = [
'xpack.apm.breadcrumb.settings.editAgentConfigurationTitle',
{ defaultMessage: 'Edit Agent Configuration' }
),
- name: RouteName.AGENT_CONFIGURATION_EDIT,
- component: () => ,
+ component: EditAgentConfigurationRouteHandler,
},
{
exact: true,
path: '/services/:serviceName',
breadcrumb: ({ match }) => match.params.serviceName,
- render: (props: RouteComponentProps) =>
+ component: (props: RouteComponentProps<{ serviceName: string }>) =>
renderAsRedirectTo(
`/services/${props.match.params.serviceName}/transactions`
)(props),
- name: RouteName.SERVICE,
- },
+ } as APMRouteDefinition<{ serviceName: string }>,
// errors
{
exact: true,
path: '/services/:serviceName/errors/:groupId',
component: ErrorGroupDetails,
breadcrumb: ({ match }) => match.params.groupId,
- name: RouteName.ERROR,
- },
+ } as APMRouteDefinition<{ groupId: string; serviceName: string }>,
{
exact: true,
path: '/services/:serviceName/errors',
- component: () => ,
+ component: ServiceDetailsErrors,
breadcrumb: i18n.translate('xpack.apm.breadcrumb.errorsTitle', {
defaultMessage: 'Errors',
}),
- name: RouteName.ERRORS,
},
// transactions
{
exact: true,
path: '/services/:serviceName/transactions',
- component: () => ,
+ component: ServiceDetailsTransactions,
breadcrumb: i18n.translate('xpack.apm.breadcrumb.transactionsTitle', {
defaultMessage: 'Transactions',
}),
- name: RouteName.TRANSACTIONS,
},
// metrics
{
exact: true,
path: '/services/:serviceName/metrics',
- component: () => ,
- breadcrumb: metricsBreadcrumb,
- name: RouteName.METRICS,
+ component: ServiceDetailsMetrics,
+ breadcrumb: i18n.translate('xpack.apm.breadcrumb.metricsTitle', {
+ defaultMessage: 'Metrics',
+ }),
},
// service nodes, only enabled for java agents for now
{
exact: true,
path: '/services/:serviceName/nodes',
- component: () => ,
+ component: ServiceDetailsNodes,
breadcrumb: i18n.translate('xpack.apm.breadcrumb.nodesTitle', {
defaultMessage: 'JVMs',
}),
- name: RouteName.SERVICE_NODES,
},
// node metrics
{
exact: true,
path: '/services/:serviceName/nodes/:serviceNodeName/metrics',
- component: () => ,
- breadcrumb: ({ location }) => {
- const { serviceNodeName } = resolveUrlParams(location, {});
+ component: ServiceNodeMetrics,
+ breadcrumb: ({ match }) => {
+ const { serviceNodeName } = match.params;
if (serviceNodeName === SERVICE_NODE_NAME_MISSING) {
return UNIDENTIFIED_SERVICE_NODES_LABEL;
@@ -214,7 +280,6 @@ export const routes: BreadcrumbRoute[] = [
return serviceNodeName || '';
},
- name: RouteName.SERVICE_NODE_METRICS,
},
{
exact: true,
@@ -224,61 +289,46 @@ export const routes: BreadcrumbRoute[] = [
const query = toQuery(location.search);
return query.transactionName as string;
},
- name: RouteName.TRANSACTION_NAME,
},
{
exact: true,
path: '/link-to/trace/:traceId',
component: TraceLink,
breadcrumb: null,
- name: RouteName.LINK_TO_TRACE,
},
-
{
exact: true,
path: '/service-map',
- component: () => ,
+ component: HomeServiceMap,
breadcrumb: i18n.translate('xpack.apm.breadcrumb.serviceMapTitle', {
defaultMessage: 'Service Map',
}),
- name: RouteName.SERVICE_MAP,
},
{
exact: true,
path: '/services/:serviceName/service-map',
- component: () => ,
+ component: ServiceDetailsServiceMap,
breadcrumb: i18n.translate('xpack.apm.breadcrumb.serviceMapTitle', {
defaultMessage: 'Service Map',
}),
- name: RouteName.SINGLE_SERVICE_MAP,
},
{
exact: true,
path: '/settings/customize-ui',
- component: () => (
-
-
-
- ),
+ component: SettingsCustomizeUI,
breadcrumb: i18n.translate('xpack.apm.breadcrumb.settings.customizeUI', {
defaultMessage: 'Customize UI',
}),
- name: RouteName.CUSTOMIZE_UI,
},
{
exact: true,
path: '/settings/anomaly-detection',
- component: () => (
-
-
-
- ),
+ component: SettingsAnomalyDetection,
breadcrumb: i18n.translate(
'xpack.apm.breadcrumb.settings.anomalyDetection',
{
defaultMessage: 'Anomaly detection',
}
),
- name: RouteName.ANOMALY_DETECTION,
},
];
diff --git a/x-pack/plugins/apm/public/components/app/Main/route_config/route_config.test.tsx b/x-pack/plugins/apm/public/components/app/Main/route_config/route_config.test.tsx
index ad12afe35fa20..21a162111bc79 100644
--- a/x-pack/plugins/apm/public/components/app/Main/route_config/route_config.test.tsx
+++ b/x-pack/plugins/apm/public/components/app/Main/route_config/route_config.test.tsx
@@ -14,7 +14,7 @@ describe('routes', () => {
it('redirects to /services', () => {
const location = { hash: '', pathname: '/', search: '' };
expect(
- (route as any).render({ location } as any).props.to.pathname
+ (route as any).component({ location } as any).props.to.pathname
).toEqual('/services');
});
});
@@ -28,7 +28,9 @@ describe('routes', () => {
search: '',
};
- expect(((route as any).render({ location }) as any).props.to).toEqual({
+ expect(
+ ((route as any).component({ location }) as any).props.to
+ ).toEqual({
hash: '',
pathname: '/services/opbeans-python/transactions/view',
search:
diff --git a/x-pack/plugins/apm/public/components/app/Main/route_config/route_handlers/agent_configuration.tsx b/x-pack/plugins/apm/public/components/app/Main/route_config/route_handlers/agent_configuration.tsx
index 7a00840daa3c5..cc07286457908 100644
--- a/x-pack/plugins/apm/public/components/app/Main/route_config/route_handlers/agent_configuration.tsx
+++ b/x-pack/plugins/apm/public/components/app/Main/route_config/route_handlers/agent_configuration.tsx
@@ -5,14 +5,17 @@
*/
import React from 'react';
-import { useHistory } from 'react-router-dom';
+import { RouteComponentProps } from 'react-router-dom';
import { useFetcher } from '../../../../../hooks/useFetcher';
import { toQuery } from '../../../../shared/Links/url_helpers';
import { Settings } from '../../../Settings';
import { AgentConfigurationCreateEdit } from '../../../Settings/AgentConfigurations/AgentConfigurationCreateEdit';
-export function EditAgentConfigurationRouteHandler() {
- const history = useHistory();
+type EditAgentConfigurationRouteHandler = RouteComponentProps<{}>;
+
+export function EditAgentConfigurationRouteHandler({
+ history,
+}: EditAgentConfigurationRouteHandler) {
const { search } = history.location;
// typescript complains because `pageStop` does not exist in `APMQueryParams`
@@ -40,8 +43,11 @@ export function EditAgentConfigurationRouteHandler() {
);
}
-export function CreateAgentConfigurationRouteHandler() {
- const history = useHistory();
+type CreateAgentConfigurationRouteHandlerProps = RouteComponentProps<{}>;
+
+export function CreateAgentConfigurationRouteHandler({
+ history,
+}: CreateAgentConfigurationRouteHandlerProps) {
const { search } = history.location;
// Ignoring here because we specifically DO NOT want to add the query params to the global route handler
diff --git a/x-pack/plugins/apm/public/components/app/Main/route_config/route_names.tsx b/x-pack/plugins/apm/public/components/app/Main/route_config/route_names.tsx
deleted file mode 100644
index 1bf798e3b26d7..0000000000000
--- a/x-pack/plugins/apm/public/components/app/Main/route_config/route_names.tsx
+++ /dev/null
@@ -1,31 +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;
- * you may not use this file except in compliance with the Elastic License.
- */
-
-export enum RouteName {
- HOME = 'home',
- SERVICES = 'services',
- SERVICE_MAP = 'service-map',
- SINGLE_SERVICE_MAP = 'single-service-map',
- TRACES = 'traces',
- SERVICE = 'service',
- TRANSACTIONS = 'transactions',
- ERRORS = 'errors',
- ERROR = 'error',
- METRICS = 'metrics',
- SERVICE_NODE_METRICS = 'node_metrics',
- TRANSACTION_TYPE = 'transaction_type',
- TRANSACTION_NAME = 'transaction_name',
- SETTINGS = 'settings',
- AGENT_CONFIGURATION = 'agent_configuration',
- AGENT_CONFIGURATION_CREATE = 'agent_configuration_create',
- AGENT_CONFIGURATION_EDIT = 'agent_configuration_edit',
- INDICES = 'indices',
- SERVICE_NODES = 'nodes',
- LINK_TO_TRACE = 'link_to_trace',
- CUSTOMIZE_UI = 'customize_ui',
- ANOMALY_DETECTION = 'anomaly_detection',
- CSM = 'csm',
-}
diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/ChartWrapper/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/ChartWrapper/index.tsx
index 970365779a0a2..f27a3d56aab55 100644
--- a/x-pack/plugins/apm/public/components/app/RumDashboard/ChartWrapper/index.tsx
+++ b/x-pack/plugins/apm/public/components/app/RumDashboard/ChartWrapper/index.tsx
@@ -26,11 +26,14 @@ interface Props {
* aria-label for accessibility
*/
'aria-label'?: string;
+
+ maxWidth?: string;
}
export function ChartWrapper({
loading = false,
height = '100%',
+ maxWidth,
children,
...rest
}: Props) {
@@ -43,6 +46,7 @@ export function ChartWrapper({
height,
opacity,
transition: 'opacity 0.2s',
+ ...(maxWidth ? { maxWidth } : {}),
}}
{...(rest as HTMLAttributes)}
>
@@ -52,7 +56,12 @@ export function ChartWrapper({
diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/Charts/VisitorBreakdownChart.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/Charts/VisitorBreakdownChart.tsx
index 9f9ffdf7168b8..213126ba4bf81 100644
--- a/x-pack/plugins/apm/public/components/app/RumDashboard/Charts/VisitorBreakdownChart.tsx
+++ b/x-pack/plugins/apm/public/components/app/RumDashboard/Charts/VisitorBreakdownChart.tsx
@@ -14,7 +14,7 @@ import {
PartitionLayout,
Settings,
} from '@elastic/charts';
-import euiLightVars from '@elastic/eui/dist/eui_theme_light.json';
+import styled from 'styled-components';
import {
EUI_CHARTS_THEME_DARK,
EUI_CHARTS_THEME_LIGHT,
@@ -22,6 +22,10 @@ import {
import { useUiSetting$ } from '../../../../../../../../src/plugins/kibana_react/public';
import { ChartWrapper } from '../ChartWrapper';
+const StyleChart = styled.div`
+ height: 100%;
+`;
+
interface Props {
options?: Array<{
count: number;
@@ -32,65 +36,47 @@ interface Props {
export function VisitorBreakdownChart({ options }: Props) {
const [darkMode] = useUiSetting$('theme:darkMode');
+ const euiChartTheme = darkMode
+ ? EUI_CHARTS_THEME_DARK
+ : EUI_CHARTS_THEME_LIGHT;
+
return (
-
-
-
- d.count as number}
- valueGetter="percent"
- percentFormatter={(d: number) =>
- `${Math.round((d + Number.EPSILON) * 100) / 100}%`
- }
- layers={[
- {
- groupByRollup: (d: Datum) => d.name,
- nodeLabel: (d: Datum) => d,
- // fillLabel: { textInvertible: true },
- shape: {
- fillColor: (d) => {
- const clrs = [
- euiLightVars.euiColorVis1_behindText,
- euiLightVars.euiColorVis0_behindText,
- euiLightVars.euiColorVis2_behindText,
- euiLightVars.euiColorVis3_behindText,
- euiLightVars.euiColorVis4_behindText,
- euiLightVars.euiColorVis5_behindText,
- euiLightVars.euiColorVis6_behindText,
- euiLightVars.euiColorVis7_behindText,
- euiLightVars.euiColorVis8_behindText,
- euiLightVars.euiColorVis9_behindText,
- ];
- return clrs[d.sortIndex];
+
+
+
+
+ d.count as number}
+ valueGetter="percent"
+ percentFormatter={(d: number) =>
+ `${Math.round((d + Number.EPSILON) * 100) / 100}%`
+ }
+ layers={[
+ {
+ groupByRollup: (d: Datum) => d.name,
+ shape: {
+ fillColor: (d) =>
+ euiChartTheme.theme.colors?.vizColors?.[d.sortIndex]!,
},
},
- },
- ]}
- config={{
- partitionLayout: PartitionLayout.sunburst,
- linkLabel: {
- maxCount: 32,
- fontSize: 14,
- },
- fontFamily: 'Arial',
- margin: { top: 0, bottom: 0, left: 0, right: 0 },
- minFontSize: 1,
- idealFontSizeJump: 1.1,
- outerSizeRatio: 0.9, // - 0.5 * Math.random(),
- emptySizeRatio: 0,
- circlePadding: 4,
- }}
- />
-
+ ]}
+ config={{
+ partitionLayout: PartitionLayout.sunburst,
+ linkLabel: { maximumSection: Infinity, maxCount: 0 },
+ margin: { top: 0, bottom: 0, left: 0, right: 0 },
+ outerSizeRatio: 1, // - 0.5 * Math.random(),
+ circlePadding: 4,
+ clockwiseSectors: false,
+ }}
+ />
+
+
);
}
diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/ClientMetrics/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/ClientMetrics/index.tsx
index 67404ece3d2c7..f54a54211359c 100644
--- a/x-pack/plugins/apm/public/components/app/RumDashboard/ClientMetrics/index.tsx
+++ b/x-pack/plugins/apm/public/components/app/RumDashboard/ClientMetrics/index.tsx
@@ -22,11 +22,11 @@ const ClFlexGroup = styled(EuiFlexGroup)`
export function ClientMetrics() {
const { urlParams, uiFilters } = useUrlParams();
- const { start, end, serviceName } = urlParams;
+ const { start, end } = urlParams;
const { data, status } = useFetcher(
(callApmApi) => {
- if (start && end && serviceName) {
+ if (start && end) {
return callApmApi({
pathname: '/api/apm/rum/client-metrics',
params: {
@@ -36,7 +36,7 @@ export function ClientMetrics() {
}
return Promise.resolve(null);
},
- [start, end, serviceName, uiFilters]
+ [start, end, uiFilters]
);
const STAT_STYLE = { width: '240px' };
@@ -45,7 +45,7 @@ export function ClientMetrics() {
<>{numeral(data?.pageViews?.value).format('0 a') ?? '-'}>
diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/CoreVitals/ColorPaletteFlexItem.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/CoreVitals/ColorPaletteFlexItem.tsx
new file mode 100644
index 0000000000000..fc2390acde0be
--- /dev/null
+++ b/x-pack/plugins/apm/public/components/app/RumDashboard/CoreVitals/ColorPaletteFlexItem.tsx
@@ -0,0 +1,72 @@
+/*
+ * 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 React from 'react';
+import { EuiFlexItem, EuiToolTip } from '@elastic/eui';
+import styled from 'styled-components';
+
+const ColoredSpan = styled.div`
+ height: 16px;
+ width: 100%;
+ cursor: pointer;
+`;
+
+const getSpanStyle = (
+ position: number,
+ inFocus: boolean,
+ hexCode: string,
+ percentage: number
+) => {
+ let first = position === 0 || percentage === 100;
+ let last = position === 2 || percentage === 100;
+ if (percentage === 100) {
+ first = true;
+ last = true;
+ }
+
+ const spanStyle: any = {
+ backgroundColor: hexCode,
+ opacity: !inFocus ? 1 : 0.3,
+ };
+ let borderRadius = '';
+
+ if (first) {
+ borderRadius = '4px 0 0 4px';
+ }
+ if (last) {
+ borderRadius = '0 4px 4px 0';
+ }
+ if (first && last) {
+ borderRadius = '4px';
+ }
+ spanStyle.borderRadius = borderRadius;
+
+ return spanStyle;
+};
+
+export function ColorPaletteFlexItem({
+ hexCode,
+ inFocus,
+ percentage,
+ tooltip,
+ position,
+}: {
+ hexCode: string;
+ position: number;
+ inFocus: boolean;
+ percentage: number;
+ tooltip: string;
+}) {
+ const spanStyle = getSpanStyle(position, inFocus, hexCode, percentage);
+
+ return (
+
+
+
+
+
+ );
+}
diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/CoreVitals/CoreVitalItem.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/CoreVitals/CoreVitalItem.tsx
new file mode 100644
index 0000000000000..a4cbebf20b54c
--- /dev/null
+++ b/x-pack/plugins/apm/public/components/app/RumDashboard/CoreVitals/CoreVitalItem.tsx
@@ -0,0 +1,124 @@
+/*
+ * 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 {
+ EuiFlexGroup,
+ euiPaletteForStatus,
+ EuiSpacer,
+ EuiStat,
+} from '@elastic/eui';
+import React, { useState } from 'react';
+import { i18n } from '@kbn/i18n';
+import { PaletteLegends } from './PaletteLegends';
+import { ColorPaletteFlexItem } from './ColorPaletteFlexItem';
+import {
+ AVERAGE_LABEL,
+ GOOD_LABEL,
+ LESS_LABEL,
+ MORE_LABEL,
+ POOR_LABEL,
+} from './translations';
+
+export interface Thresholds {
+ good: string;
+ bad: string;
+}
+
+interface Props {
+ title: string;
+ value: string;
+ ranks?: number[];
+ loading: boolean;
+ thresholds: Thresholds;
+}
+
+export function getCoreVitalTooltipMessage(
+ thresholds: Thresholds,
+ position: number,
+ title: string,
+ percentage: number
+) {
+ const good = position === 0;
+ const bad = position === 2;
+ const average = !good && !bad;
+
+ return i18n.translate('xpack.apm.csm.dashboard.webVitals.palette.tooltip', {
+ defaultMessage:
+ '{percentage} % of users have {exp} experience because the {title} takes {moreOrLess} than {value}{averageMessage}.',
+ values: {
+ percentage,
+ title: title?.toLowerCase(),
+ exp: good ? GOOD_LABEL : bad ? POOR_LABEL : AVERAGE_LABEL,
+ moreOrLess: bad || average ? MORE_LABEL : LESS_LABEL,
+ value: good || average ? thresholds.good : thresholds.bad,
+ averageMessage: average
+ ? i18n.translate('xpack.apm.rum.coreVitals.averageMessage', {
+ defaultMessage: ' and less than {bad}',
+ values: { bad: thresholds.bad },
+ })
+ : '',
+ },
+ });
+}
+
+export function CoreVitalItem({
+ loading,
+ title,
+ value,
+ thresholds,
+ ranks = [100, 0, 0],
+}: Props) {
+ const palette = euiPaletteForStatus(3);
+
+ const [inFocusInd, setInFocusInd] = useState(null);
+
+ const biggestValIndex = ranks.indexOf(Math.max(...ranks));
+
+ return (
+ <>
+
+
+
+ {palette.map((hexCode, ind) => (
+
+ ))}
+
+
+ {
+ setInFocusInd(ind);
+ }}
+ />
+
+ >
+ );
+}
diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/CoreVitals/PaletteLegends.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/CoreVitals/PaletteLegends.tsx
new file mode 100644
index 0000000000000..84cc5f1ddb230
--- /dev/null
+++ b/x-pack/plugins/apm/public/components/app/RumDashboard/CoreVitals/PaletteLegends.tsx
@@ -0,0 +1,69 @@
+/*
+ * 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 React from 'react';
+import {
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiHealth,
+ euiPaletteForStatus,
+ EuiToolTip,
+} from '@elastic/eui';
+import styled from 'styled-components';
+import { getCoreVitalTooltipMessage, Thresholds } from './CoreVitalItem';
+
+const PaletteLegend = styled(EuiHealth)`
+ &:hover {
+ cursor: pointer;
+ text-decoration: underline;
+ background-color: #e7f0f7;
+ }
+`;
+
+interface Props {
+ onItemHover: (ind: number | null) => void;
+ ranks: number[];
+ thresholds: Thresholds;
+ title: string;
+}
+
+export function PaletteLegends({
+ ranks,
+ title,
+ onItemHover,
+ thresholds,
+}: Props) {
+ const palette = euiPaletteForStatus(3);
+
+ return (
+
+ {palette.map((color, ind) => (
+ {
+ onItemHover(ind);
+ }}
+ onMouseLeave={() => {
+ onItemHover(null);
+ }}
+ >
+
+ {ranks?.[ind]}%
+
+
+ ))}
+
+ );
+}
diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/CoreVitals/__stories__/CoreVitals.stories.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/CoreVitals/__stories__/CoreVitals.stories.tsx
new file mode 100644
index 0000000000000..a611df00f1e65
--- /dev/null
+++ b/x-pack/plugins/apm/public/components/app/RumDashboard/CoreVitals/__stories__/CoreVitals.stories.tsx
@@ -0,0 +1,93 @@
+/*
+ * 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 { storiesOf } from '@storybook/react';
+import React from 'react';
+import { EuiThemeProvider } from '../../../../../../../observability/public';
+import { CoreVitalItem } from '../CoreVitalItem';
+import { LCP_LABEL } from '../translations';
+
+storiesOf('app/RumDashboard/WebCoreVitals', module)
+ .addDecorator((storyFn) => {storyFn()})
+ .add(
+ 'Basic',
+ () => {
+ return (
+
+ );
+ },
+ {
+ info: {
+ propTables: false,
+ source: false,
+ },
+ }
+ )
+ .add(
+ '50% Good',
+ () => {
+ return (
+
+ );
+ },
+ {
+ info: {
+ propTables: false,
+ source: false,
+ },
+ }
+ )
+ .add(
+ '100% Bad',
+ () => {
+ return (
+
+ );
+ },
+ {
+ info: {
+ propTables: false,
+ source: false,
+ },
+ }
+ )
+ .add(
+ '100% Average',
+ () => {
+ return (
+
+ );
+ },
+ {
+ info: {
+ propTables: false,
+ source: false,
+ },
+ }
+ );
diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/CoreVitals/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/CoreVitals/index.tsx
new file mode 100644
index 0000000000000..e8305a6aef0d4
--- /dev/null
+++ b/x-pack/plugins/apm/public/components/app/RumDashboard/CoreVitals/index.tsx
@@ -0,0 +1,73 @@
+/*
+ * 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 * as React from 'react';
+import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
+
+import { useFetcher } from '../../../../hooks/useFetcher';
+import { useUrlParams } from '../../../../hooks/useUrlParams';
+import { CLS_LABEL, FID_LABEL, LCP_LABEL } from './translations';
+import { CoreVitalItem } from './CoreVitalItem';
+
+const CoreVitalsThresholds = {
+ LCP: { good: '2.5s', bad: '4.0s' },
+ FID: { good: '100ms', bad: '300ms' },
+ CLS: { good: '0.1', bad: '0.25' },
+};
+
+export function CoreVitals() {
+ const { urlParams, uiFilters } = useUrlParams();
+
+ const { start, end, serviceName } = urlParams;
+
+ const { data, status } = useFetcher(
+ (callApmApi) => {
+ if (start && end && serviceName) {
+ return callApmApi({
+ pathname: '/api/apm/rum-client/web-core-vitals',
+ params: {
+ query: { start, end, uiFilters: JSON.stringify(uiFilters) },
+ },
+ });
+ }
+ return Promise.resolve(null);
+ },
+ [start, end, serviceName, uiFilters]
+ );
+
+ const { lcp, lcpRanks, fid, fidRanks, cls, clsRanks } = data || {};
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/CoreVitals/translations.ts b/x-pack/plugins/apm/public/components/app/RumDashboard/CoreVitals/translations.ts
new file mode 100644
index 0000000000000..136dfb279e336
--- /dev/null
+++ b/x-pack/plugins/apm/public/components/app/RumDashboard/CoreVitals/translations.ts
@@ -0,0 +1,50 @@
+/*
+ * 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 { i18n } from '@kbn/i18n';
+
+export const LCP_LABEL = i18n.translate('xpack.apm.rum.coreVitals.lcp', {
+ defaultMessage: 'Largest contentful paint',
+});
+
+export const FID_LABEL = i18n.translate('xpack.apm.rum.coreVitals.fip', {
+ defaultMessage: 'First input delay',
+});
+
+export const CLS_LABEL = i18n.translate('xpack.apm.rum.coreVitals.cls', {
+ defaultMessage: 'Cumulative layout shift',
+});
+
+export const FCP_LABEL = i18n.translate('xpack.apm.rum.coreVitals.fcp', {
+ defaultMessage: 'First contentful paint',
+});
+
+export const TBT_LABEL = i18n.translate('xpack.apm.rum.coreVitals.tbt', {
+ defaultMessage: 'Total blocking time',
+});
+
+export const POOR_LABEL = i18n.translate('xpack.apm.rum.coreVitals.poor', {
+ defaultMessage: 'a poor',
+});
+
+export const GOOD_LABEL = i18n.translate('xpack.apm.rum.coreVitals.good', {
+ defaultMessage: 'a good',
+});
+
+export const AVERAGE_LABEL = i18n.translate(
+ 'xpack.apm.rum.coreVitals.average',
+ {
+ defaultMessage: 'an average',
+ }
+);
+
+export const MORE_LABEL = i18n.translate('xpack.apm.rum.coreVitals.more', {
+ defaultMessage: 'more',
+});
+
+export const LESS_LABEL = i18n.translate('xpack.apm.rum.coreVitals.less', {
+ defaultMessage: 'less',
+});
diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/ResetPercentileZoom.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/ResetPercentileZoom.tsx
new file mode 100644
index 0000000000000..deaeed70e572b
--- /dev/null
+++ b/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/ResetPercentileZoom.tsx
@@ -0,0 +1,53 @@
+/*
+ * 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 React from 'react';
+import {
+ EuiButtonEmpty,
+ EuiHideFor,
+ EuiShowFor,
+ EuiButtonIcon,
+} from '@elastic/eui';
+import { I18LABELS } from '../translations';
+import { PercentileRange } from './index';
+
+interface Props {
+ percentileRange: PercentileRange;
+ setPercentileRange: (value: PercentileRange) => void;
+}
+export function ResetPercentileZoom({
+ percentileRange,
+ setPercentileRange,
+}: Props) {
+ const isDisabled =
+ percentileRange.min === null && percentileRange.max === null;
+ const onClick = () => {
+ setPercentileRange({ min: null, max: null });
+ };
+ return (
+ <>
+
+
+
+
+
+ {I18LABELS.resetZoom}
+
+
+ >
+ );
+}
diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/index.tsx
index 3e35f15254937..f63b914c73398 100644
--- a/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/index.tsx
+++ b/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/index.tsx
@@ -5,19 +5,14 @@
*/
import React, { useState } from 'react';
-import {
- EuiButtonEmpty,
- EuiFlexGroup,
- EuiFlexItem,
- EuiSpacer,
- EuiTitle,
-} from '@elastic/eui';
+import { EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiTitle } from '@elastic/eui';
import { useUrlParams } from '../../../../hooks/useUrlParams';
import { useFetcher } from '../../../../hooks/useFetcher';
import { I18LABELS } from '../translations';
import { BreakdownFilter } from '../Breakdowns/BreakdownFilter';
import { PageLoadDistChart } from '../Charts/PageLoadDistChart';
import { BreakdownItem } from '../../../../../typings/ui_filters';
+import { ResetPercentileZoom } from './ResetPercentileZoom';
export interface PercentileRange {
min?: number | null;
@@ -27,7 +22,7 @@ export interface PercentileRange {
export function PageLoadDistribution() {
const { urlParams, uiFilters } = useUrlParams();
- const { start, end, serviceName } = urlParams;
+ const { start, end } = urlParams;
const [percentileRange, setPercentileRange] = useState({
min: null,
@@ -38,7 +33,7 @@ export function PageLoadDistribution() {
const { data, status } = useFetcher(
(callApmApi) => {
- if (start && end && serviceName) {
+ if (start && end) {
return callApmApi({
pathname: '/api/apm/rum-client/page-load-distribution',
params: {
@@ -58,14 +53,7 @@ export function PageLoadDistribution() {
}
return Promise.resolve(null);
},
- [
- end,
- start,
- serviceName,
- uiFilters,
- percentileRange.min,
- percentileRange.max,
- ]
+ [end, start, uiFilters, percentileRange.min, percentileRange.max]
);
const onPercentileChange = (min: number, max: number) => {
@@ -81,18 +69,10 @@ export function PageLoadDistribution() {
- {
- setPercentileRange({ min: null, max: null });
- }}
- disabled={
- percentileRange.min === null && percentileRange.max === null
- }
- >
- {I18LABELS.resetZoom}
-
+
{
const { urlParams, uiFilters } = useUrlParams();
- const { start, end, serviceName } = urlParams;
+ const { start, end } = urlParams;
const { min: minP, max: maxP } = percentileRange ?? {};
return useFetcher(
(callApmApi) => {
- if (start && end && serviceName && field && value) {
+ if (start && end && field && value) {
return callApmApi({
pathname: '/api/apm/rum-client/page-load-distribution/breakdown',
params: {
@@ -43,6 +43,6 @@ export const useBreakdowns = ({ percentileRange, field, value }: Props) => {
});
}
},
- [end, start, serviceName, uiFilters, field, value, minP, maxP]
+ [end, start, uiFilters, field, value, minP, maxP]
);
};
diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/PageViewsTrend/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/PageViewsTrend/index.tsx
index a67f6dd8e3cb5..62ecc4ddbaaca 100644
--- a/x-pack/plugins/apm/public/components/app/RumDashboard/PageViewsTrend/index.tsx
+++ b/x-pack/plugins/apm/public/components/app/RumDashboard/PageViewsTrend/index.tsx
@@ -16,13 +16,13 @@ import { BreakdownItem } from '../../../../../typings/ui_filters';
export function PageViewsTrend() {
const { urlParams, uiFilters } = useUrlParams();
- const { start, end, serviceName } = urlParams;
+ const { start, end } = urlParams;
const [breakdown, setBreakdown] = useState(null);
const { data, status } = useFetcher(
(callApmApi) => {
- if (start && end && serviceName) {
+ if (start && end) {
return callApmApi({
pathname: '/api/apm/rum-client/page-view-trends',
params: {
@@ -41,7 +41,7 @@ export function PageViewsTrend() {
}
return Promise.resolve(undefined);
},
- [end, start, serviceName, uiFilters, breakdown]
+ [end, start, uiFilters, breakdown]
);
return (
diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/RumDashboard.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/RumDashboard.tsx
index 24d4470736de0..f05c07e8512ac 100644
--- a/x-pack/plugins/apm/public/components/app/RumDashboard/RumDashboard.tsx
+++ b/x-pack/plugins/apm/public/components/app/RumDashboard/RumDashboard.tsx
@@ -17,6 +17,7 @@ import { PageViewsTrend } from './PageViewsTrend';
import { PageLoadDistribution } from './PageLoadDistribution';
import { I18LABELS } from './translations';
import { VisitorBreakdown } from './VisitorBreakdown';
+import { CoreVitals } from './CoreVitals';
export function RumDashboard() {
return (
@@ -26,7 +27,7 @@ export function RumDashboard() {
- {I18LABELS.pageLoadTimes}
+ {I18LABELS.pageLoadDuration}
@@ -37,13 +38,29 @@ export function RumDashboard() {
-
-
-
-
+
+
+ {I18LABELS.coreWebVitals}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdown/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdown/index.tsx
index 5c68ebb1667ab..e18875f32ff72 100644
--- a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdown/index.tsx
+++ b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdown/index.tsx
@@ -5,20 +5,20 @@
*/
import React from 'react';
-import { EuiFlexGroup, EuiFlexItem, EuiTitle } from '@elastic/eui';
+import { EuiFlexGroup, EuiFlexItem, EuiTitle, EuiSpacer } from '@elastic/eui';
import { VisitorBreakdownChart } from '../Charts/VisitorBreakdownChart';
-import { VisitorBreakdownLabel } from '../translations';
+import { I18LABELS, VisitorBreakdownLabel } from '../translations';
import { useFetcher } from '../../../../hooks/useFetcher';
import { useUrlParams } from '../../../../hooks/useUrlParams';
export function VisitorBreakdown() {
const { urlParams, uiFilters } = useUrlParams();
- const { start, end, serviceName } = urlParams;
+ const { start, end } = urlParams;
const { data } = useFetcher(
(callApmApi) => {
- if (start && end && serviceName) {
+ if (start && end) {
return callApmApi({
pathname: '/api/apm/rum-client/visitor-breakdown',
params: {
@@ -32,32 +32,29 @@ export function VisitorBreakdown() {
}
return Promise.resolve(null);
},
- [end, start, serviceName, uiFilters]
+ [end, start, uiFilters]
);
return (
<>
-
+
{VisitorBreakdownLabel}
+
-
-
- Browser
-
-
-
-
-
- Operating System
+
+ {I18LABELS.browser}
+
+
-
-
- Device
+
+ {I18LABELS.operatingSystem}
+
+
>
diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/translations.ts b/x-pack/plugins/apm/public/components/app/RumDashboard/translations.ts
index 66eeaf433d2a1..660ed5a92a0e6 100644
--- a/x-pack/plugins/apm/public/components/app/RumDashboard/translations.ts
+++ b/x-pack/plugins/apm/public/components/app/RumDashboard/translations.ts
@@ -25,6 +25,12 @@ export const I18LABELS = {
pageLoadTimes: i18n.translate('xpack.apm.rum.dashboard.pageLoadTimes.label', {
defaultMessage: 'Page load times',
}),
+ pageLoadDuration: i18n.translate(
+ 'xpack.apm.rum.dashboard.pageLoadDuration.label',
+ {
+ defaultMessage: 'Page load duration',
+ }
+ ),
pageLoadDistribution: i18n.translate(
'xpack.apm.rum.dashboard.pageLoadDistribution.label',
{
@@ -46,6 +52,18 @@ export const I18LABELS = {
seconds: i18n.translate('xpack.apm.rum.filterGroup.seconds', {
defaultMessage: 'seconds',
}),
+ coreWebVitals: i18n.translate('xpack.apm.rum.filterGroup.coreWebVitals', {
+ defaultMessage: 'Core web vitals',
+ }),
+ browser: i18n.translate('xpack.apm.rum.visitorBreakdown.browser', {
+ defaultMessage: 'Browser',
+ }),
+ operatingSystem: i18n.translate(
+ 'xpack.apm.rum.visitorBreakdown.operatingSystem',
+ {
+ defaultMessage: 'Operating system',
+ }
+ ),
};
export const VisitorBreakdownLabel = i18n.translate(
diff --git a/x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceDetailTabs.tsx b/x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceDetailTabs.tsx
index 2f35e329720de..cbb6d9a8fbe41 100644
--- a/x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceDetailTabs.tsx
+++ b/x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceDetailTabs.tsx
@@ -10,7 +10,6 @@ import React from 'react';
import { isJavaAgentName, isRumAgentName } from '../../../../common/agent_name';
import { useAgentName } from '../../../hooks/useAgentName';
import { useApmPluginContext } from '../../../hooks/useApmPluginContext';
-import { useUrlParams } from '../../../hooks/useUrlParams';
import { EuiTabLink } from '../../shared/EuiTabLink';
import { ErrorOverviewLink } from '../../shared/Links/apm/ErrorOverviewLink';
import { MetricOverviewLink } from '../../shared/Links/apm/MetricOverviewLink';
@@ -24,20 +23,14 @@ import { ServiceNodeOverview } from '../ServiceNodeOverview';
import { TransactionOverview } from '../TransactionOverview';
interface Props {
+ serviceName: string;
tab: 'transactions' | 'errors' | 'metrics' | 'nodes' | 'service-map';
}
-export function ServiceDetailTabs({ tab }: Props) {
- const { urlParams } = useUrlParams();
- const { serviceName } = urlParams;
+export function ServiceDetailTabs({ serviceName, tab }: Props) {
const { agentName } = useAgentName();
const { serviceMapEnabled } = useApmPluginContext().config;
- if (!serviceName) {
- // this never happens, urlParams type is not accurate enough
- throw new Error('Service name was not defined');
- }
-
const transactionsTab = {
link: (
@@ -46,7 +39,7 @@ export function ServiceDetailTabs({ tab }: Props) {
})}
),
- render: () => ,
+ render: () => ,
name: 'transactions',
};
@@ -59,7 +52,7 @@ export function ServiceDetailTabs({ tab }: Props) {
),
render: () => {
- return ;
+ return ;
},
name: 'errors',
};
@@ -75,7 +68,7 @@ export function ServiceDetailTabs({ tab }: Props) {
})}
),
- render: () => ,
+ render: () => ,
name: 'nodes',
};
tabs.push(nodesListTab);
@@ -88,7 +81,9 @@ export function ServiceDetailTabs({ tab }: Props) {
})}
),
- render: () => ,
+ render: () => (
+
+ ),
name: 'metrics',
};
tabs.push(metricsTab);
diff --git a/x-pack/plugins/apm/public/components/app/ServiceDetails/index.tsx b/x-pack/plugins/apm/public/components/app/ServiceDetails/index.tsx
index b5a4ca4799afd..67c4a7c4cde1b 100644
--- a/x-pack/plugins/apm/public/components/app/ServiceDetails/index.tsx
+++ b/x-pack/plugins/apm/public/components/app/ServiceDetails/index.tsx
@@ -5,27 +5,26 @@
*/
import {
+ EuiButtonEmpty,
EuiFlexGroup,
EuiFlexItem,
EuiTitle,
- EuiButtonEmpty,
} from '@elastic/eui';
-import React from 'react';
import { i18n } from '@kbn/i18n';
+import React from 'react';
+import { RouteComponentProps } from 'react-router-dom';
+import { useApmPluginContext } from '../../../hooks/useApmPluginContext';
import { ApmHeader } from '../../shared/ApmHeader';
-import { ServiceDetailTabs } from './ServiceDetailTabs';
-import { useUrlParams } from '../../../hooks/useUrlParams';
import { AlertIntegrations } from './AlertIntegrations';
-import { useApmPluginContext } from '../../../hooks/useApmPluginContext';
+import { ServiceDetailTabs } from './ServiceDetailTabs';
-interface Props {
+interface Props extends RouteComponentProps<{ serviceName: string }> {
tab: React.ComponentProps['tab'];
}
-export function ServiceDetails({ tab }: Props) {
+export function ServiceDetails({ match, tab }: Props) {
const plugin = useApmPluginContext();
- const { urlParams } = useUrlParams();
- const { serviceName } = urlParams;
+ const { serviceName } = match.params;
const capabilities = plugin.core.application.capabilities;
const canReadAlerts = !!capabilities.apm['alerting:show'];
const canSaveAlerts = !!capabilities.apm['alerting:save'];
@@ -76,7 +75,7 @@ export function ServiceDetails({ tab }: Props) {
-
+
);
}
diff --git a/x-pack/plugins/apm/public/components/app/ServiceMetrics/index.tsx b/x-pack/plugins/apm/public/components/app/ServiceMetrics/index.tsx
index 9b01f9ebb7e99..2fb500f3c9916 100644
--- a/x-pack/plugins/apm/public/components/app/ServiceMetrics/index.tsx
+++ b/x-pack/plugins/apm/public/components/app/ServiceMetrics/index.tsx
@@ -21,11 +21,14 @@ import { LocalUIFilters } from '../../shared/LocalUIFilters';
interface ServiceMetricsProps {
agentName: string;
+ serviceName: string;
}
-export function ServiceMetrics({ agentName }: ServiceMetricsProps) {
+export function ServiceMetrics({
+ agentName,
+ serviceName,
+}: ServiceMetricsProps) {
const { urlParams } = useUrlParams();
- const { serviceName, serviceNodeName } = urlParams;
const { data } = useServiceMetricCharts(urlParams, agentName);
const { start, end } = urlParams;
@@ -34,12 +37,11 @@ export function ServiceMetrics({ agentName }: ServiceMetricsProps) {
filterNames: ['host', 'containerId', 'podName', 'serviceVersion'],
params: {
serviceName,
- serviceNodeName,
},
projection: Projection.metrics,
showCount: false,
}),
- [serviceName, serviceNodeName]
+ [serviceName]
);
return (
diff --git a/x-pack/plugins/apm/public/components/app/ServiceNodeMetrics/index.test.tsx b/x-pack/plugins/apm/public/components/app/ServiceNodeMetrics/index.test.tsx
index eced7457318d8..c6f7e68e4f4d0 100644
--- a/x-pack/plugins/apm/public/components/app/ServiceNodeMetrics/index.test.tsx
+++ b/x-pack/plugins/apm/public/components/app/ServiceNodeMetrics/index.test.tsx
@@ -8,14 +8,20 @@ import React from 'react';
import { shallow } from 'enzyme';
import { ServiceNodeMetrics } from '.';
import { MockApmPluginContextWrapper } from '../../../context/ApmPluginContext/MockApmPluginContext';
+import { RouteComponentProps } from 'react-router-dom';
describe('ServiceNodeMetrics', () => {
describe('render', () => {
it('renders', () => {
+ const props = ({} as unknown) as RouteComponentProps<{
+ serviceName: string;
+ serviceNodeName: string;
+ }>;
+
expect(() =>
shallow(
-
+
)
).not.toThrowError();
diff --git a/x-pack/plugins/apm/public/components/app/ServiceNodeMetrics/index.tsx b/x-pack/plugins/apm/public/components/app/ServiceNodeMetrics/index.tsx
index e81968fb298fa..84a1920d17fa8 100644
--- a/x-pack/plugins/apm/public/components/app/ServiceNodeMetrics/index.tsx
+++ b/x-pack/plugins/apm/public/components/app/ServiceNodeMetrics/index.tsx
@@ -5,30 +5,31 @@
*/
import {
+ EuiCallOut,
+ EuiFlexGrid,
EuiFlexGroup,
EuiFlexItem,
- EuiTitle,
EuiHorizontalRule,
- EuiFlexGrid,
EuiPanel,
EuiSpacer,
EuiStat,
+ EuiTitle,
EuiToolTip,
- EuiCallOut,
} from '@elastic/eui';
-import React from 'react';
import { i18n } from '@kbn/i18n';
-import styled from 'styled-components';
import { FormattedMessage } from '@kbn/i18n/react';
+import React from 'react';
+import { RouteComponentProps } from 'react-router-dom';
+import styled from 'styled-components';
import { SERVICE_NODE_NAME_MISSING } from '../../../../common/service_nodes';
-import { ApmHeader } from '../../shared/ApmHeader';
-import { useUrlParams } from '../../../hooks/useUrlParams';
+import { ChartsSyncContextProvider } from '../../../context/ChartsSyncContext';
import { useAgentName } from '../../../hooks/useAgentName';
+import { FETCH_STATUS, useFetcher } from '../../../hooks/useFetcher';
import { useServiceMetricCharts } from '../../../hooks/useServiceMetricCharts';
-import { ChartsSyncContextProvider } from '../../../context/ChartsSyncContext';
+import { useUrlParams } from '../../../hooks/useUrlParams';
+import { px, truncate, unit } from '../../../style/variables';
+import { ApmHeader } from '../../shared/ApmHeader';
import { MetricsChart } from '../../shared/charts/MetricsChart';
-import { useFetcher, FETCH_STATUS } from '../../../hooks/useFetcher';
-import { truncate, px, unit } from '../../../style/variables';
import { ElasticDocsLink } from '../../shared/Links/ElasticDocsLink';
const INITIAL_DATA = {
@@ -41,17 +42,21 @@ const Truncate = styled.span`
${truncate(px(unit * 12))}
`;
-export function ServiceNodeMetrics() {
- const { urlParams, uiFilters } = useUrlParams();
- const { serviceName, serviceNodeName } = urlParams;
+type ServiceNodeMetricsProps = RouteComponentProps<{
+ serviceName: string;
+ serviceNodeName: string;
+}>;
+export function ServiceNodeMetrics({ match }: ServiceNodeMetricsProps) {
+ const { urlParams, uiFilters } = useUrlParams();
+ const { serviceName, serviceNodeName } = match.params;
const { agentName } = useAgentName();
const { data } = useServiceMetricCharts(urlParams, agentName);
const { start, end } = urlParams;
const { data: { host, containerId } = INITIAL_DATA, status } = useFetcher(
(callApmApi) => {
- if (serviceName && serviceNodeName && start && end) {
+ if (start && end) {
return callApmApi({
pathname:
'/api/apm/services/{serviceName}/node/{serviceNodeName}/metadata',
@@ -167,7 +172,7 @@ export function ServiceNodeMetrics() {
)}
- {agentName && serviceNodeName && (
+ {agentName && (
{data.charts.map((chart) => (
diff --git a/x-pack/plugins/apm/public/components/app/ServiceNodeOverview/index.tsx b/x-pack/plugins/apm/public/components/app/ServiceNodeOverview/index.tsx
index 9940a7aabb219..28477d2448899 100644
--- a/x-pack/plugins/apm/public/components/app/ServiceNodeOverview/index.tsx
+++ b/x-pack/plugins/apm/public/components/app/ServiceNodeOverview/index.tsx
@@ -33,9 +33,13 @@ const ServiceNodeName = styled.div`
${truncate(px(8 * unit))}
`;
-function ServiceNodeOverview() {
+interface ServiceNodeOverviewProps {
+ serviceName: string;
+}
+
+function ServiceNodeOverview({ serviceName }: ServiceNodeOverviewProps) {
const { uiFilters, urlParams } = useUrlParams();
- const { serviceName, start, end } = urlParams;
+ const { start, end } = urlParams;
const localFiltersConfig: React.ComponentProps = useMemo(
() => ({
@@ -50,7 +54,7 @@ function ServiceNodeOverview() {
const { data: items = [] } = useFetcher(
(callApmApi) => {
- if (!serviceName || !start || !end) {
+ if (!start || !end) {
return undefined;
}
return callApmApi({
@@ -70,10 +74,6 @@ function ServiceNodeOverview() {
[serviceName, start, end, uiFilters]
);
- if (!serviceName) {
- return null;
- }
-
const columns: Array> = [
{
name: (
diff --git a/x-pack/plugins/apm/public/components/app/TraceLink/__test__/TraceLink.test.tsx b/x-pack/plugins/apm/public/components/app/TraceLink/__test__/TraceLink.test.tsx
index bbaf6340e18f7..8d37a8e54d87c 100644
--- a/x-pack/plugins/apm/public/components/app/TraceLink/__test__/TraceLink.test.tsx
+++ b/x-pack/plugins/apm/public/components/app/TraceLink/__test__/TraceLink.test.tsx
@@ -5,63 +5,84 @@
*/
import { render } from '@testing-library/react';
import { shallow } from 'enzyme';
-import React from 'react';
+import React, { ReactNode } from 'react';
+import { MemoryRouter, RouteComponentProps } from 'react-router-dom';
import { TraceLink } from '../';
+import { ApmPluginContextValue } from '../../../../context/ApmPluginContext';
+import {
+ mockApmPluginContextValue,
+ MockApmPluginContextWrapper,
+} from '../../../../context/ApmPluginContext/MockApmPluginContext';
import * as hooks from '../../../../hooks/useFetcher';
import * as urlParamsHooks from '../../../../hooks/useUrlParams';
-import { MockApmPluginContextWrapper } from '../../../../context/ApmPluginContext/MockApmPluginContext';
-const renderOptions = { wrapper: MockApmPluginContextWrapper };
+function Wrapper({ children }: { children?: ReactNode }) {
+ return (
+
+
+ {children}
+
+
+ );
+}
-jest.mock('../../Main/route_config', () => ({
- routes: [
- {
- path: '/services/:serviceName/transactions/view',
- name: 'transaction_name',
- },
- {
- path: '/traces',
- name: 'traces',
- },
- ],
-}));
+const renderOptions = { wrapper: Wrapper };
describe('TraceLink', () => {
afterAll(() => {
jest.clearAllMocks();
});
- it('renders transition page', () => {
- const component = render(, renderOptions);
+
+ it('renders a transition page', () => {
+ const props = ({
+ match: { params: { traceId: 'x' } },
+ } as unknown) as RouteComponentProps<{ traceId: string }>;
+ const component = render(, renderOptions);
+
expect(component.getByText('Fetching trace...')).toBeDefined();
});
- it('renders trace page when transaction is not found', () => {
- jest.spyOn(urlParamsHooks, 'useUrlParams').mockReturnValue({
- urlParams: {
- traceIdLink: '123',
- rangeFrom: 'now-24h',
- rangeTo: 'now',
- },
- refreshTimeRange: jest.fn(),
- uiFilters: {},
- });
- jest.spyOn(hooks, 'useFetcher').mockReturnValue({
- data: { transaction: undefined },
- status: hooks.FETCH_STATUS.SUCCESS,
- refetch: jest.fn(),
- });
+ describe('when no transaction is found', () => {
+ it('renders a trace page', () => {
+ jest.spyOn(urlParamsHooks, 'useUrlParams').mockReturnValue({
+ urlParams: {
+ rangeFrom: 'now-24h',
+ rangeTo: 'now',
+ },
+ refreshTimeRange: jest.fn(),
+ uiFilters: {},
+ });
+ jest.spyOn(hooks, 'useFetcher').mockReturnValue({
+ data: { transaction: undefined },
+ status: hooks.FETCH_STATUS.SUCCESS,
+ refetch: jest.fn(),
+ });
+
+ const props = ({
+ match: { params: { traceId: '123' } },
+ } as unknown) as RouteComponentProps<{ traceId: string }>;
+ const component = shallow();
- const component = shallow();
- expect(component.prop('to')).toEqual(
- '/traces?kuery=trace.id%2520%253A%2520%2522123%2522&rangeFrom=now-24h&rangeTo=now'
- );
+ expect(component.prop('to')).toEqual(
+ '/traces?kuery=trace.id%2520%253A%2520%2522123%2522&rangeFrom=now-24h&rangeTo=now'
+ );
+ });
});
describe('transaction page', () => {
beforeAll(() => {
jest.spyOn(urlParamsHooks, 'useUrlParams').mockReturnValue({
urlParams: {
- traceIdLink: '123',
rangeFrom: 'now-24h',
rangeTo: 'now',
},
@@ -69,6 +90,7 @@ describe('TraceLink', () => {
uiFilters: {},
});
});
+
it('renders with date range params', () => {
const transaction = {
service: { name: 'foo' },
@@ -84,7 +106,12 @@ describe('TraceLink', () => {
status: hooks.FETCH_STATUS.SUCCESS,
refetch: jest.fn(),
});
- const component = shallow();
+
+ const props = ({
+ match: { params: { traceId: '123' } },
+ } as unknown) as RouteComponentProps<{ traceId: string }>;
+ const component = shallow();
+
expect(component.prop('to')).toEqual(
'/services/foo/transactions/view?traceId=123&transactionId=456&transactionName=bar&transactionType=GET&rangeFrom=now-24h&rangeTo=now'
);
diff --git a/x-pack/plugins/apm/public/components/app/TraceLink/index.tsx b/x-pack/plugins/apm/public/components/app/TraceLink/index.tsx
index 55ab275002b4e..584af956c2022 100644
--- a/x-pack/plugins/apm/public/components/app/TraceLink/index.tsx
+++ b/x-pack/plugins/apm/public/components/app/TraceLink/index.tsx
@@ -6,7 +6,7 @@
import { EuiEmptyPrompt } from '@elastic/eui';
import React from 'react';
-import { Redirect } from 'react-router-dom';
+import { Redirect, RouteComponentProps } from 'react-router-dom';
import styled from 'styled-components';
import url from 'url';
import { TRACE_ID } from '../../../../common/elasticsearch_fieldnames';
@@ -58,9 +58,10 @@ const redirectToTracePage = ({
},
});
-export function TraceLink() {
+export function TraceLink({ match }: RouteComponentProps<{ traceId: string }>) {
+ const { traceId } = match.params;
const { urlParams } = useUrlParams();
- const { traceIdLink: traceId, rangeFrom, rangeTo } = urlParams;
+ const { rangeFrom, rangeTo } = urlParams;
const { data = { transaction: null }, status } = useFetcher(
(callApmApi) => {
diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/accordion_waterfall.tsx b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/accordion_waterfall.tsx
index 833937835f870..c447d7fba86b8 100644
--- a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/accordion_waterfall.tsx
+++ b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/accordion_waterfall.tsx
@@ -25,9 +25,7 @@ interface AccordionWaterfallProps {
location: Location;
errorsPerTransaction: IWaterfall['errorsPerTransaction'];
childrenByParentId: Record;
- onToggleEntryTransaction?: (
- nextState: EuiAccordionProps['forceState']
- ) => void;
+ onToggleEntryTransaction?: () => void;
timelineMargins: Margins;
onClickWaterfallItem: (item: IWaterfallItem) => void;
}
@@ -106,6 +104,7 @@ export function AccordionWaterfall(props: AccordionWaterfallProps) {
errorsPerTransaction,
timelineMargins,
onClickWaterfallItem,
+ onToggleEntryTransaction,
} = props;
const nextLevel = level + 1;
@@ -147,7 +146,12 @@ export function AccordionWaterfall(props: AccordionWaterfallProps) {
arrowDisplay={isEmpty(children) ? 'none' : 'left'}
initialIsOpen={true}
forceState={isOpen ? 'open' : 'closed'}
- onToggle={() => setIsOpen((isCurrentOpen) => !isCurrentOpen)}
+ onToggle={() => {
+ setIsOpen((isCurrentOpen) => !isCurrentOpen);
+ if (onToggleEntryTransaction) {
+ onToggleEntryTransaction();
+ }
+ }}
>
{children.map((child) => (
toggleFlyout({ history, item, location })
}
+ onToggleEntryTransaction={() => setIsAccordionOpen((isOpen) => !isOpen)}
/>
);
}
diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/index.tsx b/x-pack/plugins/apm/public/components/app/TransactionDetails/index.tsx
index 515fcbc88c901..bab31c9a460d0 100644
--- a/x-pack/plugins/apm/public/components/app/TransactionDetails/index.tsx
+++ b/x-pack/plugins/apm/public/components/app/TransactionDetails/index.tsx
@@ -13,6 +13,7 @@ import {
EuiTitle,
} from '@elastic/eui';
import React, { useMemo } from 'react';
+import { RouteComponentProps } from 'react-router-dom';
import { useTrackPageview } from '../../../../../observability/public';
import { Projection } from '../../../../common/projections';
import { ChartsSyncContextProvider } from '../../../context/ChartsSyncContext';
@@ -29,7 +30,10 @@ import { LocalUIFilters } from '../../shared/LocalUIFilters';
import { TransactionDistribution } from './Distribution';
import { WaterfallWithSummmary } from './WaterfallWithSummmary';
-export function TransactionDetails() {
+type TransactionDetailsProps = RouteComponentProps<{ serviceName: string }>;
+
+export function TransactionDetails({ match }: TransactionDetailsProps) {
+ const { serviceName } = match.params;
const location = useLocation();
const { urlParams } = useUrlParams();
const {
@@ -41,7 +45,7 @@ export function TransactionDetails() {
const { waterfall, exceedsMax, status: waterfallStatus } = useWaterfall(
urlParams
);
- const { transactionName, transactionType, serviceName } = urlParams;
+ const { transactionName, transactionType } = urlParams;
useTrackPageview({ app: 'apm', path: 'transaction_details' });
useTrackPageview({ app: 'apm', path: 'transaction_details', delay: 15000 });
diff --git a/x-pack/plugins/apm/public/components/app/TransactionOverview/TransactionOverview.test.tsx b/x-pack/plugins/apm/public/components/app/TransactionOverview/TransactionOverview.test.tsx
index 81fe9e2282667..b7d1b93600a73 100644
--- a/x-pack/plugins/apm/public/components/app/TransactionOverview/TransactionOverview.test.tsx
+++ b/x-pack/plugins/apm/public/components/app/TransactionOverview/TransactionOverview.test.tsx
@@ -12,7 +12,6 @@ import {
} from '@testing-library/react';
import { createMemoryHistory } from 'history';
import { CoreStart } from 'kibana/public';
-import { omit } from 'lodash';
import React from 'react';
import { Router } from 'react-router-dom';
import { createKibanaReactContext } from 'src/plugins/kibana_react/public';
@@ -42,7 +41,7 @@ function setup({
}) {
const defaultLocation = {
pathname: '/services/foo/transactions',
- search: fromQuery(omit(urlParams, 'serviceName')),
+ search: fromQuery(urlParams),
} as any;
history.replace({
@@ -60,7 +59,7 @@ function setup({
-
+
@@ -87,9 +86,7 @@ describe('TransactionOverview', () => {
it('should redirect to first type', () => {
setup({
serviceTransactionTypes: ['firstType', 'secondType'],
- urlParams: {
- serviceName: 'MyServiceName',
- },
+ urlParams: {},
});
expect(history.replace).toHaveBeenCalledWith(
expect.objectContaining({
@@ -107,7 +104,6 @@ describe('TransactionOverview', () => {
serviceTransactionTypes: ['firstType', 'secondType'],
urlParams: {
transactionType: 'secondType',
- serviceName: 'MyServiceName',
},
});
@@ -122,7 +118,6 @@ describe('TransactionOverview', () => {
serviceTransactionTypes: ['firstType', 'secondType'],
urlParams: {
transactionType: 'secondType',
- serviceName: 'MyServiceName',
},
});
@@ -143,7 +138,6 @@ describe('TransactionOverview', () => {
serviceTransactionTypes: ['firstType'],
urlParams: {
transactionType: 'firstType',
- serviceName: 'MyServiceName',
},
});
diff --git a/x-pack/plugins/apm/public/components/app/TransactionOverview/index.tsx b/x-pack/plugins/apm/public/components/app/TransactionOverview/index.tsx
index 5999988abe848..544e2450fe5d9 100644
--- a/x-pack/plugins/apm/public/components/app/TransactionOverview/index.tsx
+++ b/x-pack/plugins/apm/public/components/app/TransactionOverview/index.tsx
@@ -59,11 +59,14 @@ function getRedirectLocation({
}
}
-export function TransactionOverview() {
+interface TransactionOverviewProps {
+ serviceName: string;
+}
+
+export function TransactionOverview({ serviceName }: TransactionOverviewProps) {
const location = useLocation();
const { urlParams } = useUrlParams();
-
- const { serviceName, transactionType } = urlParams;
+ const { transactionType } = urlParams;
// TODO: fetching of transaction types should perhaps be lifted since it is needed in several places. Context?
const serviceTransactionTypes = useServiceTransactionTypes(urlParams);
diff --git a/x-pack/plugins/apm/public/components/shared/EnvironmentFilter/index.tsx b/x-pack/plugins/apm/public/components/shared/EnvironmentFilter/index.tsx
index 9a61e773d73bf..7e5c789507e07 100644
--- a/x-pack/plugins/apm/public/components/shared/EnvironmentFilter/index.tsx
+++ b/x-pack/plugins/apm/public/components/shared/EnvironmentFilter/index.tsx
@@ -8,7 +8,7 @@ import { EuiSelect } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { History } from 'history';
import React from 'react';
-import { useHistory } from 'react-router-dom';
+import { useHistory, useParams } from 'react-router-dom';
import {
ENVIRONMENT_ALL,
ENVIRONMENT_NOT_DEFINED,
@@ -63,10 +63,11 @@ function getOptions(environments: string[]) {
export function EnvironmentFilter() {
const history = useHistory();
const location = useLocation();
+ const { serviceName } = useParams<{ serviceName?: string }>();
const { uiFilters, urlParams } = useUrlParams();
const { environment } = uiFilters;
- const { serviceName, start, end } = urlParams;
+ const { start, end } = urlParams;
const { environments, status = 'loading' } = useEnvironments({
serviceName,
start,
diff --git a/x-pack/plugins/apm/public/components/shared/ErrorRateAlertTrigger/index.tsx b/x-pack/plugins/apm/public/components/shared/ErrorRateAlertTrigger/index.tsx
index 7344839795955..7b284696477f3 100644
--- a/x-pack/plugins/apm/public/components/shared/ErrorRateAlertTrigger/index.tsx
+++ b/x-pack/plugins/apm/public/components/shared/ErrorRateAlertTrigger/index.tsx
@@ -3,21 +3,21 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
-import React from 'react';
-import { EuiFieldNumber } from '@elastic/eui';
+import { EuiFieldNumber, EuiSelect } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { isFinite } from 'lodash';
-import { EuiSelect } from '@elastic/eui';
+import React from 'react';
+import { useParams } from 'react-router-dom';
import { ForLastExpression } from '../../../../../triggers_actions_ui/public';
import { ALERT_TYPES_CONFIG } from '../../../../common/alert_types';
-import { ServiceAlertTrigger } from '../ServiceAlertTrigger';
-import { PopoverExpression } from '../ServiceAlertTrigger/PopoverExpression';
-import { useEnvironments } from '../../../hooks/useEnvironments';
-import { useUrlParams } from '../../../hooks/useUrlParams';
import {
ENVIRONMENT_ALL,
getEnvironmentLabel,
} from '../../../../common/environment_filter_values';
+import { useEnvironments } from '../../../hooks/useEnvironments';
+import { useUrlParams } from '../../../hooks/useUrlParams';
+import { ServiceAlertTrigger } from '../ServiceAlertTrigger';
+import { PopoverExpression } from '../ServiceAlertTrigger/PopoverExpression';
export interface ErrorRateAlertTriggerParams {
windowSize: number;
@@ -34,9 +34,9 @@ interface Props {
export function ErrorRateAlertTrigger(props: Props) {
const { setAlertParams, setAlertProperty, alertParams } = props;
-
+ const { serviceName } = useParams<{ serviceName?: string }>();
const { urlParams } = useUrlParams();
- const { serviceName, start, end } = urlParams;
+ const { start, end } = urlParams;
const { environmentOptions } = useEnvironments({ serviceName, start, end });
const defaults = {
diff --git a/x-pack/plugins/apm/public/components/shared/KueryBar/get_bool_filter.ts b/x-pack/plugins/apm/public/components/shared/KueryBar/get_bool_filter.ts
index 5bac01cfaf55d..74d7ace20dae0 100644
--- a/x-pack/plugins/apm/public/components/shared/KueryBar/get_bool_filter.ts
+++ b/x-pack/plugins/apm/public/components/shared/KueryBar/get_bool_filter.ts
@@ -4,18 +4,29 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { ESFilter } from '../../../../typings/elasticsearch';
import {
- TRANSACTION_TYPE,
ERROR_GROUP_ID,
PROCESSOR_EVENT,
- TRANSACTION_NAME,
SERVICE_NAME,
+ TRANSACTION_NAME,
+ TRANSACTION_TYPE,
} from '../../../../common/elasticsearch_fieldnames';
+import { UIProcessorEvent } from '../../../../common/processor_event';
+import { ESFilter } from '../../../../typings/elasticsearch';
import { IUrlParams } from '../../../context/UrlParamsContext/types';
-export function getBoolFilter(urlParams: IUrlParams) {
- const { start, end, serviceName, processorEvent } = urlParams;
+export function getBoolFilter({
+ groupId,
+ processorEvent,
+ serviceName,
+ urlParams,
+}: {
+ groupId?: string;
+ processorEvent?: UIProcessorEvent;
+ serviceName?: string;
+ urlParams: IUrlParams;
+}) {
+ const { start, end } = urlParams;
if (!start || !end) {
throw new Error('Date range was not defined');
@@ -63,9 +74,9 @@ export function getBoolFilter(urlParams: IUrlParams) {
term: { [PROCESSOR_EVENT]: 'error' },
});
- if (urlParams.errorGroupId) {
+ if (groupId) {
boolFilter.push({
- term: { [ERROR_GROUP_ID]: urlParams.errorGroupId },
+ term: { [ERROR_GROUP_ID]: groupId },
});
}
break;
diff --git a/x-pack/plugins/apm/public/components/shared/KueryBar/index.tsx b/x-pack/plugins/apm/public/components/shared/KueryBar/index.tsx
index a52676ee89590..efd1446f21b21 100644
--- a/x-pack/plugins/apm/public/components/shared/KueryBar/index.tsx
+++ b/x-pack/plugins/apm/public/components/shared/KueryBar/index.tsx
@@ -7,7 +7,7 @@
import { i18n } from '@kbn/i18n';
import { startsWith, uniqueId } from 'lodash';
import React, { useState } from 'react';
-import { useHistory } from 'react-router-dom';
+import { useHistory, useParams } from 'react-router-dom';
import styled from 'styled-components';
import {
esKuery,
@@ -22,6 +22,7 @@ import { fromQuery, toQuery } from '../Links/url_helpers';
import { getBoolFilter } from './get_bool_filter';
// @ts-expect-error
import { Typeahead } from './Typeahead';
+import { useProcessorEvent } from './use_processor_event';
const Container = styled.div`
margin-bottom: 10px;
@@ -38,6 +39,10 @@ function convertKueryToEsQuery(kuery: string, indexPattern: IIndexPattern) {
}
export function KueryBar() {
+ const { groupId, serviceName } = useParams<{
+ groupId?: string;
+ serviceName?: string;
+ }>();
const history = useHistory();
const [state, setState] = useState({
suggestions: [],
@@ -49,7 +54,7 @@ export function KueryBar() {
let currentRequestCheck;
- const { processorEvent } = urlParams;
+ const processorEvent = useProcessorEvent();
const examples = {
transaction: 'transaction.duration.us > 300000',
@@ -98,7 +103,12 @@ export function KueryBar() {
(await data.autocomplete.getQuerySuggestions({
language: 'kuery',
indexPatterns: [indexPattern],
- boolFilter: getBoolFilter(urlParams),
+ boolFilter: getBoolFilter({
+ groupId,
+ processorEvent,
+ serviceName,
+ urlParams,
+ }),
query: inputValue,
selectionStart,
selectionEnd: selectionStart,
diff --git a/x-pack/plugins/apm/public/components/shared/KueryBar/use_processor_event.ts b/x-pack/plugins/apm/public/components/shared/KueryBar/use_processor_event.ts
new file mode 100644
index 0000000000000..1e8686f0fe5ee
--- /dev/null
+++ b/x-pack/plugins/apm/public/components/shared/KueryBar/use_processor_event.ts
@@ -0,0 +1,47 @@
+/*
+ * 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 { useLocation } from 'react-router-dom';
+import {
+ ProcessorEvent,
+ UIProcessorEvent,
+} from '../../../../common/processor_event';
+
+/**
+ * Infer the processor.event to used based on the route path
+ */
+export function useProcessorEvent(): UIProcessorEvent | undefined {
+ const { pathname } = useLocation();
+ const paths = pathname.split('/').slice(1);
+ const pageName = paths[0];
+
+ switch (pageName) {
+ case 'services':
+ let servicePageName = paths[2];
+
+ if (servicePageName === 'nodes' && paths.length > 3) {
+ servicePageName = 'metrics';
+ }
+
+ switch (servicePageName) {
+ case 'transactions':
+ return ProcessorEvent.transaction;
+ case 'errors':
+ return ProcessorEvent.error;
+ case 'metrics':
+ return ProcessorEvent.metric;
+ case 'nodes':
+ return ProcessorEvent.metric;
+
+ default:
+ return undefined;
+ }
+ case 'traces':
+ return ProcessorEvent.transaction;
+ default:
+ return undefined;
+ }
+}
diff --git a/x-pack/plugins/apm/public/components/shared/ServiceAlertTrigger/index.tsx b/x-pack/plugins/apm/public/components/shared/ServiceAlertTrigger/index.tsx
index 6d90a10891c21..86dc7f5a90475 100644
--- a/x-pack/plugins/apm/public/components/shared/ServiceAlertTrigger/index.tsx
+++ b/x-pack/plugins/apm/public/components/shared/ServiceAlertTrigger/index.tsx
@@ -6,7 +6,7 @@
import React, { useEffect } from 'react';
import { EuiSpacer, EuiFlexGrid, EuiFlexItem } from '@elastic/eui';
-import { useUrlParams } from '../../../hooks/useUrlParams';
+import { useParams } from 'react-router-dom';
interface Props {
alertTypeName: string;
@@ -17,7 +17,7 @@ interface Props {
}
export function ServiceAlertTrigger(props: Props) {
- const { urlParams } = useUrlParams();
+ const { serviceName } = useParams<{ serviceName?: string }>();
const {
fields,
@@ -29,7 +29,7 @@ export function ServiceAlertTrigger(props: Props) {
const params: Record = {
...defaults,
- serviceName: urlParams.serviceName!,
+ serviceName,
};
useEffect(() => {
diff --git a/x-pack/plugins/apm/public/components/shared/TransactionDurationAlertTrigger/index.tsx b/x-pack/plugins/apm/public/components/shared/TransactionDurationAlertTrigger/index.tsx
index ba12b11c9527d..3c1669c39ac4c 100644
--- a/x-pack/plugins/apm/public/components/shared/TransactionDurationAlertTrigger/index.tsx
+++ b/x-pack/plugins/apm/public/components/shared/TransactionDurationAlertTrigger/index.tsx
@@ -40,12 +40,12 @@ interface Props {
export function TransactionDurationAlertTrigger(props: Props) {
const { setAlertParams, alertParams, setAlertProperty } = props;
-
+ const { serviceName } = alertParams;
const { urlParams } = useUrlParams();
const transactionTypes = useServiceTransactionTypes(urlParams);
- const { serviceName, start, end } = urlParams;
+ const { start, end } = urlParams;
const { environmentOptions } = useEnvironments({ serviceName, start, end });
if (!transactionTypes.length) {
diff --git a/x-pack/plugins/apm/public/components/shared/TransactionDurationAnomalyAlertTrigger/index.tsx b/x-pack/plugins/apm/public/components/shared/TransactionDurationAnomalyAlertTrigger/index.tsx
index 911c51013a844..20e0a3f27c4a4 100644
--- a/x-pack/plugins/apm/public/components/shared/TransactionDurationAnomalyAlertTrigger/index.tsx
+++ b/x-pack/plugins/apm/public/components/shared/TransactionDurationAnomalyAlertTrigger/index.tsx
@@ -42,9 +42,10 @@ interface Props {
export function TransactionDurationAnomalyAlertTrigger(props: Props) {
const { setAlertParams, alertParams, setAlertProperty } = props;
+ const { serviceName } = alertParams;
const { urlParams } = useUrlParams();
const transactionTypes = useServiceTransactionTypes(urlParams);
- const { serviceName, start, end } = urlParams;
+ const { start, end } = urlParams;
const { environmentOptions } = useEnvironments({ serviceName, start, end });
const supportedTransactionTypes = transactionTypes.filter((transactionType) =>
[TRANSACTION_PAGE_LOAD, TRANSACTION_REQUEST].includes(transactionType)
diff --git a/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/ml_header.tsx b/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/ml_header.tsx
index f829b5841efa9..52b0470d31552 100644
--- a/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/ml_header.tsx
+++ b/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/ml_header.tsx
@@ -4,13 +4,12 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { EuiIconTip } from '@elastic/eui';
+import { EuiFlexItem, EuiIconTip, EuiText } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
import { isEmpty } from 'lodash';
import React from 'react';
-import { EuiFlexItem } from '@elastic/eui';
+import { useParams } from 'react-router-dom';
import styled from 'styled-components';
-import { i18n } from '@kbn/i18n';
-import { EuiText } from '@elastic/eui';
import { useUrlParams } from '../../../../hooks/useUrlParams';
import { MLJobLink } from '../../Links/MachineLearningLinks/MLJobLink';
@@ -32,16 +31,14 @@ const ShiftedEuiText = styled(EuiText)`
`;
export function MLHeader({ hasValidMlLicense, mlJobId }: Props) {
+ const { serviceName } = useParams<{ serviceName?: string }>();
const { urlParams } = useUrlParams();
if (!hasValidMlLicense || !mlJobId) {
return null;
}
- const { serviceName, kuery, transactionType } = urlParams;
- if (!serviceName) {
- return null;
- }
+ const { kuery, transactionType } = urlParams;
const hasKuery = !isEmpty(kuery);
const icon = hasKuery ? (
diff --git a/x-pack/plugins/apm/public/context/ApmPluginContext/MockApmPluginContext.tsx b/x-pack/plugins/apm/public/context/ApmPluginContext/MockApmPluginContext.tsx
index 8334efffbd511..48206572932b1 100644
--- a/x-pack/plugins/apm/public/context/ApmPluginContext/MockApmPluginContext.tsx
+++ b/x-pack/plugins/apm/public/context/ApmPluginContext/MockApmPluginContext.tsx
@@ -39,6 +39,7 @@ const mockCore = {
apm: {},
},
currentAppId$: new Observable(),
+ navigateToUrl: (url: string) => {},
},
chrome: {
docTitle: { change: () => {} },
diff --git a/x-pack/plugins/apm/public/context/ChartsSyncContext.tsx b/x-pack/plugins/apm/public/context/ChartsSyncContext.tsx
index 801c1d7e53f2e..7df35bc443226 100644
--- a/x-pack/plugins/apm/public/context/ChartsSyncContext.tsx
+++ b/x-pack/plugins/apm/public/context/ChartsSyncContext.tsx
@@ -5,7 +5,7 @@
*/
import React, { ReactNode, useMemo, useState } from 'react';
-import { useHistory } from 'react-router-dom';
+import { useHistory, useParams } from 'react-router-dom';
import { fromQuery, toQuery } from '../components/shared/Links/url_helpers';
import { useFetcher } from '../hooks/useFetcher';
import { useUrlParams } from '../hooks/useUrlParams';
@@ -20,9 +20,10 @@ const ChartsSyncContext = React.createContext<{
function ChartsSyncContextProvider({ children }: { children: ReactNode }) {
const history = useHistory();
const [time, setTime] = useState(null);
+ const { serviceName } = useParams<{ serviceName?: string }>();
const { urlParams, uiFilters } = useUrlParams();
- const { start, end, serviceName } = urlParams;
+ const { start, end } = urlParams;
const { environment } = uiFilters;
const { data = { annotations: [] } } = useFetcher(
diff --git a/x-pack/plugins/apm/public/context/MatchedRouteContext.tsx b/x-pack/plugins/apm/public/context/MatchedRouteContext.tsx
deleted file mode 100644
index 64a26a183d8cb..0000000000000
--- a/x-pack/plugins/apm/public/context/MatchedRouteContext.tsx
+++ /dev/null
@@ -1,34 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License;
- * you may not use this file except in compliance with the Elastic License.
- */
-import React, { useMemo, ReactChild } from 'react';
-import { matchPath } from 'react-router-dom';
-import { useLocation } from '../hooks/useLocation';
-import { BreadcrumbRoute } from '../components/app/Main/ProvideBreadcrumbs';
-
-export const MatchedRouteContext = React.createContext([]);
-
-interface MatchedRouteProviderProps {
- children: ReactChild;
- routes: BreadcrumbRoute[];
-}
-export function MatchedRouteProvider({
- children,
- routes,
-}: MatchedRouteProviderProps) {
- const { pathname } = useLocation();
-
- const contextValue = useMemo(() => {
- return routes.filter((route) => {
- return matchPath(pathname, {
- path: route.path,
- });
- });
- }, [pathname, routes]);
-
- return (
-
- );
-}
diff --git a/x-pack/plugins/apm/public/context/UrlParamsContext/__tests__/UrlParamsContext.test.tsx b/x-pack/plugins/apm/public/context/UrlParamsContext/__tests__/UrlParamsContext.test.tsx
index fbb79eae6a136..9989e568953f5 100644
--- a/x-pack/plugins/apm/public/context/UrlParamsContext/__tests__/UrlParamsContext.test.tsx
+++ b/x-pack/plugins/apm/public/context/UrlParamsContext/__tests__/UrlParamsContext.test.tsx
@@ -41,24 +41,6 @@ describe('UrlParamsContext', () => {
moment.tz.setDefault('');
});
- it('should have default params', () => {
- const location = {
- pathname: '/services/opbeans-node/transactions',
- } as Location;
-
- jest
- .spyOn(Date, 'now')
- .mockImplementation(() => new Date('2000-06-15T12:00:00Z').getTime());
- const wrapper = mountParams(location);
- const params = getDataFromOutput(wrapper);
-
- expect(params).toEqual({
- serviceName: 'opbeans-node',
- page: 0,
- processorEvent: 'transaction',
- });
- });
-
it('should read values in from location', () => {
const location = {
pathname: '/test/pathname',
diff --git a/x-pack/plugins/apm/public/context/UrlParamsContext/helpers.ts b/x-pack/plugins/apm/public/context/UrlParamsContext/helpers.ts
index 65514ff71d02b..45db4dcc94cce 100644
--- a/x-pack/plugins/apm/public/context/UrlParamsContext/helpers.ts
+++ b/x-pack/plugins/apm/public/context/UrlParamsContext/helpers.ts
@@ -7,18 +7,6 @@
import { compact, pickBy } from 'lodash';
import datemath from '@elastic/datemath';
import { IUrlParams } from './types';
-import {
- ProcessorEvent,
- UIProcessorEvent,
-} from '../../../common/processor_event';
-
-interface PathParams {
- processorEvent?: UIProcessorEvent;
- serviceName?: string;
- errorGroupId?: string;
- serviceNodeName?: string;
- traceId?: string;
-}
export function getParsedDate(rawDate?: string, opts = {}) {
if (rawDate) {
@@ -67,68 +55,3 @@ export function getPathAsArray(pathname: string = '') {
export function removeUndefinedProps(obj: T): Partial {
return pickBy(obj, (value) => value !== undefined);
}
-
-export function getPathParams(pathname: string = ''): PathParams {
- const paths = getPathAsArray(pathname);
- const pageName = paths[0];
- // TODO: use react router's real match params instead of guessing the path order
-
- switch (pageName) {
- case 'services':
- let servicePageName = paths[2];
- const serviceName = paths[1];
- const serviceNodeName = paths[3];
-
- if (servicePageName === 'nodes' && paths.length > 3) {
- servicePageName = 'metrics';
- }
-
- switch (servicePageName) {
- case 'transactions':
- return {
- processorEvent: ProcessorEvent.transaction,
- serviceName,
- };
- case 'errors':
- return {
- processorEvent: ProcessorEvent.error,
- serviceName,
- errorGroupId: paths[3],
- };
- case 'metrics':
- return {
- processorEvent: ProcessorEvent.metric,
- serviceName,
- serviceNodeName,
- };
- case 'nodes':
- return {
- processorEvent: ProcessorEvent.metric,
- serviceName,
- };
- case 'service-map':
- return {
- serviceName,
- };
- default:
- return {};
- }
-
- case 'traces':
- return {
- processorEvent: ProcessorEvent.transaction,
- };
- case 'link-to':
- const link = paths[1];
- switch (link) {
- case 'trace':
- return {
- traceId: paths[2],
- };
- default:
- return {};
- }
- default:
- return {};
- }
-}
diff --git a/x-pack/plugins/apm/public/context/UrlParamsContext/resolveUrlParams.ts b/x-pack/plugins/apm/public/context/UrlParamsContext/resolveUrlParams.ts
index 2201e162904a2..8feb4ac1858d1 100644
--- a/x-pack/plugins/apm/public/context/UrlParamsContext/resolveUrlParams.ts
+++ b/x-pack/plugins/apm/public/context/UrlParamsContext/resolveUrlParams.ts
@@ -7,7 +7,6 @@
import { Location } from 'history';
import { IUrlParams } from './types';
import {
- getPathParams,
removeUndefinedProps,
getStart,
getEnd,
@@ -26,14 +25,6 @@ type TimeUrlParams = Pick<
>;
export function resolveUrlParams(location: Location, state: TimeUrlParams) {
- const {
- processorEvent,
- serviceName,
- serviceNodeName,
- errorGroupId,
- traceId: traceIdLink,
- } = getPathParams(location.pathname);
-
const query = toQuery(location.search);
const {
@@ -85,15 +76,6 @@ export function resolveUrlParams(location: Location, state: TimeUrlParams) {
transactionType,
searchTerm: toString(searchTerm),
- // path params
- processorEvent,
- serviceName,
- traceIdLink,
- errorGroupId,
- serviceNodeName: serviceNodeName
- ? decodeURIComponent(serviceNodeName)
- : serviceNodeName,
-
// ui filters
environment,
...localUIFilters,
diff --git a/x-pack/plugins/apm/public/context/UrlParamsContext/types.ts b/x-pack/plugins/apm/public/context/UrlParamsContext/types.ts
index 7b50a705afa33..574eca3b74f70 100644
--- a/x-pack/plugins/apm/public/context/UrlParamsContext/types.ts
+++ b/x-pack/plugins/apm/public/context/UrlParamsContext/types.ts
@@ -6,12 +6,10 @@
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { LocalUIFilterName } from '../../../server/lib/ui_filters/local_ui_filters/config';
-import { UIProcessorEvent } from '../../../common/processor_event';
export type IUrlParams = {
detailTab?: string;
end?: string;
- errorGroupId?: string;
flyoutDetailTab?: string;
kuery?: string;
environment?: string;
@@ -19,7 +17,6 @@ export type IUrlParams = {
rangeTo?: string;
refreshInterval?: number;
refreshPaused?: boolean;
- serviceName?: string;
sortDirection?: string;
sortField?: string;
start?: string;
@@ -30,8 +27,5 @@ export type IUrlParams = {
waterfallItemId?: string;
page?: number;
pageSize?: number;
- serviceNodeName?: string;
searchTerm?: string;
- processorEvent?: UIProcessorEvent;
- traceIdLink?: string;
} & Partial>;
diff --git a/x-pack/plugins/apm/public/hooks/useAgentName.ts b/x-pack/plugins/apm/public/hooks/useAgentName.ts
index 7a11b662f06f0..1f8a3b916ecd0 100644
--- a/x-pack/plugins/apm/public/hooks/useAgentName.ts
+++ b/x-pack/plugins/apm/public/hooks/useAgentName.ts
@@ -3,13 +3,14 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
-
+import { useParams } from 'react-router-dom';
import { useFetcher } from './useFetcher';
import { useUrlParams } from './useUrlParams';
export function useAgentName() {
+ const { serviceName } = useParams<{ serviceName?: string }>();
const { urlParams } = useUrlParams();
- const { start, end, serviceName } = urlParams;
+ const { start, end } = urlParams;
const { data: agentName, error, status } = useFetcher(
(callApmApi) => {
diff --git a/x-pack/plugins/apm/public/hooks/useServiceMetricCharts.ts b/x-pack/plugins/apm/public/hooks/useServiceMetricCharts.ts
index 78f022ec6b8b5..f4a981ff0975b 100644
--- a/x-pack/plugins/apm/public/hooks/useServiceMetricCharts.ts
+++ b/x-pack/plugins/apm/public/hooks/useServiceMetricCharts.ts
@@ -4,10 +4,11 @@
* you may not use this file except in compliance with the Elastic License.
*/
+import { useParams } from 'react-router-dom';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { MetricsChartsByAgentAPIResponse } from '../../server/lib/metrics/get_metrics_chart_data_by_agent';
-import { IUrlParams } from '../context/UrlParamsContext/types';
import { useUiFilters } from '../context/UrlParamsContext';
+import { IUrlParams } from '../context/UrlParamsContext/types';
import { useFetcher } from './useFetcher';
const INITIAL_DATA: MetricsChartsByAgentAPIResponse = {
@@ -18,7 +19,8 @@ export function useServiceMetricCharts(
urlParams: IUrlParams,
agentName?: string
) {
- const { serviceName, start, end, serviceNodeName } = urlParams;
+ const { serviceName } = useParams<{ serviceName?: string }>();
+ const { start, end } = urlParams;
const uiFilters = useUiFilters(urlParams);
const { data = INITIAL_DATA, error, status } = useFetcher(
(callApmApi) => {
@@ -31,14 +33,13 @@ export function useServiceMetricCharts(
start,
end,
agentName,
- serviceNodeName,
uiFilters: JSON.stringify(uiFilters),
},
},
});
}
},
- [serviceName, start, end, agentName, serviceNodeName, uiFilters]
+ [serviceName, start, end, agentName, uiFilters]
);
return {
diff --git a/x-pack/plugins/apm/public/hooks/useServiceTransactionTypes.tsx b/x-pack/plugins/apm/public/hooks/useServiceTransactionTypes.tsx
index 227cd849d6c7c..4e110ac2d4380 100644
--- a/x-pack/plugins/apm/public/hooks/useServiceTransactionTypes.tsx
+++ b/x-pack/plugins/apm/public/hooks/useServiceTransactionTypes.tsx
@@ -4,13 +4,15 @@
* you may not use this file except in compliance with the Elastic License.
*/
+import { useParams } from 'react-router-dom';
import { IUrlParams } from '../context/UrlParamsContext/types';
import { useFetcher } from './useFetcher';
const INITIAL_DATA = { transactionTypes: [] };
export function useServiceTransactionTypes(urlParams: IUrlParams) {
- const { serviceName, start, end } = urlParams;
+ const { serviceName } = useParams<{ serviceName?: string }>();
+ const { start, end } = urlParams;
const { data = INITIAL_DATA } = useFetcher(
(callApmApi) => {
if (serviceName && start && end) {
diff --git a/x-pack/plugins/apm/public/hooks/useTransactionList.ts b/x-pack/plugins/apm/public/hooks/useTransactionList.ts
index 0ad221b95b4ff..9c3a18b9c0d0d 100644
--- a/x-pack/plugins/apm/public/hooks/useTransactionList.ts
+++ b/x-pack/plugins/apm/public/hooks/useTransactionList.ts
@@ -4,10 +4,11 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { IUrlParams } from '../context/UrlParamsContext/types';
+import { useParams } from 'react-router-dom';
import { useUiFilters } from '../context/UrlParamsContext';
-import { useFetcher } from './useFetcher';
+import { IUrlParams } from '../context/UrlParamsContext/types';
import { APIReturnType } from '../services/rest/createCallApmApi';
+import { useFetcher } from './useFetcher';
type TransactionsAPIResponse = APIReturnType<
'/api/apm/services/{serviceName}/transaction_groups'
@@ -20,7 +21,8 @@ const DEFAULT_RESPONSE: TransactionsAPIResponse = {
};
export function useTransactionList(urlParams: IUrlParams) {
- const { serviceName, transactionType, start, end } = urlParams;
+ const { serviceName } = useParams<{ serviceName?: string }>();
+ const { transactionType, start, end } = urlParams;
const uiFilters = useUiFilters(urlParams);
const { data = DEFAULT_RESPONSE, error, status } = useFetcher(
(callApmApi) => {
diff --git a/x-pack/plugins/apm/public/components/app/Main/UpdateBreadcrumbs.test.tsx b/x-pack/plugins/apm/public/hooks/use_breadcrumbs.test.tsx
similarity index 65%
rename from x-pack/plugins/apm/public/components/app/Main/UpdateBreadcrumbs.test.tsx
rename to x-pack/plugins/apm/public/hooks/use_breadcrumbs.test.tsx
index 102a3d91e4a91..dcd6ed0ba4934 100644
--- a/x-pack/plugins/apm/public/components/app/Main/UpdateBreadcrumbs.test.tsx
+++ b/x-pack/plugins/apm/public/hooks/use_breadcrumbs.test.tsx
@@ -4,63 +4,56 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { mount } from 'enzyme';
-import React from 'react';
+import { renderHook } from '@testing-library/react-hooks';
+import produce from 'immer';
+import React, { ReactNode } from 'react';
import { MemoryRouter } from 'react-router-dom';
-import { ApmPluginContextValue } from '../../../context/ApmPluginContext';
-import { routes } from './route_config';
-import { UpdateBreadcrumbs } from './UpdateBreadcrumbs';
+import { routes } from '../components/app/Main/route_config';
+import { ApmPluginContextValue } from '../context/ApmPluginContext';
import {
- MockApmPluginContextWrapper,
mockApmPluginContextValue,
-} from '../../../context/ApmPluginContext/MockApmPluginContext';
+ MockApmPluginContextWrapper,
+} from '../context/ApmPluginContext/MockApmPluginContext';
+import { useBreadcrumbs } from './use_breadcrumbs';
-const setBreadcrumbs = jest.fn();
-const changeTitle = jest.fn();
+function createWrapper(path: string) {
+ return ({ children }: { children?: ReactNode }) => {
+ const value = (produce(mockApmPluginContextValue, (draft) => {
+ draft.core.application.navigateToUrl = (url: string) => Promise.resolve();
+ draft.core.chrome.docTitle.change = changeTitle;
+ draft.core.chrome.setBreadcrumbs = setBreadcrumbs;
+ }) as unknown) as ApmPluginContextValue;
-function mountBreadcrumb(route: string, params = '') {
- mount(
-
-
-
+ return (
+
+
+ {children}
+
-
- );
- expect(setBreadcrumbs).toHaveBeenCalledTimes(1);
+ );
+ };
}
-describe('UpdateBreadcrumbs', () => {
- beforeEach(() => {
- setBreadcrumbs.mockReset();
- changeTitle.mockReset();
- });
+function mountBreadcrumb(path: string) {
+ renderHook(() => useBreadcrumbs(routes), { wrapper: createWrapper(path) });
+}
- it('Changes the homepage title', () => {
+const changeTitle = jest.fn();
+const setBreadcrumbs = jest.fn();
+
+describe('useBreadcrumbs', () => {
+ it('changes the page title', () => {
mountBreadcrumb('/');
+
expect(changeTitle).toHaveBeenCalledWith(['APM']);
});
- it('/services/:serviceName/errors/:groupId', () => {
+ test('/services/:serviceName/errors/:groupId', () => {
mountBreadcrumb(
- '/services/opbeans-node/errors/myGroupId',
- 'rangeFrom=now-24h&rangeTo=now&refreshPaused=true&refreshInterval=0'
+ '/services/opbeans-node/errors/myGroupId?kuery=myKuery&rangeFrom=now-24h&rangeTo=now&refreshPaused=true&refreshInterval=0'
);
- const breadcrumbs = setBreadcrumbs.mock.calls[0][0];
- expect(breadcrumbs).toEqual(
+
+ expect(setBreadcrumbs).toHaveBeenCalledWith(
expect.arrayContaining([
expect.objectContaining({
text: 'APM',
@@ -95,10 +88,10 @@ describe('UpdateBreadcrumbs', () => {
]);
});
- it('/services/:serviceName/errors', () => {
- mountBreadcrumb('/services/opbeans-node/errors');
- const breadcrumbs = setBreadcrumbs.mock.calls[0][0];
- expect(breadcrumbs).toEqual(
+ test('/services/:serviceName/errors', () => {
+ mountBreadcrumb('/services/opbeans-node/errors?kuery=myKuery');
+
+ expect(setBreadcrumbs).toHaveBeenCalledWith(
expect.arrayContaining([
expect.objectContaining({
text: 'APM',
@@ -115,6 +108,7 @@ describe('UpdateBreadcrumbs', () => {
expect.objectContaining({ text: 'Errors', href: undefined }),
])
);
+
expect(changeTitle).toHaveBeenCalledWith([
'Errors',
'opbeans-node',
@@ -123,10 +117,10 @@ describe('UpdateBreadcrumbs', () => {
]);
});
- it('/services/:serviceName/transactions', () => {
- mountBreadcrumb('/services/opbeans-node/transactions');
- const breadcrumbs = setBreadcrumbs.mock.calls[0][0];
- expect(breadcrumbs).toEqual(
+ test('/services/:serviceName/transactions', () => {
+ mountBreadcrumb('/services/opbeans-node/transactions?kuery=myKuery');
+
+ expect(setBreadcrumbs).toHaveBeenCalledWith(
expect.arrayContaining([
expect.objectContaining({
text: 'APM',
@@ -152,14 +146,12 @@ describe('UpdateBreadcrumbs', () => {
]);
});
- it('/services/:serviceName/transactions/view?transactionName=my-transaction-name', () => {
+ test('/services/:serviceName/transactions/view?transactionName=my-transaction-name', () => {
mountBreadcrumb(
- '/services/opbeans-node/transactions/view',
- 'transactionName=my-transaction-name'
+ '/services/opbeans-node/transactions/view?kuery=myKuery&transactionName=my-transaction-name'
);
- const breadcrumbs = setBreadcrumbs.mock.calls[0][0];
- expect(breadcrumbs).toEqual(
+ expect(setBreadcrumbs).toHaveBeenCalledWith(
expect.arrayContaining([
expect.objectContaining({
text: 'APM',
diff --git a/x-pack/plugins/apm/public/hooks/use_breadcrumbs.ts b/x-pack/plugins/apm/public/hooks/use_breadcrumbs.ts
new file mode 100644
index 0000000000000..640170bf3bff2
--- /dev/null
+++ b/x-pack/plugins/apm/public/hooks/use_breadcrumbs.ts
@@ -0,0 +1,214 @@
+/*
+ * 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 { History, Location } from 'history';
+import { ChromeBreadcrumb } from 'kibana/public';
+import { MouseEvent, ReactNode, useEffect } from 'react';
+import {
+ matchPath,
+ RouteComponentProps,
+ useHistory,
+ match as Match,
+ useLocation,
+} from 'react-router-dom';
+import { APMRouteDefinition, BreadcrumbTitle } from '../application/routes';
+import { getAPMHref } from '../components/shared/Links/apm/APMLink';
+import { useApmPluginContext } from './useApmPluginContext';
+
+interface BreadcrumbWithoutLink extends ChromeBreadcrumb {
+ match: Match>;
+}
+
+interface BreadcrumbFunctionArgs extends RouteComponentProps {
+ breadcrumbTitle: BreadcrumbTitle;
+}
+
+/**
+ * Call the breadcrumb function if there is one, otherwise return it as a string
+ */
+function getBreadcrumbText({
+ breadcrumbTitle,
+ history,
+ location,
+ match,
+}: BreadcrumbFunctionArgs) {
+ return typeof breadcrumbTitle === 'function'
+ ? breadcrumbTitle({ history, location, match })
+ : breadcrumbTitle;
+}
+
+/**
+ * Get a breadcrumb from the current path and route definitions.
+ */
+function getBreadcrumb({
+ currentPath,
+ history,
+ location,
+ routes,
+}: {
+ currentPath: string;
+ history: History;
+ location: Location;
+ routes: APMRouteDefinition[];
+}) {
+ return routes.reduce(
+ (found, { breadcrumb, ...routeDefinition }) => {
+ if (found) {
+ return found;
+ }
+
+ if (!breadcrumb) {
+ return null;
+ }
+
+ const match = matchPath>(
+ currentPath,
+ routeDefinition
+ );
+
+ if (match) {
+ return {
+ match,
+ text: getBreadcrumbText({
+ breadcrumbTitle: breadcrumb,
+ history,
+ location,
+ match,
+ }),
+ };
+ }
+
+ return null;
+ },
+ null
+ );
+}
+
+/**
+ * Once we have the breadcrumbs, we need to iterate through the list again to
+ * add the href and onClick, since we need to know which one is the final
+ * breadcrumb
+ */
+function addLinksToBreadcrumbs({
+ breadcrumbs,
+ navigateToUrl,
+ wrappedGetAPMHref,
+}: {
+ breadcrumbs: BreadcrumbWithoutLink[];
+ navigateToUrl: (url: string) => Promise;
+ wrappedGetAPMHref: (path: string) => string;
+}) {
+ return breadcrumbs.map((breadcrumb, index) => {
+ const isLastBreadcrumbItem = index === breadcrumbs.length - 1;
+
+ // Make the link not clickable if it's the last item
+ const href = isLastBreadcrumbItem
+ ? undefined
+ : wrappedGetAPMHref(breadcrumb.match.url);
+ const onClick = !href
+ ? undefined
+ : (event: MouseEvent) => {
+ event.preventDefault();
+ navigateToUrl(href);
+ };
+
+ return {
+ ...breadcrumb,
+ match: undefined,
+ href,
+ onClick,
+ };
+ });
+}
+
+/**
+ * Convert a list of route definitions to a list of breadcrumbs
+ */
+function routeDefinitionsToBreadcrumbs({
+ history,
+ location,
+ routes,
+}: {
+ history: History;
+ location: Location;
+ routes: APMRouteDefinition[];
+}) {
+ const breadcrumbs: BreadcrumbWithoutLink[] = [];
+ const { pathname } = location;
+
+ pathname
+ .split('?')[0]
+ .replace(/\/$/, '')
+ .split('/')
+ .reduce((acc, next) => {
+ // `/1/2/3` results in match checks for `/1`, `/1/2`, `/1/2/3`.
+ const currentPath = !next ? '/' : `${acc}/${next}`;
+ const breadcrumb = getBreadcrumb({
+ currentPath,
+ history,
+ location,
+ routes,
+ });
+
+ if (breadcrumb) {
+ breadcrumbs.push(breadcrumb);
+ }
+
+ return currentPath === '/' ? '' : currentPath;
+ }, '');
+
+ return breadcrumbs;
+}
+
+/**
+ * Get an array for a page title from a list of breadcrumbs
+ */
+function getTitleFromBreadcrumbs(breadcrumbs: ChromeBreadcrumb[]): string[] {
+ function removeNonStrings(item: ReactNode): item is string {
+ return typeof item === 'string';
+ }
+
+ return breadcrumbs
+ .map(({ text }) => text)
+ .reverse()
+ .filter(removeNonStrings);
+}
+
+/**
+ * Determine the breadcrumbs from the routes, set them, and update the page
+ * title when the route changes.
+ */
+export function useBreadcrumbs(routes: APMRouteDefinition[]) {
+ const history = useHistory();
+ const location = useLocation();
+ const { search } = location;
+ const { core } = useApmPluginContext();
+ const { basePath } = core.http;
+ const { navigateToUrl } = core.application;
+ const { docTitle, setBreadcrumbs } = core.chrome;
+ const changeTitle = docTitle.change;
+
+ function wrappedGetAPMHref(path: string) {
+ return getAPMHref({ basePath, path, search });
+ }
+
+ const breadcrumbsWithoutLinks = routeDefinitionsToBreadcrumbs({
+ history,
+ location,
+ routes,
+ });
+ const breadcrumbs = addLinksToBreadcrumbs({
+ breadcrumbs: breadcrumbsWithoutLinks,
+ wrappedGetAPMHref,
+ navigateToUrl,
+ });
+ const title = getTitleFromBreadcrumbs(breadcrumbs);
+
+ useEffect(() => {
+ changeTitle(title);
+ setBreadcrumbs(breadcrumbs);
+ }, [breadcrumbs, changeTitle, location, title, setBreadcrumbs]);
+}
diff --git a/x-pack/plugins/apm/readme.md b/x-pack/plugins/apm/readme.md
index 9b02972d35302..d6fdb5f52291c 100644
--- a/x-pack/plugins/apm/readme.md
+++ b/x-pack/plugins/apm/readme.md
@@ -162,4 +162,5 @@ You can access the development environment at http://localhost:9001.
- [Cypress integration tests](./e2e/README.md)
- [VSCode setup instructions](./dev_docs/vscode_setup.md)
- [Github PR commands](./dev_docs/github_commands.md)
+- [Routing and Linking](./dev_docs/routing_and_linking.md)
- [Telemetry](./dev_docs/telemetry.md)
diff --git a/x-pack/plugins/apm/scripts/aggregate-latency-metrics/index.ts b/x-pack/plugins/apm/scripts/aggregate-latency-metrics/index.ts
index c3cf363cbec05..ef85112918712 100644
--- a/x-pack/plugins/apm/scripts/aggregate-latency-metrics/index.ts
+++ b/x-pack/plugins/apm/scripts/aggregate-latency-metrics/index.ts
@@ -4,15 +4,12 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { Client } from '@elastic/elasticsearch';
import { argv } from 'yargs';
import pLimit from 'p-limit';
import pRetry from 'p-retry';
-import { parse, format } from 'url';
import { set } from '@elastic/safer-lodash-set';
import { uniq, without, merge, flatten } from 'lodash';
import * as histogram from 'hdr-histogram-js';
-import { ESSearchResponse } from '../../typings/elasticsearch';
import {
HOST_NAME,
SERVICE_NAME,
@@ -28,6 +25,8 @@ import {
} from '../../common/elasticsearch_fieldnames';
import { stampLogger } from '../shared/stamp-logger';
import { createOrUpdateIndex } from '../shared/create-or-update-index';
+import { parseIndexUrl } from '../shared/parse_index_url';
+import { ESClient, getEsClient } from '../shared/get_es_client';
// This script will try to estimate how many latency metric documents
// will be created based on the available transaction documents.
@@ -125,41 +124,18 @@ export async function aggregateLatencyMetrics() {
const source = String(argv.source ?? '');
const dest = String(argv.dest ?? '');
- function getClientOptionsFromIndexUrl(
- url: string
- ): { node: string; index: string } {
- const parsed = parse(url);
- const { pathname, ...rest } = parsed;
+ const sourceOptions = parseIndexUrl(source);
- return {
- node: format(rest),
- index: pathname!.replace('/', ''),
- };
- }
-
- const sourceOptions = getClientOptionsFromIndexUrl(source);
-
- const sourceClient = new Client({
- node: sourceOptions.node,
- ssl: {
- rejectUnauthorized: false,
- },
- requestTimeout: 120000,
- });
+ const sourceClient = getEsClient({ node: sourceOptions.node });
- let destClient: Client | undefined;
+ let destClient: ESClient | undefined;
let destOptions: { node: string; index: string } | undefined;
const uploadMetrics = !!dest;
if (uploadMetrics) {
- destOptions = getClientOptionsFromIndexUrl(dest);
- destClient = new Client({
- node: destOptions.node,
- ssl: {
- rejectUnauthorized: false,
- },
- });
+ destOptions = parseIndexUrl(dest);
+ destClient = getEsClient({ node: destOptions.node });
const mappings = (
await sourceClient.indices.getMapping({
@@ -298,10 +274,9 @@ export async function aggregateLatencyMetrics() {
},
};
- const response = (await sourceClient.search(params))
- .body as ESSearchResponse;
+ const response = await sourceClient.search(params);
- const { aggregations } = response;
+ const { aggregations } = response.body;
if (!aggregations) {
return buckets;
@@ -333,10 +308,9 @@ export async function aggregateLatencyMetrics() {
},
};
- const response = (await sourceClient.search(params))
- .body as ESSearchResponse;
+ const response = await sourceClient.search(params);
- return response.hits.total.value;
+ return response.body.hits.total.value;
}
const [buckets, numberOfTransactionDocuments] = await Promise.all([
diff --git a/x-pack/plugins/apm/scripts/create-functional-tests-archive.js b/x-pack/plugins/apm/scripts/create-functional-tests-archive.js
new file mode 100644
index 0000000000000..6b3473dc2ac0a
--- /dev/null
+++ b/x-pack/plugins/apm/scripts/create-functional-tests-archive.js
@@ -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.
+ */
+
+// compile typescript on the fly
+// eslint-disable-next-line import/no-extraneous-dependencies
+require('@babel/register')({
+ extensions: ['.js', '.ts'],
+ plugins: ['@babel/plugin-proposal-optional-chaining'],
+ presets: [
+ '@babel/typescript',
+ ['@babel/preset-env', { targets: { node: 'current' } }],
+ ],
+});
+
+require('./create-functional-tests-archive/index.ts');
diff --git a/x-pack/plugins/apm/scripts/create-functional-tests-archive/index.ts b/x-pack/plugins/apm/scripts/create-functional-tests-archive/index.ts
new file mode 100644
index 0000000000000..cbd63262bd08d
--- /dev/null
+++ b/x-pack/plugins/apm/scripts/create-functional-tests-archive/index.ts
@@ -0,0 +1,179 @@
+/*
+ * 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 { argv } from 'yargs';
+import { execSync } from 'child_process';
+import moment from 'moment';
+import path from 'path';
+import fs from 'fs';
+import { stampLogger } from '../shared/stamp-logger';
+
+async function run() {
+ stampLogger();
+
+ const archiveName = 'apm_8.0.0';
+
+ // include important APM data and ML data
+ const indices =
+ 'apm-*-transaction,apm-*-span,apm-*-error,apm-*-metric,.ml-anomalies*,.ml-config';
+
+ const esUrl = argv['es-url'] as string | undefined;
+
+ if (!esUrl) {
+ throw new Error('--es-url is not set');
+ }
+ const kibanaUrl = argv['kibana-url'] as string | undefined;
+
+ if (!kibanaUrl) {
+ throw new Error('--kibana-url is not set');
+ }
+ const gte = moment().subtract(1, 'hour').toISOString();
+ const lt = moment(gte).add(30, 'minutes').toISOString();
+
+ // eslint-disable-next-line no-console
+ console.log(`Archiving from ${gte} to ${lt}...`);
+
+ // APM data uses '@timestamp' (ECS), ML data uses 'timestamp'
+
+ const rangeQueries = [
+ {
+ range: {
+ '@timestamp': {
+ gte,
+ lt,
+ },
+ },
+ },
+ {
+ range: {
+ timestamp: {
+ gte,
+ lt,
+ },
+ },
+ },
+ ];
+
+ // some of the data is timeless/content
+ const query = {
+ bool: {
+ should: [
+ ...rangeQueries,
+ {
+ bool: {
+ must_not: [
+ {
+ exists: {
+ field: '@timestamp',
+ },
+ },
+ {
+ exists: {
+ field: 'timestamp',
+ },
+ },
+ ],
+ },
+ },
+ ],
+ minimum_should_match: 1,
+ },
+ };
+
+ const archivesDir = path.join(__dirname, '.archives');
+ const root = path.join(__dirname, '../../../../..');
+
+ // create the archive
+
+ execSync(
+ `node scripts/es_archiver save ${archiveName} ${indices} --dir=${archivesDir} --kibana-url=${kibanaUrl} --es-url=${esUrl} --query='${JSON.stringify(
+ query
+ )}'`,
+ {
+ cwd: root,
+ stdio: 'inherit',
+ }
+ );
+
+ const targetDirs = ['trial', 'basic'];
+
+ // copy the archives to the test fixtures
+
+ await Promise.all(
+ targetDirs.map(async (target) => {
+ const targetPath = path.resolve(
+ __dirname,
+ '../../../../test/apm_api_integration/',
+ target
+ );
+ const targetArchivesPath = path.resolve(
+ targetPath,
+ 'fixtures/es_archiver',
+ archiveName
+ );
+
+ if (!fs.existsSync(targetArchivesPath)) {
+ fs.mkdirSync(targetArchivesPath);
+ }
+
+ fs.copyFileSync(
+ path.join(archivesDir, archiveName, 'data.json.gz'),
+ path.join(targetArchivesPath, 'data.json.gz')
+ );
+ fs.copyFileSync(
+ path.join(archivesDir, archiveName, 'mappings.json'),
+ path.join(targetArchivesPath, 'mappings.json')
+ );
+
+ const currentConfig = {};
+
+ // get the current metadata and extend/override metadata for the new archive
+ const configFilePath = path.join(targetPath, 'archives_metadata.ts');
+
+ try {
+ Object.assign(currentConfig, (await import(configFilePath)).default);
+ } catch (error) {
+ // do nothing
+ }
+
+ const newConfig = {
+ ...currentConfig,
+ [archiveName]: {
+ start: gte,
+ end: lt,
+ },
+ };
+
+ fs.writeFileSync(
+ configFilePath,
+ `export default ${JSON.stringify(newConfig, null, 2)}`,
+ { encoding: 'utf-8' }
+ );
+ })
+ );
+
+ fs.unlinkSync(path.join(archivesDir, archiveName, 'data.json.gz'));
+ fs.unlinkSync(path.join(archivesDir, archiveName, 'mappings.json'));
+ fs.rmdirSync(path.join(archivesDir, archiveName));
+ fs.rmdirSync(archivesDir);
+
+ // run ESLint on the generated metadata files
+
+ execSync('node scripts/eslint **/*/archives_metadata.ts --fix', {
+ cwd: root,
+ stdio: 'inherit',
+ });
+}
+
+run()
+ .then(() => {
+ process.exit(0);
+ })
+ .catch((err) => {
+ // eslint-disable-next-line no-console
+ console.log(err);
+ process.exit(1);
+ });
diff --git a/x-pack/plugins/apm/scripts/shared/create-or-update-index.ts b/x-pack/plugins/apm/scripts/shared/create-or-update-index.ts
index 6d44e12fb00a2..01fa5b0509bcd 100644
--- a/x-pack/plugins/apm/scripts/shared/create-or-update-index.ts
+++ b/x-pack/plugins/apm/scripts/shared/create-or-update-index.ts
@@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { Client } from '@elastic/elasticsearch';
+import { ESClient } from './get_es_client';
export async function createOrUpdateIndex({
client,
@@ -12,7 +12,7 @@ export async function createOrUpdateIndex({
indexName,
template,
}: {
- client: Client;
+ client: ESClient;
clear: boolean;
indexName: string;
template: any;
diff --git a/x-pack/plugins/apm/scripts/shared/get_es_client.ts b/x-pack/plugins/apm/scripts/shared/get_es_client.ts
new file mode 100644
index 0000000000000..86dfd92190fdf
--- /dev/null
+++ b/x-pack/plugins/apm/scripts/shared/get_es_client.ts
@@ -0,0 +1,42 @@
+/*
+ * 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 { Client } from '@elastic/elasticsearch';
+import { ApiKeyAuth, BasicAuth } from '@elastic/elasticsearch/lib/pool';
+import { ESSearchResponse, ESSearchRequest } from '../../typings/elasticsearch';
+
+export type ESClient = ReturnType;
+
+export function getEsClient({
+ node,
+ auth,
+}: {
+ node: string;
+ auth?: BasicAuth | ApiKeyAuth;
+}) {
+ const client = new Client({
+ node,
+ ssl: {
+ rejectUnauthorized: false,
+ },
+ requestTimeout: 120000,
+ auth,
+ });
+
+ return {
+ ...client,
+ async search(
+ request: TSearchRequest
+ ) {
+ const response = await client.search(request as any);
+
+ return {
+ ...response,
+ body: response.body as ESSearchResponse,
+ };
+ },
+ };
+}
diff --git a/x-pack/plugins/apm/scripts/shared/parse_index_url.ts b/x-pack/plugins/apm/scripts/shared/parse_index_url.ts
new file mode 100644
index 0000000000000..190f7fda396bd
--- /dev/null
+++ b/x-pack/plugins/apm/scripts/shared/parse_index_url.ts
@@ -0,0 +1,17 @@
+/*
+ * 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 { parse, format } from 'url';
+
+export function parseIndexUrl(url: string): { node: string; index: string } {
+ const parsed = parse(url);
+ const { pathname, ...rest } = parsed;
+
+ return {
+ node: format(rest),
+ index: pathname!.replace('/', ''),
+ };
+}
diff --git a/x-pack/plugins/apm/server/lib/rum_client/get_web_core_vitals.ts b/x-pack/plugins/apm/server/lib/rum_client/get_web_core_vitals.ts
new file mode 100644
index 0000000000000..9395e5fe14336
--- /dev/null
+++ b/x-pack/plugins/apm/server/lib/rum_client/get_web_core_vitals.ts
@@ -0,0 +1,123 @@
+/*
+ * 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 { getRumOverviewProjection } from '../../projections/rum_overview';
+import { mergeProjection } from '../../projections/util/merge_projection';
+import {
+ Setup,
+ SetupTimeRange,
+ SetupUIFilters,
+} from '../helpers/setup_request';
+import {
+ CLS_FIELD,
+ FID_FIELD,
+ LCP_FIELD,
+} from '../../../common/elasticsearch_fieldnames';
+
+export async function getWebCoreVitals({
+ setup,
+}: {
+ setup: Setup & SetupTimeRange & SetupUIFilters;
+}) {
+ const projection = getRumOverviewProjection({
+ setup,
+ });
+
+ const params = mergeProjection(projection, {
+ body: {
+ size: 0,
+ query: {
+ bool: {
+ filter: [
+ ...projection.body.query.bool.filter,
+ {
+ term: {
+ 'user_agent.name': 'Chrome',
+ },
+ },
+ ],
+ },
+ },
+ aggs: {
+ lcp: {
+ percentiles: {
+ field: LCP_FIELD,
+ percents: [50],
+ },
+ },
+ fid: {
+ percentiles: {
+ field: FID_FIELD,
+ percents: [50],
+ },
+ },
+ cls: {
+ percentiles: {
+ field: CLS_FIELD,
+ percents: [50],
+ },
+ },
+ lcpRanks: {
+ percentile_ranks: {
+ field: LCP_FIELD,
+ values: [2500, 4000],
+ keyed: false,
+ },
+ },
+ fidRanks: {
+ percentile_ranks: {
+ field: FID_FIELD,
+ values: [100, 300],
+ keyed: false,
+ },
+ },
+ clsRanks: {
+ percentile_ranks: {
+ field: CLS_FIELD,
+ values: [0.1, 0.25],
+ keyed: false,
+ },
+ },
+ },
+ },
+ });
+
+ const { apmEventClient } = setup;
+
+ const response = await apmEventClient.search(params);
+ const {
+ lcp,
+ cls,
+ fid,
+ lcpRanks,
+ fidRanks,
+ clsRanks,
+ } = response.aggregations!;
+
+ const getRanksPercentages = (
+ ranks: Array<{ key: number; value: number }>
+ ) => {
+ const ranksVal = (ranks ?? [0, 0]).map(
+ ({ value }) => value?.toFixed(0) ?? 0
+ );
+ return [
+ Number(ranksVal?.[0]),
+ Number(ranksVal?.[1]) - Number(ranksVal?.[0]),
+ 100 - Number(ranksVal?.[1]),
+ ];
+ };
+
+ // Divide by 1000 to convert ms into seconds
+ return {
+ cls: String(cls.values['50.0'] || 0),
+ fid: ((fid.values['50.0'] || 0) / 1000).toFixed(2),
+ lcp: ((lcp.values['50.0'] || 0) / 1000).toFixed(2),
+
+ lcpRanks: getRanksPercentages(lcpRanks.values),
+ fidRanks: getRanksPercentages(fidRanks.values),
+ clsRanks: getRanksPercentages(clsRanks.values),
+ };
+}
diff --git a/x-pack/plugins/apm/server/lib/settings/apm_indices/get_apm_indices.ts b/x-pack/plugins/apm/server/lib/settings/apm_indices/get_apm_indices.ts
index 2f3b2a602048c..926b2025f4253 100644
--- a/x-pack/plugins/apm/server/lib/settings/apm_indices/get_apm_indices.ts
+++ b/x-pack/plugins/apm/server/lib/settings/apm_indices/get_apm_indices.ts
@@ -5,7 +5,7 @@
*/
import { merge } from 'lodash';
-import { Server } from 'hapi';
+
import { SavedObjectsClient } from 'src/core/server';
import { PromiseReturnType } from '../../../../../observability/typings/common';
import {
@@ -32,10 +32,6 @@ export interface ApmIndicesConfig {
export type ApmIndicesName = keyof ApmIndicesConfig;
-export type ScopedSavedObjectsClient = ReturnType<
- Server['savedObjects']['getScopedSavedObjectsClient']
->;
-
async function getApmIndicesSavedObject(
savedObjectsClient: ISavedObjectsClient
) {
diff --git a/x-pack/plugins/apm/server/routes/create_apm_api.ts b/x-pack/plugins/apm/server/routes/create_apm_api.ts
index 5dff13e5b37e0..cf7a02cde975c 100644
--- a/x-pack/plugins/apm/server/routes/create_apm_api.ts
+++ b/x-pack/plugins/apm/server/routes/create_apm_api.ts
@@ -77,6 +77,7 @@ import {
rumPageLoadDistBreakdownRoute,
rumServicesRoute,
rumVisitorsBreakdownRoute,
+ rumWebCoreVitals,
} from './rum_client';
import {
observabilityOverviewHasDataRoute,
@@ -172,6 +173,7 @@ const createApmApi = () => {
.add(rumClientMetricsRoute)
.add(rumServicesRoute)
.add(rumVisitorsBreakdownRoute)
+ .add(rumWebCoreVitals)
// Observability dashboard
.add(observabilityOverviewHasDataRoute)
diff --git a/x-pack/plugins/apm/server/routes/rum_client.ts b/x-pack/plugins/apm/server/routes/rum_client.ts
index 0781512c6f7a0..e17791f56eef2 100644
--- a/x-pack/plugins/apm/server/routes/rum_client.ts
+++ b/x-pack/plugins/apm/server/routes/rum_client.ts
@@ -14,6 +14,7 @@ import { getPageLoadDistribution } from '../lib/rum_client/get_page_load_distrib
import { getPageLoadDistBreakdown } from '../lib/rum_client/get_pl_dist_breakdown';
import { getRumServices } from '../lib/rum_client/get_rum_services';
import { getVisitorBreakdown } from '../lib/rum_client/get_visitor_breakdown';
+import { getWebCoreVitals } from '../lib/rum_client/get_web_core_vitals';
export const percentileRangeRt = t.partial({
minPercentile: t.string,
@@ -117,3 +118,15 @@ export const rumVisitorsBreakdownRoute = createRoute(() => ({
return getVisitorBreakdown({ setup });
},
}));
+
+export const rumWebCoreVitals = createRoute(() => ({
+ path: '/api/apm/rum-client/web-core-vitals',
+ params: {
+ query: t.intersection([uiFiltersRt, rangeRt]),
+ },
+ handler: async ({ context, request }) => {
+ const setup = await setupRequest(context, request);
+
+ return getWebCoreVitals({ setup });
+ },
+}));
diff --git a/x-pack/plugins/apm/server/routes/typings.ts b/x-pack/plugins/apm/server/routes/typings.ts
index 97013273c9bcf..78c820fbf4ecd 100644
--- a/x-pack/plugins/apm/server/routes/typings.ts
+++ b/x-pack/plugins/apm/server/routes/typings.ts
@@ -13,7 +13,6 @@ import {
} from 'src/core/server';
import { PickByValue, Optional } from 'utility-types';
import { Observable } from 'rxjs';
-import { Server } from 'hapi';
import { ObservabilityPluginSetup } from '../../../observability/server';
import { SecurityPluginSetup } from '../../../security/server';
import { MlPluginSetup } from '../../../ml/server';
@@ -57,12 +56,6 @@ export interface Route<
}) => Promise;
}
-export type APMLegacyServer = Pick & {
- plugins: {
- elasticsearch: Server['plugins']['elasticsearch'];
- };
-};
-
export type APMRequestHandlerContext<
TDecodedParams extends { [key in keyof Params]: any } = {}
> = RequestHandlerContext & {
diff --git a/x-pack/plugins/apm/typings/elasticsearch/aggregations.ts b/x-pack/plugins/apm/typings/elasticsearch/aggregations.ts
index f957614122547..7a7592b248960 100644
--- a/x-pack/plugins/apm/typings/elasticsearch/aggregations.ts
+++ b/x-pack/plugins/apm/typings/elasticsearch/aggregations.ts
@@ -146,7 +146,7 @@ export interface AggregationOptionsByType {
buckets: number;
} & AggregationSourceOptions;
percentile_ranks: {
- values: string[];
+ values: Array;
keyed?: boolean;
hdr?: { number_of_significant_value_digits: number };
} & AggregationSourceOptions;
diff --git a/x-pack/plugins/case/common/constants.ts b/x-pack/plugins/case/common/constants.ts
index bd12c258a5388..15a318002390f 100644
--- a/x-pack/plugins/case/common/constants.ts
+++ b/x-pack/plugins/case/common/constants.ts
@@ -28,5 +28,11 @@ export const CASE_USER_ACTIONS_URL = `${CASE_DETAILS_URL}/user_actions`;
export const ACTION_URL = '/api/actions';
export const ACTION_TYPES_URL = '/api/actions/list_action_types';
export const SERVICENOW_ACTION_TYPE_ID = '.servicenow';
+export const JIRA_ACTION_TYPE_ID = '.jira';
+export const RESILIENT_ACTION_TYPE_ID = '.resilient';
-export const SUPPORTED_CONNECTORS = ['.servicenow', '.jira', '.resilient'];
+export const SUPPORTED_CONNECTORS = [
+ SERVICENOW_ACTION_TYPE_ID,
+ JIRA_ACTION_TYPE_ID,
+ RESILIENT_ACTION_TYPE_ID,
+];
diff --git a/x-pack/plugins/case/server/routes/api/cases/configure/get_connectors.ts b/x-pack/plugins/case/server/routes/api/cases/configure/get_connectors.ts
index 28e75dd2f8c32..a22d7ae5cea21 100644
--- a/x-pack/plugins/case/server/routes/api/cases/configure/get_connectors.ts
+++ b/x-pack/plugins/case/server/routes/api/cases/configure/get_connectors.ts
@@ -12,6 +12,7 @@ import {
CASE_CONFIGURE_CONNECTORS_URL,
SUPPORTED_CONNECTORS,
SERVICENOW_ACTION_TYPE_ID,
+ JIRA_ACTION_TYPE_ID,
} from '../../../../../common/constants';
/*
@@ -36,8 +37,9 @@ export function initCaseConfigureGetActionConnector({ caseService, router }: Rou
(action) =>
SUPPORTED_CONNECTORS.includes(action.actionTypeId) &&
// Need this filtering temporary to display only Case owned ServiceNow connectors
- (action.actionTypeId !== SERVICENOW_ACTION_TYPE_ID ||
- (action.actionTypeId === SERVICENOW_ACTION_TYPE_ID && action.config!.isCaseOwned))
+ (![SERVICENOW_ACTION_TYPE_ID, JIRA_ACTION_TYPE_ID].includes(action.actionTypeId) ||
+ ([SERVICENOW_ACTION_TYPE_ID, JIRA_ACTION_TYPE_ID].includes(action.actionTypeId) &&
+ action.config?.isCaseOwned === true))
);
return response.ok({ body: results });
} catch (error) {
diff --git a/x-pack/plugins/data_enhanced/common/index.ts b/x-pack/plugins/data_enhanced/common/index.ts
index d6a3c73aaf363..012f1204da46a 100644
--- a/x-pack/plugins/data_enhanced/common/index.ts
+++ b/x-pack/plugins/data_enhanced/common/index.ts
@@ -5,7 +5,6 @@
*/
export {
- EnhancedSearchParams,
IEnhancedEsSearchRequest,
IAsyncSearchRequest,
ENHANCED_ES_SEARCH_STRATEGY,
diff --git a/x-pack/plugins/data_enhanced/common/search/index.ts b/x-pack/plugins/data_enhanced/common/search/index.ts
index 2ae422bd6b7d7..696938a403e89 100644
--- a/x-pack/plugins/data_enhanced/common/search/index.ts
+++ b/x-pack/plugins/data_enhanced/common/search/index.ts
@@ -5,7 +5,6 @@
*/
export {
- EnhancedSearchParams,
IEnhancedEsSearchRequest,
IAsyncSearchRequest,
ENHANCED_ES_SEARCH_STRATEGY,
diff --git a/x-pack/plugins/data_enhanced/common/search/types.ts b/x-pack/plugins/data_enhanced/common/search/types.ts
index 0d3d3a69e1e57..24d459ade4bf9 100644
--- a/x-pack/plugins/data_enhanced/common/search/types.ts
+++ b/x-pack/plugins/data_enhanced/common/search/types.ts
@@ -4,21 +4,15 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { IEsSearchRequest, ISearchRequestParams } from '../../../../../src/plugins/data/common';
+import { IEsSearchRequest } from '../../../../../src/plugins/data/common';
export const ENHANCED_ES_SEARCH_STRATEGY = 'ese';
-export interface EnhancedSearchParams extends ISearchRequestParams {
- ignoreThrottled: boolean;
-}
-
export interface IAsyncSearchRequest extends IEsSearchRequest {
/**
* The ID received from the response from the initial request
*/
id?: string;
-
- params?: EnhancedSearchParams;
}
export interface IEnhancedEsSearchRequest extends IEsSearchRequest {
diff --git a/x-pack/plugins/data_enhanced/public/plugin.ts b/x-pack/plugins/data_enhanced/public/plugin.ts
index 7f6e3feac0671..ccc93316482c2 100644
--- a/x-pack/plugins/data_enhanced/public/plugin.ts
+++ b/x-pack/plugins/data_enhanced/public/plugin.ts
@@ -23,6 +23,8 @@ export type DataEnhancedStart = ReturnType;
export class DataEnhancedPlugin
implements Plugin {
+ private enhancedSearchInterceptor!: EnhancedSearchInterceptor;
+
public setup(
core: CoreSetup,
{ data }: DataEnhancedSetupDependencies
@@ -32,20 +34,17 @@ export class DataEnhancedPlugin
setupKqlQuerySuggestionProvider(core)
);
- const enhancedSearchInterceptor = new EnhancedSearchInterceptor(
- {
- toasts: core.notifications.toasts,
- http: core.http,
- uiSettings: core.uiSettings,
- startServices: core.getStartServices(),
- usageCollector: data.search.usageCollector,
- },
- core.injectedMetadata.getInjectedVar('esRequestTimeout') as number
- );
+ this.enhancedSearchInterceptor = new EnhancedSearchInterceptor({
+ toasts: core.notifications.toasts,
+ http: core.http,
+ uiSettings: core.uiSettings,
+ startServices: core.getStartServices(),
+ usageCollector: data.search.usageCollector,
+ });
data.__enhance({
search: {
- searchInterceptor: enhancedSearchInterceptor,
+ searchInterceptor: this.enhancedSearchInterceptor,
},
});
}
@@ -53,4 +52,8 @@ export class DataEnhancedPlugin
public start(core: CoreStart, plugins: DataEnhancedStartDependencies) {
setAutocompleteService(plugins.data.autocomplete);
}
+
+ public stop() {
+ this.enhancedSearchInterceptor.stop();
+ }
}
diff --git a/x-pack/plugins/data_enhanced/public/search/search_interceptor.test.ts b/x-pack/plugins/data_enhanced/public/search/search_interceptor.test.ts
index 1e2c7987b7041..261e03887acdb 100644
--- a/x-pack/plugins/data_enhanced/public/search/search_interceptor.test.ts
+++ b/x-pack/plugins/data_enhanced/public/search/search_interceptor.test.ts
@@ -7,7 +7,7 @@
import { coreMock } from '../../../../../src/core/public/mocks';
import { EnhancedSearchInterceptor } from './search_interceptor';
import { CoreSetup, CoreStart } from 'kibana/public';
-import { AbortError } from '../../../../../src/plugins/data/common';
+import { AbortError, UI_SETTINGS } from '../../../../../src/plugins/data/common';
const timeTravel = (msToRun = 0) => {
jest.advanceTimersByTime(msToRun);
@@ -43,6 +43,15 @@ describe('EnhancedSearchInterceptor', () => {
mockCoreSetup = coreMock.createSetup();
mockCoreStart = coreMock.createStart();
+ mockCoreSetup.uiSettings.get.mockImplementation((name: string) => {
+ switch (name) {
+ case UI_SETTINGS.SEARCH_TIMEOUT:
+ return 1000;
+ default:
+ return;
+ }
+ });
+
next.mockClear();
error.mockClear();
complete.mockClear();
@@ -64,16 +73,13 @@ describe('EnhancedSearchInterceptor', () => {
]);
});
- searchInterceptor = new EnhancedSearchInterceptor(
- {
- toasts: mockCoreSetup.notifications.toasts,
- startServices: mockPromise as any,
- http: mockCoreSetup.http,
- uiSettings: mockCoreSetup.uiSettings,
- usageCollector: mockUsageCollector,
- },
- 1000
- );
+ searchInterceptor = new EnhancedSearchInterceptor({
+ toasts: mockCoreSetup.notifications.toasts,
+ startServices: mockPromise as any,
+ http: mockCoreSetup.http,
+ uiSettings: mockCoreSetup.uiSettings,
+ usageCollector: mockUsageCollector,
+ });
});
describe('search', () => {
diff --git a/x-pack/plugins/data_enhanced/public/search/search_interceptor.ts b/x-pack/plugins/data_enhanced/public/search/search_interceptor.ts
index 6f7899d1188b4..61cf579d3136b 100644
--- a/x-pack/plugins/data_enhanced/public/search/search_interceptor.ts
+++ b/x-pack/plugins/data_enhanced/public/search/search_interceptor.ts
@@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { throwError, EMPTY, timer, from } from 'rxjs';
+import { throwError, EMPTY, timer, from, Subscription } from 'rxjs';
import { mergeMap, expand, takeUntil, finalize, tap } from 'rxjs/operators';
import { getLongQueryNotification } from './long_query_notification';
import {
@@ -17,14 +17,25 @@ import { IAsyncSearchOptions } from '.';
import { IAsyncSearchRequest, ENHANCED_ES_SEARCH_STRATEGY } from '../../common';
export class EnhancedSearchInterceptor extends SearchInterceptor {
+ private uiSettingsSub: Subscription;
+ private searchTimeout: number;
+
/**
- * This class should be instantiated with a `requestTimeout` corresponding with how many ms after
- * requests are initiated that they should automatically cancel.
- * @param deps `SearchInterceptorDeps`
- * @param requestTimeout Usually config value `elasticsearch.requestTimeout`
+ * @internal
*/
- constructor(deps: SearchInterceptorDeps, requestTimeout?: number) {
- super(deps, requestTimeout);
+ constructor(deps: SearchInterceptorDeps) {
+ super(deps);
+ this.searchTimeout = deps.uiSettings.get(UI_SETTINGS.SEARCH_TIMEOUT);
+
+ this.uiSettingsSub = deps.uiSettings
+ .get$(UI_SETTINGS.SEARCH_TIMEOUT)
+ .subscribe((timeout: number) => {
+ this.searchTimeout = timeout;
+ });
+ }
+
+ public stop() {
+ this.uiSettingsSub.unsubscribe();
}
/**
@@ -69,12 +80,10 @@ export class EnhancedSearchInterceptor extends SearchInterceptor {
) {
let { id } = request;
- request.params = {
- ignoreThrottled: !this.deps.uiSettings.get(UI_SETTINGS.SEARCH_INCLUDE_FROZEN),
- ...request.params,
- };
-
- const { combinedSignal, cleanup } = this.setupTimers(options);
+ const { combinedSignal, cleanup } = this.setupAbortSignal({
+ abortSignal: options.abortSignal,
+ timeout: this.searchTimeout,
+ });
const aborted$ = from(toPromise(combinedSignal));
const strategy = options?.strategy || ENHANCED_ES_SEARCH_STRATEGY;
@@ -108,7 +117,7 @@ export class EnhancedSearchInterceptor extends SearchInterceptor {
// we don't need to send a follow-up request to delete this search. Otherwise, we
// send the follow-up request to delete this search, then throw an abort error.
if (id !== undefined) {
- this.deps.http.delete(`/internal/search/es/${id}`);
+ this.deps.http.delete(`/internal/search/${strategy}/${id}`);
}
},
}),
diff --git a/x-pack/plugins/data_enhanced/server/plugin.ts b/x-pack/plugins/data_enhanced/server/plugin.ts
index f9b6fd4e9ad64..3b05e83d208b7 100644
--- a/x-pack/plugins/data_enhanced/server/plugin.ts
+++ b/x-pack/plugins/data_enhanced/server/plugin.ts
@@ -19,6 +19,7 @@ import {
import { enhancedEsSearchStrategyProvider } from './search';
import { UsageCollectionSetup } from '../../../../src/plugins/usage_collection/server';
import { ENHANCED_ES_SEARCH_STRATEGY } from '../common';
+import { getUiSettings } from './ui_settings';
interface SetupDependencies {
data: DataPluginSetup;
@@ -35,6 +36,8 @@ export class EnhancedDataServerPlugin implements Plugin, deps: SetupDependencies) {
const usage = deps.usageCollection ? usageProvider(core) : undefined;
+ core.uiSettings.register(getUiSettings());
+
deps.data.search.registerSearchStrategy(
ENHANCED_ES_SEARCH_STRATEGY,
enhancedEsSearchStrategyProvider(
diff --git a/x-pack/plugins/data_enhanced/server/search/es_search_strategy.test.ts b/x-pack/plugins/data_enhanced/server/search/es_search_strategy.test.ts
index a287f72ca9161..f4f3d894a4576 100644
--- a/x-pack/plugins/data_enhanced/server/search/es_search_strategy.test.ts
+++ b/x-pack/plugins/data_enhanced/server/search/es_search_strategy.test.ts
@@ -5,8 +5,8 @@
*/
import { RequestHandlerContext } from '../../../../../src/core/server';
-import { pluginInitializerContextConfigMock } from '../../../../../src/core/server/mocks';
import { enhancedEsSearchStrategyProvider } from './es_search_strategy';
+import { BehaviorSubject } from 'rxjs';
const mockAsyncResponse = {
body: {
@@ -42,6 +42,11 @@ describe('ES search strategy', () => {
};
const mockContext = {
core: {
+ uiSettings: {
+ client: {
+ get: jest.fn(),
+ },
+ },
elasticsearch: {
client: {
asCurrentUser: {
@@ -55,7 +60,15 @@ describe('ES search strategy', () => {
},
},
};
- const mockConfig$ = pluginInitializerContextConfigMock({}).legacy.globalConfig$;
+ const mockConfig$ = new BehaviorSubject({
+ elasticsearch: {
+ shardTimeout: {
+ asMilliseconds: () => {
+ return 100;
+ },
+ },
+ },
+ });
beforeEach(() => {
mockApiCaller.mockClear();
diff --git a/x-pack/plugins/data_enhanced/server/search/es_search_strategy.ts b/x-pack/plugins/data_enhanced/server/search/es_search_strategy.ts
index 4ace1c4c5385b..eda6178dc8e5b 100644
--- a/x-pack/plugins/data_enhanced/server/search/es_search_strategy.ts
+++ b/x-pack/plugins/data_enhanced/server/search/es_search_strategy.ts
@@ -5,23 +5,19 @@
*/
import { first } from 'rxjs/operators';
-import { mapKeys, snakeCase } from 'lodash';
-import { Observable } from 'rxjs';
import { SearchResponse } from 'elasticsearch';
+import { Observable } from 'rxjs';
+import { SharedGlobalConfig, RequestHandlerContext, Logger } from '../../../../../src/core/server';
import {
- SharedGlobalConfig,
- RequestHandlerContext,
- ElasticsearchClient,
- Logger,
-} from '../../../../../src/core/server';
-import {
- getDefaultSearchParams,
getTotalLoaded,
ISearchStrategy,
SearchUsage,
+ getDefaultSearchParams,
+ getShardTimeout,
+ toSnakeCase,
+ shimHitsTotal,
} from '../../../../../src/plugins/data/server';
import { IEnhancedEsSearchRequest } from '../../common';
-import { shimHitsTotal } from './shim_hits_total';
import { ISearchOptions, IEsSearchResponse } from '../../../../../src/plugins/data/common/search';
function isEnhancedEsSearchResponse(response: any): response is IEsSearchResponse {
@@ -39,17 +35,13 @@ export const enhancedEsSearchStrategyProvider = (
options?: ISearchOptions
) => {
logger.debug(`search ${JSON.stringify(request.params) || request.id}`);
- const config = await config$.pipe(first()).toPromise();
- const client = context.core.elasticsearch.client.asCurrentUser;
- const defaultParams = getDefaultSearchParams(config);
- const params = { ...defaultParams, ...request.params };
const isAsync = request.indexType !== 'rollup';
try {
const response = isAsync
- ? await asyncSearch(client, { ...request, params }, options)
- : await rollupSearch(client, { ...request, params }, options);
+ ? await asyncSearch(context, request)
+ : await rollupSearch(context, request);
if (
usage &&
@@ -75,72 +67,75 @@ export const enhancedEsSearchStrategyProvider = (
});
};
- return { search, cancel };
-};
-
-async function asyncSearch(
- client: ElasticsearchClient,
- request: IEnhancedEsSearchRequest,
- options?: ISearchOptions
-): Promise {
- let esResponse;
+ async function asyncSearch(
+ context: RequestHandlerContext,
+ request: IEnhancedEsSearchRequest
+ ): Promise {
+ let esResponse;
+ const esClient = context.core.elasticsearch.client.asCurrentUser;
+ const uiSettingsClient = await context.core.uiSettings.client;
+
+ const asyncOptions = {
+ waitForCompletionTimeout: '100ms', // Wait up to 100ms for the response to return
+ keepAlive: '1m', // Extend the TTL for this search request by one minute
+ };
+
+ // If we have an ID, then just poll for that ID, otherwise send the entire request body
+ if (!request.id) {
+ const submitOptions = toSnakeCase({
+ batchedReduceSize: 64, // Only report partial results every 64 shards; this should be reduced when we actually display partial results
+ ...(await getDefaultSearchParams(uiSettingsClient)),
+ ...asyncOptions,
+ ...request.params,
+ });
+
+ esResponse = await esClient.asyncSearch.submit(submitOptions);
+ } else {
+ esResponse = await esClient.asyncSearch.get({
+ id: request.id,
+ ...toSnakeCase(asyncOptions),
+ });
+ }
- const asyncOptions = {
- waitForCompletionTimeout: '100ms', // Wait up to 100ms for the response to return
- keepAlive: '1m', // Extend the TTL for this search request by one minute
- };
+ const { id, response, is_partial: isPartial, is_running: isRunning } = esResponse.body;
+ return {
+ id,
+ isPartial,
+ isRunning,
+ rawResponse: shimHitsTotal(response),
+ ...getTotalLoaded(response._shards),
+ };
+ }
- // If we have an ID, then just poll for that ID, otherwise send the entire request body
- if (!request.id) {
- const submitOptions = toSnakeCase({
- batchedReduceSize: 64, // Only report partial results every 64 shards; this should be reduced when we actually display partial results
- trackTotalHits: true, // Get the exact count of hits
- ...asyncOptions,
- ...request.params,
+ const rollupSearch = async function (
+ context: RequestHandlerContext,
+ request: IEnhancedEsSearchRequest
+ ): Promise {
+ const esClient = context.core.elasticsearch.client.asCurrentUser;
+ const uiSettingsClient = await context.core.uiSettings.client;
+ const config = await config$.pipe(first()).toPromise();
+ const { body, index, ...params } = request.params!;
+ const method = 'POST';
+ const path = encodeURI(`/${index}/_rollup_search`);
+ const querystring = toSnakeCase({
+ ...getShardTimeout(config),
+ ...(await getDefaultSearchParams(uiSettingsClient)),
+ ...params,
});
- esResponse = await client.asyncSearch.submit(submitOptions);
- } else {
- esResponse = await client.asyncSearch.get({
- id: request.id,
- ...toSnakeCase(asyncOptions),
+ const esResponse = await esClient.transport.request({
+ method,
+ path,
+ body,
+ querystring,
});
- }
-
- const { id, response, is_partial: isPartial, is_running: isRunning } = esResponse.body;
- return {
- id,
- isPartial,
- isRunning,
- rawResponse: shimHitsTotal(response),
- ...getTotalLoaded(response._shards),
- };
-}
-async function rollupSearch(
- client: ElasticsearchClient,
- request: IEnhancedEsSearchRequest,
- options?: ISearchOptions
-): Promise {
- const { body, index, ...params } = request.params!;
- const method = 'POST';
- const path = encodeURI(`/${index}/_rollup_search`);
- const querystring = toSnakeCase(params);
-
- const esResponse = await client.transport.request({
- method,
- path,
- body,
- querystring,
- });
-
- const response = esResponse.body as SearchResponse;
- return {
- rawResponse: shimHitsTotal(response),
- ...getTotalLoaded(response._shards),
+ const response = esResponse.body as SearchResponse;
+ return {
+ rawResponse: response,
+ ...getTotalLoaded(response._shards),
+ };
};
-}
-function toSnakeCase(obj: Record) {
- return mapKeys(obj, (value, key) => snakeCase(key));
-}
+ return { search, cancel };
+};
diff --git a/x-pack/plugins/data_enhanced/server/search/shim_hits_total.ts b/x-pack/plugins/data_enhanced/server/search/shim_hits_total.ts
deleted file mode 100644
index 10d45be01563a..0000000000000
--- a/x-pack/plugins/data_enhanced/server/search/shim_hits_total.ts
+++ /dev/null
@@ -1,18 +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;
- * you may not use this file except in compliance with the Elastic License.
- */
-
-import { SearchResponse } from 'elasticsearch';
-
-/**
- * Temporary workaround until https://github.com/elastic/kibana/issues/26356 is addressed.
- * Since we are setting `track_total_hits` in the request, `hits.total` will be an object
- * containing the `value`.
- */
-export function shimHitsTotal(response: SearchResponse) {
- const total = (response.hits?.total as any)?.value ?? response.hits?.total;
- const hits = { ...response.hits, total };
- return { ...response, hits };
-}
diff --git a/x-pack/plugins/data_enhanced/server/ui_settings.ts b/x-pack/plugins/data_enhanced/server/ui_settings.ts
new file mode 100644
index 0000000000000..f2842da8b8337
--- /dev/null
+++ b/x-pack/plugins/data_enhanced/server/ui_settings.ts
@@ -0,0 +1,28 @@
+/*
+ * 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 { i18n } from '@kbn/i18n';
+import { schema } from '@kbn/config-schema';
+import { UiSettingsParams } from 'kibana/server';
+import { UI_SETTINGS } from '../../../../src/plugins/data/server';
+
+export function getUiSettings(): Record> {
+ return {
+ [UI_SETTINGS.SEARCH_TIMEOUT]: {
+ name: i18n.translate('xpack.data.advancedSettings.searchTimeout', {
+ defaultMessage: 'Search Timeout',
+ }),
+ value: 600000,
+ description: i18n.translate('xpack.data.advancedSettings.searchTimeoutDesc', {
+ defaultMessage:
+ 'Change the maximum timeout for a search session or set to 0 to disable the timeout and allow queries to run to completion.',
+ }),
+ type: 'number',
+ category: ['search'],
+ schema: schema.number(),
+ },
+ };
+}
diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/types/document_fields.ts b/x-pack/plugins/index_management/public/application/components/mappings_editor/types/document_fields.ts
index a9f6d2ea03bdf..6882ddea4ad5d 100644
--- a/x-pack/plugins/index_management/public/application/components/mappings_editor/types/document_fields.ts
+++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/types/document_fields.ts
@@ -24,7 +24,7 @@ export interface DataTypeDefinition {
export interface ParameterDefinition {
title?: string;
description?: JSX.Element | string;
- fieldConfig: FieldConfig;
+ fieldConfig: FieldConfig;
schema?: any;
props?: { [key: string]: ParameterDefinition };
documentation?: {
diff --git a/x-pack/plugins/infra/server/lib/adapters/log_entries/kibana_log_entries_adapter.ts b/x-pack/plugins/infra/server/lib/adapters/log_entries/kibana_log_entries_adapter.ts
index 939498305eb98..c5b667fb20538 100644
--- a/x-pack/plugins/infra/server/lib/adapters/log_entries/kibana_log_entries_adapter.ts
+++ b/x-pack/plugins/infra/server/lib/adapters/log_entries/kibana_log_entries_adapter.ts
@@ -8,12 +8,12 @@
import { timeMilliseconds } from 'd3-time';
import * as runtimeTypes from 'io-ts';
-import { compact, first, get, has } from 'lodash';
+import { compact, first } from 'lodash';
import { pipe } from 'fp-ts/lib/pipeable';
import { map, fold } from 'fp-ts/lib/Either';
import { identity, constant } from 'fp-ts/lib/function';
import { RequestHandlerContext } from 'src/core/server';
-import { JsonObject, JsonValue } from '../../../../common/typed_json';
+import { JsonValue } from '../../../../common/typed_json';
import {
LogEntriesAdapter,
LogEntriesParams,
@@ -31,7 +31,7 @@ const TIMESTAMP_FORMAT = 'epoch_millis';
interface LogItemHit {
_index: string;
_id: string;
- _source: JsonObject;
+ fields: { [key: string]: [value: unknown] };
sort: [number, number];
}
@@ -82,7 +82,8 @@ export class InfraKibanaLogEntriesAdapter implements LogEntriesAdapter {
body: {
size: typeof size !== 'undefined' ? size : LOG_ENTRIES_PAGE_SIZE,
track_total_hits: false,
- _source: fields,
+ _source: false,
+ fields,
query: {
bool: {
filter: [
@@ -214,6 +215,8 @@ export class InfraKibanaLogEntriesAdapter implements LogEntriesAdapter {
values: [id],
},
},
+ fields: ['*'],
+ _source: false,
},
};
@@ -230,8 +233,8 @@ function mapHitsToLogEntryDocuments(hits: SortedSearchHit[], fields: string[]):
return hits.map((hit) => {
const logFields = fields.reduce<{ [fieldName: string]: JsonValue }>(
(flattenedFields, field) => {
- if (has(hit._source, field)) {
- flattenedFields[field] = get(hit._source, field);
+ if (field in hit.fields) {
+ flattenedFields[field] = hit.fields[field][0];
}
return flattenedFields;
},
diff --git a/x-pack/plugins/infra/server/lib/domains/log_entries_domain/convert_document_source_to_log_item_fields.ts b/x-pack/plugins/infra/server/lib/domains/log_entries_domain/convert_document_source_to_log_item_fields.ts
index 099e7c3b5038c..7c8560d72ff97 100644
--- a/x-pack/plugins/infra/server/lib/domains/log_entries_domain/convert_document_source_to_log_item_fields.ts
+++ b/x-pack/plugins/infra/server/lib/domains/log_entries_domain/convert_document_source_to_log_item_fields.ts
@@ -20,6 +20,11 @@ const serializeValue = (value: any): string => {
}
return `${value}`;
};
+export const convertESFieldsToLogItemFields = (fields: {
+ [field: string]: [value: unknown];
+}): LogEntriesItemField[] => {
+ return Object.keys(fields).map((field) => ({ field, value: serializeValue(fields[field][0]) }));
+};
export const convertDocumentSourceToLogItemFields = (
source: JsonObject,
diff --git a/x-pack/plugins/infra/server/lib/domains/log_entries_domain/log_entries_domain.ts b/x-pack/plugins/infra/server/lib/domains/log_entries_domain/log_entries_domain.ts
index 9b3e31f4da87a..e211f72b4e076 100644
--- a/x-pack/plugins/infra/server/lib/domains/log_entries_domain/log_entries_domain.ts
+++ b/x-pack/plugins/infra/server/lib/domains/log_entries_domain/log_entries_domain.ts
@@ -22,7 +22,7 @@ import {
SavedSourceConfigurationFieldColumnRuntimeType,
} from '../../sources';
import { getBuiltinRules } from './builtin_rules';
-import { convertDocumentSourceToLogItemFields } from './convert_document_source_to_log_item_fields';
+import { convertESFieldsToLogItemFields } from './convert_document_source_to_log_item_fields';
import {
CompiledLogMessageFormattingRule,
Fields,
@@ -264,7 +264,7 @@ export class InfraLogEntriesDomain {
tiebreaker: document.sort[1],
},
fields: sortBy(
- [...defaultFields, ...convertDocumentSourceToLogItemFields(document._source)],
+ [...defaultFields, ...convertESFieldsToLogItemFields(document.fields)],
'field'
),
};
@@ -313,7 +313,7 @@ export class InfraLogEntriesDomain {
interface LogItemHit {
_index: string;
_id: string;
- _source: JsonObject;
+ fields: { [field: string]: [value: unknown] };
sort: [number, number];
}
diff --git a/x-pack/plugins/infra/server/routes/log_entries/entries.ts b/x-pack/plugins/infra/server/routes/log_entries/entries.ts
index 2cd889d9c5568..c1f63d9c29577 100644
--- a/x-pack/plugins/infra/server/routes/log_entries/entries.ts
+++ b/x-pack/plugins/infra/server/routes/log_entries/entries.ts
@@ -4,14 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import Boom from 'boom';
-
-import { pipe } from 'fp-ts/lib/pipeable';
-import { fold } from 'fp-ts/lib/Either';
-import { identity } from 'fp-ts/lib/function';
-import { schema } from '@kbn/config-schema';
-
-import { throwErrors } from '../../../common/runtime_types';
+import { createValidationFunction } from '../../../common/runtime_types';
import { InfraBackendLibs } from '../../lib/infra_types';
import {
@@ -22,22 +15,16 @@ import {
import { parseFilterQuery } from '../../utils/serialized_query';
import { LogEntriesParams } from '../../lib/domains/log_entries_domain';
-const escapeHatch = schema.object({}, { unknowns: 'allow' });
-
export const initLogEntriesRoute = ({ framework, logEntries }: InfraBackendLibs) => {
framework.registerRoute(
{
method: 'post',
path: LOG_ENTRIES_PATH,
- validate: { body: escapeHatch },
+ validate: { body: createValidationFunction(logEntriesRequestRT) },
},
async (requestContext, request, response) => {
try {
- const payload = pipe(
- logEntriesRequestRT.decode(request.body),
- fold(throwErrors(Boom.badRequest), identity)
- );
-
+ const payload = request.body;
const {
startTimestamp: startTimestamp,
endTimestamp: endTimestamp,
diff --git a/x-pack/plugins/infra/server/routes/log_entries/item.ts b/x-pack/plugins/infra/server/routes/log_entries/item.ts
index 85dba8f598a89..67ca481ff4fcb 100644
--- a/x-pack/plugins/infra/server/routes/log_entries/item.ts
+++ b/x-pack/plugins/infra/server/routes/log_entries/item.ts
@@ -4,14 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import Boom from 'boom';
-
-import { pipe } from 'fp-ts/lib/pipeable';
-import { fold } from 'fp-ts/lib/Either';
-import { identity } from 'fp-ts/lib/function';
-import { schema } from '@kbn/config-schema';
-
-import { throwErrors } from '../../../common/runtime_types';
+import { createValidationFunction } from '../../../common/runtime_types';
import { InfraBackendLibs } from '../../lib/infra_types';
import {
@@ -20,22 +13,16 @@ import {
logEntriesItemResponseRT,
} from '../../../common/http_api';
-const escapeHatch = schema.object({}, { unknowns: 'allow' });
-
export const initLogEntriesItemRoute = ({ framework, sources, logEntries }: InfraBackendLibs) => {
framework.registerRoute(
{
method: 'post',
path: LOG_ENTRIES_ITEM_PATH,
- validate: { body: escapeHatch },
+ validate: { body: createValidationFunction(logEntriesItemRequestRT) },
},
async (requestContext, request, response) => {
try {
- const payload = pipe(
- logEntriesItemRequestRT.decode(request.body),
- fold(throwErrors(Boom.badRequest), identity)
- );
-
+ const payload = request.body;
const { id, sourceId } = payload;
const sourceConfiguration = (
await sources.getSourceConfiguration(requestContext.core.savedObjects.client, sourceId)
diff --git a/x-pack/plugins/ingest_manager/common/types/models/epm.ts b/x-pack/plugins/ingest_manager/common/types/models/epm.ts
index 140a76ac85e61..f083400997870 100644
--- a/x-pack/plugins/ingest_manager/common/types/models/epm.ts
+++ b/x-pack/plugins/ingest_manager/common/types/models/epm.ts
@@ -19,6 +19,8 @@ export enum InstallStatus {
uninstalling = 'uninstalling',
}
+export type InstallType = 'reinstall' | 'reupdate' | 'rollback' | 'update' | 'install';
+
export type EpmPackageInstallStatus = 'installed' | 'installing';
export type DetailViewPanelName = 'overview' | 'usages' | 'settings';
@@ -38,6 +40,7 @@ export enum ElasticsearchAssetType {
ingestPipeline = 'ingest_pipeline',
indexTemplate = 'index_template',
ilmPolicy = 'ilm_policy',
+ transform = 'transform',
}
export enum AgentAssetType {
diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/constants.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/constants.tsx
index 31c6d76446447..da3cab1a4b8a3 100644
--- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/constants.tsx
+++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/constants.tsx
@@ -19,6 +19,7 @@ export const AssetTitleMap: Record = {
dashboard: 'Dashboard',
ilm_policy: 'ILM Policy',
ingest_pipeline: 'Ingest Pipeline',
+ transform: 'Transform',
'index-pattern': 'Index Pattern',
index_template: 'Index Template',
component_template: 'Component Template',
diff --git a/x-pack/plugins/ingest_manager/server/routes/epm/handlers.ts b/x-pack/plugins/ingest_manager/server/routes/epm/handlers.ts
index 6d7252ffec41a..b19960cc90228 100644
--- a/x-pack/plugins/ingest_manager/server/routes/epm/handlers.ts
+++ b/x-pack/plugins/ingest_manager/server/routes/epm/handlers.ts
@@ -34,6 +34,7 @@ import {
} from '../../services/epm/packages';
import { IngestManagerError, defaultIngestErrorHandler } from '../../errors';
import { splitPkgKey } from '../../services/epm/registry';
+import { getInstallType } from '../../services/epm/packages/install';
export const getCategoriesHandler: RequestHandler<
undefined,
@@ -138,6 +139,8 @@ export const installPackageHandler: RequestHandler<
const callCluster = context.core.elasticsearch.legacy.client.callAsCurrentUser;
const { pkgkey } = request.params;
const { pkgName, pkgVersion } = splitPkgKey(pkgkey);
+ const installedPkg = await getInstallationObject({ savedObjectsClient, pkgName });
+ const installType = getInstallType({ pkgVersion, installedPkg });
try {
const res = await installPackage({
savedObjectsClient,
@@ -156,15 +159,25 @@ export const installPackageHandler: RequestHandler<
if (e instanceof IngestManagerError) {
return defaultResult;
}
- // if there is an unknown server error, uninstall any package assets
+
+ // if there is an unknown server error, uninstall any package assets or reinstall the previous version if update
try {
- const installedPkg = await getInstallationObject({ savedObjectsClient, pkgName });
- const isUpdate = installedPkg && installedPkg.attributes.version < pkgVersion ? true : false;
- if (!isUpdate) {
+ if (installType === 'install' || installType === 'reinstall') {
+ logger.error(`uninstalling ${pkgkey} after error installing`);
await removeInstallation({ savedObjectsClient, pkgkey, callCluster });
}
+ if (installType === 'update') {
+ // @ts-ignore installType conditions already check for existence of installedPkg
+ const prevVersion = `${pkgName}-${installedPkg.attributes.version}`;
+ logger.error(`rolling back to ${prevVersion} after error installing ${pkgkey}`);
+ await installPackage({
+ savedObjectsClient,
+ pkgkey: prevVersion,
+ callCluster,
+ });
+ }
} catch (error) {
- logger.error(`could not remove failed installation ${error}`);
+ logger.error(`failed to uninstall or rollback package after installation error ${error}`);
}
return defaultResult;
}
diff --git a/x-pack/plugins/maps/public/routing/bootstrap/get_initial_layers.d.ts b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/transform/common.ts
similarity index 56%
rename from x-pack/plugins/maps/public/routing/bootstrap/get_initial_layers.d.ts
rename to x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/transform/common.ts
index a23e715a08295..46f36dba96747 100644
--- a/x-pack/plugins/maps/public/routing/bootstrap/get_initial_layers.d.ts
+++ b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/transform/common.ts
@@ -4,9 +4,8 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { LayerDescriptor } from '../../../common/descriptor_types';
+import * as Registry from '../../registry';
-export function getInitialLayers(
- layerListJSON?: string,
- initialLayers?: LayerDescriptor[]
-): LayerDescriptor[];
+export const getAsset = (path: string): Buffer => {
+ return Registry.getAsset(path);
+};
diff --git a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/transform/install.ts b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/transform/install.ts
new file mode 100644
index 0000000000000..1e58319183c7d
--- /dev/null
+++ b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/transform/install.ts
@@ -0,0 +1,165 @@
+/*
+ * 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 { SavedObjectsClientContract } from 'kibana/server';
+
+import { saveInstalledEsRefs } from '../../packages/install';
+import * as Registry from '../../registry';
+import {
+ Dataset,
+ ElasticsearchAssetType,
+ EsAssetReference,
+ RegistryPackage,
+} from '../../../../../common/types/models';
+import { CallESAsCurrentUser } from '../../../../types';
+import { getInstallation } from '../../packages';
+import { deleteTransforms, deleteTransformRefs } from './remove';
+import { getAsset } from './common';
+
+interface TransformInstallation {
+ installationName: string;
+ content: string;
+}
+
+interface TransformPathDataset {
+ path: string;
+ dataset: Dataset;
+}
+
+export const installTransformForDataset = async (
+ registryPackage: RegistryPackage,
+ paths: string[],
+ callCluster: CallESAsCurrentUser,
+ savedObjectsClient: SavedObjectsClientContract
+) => {
+ const installation = await getInstallation({ savedObjectsClient, pkgName: registryPackage.name });
+ let previousInstalledTransformEsAssets: EsAssetReference[] = [];
+ if (installation) {
+ previousInstalledTransformEsAssets = installation.installed_es.filter(
+ ({ type, id }) => type === ElasticsearchAssetType.transform
+ );
+ }
+
+ // delete all previous transform
+ await deleteTransforms(
+ callCluster,
+ previousInstalledTransformEsAssets.map((asset) => asset.id)
+ );
+ // install the latest dataset
+ const datasets = registryPackage.datasets;
+ if (!datasets?.length) return [];
+ const installNameSuffix = `${registryPackage.version}`;
+
+ const transformPaths = paths.filter((path) => isTransform(path));
+ let installedTransforms: EsAssetReference[] = [];
+ if (transformPaths.length > 0) {
+ const transformPathDatasets = datasets.reduce((acc, dataset) => {
+ transformPaths.forEach((path) => {
+ if (isDatasetTransform(path, dataset.path)) {
+ acc.push({ path, dataset });
+ }
+ });
+ return acc;
+ }, []);
+
+ const transformRefs = transformPathDatasets.reduce(
+ (acc, transformPathDataset) => {
+ if (transformPathDataset) {
+ acc.push({
+ id: getTransformNameForInstallation(transformPathDataset, installNameSuffix),
+ type: ElasticsearchAssetType.transform,
+ });
+ }
+ return acc;
+ },
+ []
+ );
+
+ // get and save transform refs before installing transforms
+ await saveInstalledEsRefs(savedObjectsClient, registryPackage.name, transformRefs);
+
+ const transforms: TransformInstallation[] = transformPathDatasets.map(
+ (transformPathDataset: TransformPathDataset) => {
+ return {
+ installationName: getTransformNameForInstallation(
+ transformPathDataset,
+ installNameSuffix
+ ),
+ content: getAsset(transformPathDataset.path).toString('utf-8'),
+ };
+ }
+ );
+
+ const installationPromises = transforms.map(async (transform) => {
+ return installTransform({ callCluster, transform });
+ });
+
+ installedTransforms = await Promise.all(installationPromises).then((results) => results.flat());
+ }
+
+ if (previousInstalledTransformEsAssets.length > 0) {
+ const currentInstallation = await getInstallation({
+ savedObjectsClient,
+ pkgName: registryPackage.name,
+ });
+
+ // remove the saved object reference
+ await deleteTransformRefs(
+ savedObjectsClient,
+ currentInstallation?.installed_es || [],
+ registryPackage.name,
+ previousInstalledTransformEsAssets.map((asset) => asset.id),
+ installedTransforms.map((installed) => installed.id)
+ );
+ }
+ return installedTransforms;
+};
+
+const isTransform = (path: string) => {
+ const pathParts = Registry.pathParts(path);
+ return pathParts.type === ElasticsearchAssetType.transform;
+};
+
+const isDatasetTransform = (path: string, datasetName: string) => {
+ const pathParts = Registry.pathParts(path);
+ return (
+ !path.endsWith('/') &&
+ pathParts.type === ElasticsearchAssetType.transform &&
+ pathParts.dataset !== undefined &&
+ datasetName === pathParts.dataset
+ );
+};
+
+async function installTransform({
+ callCluster,
+ transform,
+}: {
+ callCluster: CallESAsCurrentUser;
+ transform: TransformInstallation;
+}): Promise {
+ // defer validation on put if the source index is not available
+ await callCluster('transport.request', {
+ method: 'PUT',
+ path: `_transform/${transform.installationName}`,
+ query: 'defer_validation=true',
+ body: transform.content,
+ });
+
+ await callCluster('transport.request', {
+ method: 'POST',
+ path: `_transform/${transform.installationName}/_start`,
+ });
+
+ return { id: transform.installationName, type: ElasticsearchAssetType.transform };
+}
+
+const getTransformNameForInstallation = (
+ transformDataset: TransformPathDataset,
+ suffix: string
+) => {
+ const filename = transformDataset?.path.split('/')?.pop()?.split('.')[0];
+ return `${transformDataset.dataset.type}-${transformDataset.dataset.name}-${filename}-${suffix}`;
+};
diff --git a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/transform/remove.test.ts b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/transform/remove.test.ts
new file mode 100644
index 0000000000000..3f85ee9b550b2
--- /dev/null
+++ b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/transform/remove.test.ts
@@ -0,0 +1,69 @@
+/*
+ * 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 { SavedObjectsClientContract } from 'kibana/server';
+// eslint-disable-next-line @kbn/eslint/no-restricted-paths
+import { savedObjectsClientMock } from '../../../../../../../../src/core/server/saved_objects/service/saved_objects_client.mock';
+import { deleteTransformRefs } from './remove';
+import { EsAssetReference } from '../../../../../common/types/models';
+
+describe('test transform install', () => {
+ let savedObjectsClient: jest.Mocked;
+ beforeEach(() => {
+ savedObjectsClient = savedObjectsClientMock.create();
+ });
+
+ test('can delete transform ref and handle duplicate when previous version and current version are the same', async () => {
+ await deleteTransformRefs(
+ savedObjectsClient,
+ [
+ { id: 'metrics-endpoint.policy-0.16.0-dev.0', type: 'ingest_pipeline' },
+ { id: 'metrics-endpoint.metadata-current-default-0.16.0-dev.0', type: 'transform' },
+ ] as EsAssetReference[],
+ 'endpoint',
+ ['metrics-endpoint.metadata-current-default-0.16.0-dev.0'],
+ ['metrics-endpoint.metadata-current-default-0.16.0-dev.0']
+ );
+ expect(savedObjectsClient.update.mock.calls).toEqual([
+ [
+ 'epm-packages',
+ 'endpoint',
+ {
+ installed_es: [
+ { id: 'metrics-endpoint.policy-0.16.0-dev.0', type: 'ingest_pipeline' },
+ { id: 'metrics-endpoint.metadata-current-default-0.16.0-dev.0', type: 'transform' },
+ ],
+ },
+ ],
+ ]);
+ });
+
+ test('can delete transform ref when previous version and current version are not the same', async () => {
+ await deleteTransformRefs(
+ savedObjectsClient,
+ [
+ { id: 'metrics-endpoint.policy-0.16.0-dev.0', type: 'ingest_pipeline' },
+ { id: 'metrics-endpoint.metadata-current-default-0.16.0-dev.0', type: 'transform' },
+ ] as EsAssetReference[],
+ 'endpoint',
+ ['metrics-endpoint.metadata-current-default-0.15.0-dev.0'],
+ ['metrics-endpoint.metadata-current-default-0.16.0-dev.0']
+ );
+
+ expect(savedObjectsClient.update.mock.calls).toEqual([
+ [
+ 'epm-packages',
+ 'endpoint',
+ {
+ installed_es: [
+ { id: 'metrics-endpoint.policy-0.16.0-dev.0', type: 'ingest_pipeline' },
+ { id: 'metrics-endpoint.metadata-current-default-0.16.0-dev.0', type: 'transform' },
+ ],
+ },
+ ],
+ ]);
+ });
+});
diff --git a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/transform/remove.ts b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/transform/remove.ts
new file mode 100644
index 0000000000000..5c9d3e2846200
--- /dev/null
+++ b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/transform/remove.ts
@@ -0,0 +1,58 @@
+/*
+ * 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 { SavedObjectsClientContract } from 'kibana/server';
+import { CallESAsCurrentUser, ElasticsearchAssetType, EsAssetReference } from '../../../../types';
+import { PACKAGES_SAVED_OBJECT_TYPE } from '../../../../../common/constants';
+
+export const stopTransforms = async (transformIds: string[], callCluster: CallESAsCurrentUser) => {
+ for (const transformId of transformIds) {
+ await callCluster('transport.request', {
+ method: 'POST',
+ path: `_transform/${transformId}/_stop`,
+ query: 'force=true',
+ ignore: [404],
+ });
+ }
+};
+
+export const deleteTransforms = async (
+ callCluster: CallESAsCurrentUser,
+ transformIds: string[]
+) => {
+ await Promise.all(
+ transformIds.map(async (transformId) => {
+ await stopTransforms([transformId], callCluster);
+ await callCluster('transport.request', {
+ method: 'DELETE',
+ query: 'force=true',
+ path: `_transform/${transformId}`,
+ ignore: [404],
+ });
+ })
+ );
+};
+
+export const deleteTransformRefs = async (
+ savedObjectsClient: SavedObjectsClientContract,
+ installedEsAssets: EsAssetReference[],
+ pkgName: string,
+ installedEsIdToRemove: string[],
+ currentInstalledEsTransformIds: string[]
+) => {
+ const seen = new Set();
+ const filteredAssets = installedEsAssets.filter(({ type, id }) => {
+ if (type !== ElasticsearchAssetType.transform) return true;
+ const add =
+ (currentInstalledEsTransformIds.includes(id) || !installedEsIdToRemove.includes(id)) &&
+ !seen.has(id);
+ seen.add(id);
+ return add;
+ });
+ return savedObjectsClient.update(PACKAGES_SAVED_OBJECT_TYPE, pkgName, {
+ installed_es: filteredAssets,
+ });
+};
diff --git a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/transform/transform.test.ts b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/transform/transform.test.ts
new file mode 100644
index 0000000000000..0b66077b8699a
--- /dev/null
+++ b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/transform/transform.test.ts
@@ -0,0 +1,420 @@
+/*
+ * 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.
+ */
+
+jest.mock('../../packages/get', () => {
+ return { getInstallation: jest.fn(), getInstallationObject: jest.fn() };
+});
+
+jest.mock('./common', () => {
+ return {
+ getAsset: jest.fn(),
+ };
+});
+
+import { installTransformForDataset } from './install';
+import { ILegacyScopedClusterClient, SavedObject, SavedObjectsClientContract } from 'kibana/server';
+import { ElasticsearchAssetType, Installation, RegistryPackage } from '../../../../types';
+import { getInstallation, getInstallationObject } from '../../packages';
+import { getAsset } from './common';
+// eslint-disable-next-line @kbn/eslint/no-restricted-paths
+import { savedObjectsClientMock } from '../../../../../../../../src/core/server/saved_objects/service/saved_objects_client.mock';
+
+describe('test transform install', () => {
+ let legacyScopedClusterClient: jest.Mocked;
+ let savedObjectsClient: jest.Mocked;
+ beforeEach(() => {
+ legacyScopedClusterClient = {
+ callAsInternalUser: jest.fn(),
+ callAsCurrentUser: jest.fn(),
+ };
+ (getInstallation as jest.MockedFunction).mockReset();
+ (getInstallationObject as jest.MockedFunction).mockReset();
+ savedObjectsClient = savedObjectsClientMock.create();
+ });
+
+ afterEach(() => {
+ jest.clearAllMocks();
+ });
+
+ test('can install new versions and removes older version', async () => {
+ const previousInstallation: Installation = ({
+ installed_es: [
+ {
+ id: 'metrics-endpoint.policy-0.16.0-dev.0',
+ type: ElasticsearchAssetType.ingestPipeline,
+ },
+ {
+ id: 'metrics-endpoint.metadata_current-default-0.15.0-dev.0',
+ type: ElasticsearchAssetType.transform,
+ },
+ ],
+ } as unknown) as Installation;
+
+ const currentInstallation: Installation = ({
+ installed_es: [
+ {
+ id: 'metrics-endpoint.policy-0.16.0-dev.0',
+ type: ElasticsearchAssetType.ingestPipeline,
+ },
+ {
+ id: 'metrics-endpoint.metadata_current-default-0.15.0-dev.0',
+ type: ElasticsearchAssetType.transform,
+ },
+ {
+ id: 'metrics-endpoint.metadata_current-default-0.16.0-dev.0',
+ type: ElasticsearchAssetType.transform,
+ },
+ {
+ id: 'metrics-endpoint.metadata-default-0.16.0-dev.0',
+ type: ElasticsearchAssetType.transform,
+ },
+ ],
+ } as unknown) as Installation;
+ (getAsset as jest.MockedFunction)
+ .mockReturnValueOnce(Buffer.from('{"content": "data"}', 'utf8'))
+ .mockReturnValueOnce(Buffer.from('{"content": "data"}', 'utf8'));
+
+ (getInstallation as jest.MockedFunction)
+ .mockReturnValueOnce(Promise.resolve(previousInstallation))
+ .mockReturnValueOnce(Promise.resolve(currentInstallation));
+
+ (getInstallationObject as jest.MockedFunction<
+ typeof getInstallationObject
+ >).mockReturnValueOnce(
+ Promise.resolve(({
+ attributes: {
+ installed_es: previousInstallation.installed_es,
+ },
+ } as unknown) as SavedObject)
+ );
+
+ await installTransformForDataset(
+ ({
+ name: 'endpoint',
+ version: '0.16.0-dev.0',
+ datasets: [
+ {
+ type: 'metrics',
+ name: 'endpoint.metadata',
+ title: 'Endpoint Metadata',
+ release: 'experimental',
+ package: 'endpoint',
+ ingest_pipeline: 'default',
+ elasticsearch: {
+ 'index_template.mappings': {
+ dynamic: false,
+ },
+ },
+ path: 'metadata',
+ },
+ {
+ type: 'metrics',
+ name: 'endpoint.metadata_current',
+ title: 'Endpoint Metadata Current',
+ release: 'experimental',
+ package: 'endpoint',
+ ingest_pipeline: 'default',
+ elasticsearch: {
+ 'index_template.mappings': {
+ dynamic: false,
+ },
+ },
+ path: 'metadata_current',
+ },
+ ],
+ } as unknown) as RegistryPackage,
+ [
+ 'endpoint-0.16.0-dev.0/dataset/policy/elasticsearch/ingest_pipeline/default.json',
+ 'endpoint-0.16.0-dev.0/dataset/metadata/elasticsearch/transform/default.json',
+ 'endpoint-0.16.0-dev.0/dataset/metadata_current/elasticsearch/transform/default.json',
+ ],
+ legacyScopedClusterClient.callAsCurrentUser,
+ savedObjectsClient
+ );
+
+ expect(legacyScopedClusterClient.callAsCurrentUser.mock.calls).toEqual([
+ [
+ 'transport.request',
+ {
+ method: 'POST',
+ path: '_transform/metrics-endpoint.metadata_current-default-0.15.0-dev.0/_stop',
+ query: 'force=true',
+ ignore: [404],
+ },
+ ],
+ [
+ 'transport.request',
+ {
+ method: 'DELETE',
+ query: 'force=true',
+ path: '_transform/metrics-endpoint.metadata_current-default-0.15.0-dev.0',
+ ignore: [404],
+ },
+ ],
+ [
+ 'transport.request',
+ {
+ method: 'PUT',
+ path: '_transform/metrics-endpoint.metadata-default-0.16.0-dev.0',
+ query: 'defer_validation=true',
+ body: '{"content": "data"}',
+ },
+ ],
+ [
+ 'transport.request',
+ {
+ method: 'PUT',
+ path: '_transform/metrics-endpoint.metadata_current-default-0.16.0-dev.0',
+ query: 'defer_validation=true',
+ body: '{"content": "data"}',
+ },
+ ],
+ [
+ 'transport.request',
+ {
+ method: 'POST',
+ path: '_transform/metrics-endpoint.metadata-default-0.16.0-dev.0/_start',
+ },
+ ],
+ [
+ 'transport.request',
+ {
+ method: 'POST',
+ path: '_transform/metrics-endpoint.metadata_current-default-0.16.0-dev.0/_start',
+ },
+ ],
+ ]);
+
+ expect(savedObjectsClient.update.mock.calls).toEqual([
+ [
+ 'epm-packages',
+ 'endpoint',
+ {
+ installed_es: [
+ {
+ id: 'metrics-endpoint.policy-0.16.0-dev.0',
+ type: 'ingest_pipeline',
+ },
+ {
+ id: 'metrics-endpoint.metadata_current-default-0.15.0-dev.0',
+ type: 'transform',
+ },
+ {
+ id: 'metrics-endpoint.metadata-default-0.16.0-dev.0',
+ type: 'transform',
+ },
+ {
+ id: 'metrics-endpoint.metadata_current-default-0.16.0-dev.0',
+ type: 'transform',
+ },
+ ],
+ },
+ ],
+ [
+ 'epm-packages',
+ 'endpoint',
+ {
+ installed_es: [
+ {
+ id: 'metrics-endpoint.policy-0.16.0-dev.0',
+ type: 'ingest_pipeline',
+ },
+ {
+ id: 'metrics-endpoint.metadata_current-default-0.16.0-dev.0',
+ type: 'transform',
+ },
+ {
+ id: 'metrics-endpoint.metadata-default-0.16.0-dev.0',
+ type: 'transform',
+ },
+ ],
+ },
+ ],
+ ]);
+ });
+
+ test('can install new version and when no older version', async () => {
+ const previousInstallation: Installation = ({
+ installed_es: [],
+ } as unknown) as Installation;
+
+ const currentInstallation: Installation = ({
+ installed_es: [
+ {
+ id: 'metrics-endpoint.metadata-current-default-0.16.0-dev.0',
+ type: ElasticsearchAssetType.transform,
+ },
+ ],
+ } as unknown) as Installation;
+ (getAsset as jest.MockedFunction).mockReturnValueOnce(
+ Buffer.from('{"content": "data"}', 'utf8')
+ );
+ (getInstallation as jest.MockedFunction)
+ .mockReturnValueOnce(Promise.resolve(previousInstallation))
+ .mockReturnValueOnce(Promise.resolve(currentInstallation));
+
+ (getInstallationObject as jest.MockedFunction<
+ typeof getInstallationObject
+ >).mockReturnValueOnce(
+ Promise.resolve(({ attributes: { installed_es: [] } } as unknown) as SavedObject<
+ Installation
+ >)
+ );
+ legacyScopedClusterClient.callAsCurrentUser = jest.fn();
+ await installTransformForDataset(
+ ({
+ name: 'endpoint',
+ version: '0.16.0-dev.0',
+ datasets: [
+ {
+ type: 'metrics',
+ name: 'endpoint.metadata_current',
+ title: 'Endpoint Metadata',
+ release: 'experimental',
+ package: 'endpoint',
+ ingest_pipeline: 'default',
+ elasticsearch: {
+ 'index_template.mappings': {
+ dynamic: false,
+ },
+ },
+ path: 'metadata_current',
+ },
+ ],
+ } as unknown) as RegistryPackage,
+ ['endpoint-0.16.0-dev.0/dataset/metadata_current/elasticsearch/transform/default.json'],
+ legacyScopedClusterClient.callAsCurrentUser,
+ savedObjectsClient
+ );
+
+ expect(legacyScopedClusterClient.callAsCurrentUser.mock.calls).toEqual([
+ [
+ 'transport.request',
+ {
+ method: 'PUT',
+ path: '_transform/metrics-endpoint.metadata_current-default-0.16.0-dev.0',
+ query: 'defer_validation=true',
+ body: '{"content": "data"}',
+ },
+ ],
+ [
+ 'transport.request',
+ {
+ method: 'POST',
+ path: '_transform/metrics-endpoint.metadata_current-default-0.16.0-dev.0/_start',
+ },
+ ],
+ ]);
+ expect(savedObjectsClient.update.mock.calls).toEqual([
+ [
+ 'epm-packages',
+ 'endpoint',
+ {
+ installed_es: [
+ { id: 'metrics-endpoint.metadata_current-default-0.16.0-dev.0', type: 'transform' },
+ ],
+ },
+ ],
+ ]);
+ });
+
+ test('can removes older version when no new install in package', async () => {
+ const previousInstallation: Installation = ({
+ installed_es: [
+ {
+ id: 'metrics-endpoint.metadata-current-default-0.15.0-dev.0',
+ type: ElasticsearchAssetType.transform,
+ },
+ ],
+ } as unknown) as Installation;
+
+ const currentInstallation: Installation = ({
+ installed_es: [],
+ } as unknown) as Installation;
+
+ (getInstallation as jest.MockedFunction)
+ .mockReturnValueOnce(Promise.resolve(previousInstallation))
+ .mockReturnValueOnce(Promise.resolve(currentInstallation));
+
+ (getInstallationObject as jest.MockedFunction<
+ typeof getInstallationObject
+ >).mockReturnValueOnce(
+ Promise.resolve(({
+ attributes: { installed_es: currentInstallation.installed_es },
+ } as unknown) as SavedObject)
+ );
+
+ await installTransformForDataset(
+ ({
+ name: 'endpoint',
+ version: '0.16.0-dev.0',
+ datasets: [
+ {
+ type: 'metrics',
+ name: 'endpoint.metadata',
+ title: 'Endpoint Metadata',
+ release: 'experimental',
+ package: 'endpoint',
+ ingest_pipeline: 'default',
+ elasticsearch: {
+ 'index_template.mappings': {
+ dynamic: false,
+ },
+ },
+ path: 'metadata',
+ },
+ {
+ type: 'metrics',
+ name: 'endpoint.metadata_current',
+ title: 'Endpoint Metadata Current',
+ release: 'experimental',
+ package: 'endpoint',
+ ingest_pipeline: 'default',
+ elasticsearch: {
+ 'index_template.mappings': {
+ dynamic: false,
+ },
+ },
+ path: 'metadata_current',
+ },
+ ],
+ } as unknown) as RegistryPackage,
+ [],
+ legacyScopedClusterClient.callAsCurrentUser,
+ savedObjectsClient
+ );
+
+ expect(legacyScopedClusterClient.callAsCurrentUser.mock.calls).toEqual([
+ [
+ 'transport.request',
+ {
+ ignore: [404],
+ method: 'POST',
+ path: '_transform/metrics-endpoint.metadata-current-default-0.15.0-dev.0/_stop',
+ query: 'force=true',
+ },
+ ],
+ [
+ 'transport.request',
+ {
+ ignore: [404],
+ method: 'DELETE',
+ path: '_transform/metrics-endpoint.metadata-current-default-0.15.0-dev.0',
+ query: 'force=true',
+ },
+ ],
+ ]);
+ expect(savedObjectsClient.update.mock.calls).toEqual([
+ [
+ 'epm-packages',
+ 'endpoint',
+ {
+ installed_es: [],
+ },
+ ],
+ ]);
+ });
+});
diff --git a/x-pack/plugins/ingest_manager/server/services/epm/packages/install.test.ts b/x-pack/plugins/ingest_manager/server/services/epm/packages/install.test.ts
new file mode 100644
index 0000000000000..cc26e631a6215
--- /dev/null
+++ b/x-pack/plugins/ingest_manager/server/services/epm/packages/install.test.ts
@@ -0,0 +1,77 @@
+/*
+ * 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 { ElasticsearchAssetType, Installation, KibanaAssetType } from '../../../types';
+import { SavedObject } from 'src/core/server';
+import { getInstallType } from './install';
+
+const mockInstallation: SavedObject = {
+ id: 'test-pkg',
+ references: [],
+ type: 'epm-packages',
+ attributes: {
+ id: 'test-pkg',
+ installed_kibana: [{ type: KibanaAssetType.dashboard, id: 'dashboard-1' }],
+ installed_es: [{ type: ElasticsearchAssetType.ingestPipeline, id: 'pipeline' }],
+ es_index_patterns: { pattern: 'pattern-name' },
+ name: 'test packagek',
+ version: '1.0.0',
+ install_status: 'installed',
+ install_version: '1.0.0',
+ install_started_at: new Date().toISOString(),
+ },
+};
+const mockInstallationUpdateFail: SavedObject = {
+ id: 'test-pkg',
+ references: [],
+ type: 'epm-packages',
+ attributes: {
+ id: 'test-pkg',
+ installed_kibana: [{ type: KibanaAssetType.dashboard, id: 'dashboard-1' }],
+ installed_es: [{ type: ElasticsearchAssetType.ingestPipeline, id: 'pipeline' }],
+ es_index_patterns: { pattern: 'pattern-name' },
+ name: 'test packagek',
+ version: '1.0.0',
+ install_status: 'installing',
+ install_version: '1.0.1',
+ install_started_at: new Date().toISOString(),
+ },
+};
+describe('install', () => {
+ describe('getInstallType', () => {
+ it('should return correct type when installing and no other version is currently installed', () => {});
+ const installTypeInstall = getInstallType({ pkgVersion: '1.0.0', installedPkg: undefined });
+ expect(installTypeInstall).toBe('install');
+
+ it('should return correct type when installing the same version', () => {});
+ const installTypeReinstall = getInstallType({
+ pkgVersion: '1.0.0',
+ installedPkg: mockInstallation,
+ });
+ expect(installTypeReinstall).toBe('reinstall');
+
+ it('should return correct type when moving from one version to another', () => {});
+ const installTypeUpdate = getInstallType({
+ pkgVersion: '1.0.1',
+ installedPkg: mockInstallation,
+ });
+ expect(installTypeUpdate).toBe('update');
+
+ it('should return correct type when update fails and trys again', () => {});
+ const installTypeReupdate = getInstallType({
+ pkgVersion: '1.0.1',
+ installedPkg: mockInstallationUpdateFail,
+ });
+ expect(installTypeReupdate).toBe('reupdate');
+
+ it('should return correct type when attempting to rollback from a failed update', () => {});
+ const installTypeRollback = getInstallType({
+ pkgVersion: '1.0.0',
+ installedPkg: mockInstallationUpdateFail,
+ });
+ expect(installTypeRollback).toBe('rollback');
+ });
+});
diff --git a/x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts b/x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts
index e49dbe8f0b5d4..e6144e0309594 100644
--- a/x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts
+++ b/x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts
@@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { SavedObjectsClientContract } from 'src/core/server';
+import { SavedObject, SavedObjectsClientContract } from 'src/core/server';
import semver from 'semver';
import { PACKAGES_SAVED_OBJECT_TYPE, MAX_TIME_COMPLETE_INSTALL } from '../../../constants';
import {
@@ -16,6 +16,7 @@ import {
KibanaAssetReference,
EsAssetReference,
ElasticsearchAssetType,
+ InstallType,
} from '../../../types';
import { installIndexPatterns } from '../kibana/index_pattern/install';
import * as Registry from '../registry';
@@ -34,6 +35,7 @@ import { updateCurrentWriteIndices } from '../elasticsearch/template/template';
import { deleteKibanaSavedObjectsAssets } from './remove';
import { PackageOutdatedError } from '../../../errors';
import { getPackageSavedObjects } from './get';
+import { installTransformForDataset } from '../elasticsearch/transform/install';
export async function installLatestPackage(options: {
savedObjectsClient: SavedObjectsClientContract;
@@ -110,11 +112,13 @@ export async function installPackage({
const latestPackage = await Registry.fetchFindLatestPackage(pkgName);
// get the currently installed package
const installedPkg = await getInstallationObject({ savedObjectsClient, pkgName });
- const reinstall = pkgVersion === installedPkg?.attributes.version;
- const reupdate = pkgVersion === installedPkg?.attributes.install_version;
- // let the user install if using the force flag or this is a reinstall or reupdate due to intallation interruption
- if (semver.lt(pkgVersion, latestPackage.version) && !force && !reinstall && !reupdate) {
+ const installType = getInstallType({ pkgVersion, installedPkg });
+
+ // let the user install if using the force flag or needing to reinstall or install a previous version due to failed update
+ const installOutOfDateVersionOk =
+ installType === 'reinstall' || installType === 'reupdate' || installType === 'rollback';
+ if (semver.lt(pkgVersion, latestPackage.version) && !force && !installOutOfDateVersionOk) {
throw new PackageOutdatedError(`${pkgkey} is out-of-date and cannot be installed or updated`);
}
const paths = await Registry.getArchiveInfo(pkgName, pkgVersion);
@@ -188,28 +192,51 @@ export async function installPackage({
// update current backing indices of each data stream
await updateCurrentWriteIndices(callCluster, installedTemplates);
- // if this is an update, delete the previous version's pipelines
- if (installedPkg && !reinstall) {
+ const installedTransforms = await installTransformForDataset(
+ registryPackageInfo,
+ paths,
+ callCluster,
+ savedObjectsClient
+ );
+
+ // if this is an update or retrying an update, delete the previous version's pipelines
+ if (installType === 'update' || installType === 'reupdate') {
await deletePreviousPipelines(
callCluster,
savedObjectsClient,
pkgName,
+ // @ts-ignore installType conditions already check for existence of installedPkg
installedPkg.attributes.version
);
}
-
+ // pipelines from a different version may have installed during a failed update
+ if (installType === 'rollback') {
+ await deletePreviousPipelines(
+ callCluster,
+ savedObjectsClient,
+ pkgName,
+ // @ts-ignore installType conditions already check for existence of installedPkg
+ installedPkg.attributes.install_version
+ );
+ }
const installedTemplateRefs = installedTemplates.map((template) => ({
id: template.templateName,
type: ElasticsearchAssetType.indexTemplate,
}));
await Promise.all([installKibanaAssetsPromise, installIndexPatternPromise]);
+
// update to newly installed version when all assets are successfully installed
if (installedPkg) await updateVersion(savedObjectsClient, pkgName, pkgVersion);
await savedObjectsClient.update(PACKAGES_SAVED_OBJECT_TYPE, pkgName, {
install_version: pkgVersion,
install_status: 'installed',
});
- return [...installedKibanaAssetsRefs, ...installedPipelines, ...installedTemplateRefs];
+ return [
+ ...installedKibanaAssetsRefs,
+ ...installedPipelines,
+ ...installedTemplateRefs,
+ ...installedTransforms,
+ ];
}
const updateVersion = async (
@@ -326,3 +353,23 @@ export async function ensurePackagesCompletedInstall(
await Promise.all(installingPromises);
return installingPackages;
}
+
+export function getInstallType({
+ pkgVersion,
+ installedPkg,
+}: {
+ pkgVersion: string;
+ installedPkg: SavedObject | undefined;
+}): InstallType {
+ const isInstalledPkg = !!installedPkg;
+ const currentPkgVersion = installedPkg?.attributes.version;
+ const lastStartedInstallVersion = installedPkg?.attributes.install_version;
+ if (!isInstalledPkg) return 'install';
+ if (pkgVersion === currentPkgVersion && pkgVersion !== lastStartedInstallVersion)
+ return 'rollback';
+ if (pkgVersion === currentPkgVersion) return 'reinstall';
+ if (pkgVersion === lastStartedInstallVersion && pkgVersion !== currentPkgVersion)
+ return 'reupdate';
+ if (pkgVersion !== lastStartedInstallVersion && pkgVersion !== currentPkgVersion) return 'update';
+ throw new Error('unknown install type');
+}
diff --git a/x-pack/plugins/ingest_manager/server/services/epm/packages/remove.ts b/x-pack/plugins/ingest_manager/server/services/epm/packages/remove.ts
index 71eee1ee82c90..2434ebf27aa5d 100644
--- a/x-pack/plugins/ingest_manager/server/services/epm/packages/remove.ts
+++ b/x-pack/plugins/ingest_manager/server/services/epm/packages/remove.ts
@@ -6,12 +6,17 @@
import { SavedObjectsClientContract } from 'src/core/server';
import Boom from 'boom';
-import { PACKAGES_SAVED_OBJECT_TYPE, PACKAGE_POLICY_SAVED_OBJECT_TYPE } from '../../../constants';
-import { AssetReference, AssetType, ElasticsearchAssetType } from '../../../types';
-import { CallESAsCurrentUser } from '../../../types';
+import { PACKAGE_POLICY_SAVED_OBJECT_TYPE, PACKAGES_SAVED_OBJECT_TYPE } from '../../../constants';
+import {
+ AssetReference,
+ AssetType,
+ CallESAsCurrentUser,
+ ElasticsearchAssetType,
+} from '../../../types';
import { getInstallation, savedObjectTypes } from './index';
import { deletePipeline } from '../elasticsearch/ingest_pipeline/';
import { installIndexPatterns } from '../kibana/index_pattern/install';
+import { deleteTransforms } from '../elasticsearch/transform/remove';
import { packagePolicyService, appContextService } from '../..';
import { splitPkgKey, deletePackageCache, getArchiveInfo } from '../registry';
@@ -72,6 +77,8 @@ async function deleteAssets(
return deletePipeline(callCluster, id);
} else if (assetType === ElasticsearchAssetType.indexTemplate) {
return deleteTemplate(callCluster, id);
+ } else if (assetType === ElasticsearchAssetType.transform) {
+ return deleteTransforms(callCluster, [id]);
}
});
try {
diff --git a/x-pack/plugins/ingest_manager/server/types/index.tsx b/x-pack/plugins/ingest_manager/server/types/index.tsx
index e01568cfbb3c9..2746dfcd00ce3 100644
--- a/x-pack/plugins/ingest_manager/server/types/index.tsx
+++ b/x-pack/plugins/ingest_manager/server/types/index.tsx
@@ -63,6 +63,7 @@ export {
IndexTemplateMappings,
Settings,
SettingsSOAttributes,
+ InstallType,
// Agent Request types
PostAgentEnrollRequest,
PostAgentCheckinRequest,
diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/gsub.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/gsub.tsx
index a42df6873d57b..2f2a75853d9e9 100644
--- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/gsub.tsx
+++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/gsub.tsx
@@ -6,8 +6,6 @@
import React, { FunctionComponent } from 'react';
import { i18n } from '@kbn/i18n';
-import { FormattedMessage } from '@kbn/i18n/react';
-import { EuiCode } from '@elastic/eui';
import { FIELD_TYPES, fieldValidators, UseField, Field } from '../../../../../../shared_imports';
@@ -87,17 +85,7 @@ export const Gsub: FunctionComponent = () => {
- {'field'},
- }}
- />
- }
- />
+
>
diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/html_strip.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/html_strip.tsx
index fb1a2d97672b0..c3f38cb021371 100644
--- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/html_strip.tsx
+++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/html_strip.tsx
@@ -6,8 +6,6 @@
import React, { FunctionComponent } from 'react';
import { i18n } from '@kbn/i18n';
-import { FormattedMessage } from '@kbn/i18n/react';
-import { EuiCode } from '@elastic/eui';
import { FieldNameField } from './common_fields/field_name_field';
import { IgnoreMissingField } from './common_fields/ignore_missing_field';
@@ -23,15 +21,7 @@ export const HtmlStrip: FunctionComponent = () => {
)}
/>
- {'field'} }}
- />
- }
- />
+
>
diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/join.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/join.tsx
index ab077d3337f63..c70f48e0297e4 100644
--- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/join.tsx
+++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/join.tsx
@@ -6,8 +6,6 @@
import React, { FunctionComponent } from 'react';
import { i18n } from '@kbn/i18n';
-import { FormattedMessage } from '@kbn/i18n/react';
-import { EuiCode } from '@elastic/eui';
import { FIELD_TYPES, fieldValidators, UseField, Field } from '../../../../../../shared_imports';
@@ -55,17 +53,7 @@ export const Join: FunctionComponent = () => {
- {'field'},
- }}
- />
- }
- />
+
>
);
};
diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/json.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/json.tsx
index b68b398325085..f01228a26297b 100644
--- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/json.tsx
+++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/json.tsx
@@ -65,12 +65,7 @@ export const Json: FunctionComponent = () => {
)}
/>
-
+
+ {'enrich policy'}
+
+ ),
+ }}
+ />
+ );
+ },
},
fail: {
FieldsComponent: Fail,
@@ -178,6 +198,10 @@ export const mapProcessorTypeToDescriptor: MapProcessorTypeToDescriptor = {
label: i18n.translate('xpack.ingestPipelines.processors.label.fail', {
defaultMessage: 'Fail',
}),
+ description: i18n.translate('xpack.ingestPipelines.processors.description.fail', {
+ defaultMessage:
+ 'Returns a custom error message on failure. Often used to notify requesters of required conditions.',
+ }),
},
foreach: {
FieldsComponent: Foreach,
@@ -185,6 +209,9 @@ export const mapProcessorTypeToDescriptor: MapProcessorTypeToDescriptor = {
label: i18n.translate('xpack.ingestPipelines.processors.label.foreach', {
defaultMessage: 'Foreach',
}),
+ description: i18n.translate('xpack.ingestPipelines.processors.description.foreach', {
+ defaultMessage: 'Applies an ingest processor to each value in an array.',
+ }),
},
geoip: {
FieldsComponent: GeoIP,
@@ -192,6 +219,10 @@ export const mapProcessorTypeToDescriptor: MapProcessorTypeToDescriptor = {
label: i18n.translate('xpack.ingestPipelines.processors.label.geoip', {
defaultMessage: 'GeoIP',
}),
+ description: i18n.translate('xpack.ingestPipelines.processors.description.geoip', {
+ defaultMessage:
+ 'Adds geo data based on an IP address. Uses geo data from a Maxmind database file.',
+ }),
},
grok: {
FieldsComponent: Grok,
@@ -199,6 +230,25 @@ export const mapProcessorTypeToDescriptor: MapProcessorTypeToDescriptor = {
label: i18n.translate('xpack.ingestPipelines.processors.label.grok', {
defaultMessage: 'Grok',
}),
+ description: function Description() {
+ const {
+ services: { documentation },
+ } = useKibana();
+ const esDocUrl = documentation.getEsDocsBasePath();
+ return (
+
+ {'grok'}
+
+ ),
+ }}
+ />
+ );
+ },
},
gsub: {
FieldsComponent: Gsub,
@@ -206,6 +256,9 @@ export const mapProcessorTypeToDescriptor: MapProcessorTypeToDescriptor = {
label: i18n.translate('xpack.ingestPipelines.processors.label.gsub', {
defaultMessage: 'Gsub',
}),
+ description: i18n.translate('xpack.ingestPipelines.processors.description.gsub', {
+ defaultMessage: 'Uses a regular expression to replace field substrings.',
+ }),
},
html_strip: {
FieldsComponent: HtmlStrip,
@@ -213,6 +266,9 @@ export const mapProcessorTypeToDescriptor: MapProcessorTypeToDescriptor = {
label: i18n.translate('xpack.ingestPipelines.processors.label.htmlStrip', {
defaultMessage: 'HTML strip',
}),
+ description: i18n.translate('xpack.ingestPipelines.processors.description.htmlStrip', {
+ defaultMessage: 'Removes HTML tags from a field.',
+ }),
},
inference: {
FieldsComponent: Inference,
@@ -220,6 +276,10 @@ export const mapProcessorTypeToDescriptor: MapProcessorTypeToDescriptor = {
label: i18n.translate('xpack.ingestPipelines.processors.label.inference', {
defaultMessage: 'Inference',
}),
+ description: i18n.translate('xpack.ingestPipelines.processors.description.inference', {
+ defaultMessage:
+ 'Uses a pre-trained data frame analytics model to infer against incoming data.',
+ }),
},
join: {
FieldsComponent: Join,
@@ -227,6 +287,10 @@ export const mapProcessorTypeToDescriptor: MapProcessorTypeToDescriptor = {
label: i18n.translate('xpack.ingestPipelines.processors.label.join', {
defaultMessage: 'Join',
}),
+ description: i18n.translate('xpack.ingestPipelines.processors.description.join', {
+ defaultMessage:
+ 'Joins array elements into a string. Inserts a separator between each element.',
+ }),
},
json: {
FieldsComponent: Json,
@@ -234,6 +298,9 @@ export const mapProcessorTypeToDescriptor: MapProcessorTypeToDescriptor = {
label: i18n.translate('xpack.ingestPipelines.processors.label.json', {
defaultMessage: 'JSON',
}),
+ description: i18n.translate('xpack.ingestPipelines.processors.description.json', {
+ defaultMessage: 'Creates a JSON object from a compatible string.',
+ }),
},
kv: {
FieldsComponent: Kv,
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/bucket_nesting_editor.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/bucket_nesting_editor.tsx
index 198be7085f5fc..e5d63f1f92e19 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/bucket_nesting_editor.tsx
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/bucket_nesting_editor.tsx
@@ -80,7 +80,8 @@ export function BucketNestingEditor({
values: { field: fieldName },
})
: i18n.translate('xpack.lens.indexPattern.groupingOverallDateHistogram', {
- defaultMessage: 'Dates overall',
+ defaultMessage: 'Top values for each {field}',
+ values: { field: fieldName },
})
}
checked={!prevColumn}
@@ -96,7 +97,7 @@ export function BucketNestingEditor({
values: { target: target.fieldName },
})
: i18n.translate('xpack.lens.indexPattern.groupingSecondDateHistogram', {
- defaultMessage: 'Dates for each {target}',
+ defaultMessage: 'Overall top {target}',
values: { target: target.fieldName },
})
}
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx
index a0cc5ec352130..cf15c29844053 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx
@@ -117,14 +117,7 @@ export const InnerFieldItem = function InnerFieldItem(props: FieldItemProps) {
);
function fetchData() {
- if (
- state.isLoading ||
- (field.type !== 'number' &&
- field.type !== 'string' &&
- field.type !== 'date' &&
- field.type !== 'boolean' &&
- field.type !== 'ip')
- ) {
+ if (state.isLoading) {
return;
}
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/loader.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/loader.test.ts
index 660be9514a92f..19213d4afc9bc 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/loader.test.ts
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/loader.test.ts
@@ -93,6 +93,16 @@ const indexPattern1 = ({
searchable: true,
esTypes: ['keyword'],
},
+ {
+ name: 'scripted',
+ displayName: 'Scripted',
+ type: 'string',
+ searchable: true,
+ aggregatable: true,
+ scripted: true,
+ lang: 'painless',
+ script: '1234',
+ },
documentField,
],
} as unknown) as IndexPattern;
@@ -156,12 +166,13 @@ const indexPattern2 = ({
aggregatable: true,
searchable: true,
scripted: true,
+ lang: 'painless',
+ script: '1234',
aggregationRestrictions: {
terms: {
agg: 'terms',
},
},
- esTypes: ['keyword'],
},
documentField,
],
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/loader.ts b/x-pack/plugins/lens/public/indexpattern_datasource/loader.ts
index 585a1281cbf51..0ab658b961336 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/loader.ts
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/loader.ts
@@ -55,15 +55,27 @@ export async function loadIndexPatterns({
!indexPatternsUtils.isNestedField(field) && (!!field.aggregatable || !!field.scripted)
)
.map(
- (field): IndexPatternField => ({
- name: field.name,
- displayName: field.displayName,
- type: field.type,
- aggregatable: field.aggregatable,
- searchable: field.searchable,
- scripted: field.scripted,
- esTypes: field.esTypes,
- })
+ (field): IndexPatternField => {
+ // Convert the getters on the index pattern service into plain JSON
+ const base = {
+ name: field.name,
+ displayName: field.displayName,
+ type: field.type,
+ aggregatable: field.aggregatable,
+ searchable: field.searchable,
+ esTypes: field.esTypes,
+ scripted: field.scripted,
+ };
+
+ // Simplifies tests by hiding optional properties instead of undefined
+ return base.scripted
+ ? {
+ ...base,
+ lang: field.lang,
+ script: field.script,
+ }
+ : base;
+ }
)
.concat(documentField);
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/mocks.ts b/x-pack/plugins/lens/public/indexpattern_datasource/mocks.ts
index 31e6240993d36..21ed23321cf57 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/mocks.ts
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/mocks.ts
@@ -64,6 +64,16 @@ export const createMockedIndexPattern = (): IndexPattern => ({
searchable: true,
esTypes: ['keyword'],
},
+ {
+ name: 'scripted',
+ displayName: 'Scripted',
+ type: 'string',
+ searchable: true,
+ aggregatable: true,
+ scripted: true,
+ lang: 'painless',
+ script: '1234',
+ },
],
});
@@ -95,6 +105,8 @@ export const createMockedRestrictedIndexPattern = () => ({
searchable: true,
scripted: true,
esTypes: ['keyword'],
+ lang: 'painless',
+ script: '1234',
},
],
typeMeta: {
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/types.ts b/x-pack/plugins/lens/public/indexpattern_datasource/types.ts
index c101f1354b703..21ca41234fdf1 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/types.ts
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/types.ts
@@ -4,6 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
+import { IFieldType } from 'src/plugins/data/common';
import { IndexPatternColumn } from './operations';
import { IndexPatternAggRestrictions } from '../../../../../src/plugins/data/public';
@@ -22,16 +23,10 @@ export interface IndexPattern {
hasRestrictions: boolean;
}
-export interface IndexPatternField {
- name: string;
+export type IndexPatternField = IFieldType & {
displayName: string;
- type: string;
- esTypes?: string[];
- aggregatable: boolean;
- scripted?: boolean;
- searchable: boolean;
aggregationRestrictions?: Partial;
-}
+};
export interface IndexPatternLayer {
columnOrder: string[];
diff --git a/x-pack/plugins/lens/server/routes/field_stats.ts b/x-pack/plugins/lens/server/routes/field_stats.ts
index 20d3e2b4164ca..a7368a12f0e2c 100644
--- a/x-pack/plugins/lens/server/routes/field_stats.ts
+++ b/x-pack/plugins/lens/server/routes/field_stats.ts
@@ -8,6 +8,7 @@ import Boom from 'boom';
import DateMath from '@elastic/datemath';
import { schema } from '@kbn/config-schema';
import { CoreSetup } from 'src/core/server';
+import { IFieldType } from 'src/plugins/data/common';
import { ESSearchResponse } from '../../../apm/typings/elasticsearch';
import { FieldStatsResponse, BASE_API_URL } from '../../common';
@@ -33,6 +34,9 @@ export async function initFieldsRoute(setup: CoreSetup) {
name: schema.string(),
type: schema.string(),
esTypes: schema.maybe(schema.arrayOf(schema.string())),
+ scripted: schema.maybe(schema.boolean()),
+ lang: schema.maybe(schema.string()),
+ script: schema.maybe(schema.string()),
},
{ unknowns: 'allow' }
),
@@ -83,21 +87,15 @@ export async function initFieldsRoute(setup: CoreSetup) {
return res.ok({
body: await getNumberHistogram(search, field),
});
- } else if (field.type === 'string') {
- return res.ok({
- body: await getStringSamples(search, field),
- });
} else if (field.type === 'date') {
return res.ok({
body: await getDateHistogram(search, field, { fromDate, toDate }),
});
- } else if (field.type === 'boolean') {
- return res.ok({
- body: await getStringSamples(search, field),
- });
}
- return res.ok({});
+ return res.ok({
+ body: await getStringSamples(search, field),
+ });
} catch (e) {
if (e.status === 404) {
return res.notFound();
@@ -119,8 +117,10 @@ export async function initFieldsRoute(setup: CoreSetup) {
export async function getNumberHistogram(
aggSearchWithBody: (body: unknown) => Promise,
- field: { name: string; type: string; esTypes?: string[] }
+ field: IFieldType
): Promise {
+ const fieldRef = getFieldRef(field);
+
const searchBody = {
sample: {
sampler: { shard_size: SHARD_SIZE },
@@ -131,9 +131,9 @@ export async function getNumberHistogram(
max_value: {
max: { field: field.name },
},
- sample_count: { value_count: { field: field.name } },
+ sample_count: { value_count: { ...fieldRef } },
top_values: {
- terms: { field: field.name, size: 10 },
+ terms: { ...fieldRef, size: 10 },
},
},
},
@@ -206,15 +206,20 @@ export async function getNumberHistogram(
export async function getStringSamples(
aggSearchWithBody: (body: unknown) => unknown,
- field: { name: string; type: string }
+ field: IFieldType
): Promise {
+ const fieldRef = getFieldRef(field);
+
const topValuesBody = {
sample: {
sampler: { shard_size: SHARD_SIZE },
aggs: {
- sample_count: { value_count: { field: field.name } },
+ sample_count: { value_count: { ...fieldRef } },
top_values: {
- terms: { field: field.name, size: 10 },
+ terms: {
+ ...fieldRef,
+ size: 10,
+ },
},
},
},
@@ -241,7 +246,7 @@ export async function getStringSamples(
// This one is not sampled so that it returns the full date range
export async function getDateHistogram(
aggSearchWithBody: (body: unknown) => unknown,
- field: { name: string; type: string },
+ field: IFieldType,
range: { fromDate: string; toDate: string }
): Promise {
const fromDate = DateMath.parse(range.fromDate);
@@ -265,7 +270,7 @@ export async function getDateHistogram(
const fixedInterval = `${interval}ms`;
const histogramBody = {
- histo: { date_histogram: { field: field.name, fixed_interval: fixedInterval } },
+ histo: { date_histogram: { ...getFieldRef(field), fixed_interval: fixedInterval } },
};
const results = (await aggSearchWithBody(histogramBody)) as ESSearchResponse<
unknown,
@@ -283,3 +288,14 @@ export async function getDateHistogram(
},
};
}
+
+function getFieldRef(field: IFieldType) {
+ return field.scripted
+ ? {
+ script: {
+ lang: field.lang as string,
+ source: field.script as string,
+ },
+ }
+ : { field: field.name };
+}
diff --git a/x-pack/plugins/maps/public/lazy_load_bundle/index.ts b/x-pack/plugins/maps/public/lazy_load_bundle/index.ts
index 5f2a640aa9d0f..03752a1c3e11e 100644
--- a/x-pack/plugins/maps/public/lazy_load_bundle/index.ts
+++ b/x-pack/plugins/maps/public/lazy_load_bundle/index.ts
@@ -7,7 +7,7 @@
import { AnyAction } from 'redux';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { IndexPatternsContract } from 'src/plugins/data/public/index_patterns';
-import { ReactElement } from 'react';
+import { AppMountContext, AppMountParameters } from 'kibana/public';
import { IndexPattern } from 'src/plugins/data/public';
import { Embeddable, IContainer } from '../../../../../src/plugins/embeddable/public';
import { LayerDescriptor } from '../../common/descriptor_types';
@@ -40,7 +40,7 @@ interface LazyLoadedMapModules {
initialLayers?: LayerDescriptor[]
) => LayerDescriptor[];
mergeInputWithSavedMap: any;
- renderApp: (context: unknown, params: unknown) => ReactElement;
+ renderApp: (context: AppMountContext, params: AppMountParameters) => Promise<() => void>;
createSecurityLayerDescriptors: (
indexPatternId: string,
indexPatternTitle: string
@@ -57,7 +57,6 @@ export async function lazyLoadMapModules(): Promise {
loadModulesPromise = new Promise(async (resolve) => {
const {
- // @ts-expect-error
getMapsSavedObjectLoader,
getQueryableUniqueIndexPatternIds,
MapEmbeddable,
@@ -68,7 +67,6 @@ export async function lazyLoadMapModules(): Promise {
addLayerWithoutDataSync,
getInitialLayers,
mergeInputWithSavedMap,
- // @ts-expect-error
renderApp,
createSecurityLayerDescriptors,
registerLayerWizard,
diff --git a/x-pack/plugins/maps/public/lazy_load_bundle/lazy/index.ts b/x-pack/plugins/maps/public/lazy_load_bundle/lazy/index.ts
index e55160383a8f3..28f5acdc17656 100644
--- a/x-pack/plugins/maps/public/lazy_load_bundle/lazy/index.ts
+++ b/x-pack/plugins/maps/public/lazy_load_bundle/lazy/index.ts
@@ -7,7 +7,6 @@
// These are map-dependencies of the embeddable.
// By lazy-loading these, the Maps-app can register the embeddable when the plugin mounts, without actually pulling all the code.
-// @ts-expect-error
export * from '../../routing/bootstrap/services/gis_map_saved_object_loader';
export * from '../../embeddable/map_embeddable';
export * from '../../kibana_services';
@@ -16,7 +15,6 @@ export * from '../../actions';
export * from '../../selectors/map_selectors';
export * from '../../routing/bootstrap/get_initial_layers';
export * from '../../embeddable/merge_input_with_saved_map';
-// @ts-expect-error
export * from '../../routing/maps_router';
export * from '../../classes/layers/solution_layers/security';
export { registerLayerWizard } from '../../classes/layers/layer_wizard_registry';
diff --git a/x-pack/plugins/maps/public/plugin.ts b/x-pack/plugins/maps/public/plugin.ts
index b08135b4e486c..00ee7f376efc6 100644
--- a/x-pack/plugins/maps/public/plugin.ts
+++ b/x-pack/plugins/maps/public/plugin.ts
@@ -123,7 +123,6 @@ export class MapsPlugin
icon: `plugins/${APP_ID}/icon.svg`,
euiIconType: APP_ICON,
category: DEFAULT_APP_CATEGORIES.kibana,
- // @ts-expect-error
async mount(context, params) {
const { renderApp } = await lazyLoadMapModules();
return renderApp(context, params);
diff --git a/x-pack/plugins/maps/public/routing/bootstrap/get_initial_layers.js b/x-pack/plugins/maps/public/routing/bootstrap/get_initial_layers.ts
similarity index 87%
rename from x-pack/plugins/maps/public/routing/bootstrap/get_initial_layers.js
rename to x-pack/plugins/maps/public/routing/bootstrap/get_initial_layers.ts
index b47f83d5a6664..e828dc88409cb 100644
--- a/x-pack/plugins/maps/public/routing/bootstrap/get_initial_layers.js
+++ b/x-pack/plugins/maps/public/routing/bootstrap/get_initial_layers.ts
@@ -15,15 +15,19 @@ import '../../classes/sources/es_pew_pew_source';
import '../../classes/sources/kibana_regionmap_source';
import '../../classes/sources/es_geo_grid_source';
import '../../classes/sources/xyz_tms_source';
+import { LayerDescriptor } from '../../../common/descriptor_types';
+// @ts-expect-error
import { KibanaTilemapSource } from '../../classes/sources/kibana_tilemap_source';
import { TileLayer } from '../../classes/layers/tile_layer/tile_layer';
+// @ts-expect-error
import { EMSTMSSource } from '../../classes/sources/ems_tms_source';
+// @ts-expect-error
import { VectorTileLayer } from '../../classes/layers/vector_tile_layer/vector_tile_layer';
import { getIsEmsEnabled, getToasts } from '../../kibana_services';
import { INITIAL_LAYERS_KEY } from '../../../common/constants';
import { getKibanaTileMap } from '../../meta';
-export function getInitialLayers(layerListJSON, initialLayers = []) {
+export function getInitialLayers(layerListJSON?: string, initialLayers: LayerDescriptor[] = []) {
if (layerListJSON) {
return JSON.parse(layerListJSON);
}
@@ -58,9 +62,10 @@ export function getInitialLayersFromUrlParam() {
try {
let mapInitLayers = mapAppParams.get(INITIAL_LAYERS_KEY);
- if (mapInitLayers[mapInitLayers.length - 1] === '#') {
- mapInitLayers = mapInitLayers.substr(0, mapInitLayers.length - 1);
+ if (mapInitLayers![mapInitLayers!.length - 1] === '#') {
+ mapInitLayers = mapInitLayers!.substr(0, mapInitLayers!.length - 1);
}
+ // @ts-ignore
return rison.decode_array(mapInitLayers);
} catch (e) {
getToasts().addWarning({
diff --git a/x-pack/plugins/maps/public/routing/bootstrap/get_initial_query.js b/x-pack/plugins/maps/public/routing/bootstrap/get_initial_query.ts
similarity index 73%
rename from x-pack/plugins/maps/public/routing/bootstrap/get_initial_query.js
rename to x-pack/plugins/maps/public/routing/bootstrap/get_initial_query.ts
index 1f2cf27077623..43293d152dbff 100644
--- a/x-pack/plugins/maps/public/routing/bootstrap/get_initial_query.js
+++ b/x-pack/plugins/maps/public/routing/bootstrap/get_initial_query.ts
@@ -5,8 +5,15 @@
*/
import { getData } from '../../kibana_services';
+import { MapsAppState } from '../state_syncing/app_state_manager';
-export function getInitialQuery({ mapStateJSON, appState = {} }) {
+export function getInitialQuery({
+ mapStateJSON,
+ appState = {},
+}: {
+ mapStateJSON?: string;
+ appState: MapsAppState;
+}) {
if (appState.query) {
return appState.query;
}
diff --git a/x-pack/plugins/maps/public/routing/bootstrap/get_initial_refresh_config.js b/x-pack/plugins/maps/public/routing/bootstrap/get_initial_refresh_config.ts
similarity index 81%
rename from x-pack/plugins/maps/public/routing/bootstrap/get_initial_refresh_config.js
rename to x-pack/plugins/maps/public/routing/bootstrap/get_initial_refresh_config.ts
index d7b3bbf5b4ab2..7d759cb25052f 100644
--- a/x-pack/plugins/maps/public/routing/bootstrap/get_initial_refresh_config.js
+++ b/x-pack/plugins/maps/public/routing/bootstrap/get_initial_refresh_config.ts
@@ -4,10 +4,17 @@
* you may not use this file except in compliance with the Elastic License.
*/
+import { QueryState } from 'src/plugins/data/public';
import { getUiSettings } from '../../kibana_services';
import { UI_SETTINGS } from '../../../../../../src/plugins/data/public';
-export function getInitialRefreshConfig({ mapStateJSON, globalState = {} }) {
+export function getInitialRefreshConfig({
+ mapStateJSON,
+ globalState = {},
+}: {
+ mapStateJSON?: string;
+ globalState: QueryState;
+}) {
const uiSettings = getUiSettings();
if (mapStateJSON) {
diff --git a/x-pack/plugins/maps/public/routing/bootstrap/get_initial_time_filters.js b/x-pack/plugins/maps/public/routing/bootstrap/get_initial_time_filters.ts
similarity index 75%
rename from x-pack/plugins/maps/public/routing/bootstrap/get_initial_time_filters.js
rename to x-pack/plugins/maps/public/routing/bootstrap/get_initial_time_filters.ts
index 9c11dabe03923..549cc154fe487 100644
--- a/x-pack/plugins/maps/public/routing/bootstrap/get_initial_time_filters.js
+++ b/x-pack/plugins/maps/public/routing/bootstrap/get_initial_time_filters.ts
@@ -4,9 +4,16 @@
* you may not use this file except in compliance with the Elastic License.
*/
+import { QueryState } from 'src/plugins/data/public';
import { getUiSettings } from '../../kibana_services';
-export function getInitialTimeFilters({ mapStateJSON, globalState }) {
+export function getInitialTimeFilters({
+ mapStateJSON,
+ globalState,
+}: {
+ mapStateJSON?: string;
+ globalState: QueryState;
+}) {
if (mapStateJSON) {
const mapState = JSON.parse(mapStateJSON);
if (mapState.timeFilters) {
diff --git a/x-pack/plugins/maps/public/routing/bootstrap/services/gis_map_saved_object_loader.js b/x-pack/plugins/maps/public/routing/bootstrap/services/gis_map_saved_object_loader.ts
similarity index 100%
rename from x-pack/plugins/maps/public/routing/bootstrap/services/gis_map_saved_object_loader.js
rename to x-pack/plugins/maps/public/routing/bootstrap/services/gis_map_saved_object_loader.ts
diff --git a/x-pack/plugins/maps/public/routing/bootstrap/services/saved_gis_map.ts b/x-pack/plugins/maps/public/routing/bootstrap/services/saved_gis_map.ts
index 6f8e7777f671b..511f015b0ff80 100644
--- a/x-pack/plugins/maps/public/routing/bootstrap/services/saved_gis_map.ts
+++ b/x-pack/plugins/maps/public/routing/bootstrap/services/saved_gis_map.ts
@@ -27,7 +27,6 @@ import { copyPersistentState } from '../../../reducers/util';
// @ts-expect-error
import { extractReferences, injectReferences } from '../../../../common/migrations/references';
import { getExistingMapPath, MAP_SAVED_OBJECT_TYPE } from '../../../../common/constants';
-// @ts-expect-error
import { getStore } from '../../store_operations';
import { MapStoreState } from '../../../reducers/store';
import { LayerDescriptor } from '../../../../common/descriptor_types';
diff --git a/x-pack/plugins/maps/public/routing/maps_router.js b/x-pack/plugins/maps/public/routing/maps_router.tsx
similarity index 80%
rename from x-pack/plugins/maps/public/routing/maps_router.js
rename to x-pack/plugins/maps/public/routing/maps_router.tsx
index f0f5234e3f989..5291d9c361161 100644
--- a/x-pack/plugins/maps/public/routing/maps_router.js
+++ b/x-pack/plugins/maps/public/routing/maps_router.tsx
@@ -6,8 +6,10 @@
import React from 'react';
import { render, unmountComponentAtNode } from 'react-dom';
-import { Router, Switch, Route, Redirect } from 'react-router-dom';
+import { Router, Switch, Route, Redirect, RouteComponentProps } from 'react-router-dom';
import { i18n } from '@kbn/i18n';
+import { Provider } from 'react-redux';
+import { AppMountContext, AppMountParameters } from 'kibana/public';
import {
getCoreChrome,
getCoreI18n,
@@ -18,16 +20,19 @@ import {
import {
createKbnUrlStateStorage,
withNotifyOnErrors,
+ IKbnUrlStateStorage,
} from '../../../../../src/plugins/kibana_utils/public';
import { getStore } from './store_operations';
-import { Provider } from 'react-redux';
import { LoadListAndRender } from './routes/list/load_list_and_render';
import { LoadMapAndRender } from './routes/maps_app/load_map_and_render';
-export let goToSpecifiedPath;
-export let kbnUrlStateStorage;
+export let goToSpecifiedPath: (path: string) => void;
+export let kbnUrlStateStorage: IKbnUrlStateStorage;
-export async function renderApp(context, { appBasePath, element, history, onAppLeave }) {
+export async function renderApp(
+ context: AppMountContext,
+ { appBasePath, element, history, onAppLeave }: AppMountParameters
+) {
goToSpecifiedPath = (path) => history.push(path);
kbnUrlStateStorage = createKbnUrlStateStorage({
useHash: false,
@@ -42,11 +47,19 @@ export async function renderApp(context, { appBasePath, element, history, onAppL
};
}
-const App = ({ history, appBasePath, onAppLeave }) => {
+interface Props {
+ history: AppMountParameters['history'] | RouteComponentProps['history'];
+ appBasePath: AppMountParameters['appBasePath'];
+ onAppLeave: AppMountParameters['onAppLeave'];
+}
+
+const App: React.FC = ({ history, appBasePath, onAppLeave }) => {
const store = getStore();
const I18nContext = getCoreI18n().Context;
- const stateTransfer = getEmbeddableService()?.getStateTransfer(history);
+ const stateTransfer = getEmbeddableService()?.getStateTransfer(
+ history as AppMountParameters['history']
+ );
const { originatingApp } =
stateTransfer?.getIncomingEditorState({ keysToRemoveAfterFetch: ['originatingApp'] }) || {};
@@ -66,7 +79,7 @@ const App = ({ history, appBasePath, onAppLeave }) => {
return (
-
+
getMapsSavedObjectLoader().find(search, this.state.listingLimit);
+ _find = (search: string) => getMapsSavedObjectLoader().find(search, this.state.listingLimit);
- _delete = (ids) => getMapsSavedObjectLoader().delete(ids);
+ _delete = (ids: string[]) => getMapsSavedObjectLoader().delete(ids);
debouncedFetch = _.debounce(async (filter) => {
const response = await this._find(filter);
@@ -135,10 +163,10 @@ export class MapsListView extends React.Component {
this.setState({ showDeleteModal: true });
};
- onTableChange = ({ page, sort = {} }) => {
+ onTableChange = ({ page, sort }: CriteriaWithPagination) => {
const { index: pageIndex, size: pageSize } = page;
- let { field: sortField, direction: sortDirection } = sort;
+ let { field: sortField, direction: sortDirection } = sort || {};
// 3rd sorting state that is not captured by sort - native order (no sort)
// when switching from desc to asc for the same field - use native order
@@ -147,8 +175,8 @@ export class MapsListView extends React.Component {
this.state.sortDirection === 'desc' &&
sortDirection === 'asc'
) {
- sortField = null;
- sortDirection = null;
+ sortField = undefined;
+ sortDirection = undefined;
}
this.setState({
@@ -165,8 +193,8 @@ export class MapsListView extends React.Component {
if (this.state.sortField) {
itemsCopy.sort((a, b) => {
- const fieldA = _.get(a, this.state.sortField, '');
- const fieldB = _.get(b, this.state.sortField, '');
+ const fieldA = _.get(a, this.state.sortField!, '');
+ const fieldB = _.get(b, this.state.sortField!, '');
let order = 1;
if (this.state.sortDirection === 'desc') {
order = -1;
@@ -320,7 +348,7 @@ export class MapsListView extends React.Component {
}
renderTable() {
- const tableColumns = [
+ const tableColumns: Array> = [
{
field: 'title',
name: i18n.translate('xpack.maps.mapListing.titleFieldTitle', {
@@ -329,7 +357,7 @@ export class MapsListView extends React.Component {
sortable: true,
render: (field, record) => (
{
+ onClick={(e: MouseEvent) => {
e.preventDefault();
goToSpecifiedPath(`/map/${record.id}`);
}}
@@ -355,12 +383,12 @@ export class MapsListView extends React.Component {
pageSizeOptions: [10, 20, 50],
};
- let selection = false;
+ let selection;
if (!this.state.readOnly) {
selection = {
- onSelectionChange: (selection) => {
+ onSelectionChange: (s: SelectionItem[]) => {
this.setState({
- selectedIds: selection.map((item) => {
+ selectedIds: s.map((item) => {
return item.id;
}),
});
@@ -368,11 +396,11 @@ export class MapsListView extends React.Component {
};
}
- const sorting = {};
+ const sorting: EuiTableSortingType = {};
if (this.state.sortField) {
sorting.sort = {
field: this.state.sortField,
- direction: this.state.sortDirection,
+ direction: this.state.sortDirection!,
};
}
const items = this.state.items.length === 0 ? [] : this.getPageOfItems();
diff --git a/x-pack/plugins/maps/public/routing/routes/maps_app/get_breadcrumbs.tsx b/x-pack/plugins/maps/public/routing/routes/maps_app/get_breadcrumbs.tsx
index 1ccf890597edc..149c04b414c18 100644
--- a/x-pack/plugins/maps/public/routing/routes/maps_app/get_breadcrumbs.tsx
+++ b/x-pack/plugins/maps/public/routing/routes/maps_app/get_breadcrumbs.tsx
@@ -6,7 +6,6 @@
import { i18n } from '@kbn/i18n';
import { getNavigateToApp } from '../../../kibana_services';
-// @ts-expect-error
import { goToSpecifiedPath } from '../../maps_router';
export const unsavedChangesWarning = i18n.translate(
@@ -25,7 +24,7 @@ export function getBreadcrumbs({
title: string;
getHasUnsavedChanges: () => boolean;
originatingApp?: string;
- getAppNameFromId?: (id: string) => string;
+ getAppNameFromId?: (id: string) => string | undefined;
}) {
const breadcrumbs = [];
if (originatingApp && getAppNameFromId) {
diff --git a/x-pack/plugins/maps/public/routing/routes/maps_app/index.js b/x-pack/plugins/maps/public/routing/routes/maps_app/index.ts
similarity index 62%
rename from x-pack/plugins/maps/public/routing/routes/maps_app/index.js
rename to x-pack/plugins/maps/public/routing/routes/maps_app/index.ts
index 326db7289e60d..812d7fcf30981 100644
--- a/x-pack/plugins/maps/public/routing/routes/maps_app/index.js
+++ b/x-pack/plugins/maps/public/routing/routes/maps_app/index.ts
@@ -5,6 +5,9 @@
*/
import { connect } from 'react-redux';
+import { ThunkDispatch } from 'redux-thunk';
+import { AnyAction } from 'redux';
+import { Filter, Query, TimeRange } from 'src/plugins/data/public';
import { MapsAppView } from './maps_app_view';
import { getFlyoutDisplay, getIsFullScreen } from '../../../selectors/ui_selectors';
import {
@@ -33,8 +36,15 @@ import {
import { FLYOUT_STATE } from '../../../reducers/ui';
import { getMapsCapabilities } from '../../../kibana_services';
import { getInspectorAdapters } from '../../../reducers/non_serializable_instances';
+import { MapStoreState } from '../../../reducers/store';
+import {
+ MapRefreshConfig,
+ MapCenterAndZoom,
+ LayerDescriptor,
+} from '../../../../common/descriptor_types';
+import { MapSettings } from '../../../reducers/map';
-function mapStateToProps(state = {}) {
+function mapStateToProps(state: MapStoreState) {
return {
isFullScreen: getIsFullScreen(state),
isOpenSettingsDisabled: getFlyoutDisplay(state) !== FLYOUT_STATE.NONE,
@@ -50,9 +60,19 @@ function mapStateToProps(state = {}) {
};
}
-function mapDispatchToProps(dispatch) {
+function mapDispatchToProps(dispatch: ThunkDispatch) {
return {
- dispatchSetQuery: ({ forceRefresh, filters, query, timeFilters }) => {
+ dispatchSetQuery: ({
+ forceRefresh,
+ filters,
+ query,
+ timeFilters,
+ }: {
+ filters?: Filter[];
+ query?: Query;
+ timeFilters?: TimeRange;
+ forceRefresh?: boolean;
+ }) => {
dispatch(
setQuery({
filters,
@@ -62,12 +82,13 @@ function mapDispatchToProps(dispatch) {
})
);
},
- setRefreshConfig: (refreshConfig) => dispatch(setRefreshConfig(refreshConfig)),
- replaceLayerList: (layerList) => dispatch(replaceLayerList(layerList)),
- setGotoWithCenter: (latLonZoom) => dispatch(setGotoWithCenter(latLonZoom)),
- setMapSettings: (mapSettings) => dispatch(setMapSettings(mapSettings)),
- setIsLayerTOCOpen: (isLayerTOCOpen) => dispatch(setIsLayerTOCOpen(isLayerTOCOpen)),
- setOpenTOCDetails: (openTOCDetails) => dispatch(setOpenTOCDetails(openTOCDetails)),
+ setRefreshConfig: (refreshConfig: MapRefreshConfig) =>
+ dispatch(setRefreshConfig(refreshConfig)),
+ replaceLayerList: (layerList: LayerDescriptor[]) => dispatch(replaceLayerList(layerList)),
+ setGotoWithCenter: (latLonZoom: MapCenterAndZoom) => dispatch(setGotoWithCenter(latLonZoom)),
+ setMapSettings: (mapSettings: MapSettings) => dispatch(setMapSettings(mapSettings)),
+ setIsLayerTOCOpen: (isLayerTOCOpen: boolean) => dispatch(setIsLayerTOCOpen(isLayerTOCOpen)),
+ setOpenTOCDetails: (openTOCDetails: string[]) => dispatch(setOpenTOCDetails(openTOCDetails)),
clearUi: () => {
dispatch(setSelectedLayer(null));
dispatch(updateFlyout(FLYOUT_STATE.NONE));
diff --git a/x-pack/plugins/maps/public/routing/routes/maps_app/load_map_and_render.js b/x-pack/plugins/maps/public/routing/routes/maps_app/load_map_and_render.tsx
similarity index 75%
rename from x-pack/plugins/maps/public/routing/routes/maps_app/load_map_and_render.js
rename to x-pack/plugins/maps/public/routing/routes/maps_app/load_map_and_render.tsx
index eebbb17582821..7ab138300dc4c 100644
--- a/x-pack/plugins/maps/public/routing/routes/maps_app/load_map_and_render.js
+++ b/x-pack/plugins/maps/public/routing/routes/maps_app/load_map_and_render.tsx
@@ -5,15 +5,31 @@
*/
import React from 'react';
-import { MapsAppView } from '.';
-import { getMapsSavedObjectLoader } from '../../bootstrap/services/gis_map_saved_object_loader';
-import { getCoreChrome, getToasts } from '../../../kibana_services';
import { i18n } from '@kbn/i18n';
import { Redirect } from 'react-router-dom';
+import { AppMountParameters } from 'kibana/public';
+import { EmbeddableStateTransfer } from 'src/plugins/embeddable/public';
+import { getCoreChrome, getToasts } from '../../../kibana_services';
+import { getMapsSavedObjectLoader } from '../../bootstrap/services/gis_map_saved_object_loader';
+import { MapsAppView } from '.';
+import { ISavedGisMap } from '../../bootstrap/services/saved_gis_map';
+
+interface Props {
+ savedMapId?: string;
+ onAppLeave: AppMountParameters['onAppLeave'];
+ stateTransfer: EmbeddableStateTransfer;
+ originatingApp?: string;
+}
+
+interface State {
+ savedMap?: ISavedGisMap;
+ failedToLoad: boolean;
+}
-export const LoadMapAndRender = class extends React.Component {
- state = {
- savedMap: null,
+export const LoadMapAndRender = class extends React.Component {
+ _isMounted: boolean = false;
+ state: State = {
+ savedMap: undefined,
failedToLoad: false,
};
diff --git a/x-pack/plugins/maps/public/routing/routes/maps_app/maps_app_view.js b/x-pack/plugins/maps/public/routing/routes/maps_app/maps_app_view.tsx
similarity index 73%
rename from x-pack/plugins/maps/public/routing/routes/maps_app/maps_app_view.js
rename to x-pack/plugins/maps/public/routing/routes/maps_app/maps_app_view.tsx
index 485b0ed7682fa..b3377547b2dd1 100644
--- a/x-pack/plugins/maps/public/routing/routes/maps_app/maps_app_view.js
+++ b/x-pack/plugins/maps/public/routing/routes/maps_app/maps_app_view.tsx
@@ -7,6 +7,9 @@
import React from 'react';
import 'mapbox-gl/dist/mapbox-gl.css';
import _ from 'lodash';
+import { AppLeaveAction, AppMountParameters } from 'kibana/public';
+import { EmbeddableStateTransfer, Adapters } from 'src/plugins/embeddable/public';
+import { Subscription } from 'rxjs';
import { DEFAULT_IS_LAYER_TOC_OPEN } from '../../../reducers/ui';
import {
getData,
@@ -23,29 +26,91 @@ import {
getGlobalState,
updateGlobalState,
startGlobalStateSyncing,
+ MapsGlobalState,
} from '../../state_syncing/global_sync';
import { AppStateManager } from '../../state_syncing/app_state_manager';
import { startAppStateSyncing } from '../../state_syncing/app_sync';
-import { esFilters } from '../../../../../../../src/plugins/data/public';
+import {
+ esFilters,
+ Filter,
+ Query,
+ TimeRange,
+ IndexPattern,
+ SavedQuery,
+ QueryStateChange,
+ QueryState,
+} from '../../../../../../../src/plugins/data/public';
import { MapContainer } from '../../../connected_components/map_container';
import { getIndexPatternsFromIds } from '../../../index_pattern_util';
import { getTopNavConfig } from './top_nav_config';
import { getBreadcrumbs, unsavedChangesWarning } from './get_breadcrumbs';
+import {
+ LayerDescriptor,
+ MapRefreshConfig,
+ MapCenterAndZoom,
+ MapQuery,
+} from '../../../../common/descriptor_types';
+import { MapSettings } from '../../../reducers/map';
+import { ISavedGisMap } from '../../bootstrap/services/saved_gis_map';
+
+interface Props {
+ savedMap: ISavedGisMap;
+ onAppLeave: AppMountParameters['onAppLeave'];
+ stateTransfer: EmbeddableStateTransfer;
+ originatingApp?: string;
+ layerListConfigOnly: LayerDescriptor[];
+ replaceLayerList: (layerList: LayerDescriptor[]) => void;
+ filters: Filter[];
+ isFullScreen: boolean;
+ isOpenSettingsDisabled: boolean;
+ enableFullScreen: () => void;
+ openMapSettings: () => void;
+ inspectorAdapters: Adapters;
+ nextIndexPatternIds: string[];
+ dispatchSetQuery: ({
+ forceRefresh,
+ filters,
+ query,
+ timeFilters,
+ }: {
+ filters?: Filter[];
+ query?: Query;
+ timeFilters?: TimeRange;
+ forceRefresh?: boolean;
+ }) => void;
+ timeFilters: TimeRange;
+ refreshConfig: MapRefreshConfig;
+ setRefreshConfig: (refreshConfig: MapRefreshConfig) => void;
+ isSaveDisabled: boolean;
+ clearUi: () => void;
+ setGotoWithCenter: (latLonZoom: MapCenterAndZoom) => void;
+ setMapSettings: (mapSettings: MapSettings) => void;
+ setIsLayerTOCOpen: (isLayerTOCOpen: boolean) => void;
+ setOpenTOCDetails: (openTOCDetails: string[]) => void;
+ query: MapQuery | undefined;
+}
-export class MapsAppView extends React.Component {
- _globalSyncUnsubscribe = null;
- _globalSyncChangeMonitorSubscription = null;
- _appSyncUnsubscribe = null;
+interface State {
+ initialized: boolean;
+ initialLayerListConfig?: LayerDescriptor[];
+ indexPatterns: IndexPattern[];
+ savedQuery?: SavedQuery;
+ originatingApp?: string;
+}
+
+export class MapsAppView extends React.Component {
+ _globalSyncUnsubscribe: (() => void) | null = null;
+ _globalSyncChangeMonitorSubscription: Subscription | null = null;
+ _appSyncUnsubscribe: (() => void) | null = null;
_appStateManager = new AppStateManager();
- _prevIndexPatternIds = null;
+ _prevIndexPatternIds: string[] | null = null;
+ _isMounted: boolean = false;
- constructor(props) {
+ constructor(props: Props) {
super(props);
this.state = {
indexPatterns: [],
initialized: false,
- savedQuery: '',
- initialLayerListConfig: null,
// tracking originatingApp in state so the connection can be broken by users
originatingApp: props.originatingApp,
};
@@ -60,10 +125,11 @@ export class MapsAppView extends React.Component {
this._updateFromGlobalState
);
- const initialSavedQuery = this._appStateManager.getAppState().savedQuery;
- if (initialSavedQuery) {
- this._updateStateFromSavedQuery(initialSavedQuery);
- }
+ // savedQuery must be fetched from savedQueryId
+ // const initialSavedQuery = this._appStateManager.getAppState().savedQuery;
+ // if (initialSavedQuery) {
+ // this._updateStateFromSavedQuery(initialSavedQuery as SavedQuery);
+ // }
this._initMap();
@@ -72,10 +138,10 @@ export class MapsAppView extends React.Component {
this.props.onAppLeave((actions) => {
if (this._hasUnsavedChanges()) {
if (!window.confirm(unsavedChangesWarning)) {
- return;
+ return {} as AppLeaveAction;
}
}
- return actions.default();
+ return actions.default() as AppLeaveAction;
});
}
@@ -121,7 +187,13 @@ export class MapsAppView extends React.Component {
getCoreChrome().setBreadcrumbs(breadcrumbs);
};
- _updateFromGlobalState = ({ changes, state: globalState }) => {
+ _updateFromGlobalState = ({
+ changes,
+ state: globalState,
+ }: {
+ changes: QueryStateChange;
+ state: QueryState;
+ }) => {
if (!this.state.initialized || !changes || !globalState) {
return;
}
@@ -144,7 +216,17 @@ export class MapsAppView extends React.Component {
}
}
- _onQueryChange = ({ filters, query, time, forceRefresh = false }) => {
+ _onQueryChange = ({
+ filters,
+ query,
+ time,
+ forceRefresh = false,
+ }: {
+ filters?: Filter[];
+ query?: Query;
+ time?: TimeRange;
+ forceRefresh?: boolean;
+ }) => {
const { filterManager } = getData().query;
if (filters) {
@@ -165,7 +247,9 @@ export class MapsAppView extends React.Component {
});
// sync globalState
- const updatedGlobalState = { filters: filterManager.getGlobalFilters() };
+ const updatedGlobalState: MapsGlobalState = {
+ filters: filterManager.getGlobalFilters(),
+ };
if (time) {
updatedGlobalState.time = time;
}
@@ -173,7 +257,7 @@ export class MapsAppView extends React.Component {
};
_initMapAndLayerSettings() {
- const globalState = getGlobalState();
+ const globalState: MapsGlobalState = getGlobalState();
const mapStateJSON = this.props.savedMap.mapStateJSON;
let savedObjectFilters = [];
@@ -219,14 +303,14 @@ export class MapsAppView extends React.Component {
});
}
- _onFiltersChange = (filters) => {
+ _onFiltersChange = (filters: Filter[]) => {
this._onQueryChange({
filters,
});
};
// mapRefreshConfig: MapRefreshConfig
- _onRefreshConfigChange(mapRefreshConfig) {
+ _onRefreshConfigChange(mapRefreshConfig: MapRefreshConfig) {
this.props.setRefreshConfig(mapRefreshConfig);
updateGlobalState(
{
@@ -239,9 +323,9 @@ export class MapsAppView extends React.Component {
);
}
- _updateStateFromSavedQuery = (savedQuery) => {
+ _updateStateFromSavedQuery = (savedQuery: SavedQuery) => {
this.setState({ savedQuery: { ...savedQuery } });
- this._appStateManager.setQueryAndFilters({ savedQuery });
+ this._appStateManager.setQueryAndFilters({ savedQueryId: savedQuery.id });
const { filterManager } = getData().query;
const savedQueryFilters = savedQuery.attributes.filters || [];
@@ -328,7 +412,13 @@ export class MapsAppView extends React.Component {
dateRangeTo={this.props.timeFilters.to}
isRefreshPaused={this.props.refreshConfig.isPaused}
refreshInterval={this.props.refreshConfig.interval}
- onRefreshChange={({ isPaused, refreshInterval }) => {
+ onRefreshChange={({
+ isPaused,
+ refreshInterval,
+ }: {
+ isPaused: boolean;
+ refreshInterval: number;
+ }) => {
this._onRefreshConfigChange({
isPaused,
interval: refreshInterval,
@@ -337,14 +427,14 @@ export class MapsAppView extends React.Component {
showSearchBar={true}
showFilterBar={true}
showDatePicker={true}
- showSaveQuery={getMapsCapabilities().saveQuery}
+ showSaveQuery={!!getMapsCapabilities().saveQuery}
savedQuery={this.state.savedQuery}
onSaved={this._updateStateFromSavedQuery}
onSavedQueryUpdated={this._updateStateFromSavedQuery}
onClearSavedQuery={() => {
const { filterManager, queryString } = getData().query;
- this.setState({ savedQuery: '' });
- this._appStateManager.setQueryAndFilters({ savedQuery: '' });
+ this.setState({ savedQuery: undefined });
+ this._appStateManager.setQueryAndFilters({ savedQueryId: '' });
this._onQueryChange({
filters: filterManager.getGlobalFilters(),
query: queryString.getDefaultQuery(),
@@ -354,7 +444,7 @@ export class MapsAppView extends React.Component {
);
}
- _addFilter = (newFilters) => {
+ _addFilter = async (newFilters: Filter[]) => {
newFilters.forEach((filter) => {
filter.$state = { store: esFilters.FilterStateStore.APP_STATE };
});
diff --git a/x-pack/plugins/maps/public/routing/routes/maps_app/top_nav_config.tsx b/x-pack/plugins/maps/public/routing/routes/maps_app/top_nav_config.tsx
index 497c87ad533a6..47f41f2b76f3e 100644
--- a/x-pack/plugins/maps/public/routing/routes/maps_app/top_nav_config.tsx
+++ b/x-pack/plugins/maps/public/routing/routes/maps_app/top_nav_config.tsx
@@ -21,7 +21,6 @@ import {
showSaveModal,
} from '../../../../../../../src/plugins/saved_objects/public';
import { MAP_SAVED_OBJECT_TYPE } from '../../../../common/constants';
-// @ts-expect-error
import { goToSpecifiedPath } from '../../maps_router';
import { ISavedGisMap } from '../../bootstrap/services/saved_gis_map';
import { EmbeddableStateTransfer } from '../../../../../../../src/plugins/embeddable/public';
diff --git a/x-pack/plugins/maps/public/routing/state_syncing/app_state_manager.js b/x-pack/plugins/maps/public/routing/state_syncing/app_state_manager.ts
similarity index 58%
rename from x-pack/plugins/maps/public/routing/state_syncing/app_state_manager.js
rename to x-pack/plugins/maps/public/routing/state_syncing/app_state_manager.ts
index 4cdba13bd85d2..122b50f823a95 100644
--- a/x-pack/plugins/maps/public/routing/state_syncing/app_state_manager.js
+++ b/x-pack/plugins/maps/public/routing/state_syncing/app_state_manager.ts
@@ -5,20 +5,27 @@
*/
import { Subject } from 'rxjs';
+import { Filter, Query } from 'src/plugins/data/public';
+
+export interface MapsAppState {
+ query?: Query | null;
+ savedQueryId?: string;
+ filters?: Filter[];
+}
export class AppStateManager {
- _query = '';
- _savedQuery = '';
- _filters = [];
+ _query: Query | null = null;
+ _savedQueryId: string = '';
+ _filters: Filter[] = [];
_updated$ = new Subject();
- setQueryAndFilters({ query, savedQuery, filters }) {
+ setQueryAndFilters({ query, savedQueryId, filters }: MapsAppState) {
if (query && this._query !== query) {
this._query = query;
}
- if (savedQuery && this._savedQuery !== savedQuery) {
- this._savedQuery = savedQuery;
+ if (savedQueryId && this._savedQueryId !== savedQueryId) {
+ this._savedQueryId = savedQueryId;
}
if (filters && this._filters !== filters) {
this._filters = filters;
@@ -34,10 +41,10 @@ export class AppStateManager {
return this._filters;
}
- getAppState() {
+ getAppState(): MapsAppState {
return {
query: this._query,
- savedQuery: this._savedQuery,
+ savedQueryId: this._savedQueryId,
filters: this._filters,
};
}
diff --git a/x-pack/plugins/maps/public/routing/state_syncing/app_sync.js b/x-pack/plugins/maps/public/routing/state_syncing/app_sync.ts
similarity index 88%
rename from x-pack/plugins/maps/public/routing/state_syncing/app_sync.js
rename to x-pack/plugins/maps/public/routing/state_syncing/app_sync.ts
index 60e8dc9cd574c..b346822913bec 100644
--- a/x-pack/plugins/maps/public/routing/state_syncing/app_sync.js
+++ b/x-pack/plugins/maps/public/routing/state_syncing/app_sync.ts
@@ -4,13 +4,14 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { connectToQueryState, esFilters } from '../../../../../../src/plugins/data/public';
-import { syncState } from '../../../../../../src/plugins/kibana_utils/public';
import { map } from 'rxjs/operators';
+import { connectToQueryState, esFilters } from '../../../../../../src/plugins/data/public';
+import { syncState, BaseStateContainer } from '../../../../../../src/plugins/kibana_utils/public';
import { getData } from '../../kibana_services';
import { kbnUrlStateStorage } from '../maps_router';
+import { AppStateManager } from './app_state_manager';
-export function startAppStateSyncing(appStateManager) {
+export function startAppStateSyncing(appStateManager: AppStateManager) {
// get appStateContainer
// sync app filters with app state container from data.query to state container
const { query } = getData();
@@ -19,7 +20,7 @@ export function startAppStateSyncing(appStateManager) {
// clear app state filters to prevent application filters from other applications being transfered to maps
query.filterManager.setAppFilters([]);
- const stateContainer = {
+ const stateContainer: BaseStateContainer = {
get: () => ({
query: appStateManager.getQuery(),
filters: appStateManager.getFilters(),
@@ -48,6 +49,7 @@ export function startAppStateSyncing(appStateManager) {
// merge initial state from app state container and current state in url
const initialAppState = {
...stateContainer.get(),
+ // @ts-ignore
...kbnUrlStateStorage.get('_a'),
};
// trigger state update. actually needed in case some data was in url
diff --git a/x-pack/plugins/maps/public/routing/state_syncing/global_sync.ts b/x-pack/plugins/maps/public/routing/state_syncing/global_sync.ts
index 4e17241752f53..1e779831c5e0c 100644
--- a/x-pack/plugins/maps/public/routing/state_syncing/global_sync.ts
+++ b/x-pack/plugins/maps/public/routing/state_syncing/global_sync.ts
@@ -3,27 +3,30 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
-
+import { TimeRange, RefreshInterval, Filter } from 'src/plugins/data/public';
import { syncQueryStateWithUrl } from '../../../../../../src/plugins/data/public';
import { getData } from '../../kibana_services';
-// @ts-ignore
import { kbnUrlStateStorage } from '../maps_router';
+export interface MapsGlobalState {
+ time?: TimeRange;
+ refreshInterval?: RefreshInterval;
+ filters?: Filter[];
+}
+
export function startGlobalStateSyncing() {
const { stop } = syncQueryStateWithUrl(getData().query, kbnUrlStateStorage);
return stop;
}
-export function getGlobalState() {
- return kbnUrlStateStorage.get('_g');
+export function getGlobalState(): MapsGlobalState {
+ return kbnUrlStateStorage.get('_g') as MapsGlobalState;
}
-export function updateGlobalState(newState: unknown, flushUrlState = false) {
+export function updateGlobalState(newState: MapsGlobalState, flushUrlState = false) {
const globalState = getGlobalState();
kbnUrlStateStorage.set('_g', {
- // @ts-ignore
...globalState,
- // @ts-ignore
...newState,
});
if (flushUrlState) {
diff --git a/x-pack/plugins/maps/public/routing/store_operations.js b/x-pack/plugins/maps/public/routing/store_operations.ts
similarity index 100%
rename from x-pack/plugins/maps/public/routing/store_operations.js
rename to x-pack/plugins/maps/public/routing/store_operations.ts
diff --git a/x-pack/plugins/ml/common/types/__mocks__/job_config_farequote.json b/x-pack/plugins/ml/common/types/__mocks__/job_config_farequote.json
index 18a49bb3841b3..6bc0e55b5aadd 100644
--- a/x-pack/plugins/ml/common/types/__mocks__/job_config_farequote.json
+++ b/x-pack/plugins/ml/common/types/__mocks__/job_config_farequote.json
@@ -69,9 +69,7 @@
"datafeed_id": "datafeed-farequote",
"job_id": "farequote",
"query_delay": "115823ms",
- "indices": [
- "farequote"
- ],
+ "indices": ["farequote"],
"query": {
"bool": {
"must": [
@@ -103,7 +101,7 @@
"buckets": {
"date_histogram": {
"field": "@timestamp",
- "interval": 900000,
+ "fixed_interval": "15m",
"offset": 0,
"order": {
"_key": "asc"
diff --git a/x-pack/plugins/ml/common/util/errors.ts b/x-pack/plugins/ml/common/util/errors.ts
index 6c5fa7bd75daf..a5f89db96cfd7 100644
--- a/x-pack/plugins/ml/common/util/errors.ts
+++ b/x-pack/plugins/ml/common/util/errors.ts
@@ -135,7 +135,14 @@ export const extractErrorProperties = (
typeof error.body.attributes === 'object' &&
error.body.attributes.body?.status !== undefined
) {
- statusCode = error.body.attributes.body?.status;
+ statusCode = error.body.attributes.body.status;
+
+ if (typeof error.body.attributes.body.error?.reason === 'string') {
+ return {
+ message: error.body.attributes.body.error.reason,
+ statusCode,
+ };
+ }
}
if (typeof error.body.message === 'string') {
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/configuration_step_form.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/configuration_step_form.tsx
index 25baff98556a6..dd9ecc963840a 100644
--- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/configuration_step_form.tsx
+++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/configuration_step_form.tsx
@@ -209,7 +209,6 @@ export const ConfigurationStepForm: FC = ({
let unsupportedFieldsErrorMessage;
if (
jobType === ANALYSIS_CONFIG_TYPE.CLASSIFICATION &&
- errorMessage.includes('status_exception') &&
(errorMessage.includes('must have at most') || errorMessage.includes('must have at least'))
) {
maxDistinctValuesErrorMessage = errorMessage;
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/analytics_list.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/analytics_list.tsx
index 6d73340cc396a..0c3bff58c25cd 100644
--- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/analytics_list.tsx
+++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/analytics_list.tsx
@@ -99,13 +99,7 @@ export const DataFrameAnalyticsList: FC = ({
const [isInitialized, setIsInitialized] = useState(false);
const [isSourceIndexModalVisible, setIsSourceIndexModalVisible] = useState(false);
const [isLoading, setIsLoading] = useState(false);
- const [filteredAnalytics, setFilteredAnalytics] = useState<{
- active: boolean;
- items: DataFrameAnalyticsListRow[];
- }>({
- active: false,
- items: [],
- });
+ const [filteredAnalytics, setFilteredAnalytics] = useState([]);
const [searchQueryText, setSearchQueryText] = useState('');
const [analytics, setAnalytics] = useState([]);
const [analyticsStats, setAnalyticsStats] = useState(
@@ -129,12 +123,12 @@ export const DataFrameAnalyticsList: FC = ({
blockRefresh
);
- const setQueryClauses = (queryClauses: any) => {
+ const updateFilteredItems = (queryClauses: any) => {
if (queryClauses.length) {
const filtered = filterAnalytics(analytics, queryClauses);
- setFilteredAnalytics({ active: true, items: filtered });
+ setFilteredAnalytics(filtered);
} else {
- setFilteredAnalytics({ active: false, items: [] });
+ setFilteredAnalytics(analytics);
}
};
@@ -146,9 +140,9 @@ export const DataFrameAnalyticsList: FC = ({
if (query && query.ast !== undefined && query.ast.clauses !== undefined) {
clauses = query.ast.clauses;
}
- setQueryClauses(clauses);
+ updateFilteredItems(clauses);
} else {
- setQueryClauses([]);
+ updateFilteredItems([]);
}
};
@@ -192,9 +186,9 @@ export const DataFrameAnalyticsList: FC = ({
isMlEnabledInSpace
);
- const { onTableChange, pageOfItems, pagination, sorting } = useTableSettings(
- filteredAnalytics.active ? filteredAnalytics.items : analytics
- );
+ const { onTableChange, pageOfItems, pagination, sorting } = useTableSettings<
+ DataFrameAnalyticsListRow
+ >(DataFrameAnalyticsListColumn.id, filteredAnalytics);
// Before the analytics have been loaded for the first time, display the loading indicator only.
// Otherwise a user would see 'No data frame analytics found' during the initial loading.
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/use_table_settings.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/use_table_settings.ts
index 57eb9f6857053..052068c30b84c 100644
--- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/use_table_settings.ts
+++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/use_table_settings.ts
@@ -8,7 +8,6 @@ import { useState } from 'react';
import { Direction, EuiBasicTableProps, EuiTableSortingType } from '@elastic/eui';
import sortBy from 'lodash/sortBy';
import get from 'lodash/get';
-import { DataFrameAnalyticsListColumn, DataFrameAnalyticsListRow } from './common';
const PAGE_SIZE = 10;
const PAGE_SIZE_OPTIONS = [10, 25, 50];
@@ -19,37 +18,59 @@ const jobPropertyMap = {
Type: 'job_type',
};
-interface AnalyticsBasicTableSettings {
+// Copying from EUI EuiBasicTable types as type is not correctly picked up for table's onChange
+// Can be removed when https://github.com/elastic/eui/issues/4011 is addressed in EUI
+export interface Criteria {
+ page?: {
+ index: number;
+ size: number;
+ };
+ sort?: {
+ field: keyof T;
+ direction: Direction;
+ };
+}
+export interface CriteriaWithPagination extends Criteria {
+ page: {
+ index: number;
+ size: number;
+ };
+}
+
+interface AnalyticsBasicTableSettings {
pageIndex: number;
pageSize: number;
totalItemCount: number;
hidePerPageOptions: boolean;
- sortField: string;
+ sortField: keyof T;
sortDirection: Direction;
}
-interface UseTableSettingsReturnValue {
- onTableChange: EuiBasicTableProps['onChange'];
- pageOfItems: DataFrameAnalyticsListRow[];
- pagination: EuiBasicTableProps['pagination'];
+interface UseTableSettingsReturnValue {
+ onTableChange: EuiBasicTableProps['onChange'];
+ pageOfItems: T[];
+ pagination: EuiBasicTableProps['pagination'];
sorting: EuiTableSortingType;
}
-export function useTableSettings(items: DataFrameAnalyticsListRow[]): UseTableSettingsReturnValue {
- const [tableSettings, setTableSettings] = useState({
+export function useTableSettings(
+ sortByField: keyof TypeOfItem,
+ items: TypeOfItem[]
+): UseTableSettingsReturnValue {
+ const [tableSettings, setTableSettings] = useState>({
pageIndex: 0,
pageSize: PAGE_SIZE,
totalItemCount: 0,
hidePerPageOptions: false,
- sortField: DataFrameAnalyticsListColumn.id,
+ sortField: sortByField,
sortDirection: 'asc',
});
const getPageOfItems = (
- list: any[],
+ list: TypeOfItem[],
index: number,
size: number,
- sortField: string,
+ sortField: keyof TypeOfItem,
sortDirection: Direction
) => {
list = sortBy(list, (item) =>
@@ -72,13 +93,10 @@ export function useTableSettings(items: DataFrameAnalyticsListRow[]): UseTableSe
};
};
- const onTableChange = ({
+ const onTableChange: EuiBasicTableProps['onChange'] = ({
page = { index: 0, size: PAGE_SIZE },
- sort = { field: DataFrameAnalyticsListColumn.id, direction: 'asc' },
- }: {
- page?: { index: number; size: number };
- sort?: { field: string; direction: Direction };
- }) => {
+ sort = { field: sortByField, direction: 'asc' },
+ }: CriteriaWithPagination) => {
const { index, size } = page;
const { field, direction } = sort;
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_search_bar/analytics_search_bar.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_search_bar/analytics_search_bar.tsx
index 44a6572a3766c..7a366bb63420c 100644
--- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_search_bar/analytics_search_bar.tsx
+++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_search_bar/analytics_search_bar.tsx
@@ -20,6 +20,68 @@ import {
Value,
DataFrameAnalyticsListRow,
} from '../analytics_list/common';
+import { ModelItem } from '../models_management/models_list';
+
+export function filterAnalyticsModels(
+ items: ModelItem[],
+ clauses: Array
+) {
+ if (clauses.length === 0) {
+ return items;
+ }
+
+ // keep count of the number of matches we make as we're looping over the clauses
+ // we only want to return items which match all clauses, i.e. each search term is ANDed
+ const matches: Record = items.reduce((p: Record, c) => {
+ p[c.model_id] = {
+ model: c,
+ count: 0,
+ };
+ return p;
+ }, {});
+
+ clauses.forEach((c) => {
+ // the search term could be negated with a minus, e.g. -bananas
+ const bool = c.match === 'must';
+ let ms = [];
+
+ if (c.type === 'term') {
+ // filter term based clauses, e.g. bananas
+ // match on model_id and type
+ // if the term has been negated, AND the matches
+ if (bool === true) {
+ ms = items.filter(
+ (item) =>
+ stringMatch(item.model_id, c.value) === bool || stringMatch(item.type, c.value) === bool
+ );
+ } else {
+ ms = items.filter(
+ (item) =>
+ stringMatch(item.model_id, c.value) === bool && stringMatch(item.type, c.value) === bool
+ );
+ }
+ } else {
+ // filter other clauses, i.e. the filters for type
+ if (Array.isArray(c.value)) {
+ // type value is an array of string(s) e.g. c.value => ['classification']
+ ms = items.filter((item) => {
+ return item.type !== undefined && (c.value as Value[]).includes(item.type);
+ });
+ } else {
+ ms = items.filter((item) => item[c.field as keyof typeof item] === c.value);
+ }
+ }
+
+ ms.forEach((j) => matches[j.model_id].count++);
+ });
+
+ // loop through the matches and return only those items which have match all the clauses
+ const filtered = Object.values(matches)
+ .filter((m) => (m && m.count) >= clauses.length)
+ .map((m) => m.model);
+
+ return filtered;
+}
export function filterAnalytics(
items: DataFrameAnalyticsListRow[],
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_search_bar/index.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_search_bar/index.ts
index 3b901f5063eb1..2748764d7f46e 100644
--- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_search_bar/index.ts
+++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_search_bar/index.ts
@@ -4,4 +4,4 @@
* you may not use this file except in compliance with the Elastic License.
*/
-export { AnalyticsSearchBar, filterAnalytics } from './analytics_search_bar';
+export { AnalyticsSearchBar, filterAnalytics, filterAnalyticsModels } from './analytics_search_bar';
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/models_management/models_list.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/models_management/models_list.tsx
index 3104ec55c3a6d..338b6444671a6 100644
--- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/models_management/models_list.tsx
+++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/models_management/models_list.tsx
@@ -4,20 +4,20 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import React, { FC, useState, useCallback, useMemo } from 'react';
+import React, { FC, useState, useCallback, useEffect, useMemo } from 'react';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import {
- Direction,
+ EuiBasicTable,
EuiFlexGroup,
EuiFlexItem,
- EuiInMemoryTable,
EuiTitle,
EuiButton,
- EuiSearchBarProps,
+ EuiSearchBar,
EuiSpacer,
EuiButtonIcon,
EuiBadge,
+ SearchFilterConfig,
} from '@elastic/eui';
// @ts-ignore
import { formatDate } from '@elastic/eui/lib/services/format';
@@ -42,6 +42,8 @@ import {
refreshAnalyticsList$,
useRefreshAnalyticsList,
} from '../../../../common';
+import { useTableSettings } from '../analytics_list/use_table_settings';
+import { filterAnalyticsModels, AnalyticsSearchBar } from '../analytics_search_bar';
type Stats = Omit;
@@ -66,22 +68,41 @@ export const ModelsList: FC = () => {
const { toasts } = useNotifications();
const [searchQueryText, setSearchQueryText] = useState('');
-
- const [pageIndex, setPageIndex] = useState(0);
- const [pageSize, setPageSize] = useState(10);
- const [sortField, setSortField] = useState(ModelsTableToConfigMapping.id);
- const [sortDirection, setSortDirection] = useState('asc');
-
+ const [filteredModels, setFilteredModels] = useState([]);
const [isLoading, setIsLoading] = useState(false);
const [items, setItems] = useState([]);
const [selectedModels, setSelectedModels] = useState([]);
-
const [modelsToDelete, setModelsToDelete] = useState([]);
-
const [itemIdToExpandedRowMap, setItemIdToExpandedRowMap] = useState>(
{}
);
+ const updateFilteredItems = (queryClauses: any) => {
+ if (queryClauses.length) {
+ const filtered = filterAnalyticsModels(items, queryClauses);
+ setFilteredModels(filtered);
+ } else {
+ setFilteredModels(items);
+ }
+ };
+
+ const filterList = () => {
+ if (searchQueryText !== '') {
+ const query = EuiSearchBar.Query.parse(searchQueryText);
+ let clauses: any = [];
+ if (query && query.ast !== undefined && query.ast.clauses !== undefined) {
+ clauses = query.ast.clauses;
+ }
+ updateFilteredItems(clauses);
+ } else {
+ updateFilteredItems([]);
+ }
+ };
+
+ useEffect(() => {
+ filterList();
+ }, [searchQueryText, items]);
+
/**
* Fetches inference trained models.
*/
@@ -355,91 +376,51 @@ export const ModelsList: FC = () => {
},
];
- const pagination = {
- initialPageIndex: pageIndex,
- initialPageSize: pageSize,
- totalItemCount: items.length,
- pageSizeOptions: [10, 20, 50],
- hidePerPageOptions: false,
- };
+ const filters: SearchFilterConfig[] =
+ inferenceTypesOptions && inferenceTypesOptions.length > 0
+ ? [
+ {
+ type: 'field_value_selection',
+ field: 'type',
+ name: i18n.translate('xpack.ml.dataframe.analyticsList.typeFilter', {
+ defaultMessage: 'Type',
+ }),
+ multiSelect: 'or',
+ options: inferenceTypesOptions,
+ },
+ ]
+ : [];
- const sorting = {
- sort: {
- field: sortField,
- direction: sortDirection,
- },
- };
- const search: EuiSearchBarProps = {
- query: searchQueryText,
- onChange: (searchChange) => {
- if (searchChange.error !== null) {
- return false;
- }
- setSearchQueryText(searchChange.queryText);
- return true;
- },
- box: {
- incremental: true,
- },
- ...(inferenceTypesOptions && inferenceTypesOptions.length > 0
- ? {
- filters: [
- {
- type: 'field_value_selection',
- field: 'type',
- name: i18n.translate('xpack.ml.dataframe.analyticsList.typeFilter', {
- defaultMessage: 'Type',
- }),
- multiSelect: 'or',
- options: inferenceTypesOptions,
- },
- ],
- }
- : {}),
- ...(selectedModels.length > 0
- ? {
- toolsLeft: (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- ),
- }
- : {}),
- };
+ const { onTableChange, pageOfItems, pagination, sorting } = useTableSettings(
+ ModelsTableToConfigMapping.id,
+ filteredModels
+ );
- const onTableChange: EuiInMemoryTable['onTableChange'] = ({
- page = { index: 0, size: 10 },
- sort = { field: ModelsTableToConfigMapping.id, direction: 'asc' },
- }) => {
- const { index, size } = page;
- setPageIndex(index);
- setPageSize(size);
-
- const { field, direction } = sort;
- setSortField(field);
- setSortDirection(direction);
- };
+ const toolsLeft = (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
const isSelectionAllowed = canDeleteDataFrameAnalytics;
@@ -473,21 +454,31 @@ export const ModelsList: FC = () => {
-
+ {selectedModels.length > 0 && toolsLeft}
+
+
+
+
+
+
columns={columns}
hasActions={true}
isExpandable={true}
- itemIdToExpandedRowMap={itemIdToExpandedRowMap}
isSelectable={false}
- items={items}
+ items={pageOfItems}
itemId={ModelsTableToConfigMapping.id}
+ itemIdToExpandedRowMap={itemIdToExpandedRowMap}
loading={isLoading}
- onTableChange={onTableChange}
- pagination={pagination}
- sorting={sorting}
- search={search}
+ onChange={onTableChange}
selection={selection}
+ pagination={pagination!}
+ sorting={sorting}
+ data-test-subj={isLoading ? 'mlModelsTable loading' : 'mlModelsTable loaded'}
rowProps={(item) => ({
'data-test-subj': `mlModelsTableRow row-${item.model_id}`,
})}
diff --git a/x-pack/plugins/ml/public/application/datavisualizer/index_based/data_loader/data_loader.ts b/x-pack/plugins/ml/public/application/datavisualizer/index_based/data_loader/data_loader.ts
index 34f86ffa18788..b36eccdde2798 100644
--- a/x-pack/plugins/ml/public/application/datavisualizer/index_based/data_loader/data_loader.ts
+++ b/x-pack/plugins/ml/public/application/datavisualizer/index_based/data_loader/data_loader.ts
@@ -80,7 +80,7 @@ export class DataLoader {
earliest: number | undefined,
latest: number | undefined,
fields: FieldRequestConfig[],
- interval?: string
+ interval?: number
): Promise {
const stats = await ml.getVisualizerFieldStats({
indexPatternTitle: this._indexPatternTitle,
diff --git a/x-pack/plugins/ml/public/application/datavisualizer/index_based/page.tsx b/x-pack/plugins/ml/public/application/datavisualizer/index_based/page.tsx
index 26ed3152058dd..bad1488166e23 100644
--- a/x-pack/plugins/ml/public/application/datavisualizer/index_based/page.tsx
+++ b/x-pack/plugins/ml/public/application/datavisualizer/index_based/page.tsx
@@ -348,7 +348,7 @@ export const Page: FC = () => {
earliest,
latest,
existMetricFields,
- aggInterval.expression
+ aggInterval.asMilliseconds()
);
// Add the metric stats to the existing stats in the corresponding config.
diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container_service.js b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container_service.js
index 712b64af2db80..a6fda86f27a7c 100644
--- a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container_service.js
+++ b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container_service.js
@@ -111,7 +111,7 @@ export const anomalyDataChange = function (
// Query 1 - load the raw metric data.
function getMetricData(config, range) {
- const { jobId, detectorIndex, entityFields, interval } = config;
+ const { jobId, detectorIndex, entityFields, bucketSpanSeconds } = config;
const job = mlJobService.getJob(jobId);
@@ -122,14 +122,14 @@ export const anomalyDataChange = function (
return mlResultsService
.getMetricData(
config.datafeedConfig.indices,
- config.entityFields,
+ entityFields,
datafeedQuery,
config.metricFunction,
config.metricFieldName,
config.timeField,
range.min,
range.max,
- config.interval
+ bucketSpanSeconds * 1000
)
.toPromise();
} else {
@@ -175,7 +175,14 @@ export const anomalyDataChange = function (
};
return mlResultsService
- .getModelPlotOutput(jobId, detectorIndex, criteriaFields, range.min, range.max, interval)
+ .getModelPlotOutput(
+ jobId,
+ detectorIndex,
+ criteriaFields,
+ range.min,
+ range.max,
+ bucketSpanSeconds * 1000
+ )
.toPromise()
.then((resp) => {
// Return data in format required by the explorer charts.
@@ -218,7 +225,7 @@ export const anomalyDataChange = function (
[config.jobId],
range.min,
range.max,
- config.interval,
+ config.bucketSpanSeconds * 1000,
1,
MAX_SCHEDULED_EVENTS
)
@@ -252,7 +259,7 @@ export const anomalyDataChange = function (
config.timeField,
range.min,
range.max,
- config.interval
+ config.bucketSpanSeconds * 1000
);
}
diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/common/results_loader/results_loader.ts b/x-pack/plugins/ml/public/application/jobs/new_job/common/results_loader/results_loader.ts
index 2b250b9622286..af31df863ab76 100644
--- a/x-pack/plugins/ml/public/application/jobs/new_job/common/results_loader/results_loader.ts
+++ b/x-pack/plugins/ml/public/application/jobs/new_job/common/results_loader/results_loader.ts
@@ -161,7 +161,7 @@ export class ResultsLoader {
[],
this._lastModelTimeStamp,
this._jobCreator.end,
- `${this._chartInterval.getInterval().asMilliseconds()}ms`,
+ this._chartInterval.getInterval().asMilliseconds(),
agg.mlModelPlotAgg
)
.toPromise();
@@ -211,7 +211,7 @@ export class ResultsLoader {
[this._jobCreator.jobId],
this._jobCreator.start,
this._jobCreator.end,
- `${this._chartInterval.getInterval().asMilliseconds()}ms`,
+ this._chartInterval.getInterval().asMilliseconds(),
1
);
@@ -233,7 +233,7 @@ export class ResultsLoader {
this._jobCreator.jobId,
this._jobCreator.start,
this._jobCreator.end,
- `${this._chartInterval.getInterval().asMilliseconds()}ms`,
+ this._chartInterval.getInterval().asMilliseconds(),
this._detectorSplitFieldFilters
);
diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/common/results_loader/searches.ts b/x-pack/plugins/ml/public/application/jobs/new_job/common/results_loader/searches.ts
index 51c396518c851..02a6f47bed6c9 100644
--- a/x-pack/plugins/ml/public/application/jobs/new_job/common/results_loader/searches.ts
+++ b/x-pack/plugins/ml/public/application/jobs/new_job/common/results_loader/searches.ts
@@ -32,7 +32,7 @@ export function getScoresByRecord(
jobId: string,
earliestMs: number,
latestMs: number,
- interval: string,
+ intervalMs: number,
firstSplitField: SplitFieldWithValue | null
): Promise {
return new Promise((resolve, reject) => {
@@ -104,7 +104,7 @@ export function getScoresByRecord(
byTime: {
date_histogram: {
field: 'timestamp',
- interval,
+ fixed_interval: `${intervalMs}ms`,
min_doc_count: 1,
extended_bounds: {
min: earliestMs,
diff --git a/x-pack/plugins/ml/public/application/services/anomaly_timeline_service.ts b/x-pack/plugins/ml/public/application/services/anomaly_timeline_service.ts
index 2bdb758be874c..3ee50a4759006 100644
--- a/x-pack/plugins/ml/public/application/services/anomaly_timeline_service.ts
+++ b/x-pack/plugins/ml/public/application/services/anomaly_timeline_service.ts
@@ -180,7 +180,7 @@ export class AnomalyTimelineService {
// Pass the interval in seconds as the swim lane relies on a fixed number of seconds between buckets
// which wouldn't be the case if e.g. '1M' was used.
- const interval = `${swimlaneBucketInterval.asSeconds()}s`;
+ const intervalMs = swimlaneBucketInterval.asMilliseconds();
let response;
if (viewBySwimlaneFieldName === VIEW_BY_JOB_LABEL) {
@@ -190,7 +190,7 @@ export class AnomalyTimelineService {
jobIds,
searchBounds.min.valueOf(),
searchBounds.max.valueOf(),
- interval,
+ intervalMs,
perPage,
fromPage
);
@@ -201,7 +201,7 @@ export class AnomalyTimelineService {
fieldValues,
searchBounds.min.valueOf(),
searchBounds.max.valueOf(),
- interval,
+ intervalMs,
swimlaneLimit,
perPage,
fromPage,
@@ -269,7 +269,7 @@ export class AnomalyTimelineService {
selectedJobIds,
earliestMs,
latestMs,
- this.getSwimlaneBucketInterval(selectedJobs, swimlaneContainerWidth).asSeconds() + 's',
+ this.getSwimlaneBucketInterval(selectedJobs, swimlaneContainerWidth).asMilliseconds(),
swimlaneLimit
);
return Object.keys(resp.results);
diff --git a/x-pack/plugins/ml/public/application/services/forecast_service.d.ts b/x-pack/plugins/ml/public/application/services/forecast_service.d.ts
index 9eff86c753da9..e30790c57966b 100644
--- a/x-pack/plugins/ml/public/application/services/forecast_service.d.ts
+++ b/x-pack/plugins/ml/public/application/services/forecast_service.d.ts
@@ -25,7 +25,7 @@ export const mlForecastService: {
entityFields: any[],
earliestMs: number,
latestMs: number,
- interval: string,
+ intervalMs: number,
aggType: any
) => Observable;
diff --git a/x-pack/plugins/ml/public/application/services/forecast_service.js b/x-pack/plugins/ml/public/application/services/forecast_service.js
index 57e50387a03ab..c13e265b4655c 100644
--- a/x-pack/plugins/ml/public/application/services/forecast_service.js
+++ b/x-pack/plugins/ml/public/application/services/forecast_service.js
@@ -153,7 +153,7 @@ function getForecastData(
entityFields,
earliestMs,
latestMs,
- interval,
+ intervalMs,
aggType
) {
// Extract the partition, by, over fields on which to filter.
@@ -257,7 +257,7 @@ function getForecastData(
times: {
date_histogram: {
field: 'timestamp',
- interval: interval,
+ fixed_interval: `${intervalMs}ms`,
min_doc_count: 1,
},
aggs: {
diff --git a/x-pack/plugins/ml/public/application/services/ml_api_service/index.ts b/x-pack/plugins/ml/public/application/services/ml_api_service/index.ts
index 184039729f9ef..9d7ce4f3df59b 100644
--- a/x-pack/plugins/ml/public/application/services/ml_api_service/index.ts
+++ b/x-pack/plugins/ml/public/application/services/ml_api_service/index.ts
@@ -485,7 +485,7 @@ export function mlApiServicesProvider(httpService: HttpService) {
earliest?: number;
latest?: number;
samplerShardSize?: number;
- interval?: string;
+ interval?: number;
fields?: FieldRequestConfig[];
maxExamples?: number;
}) {
diff --git a/x-pack/plugins/ml/public/application/services/results_service/result_service_rx.ts b/x-pack/plugins/ml/public/application/services/results_service/result_service_rx.ts
index 898ca8894cbda..22f878a337f51 100644
--- a/x-pack/plugins/ml/public/application/services/results_service/result_service_rx.ts
+++ b/x-pack/plugins/ml/public/application/services/results_service/result_service_rx.ts
@@ -70,7 +70,7 @@ export function resultsServiceRxProvider(mlApiServices: MlApiServices) {
timeFieldName: string,
earliestMs: number,
latestMs: number,
- interval: string
+ intervalMs: number
): Observable {
// Build the criteria to use in the bool filter part of the request.
// Add criteria for the time range, entity fields,
@@ -136,7 +136,7 @@ export function resultsServiceRxProvider(mlApiServices: MlApiServices) {
byTime: {
date_histogram: {
field: timeFieldName,
- interval,
+ fixed_interval: `${intervalMs}ms`,
min_doc_count: 0,
},
},
@@ -202,7 +202,7 @@ export function resultsServiceRxProvider(mlApiServices: MlApiServices) {
criteriaFields: any[],
earliestMs: number,
latestMs: number,
- interval: string,
+ intervalMs: number,
aggType?: { min: any; max: any }
): Observable {
const obj: ModelPlotOutput = {
@@ -291,7 +291,7 @@ export function resultsServiceRxProvider(mlApiServices: MlApiServices) {
times: {
date_histogram: {
field: 'timestamp',
- interval,
+ fixed_interval: `${intervalMs}ms`,
min_doc_count: 0,
},
aggs: {
@@ -446,7 +446,7 @@ export function resultsServiceRxProvider(mlApiServices: MlApiServices) {
jobIds: string[] | undefined,
earliestMs: number,
latestMs: number,
- interval: string,
+ intervalMs: number,
maxJobs: number,
maxEvents: number
): Observable {
@@ -518,7 +518,7 @@ export function resultsServiceRxProvider(mlApiServices: MlApiServices) {
times: {
date_histogram: {
field: 'timestamp',
- interval,
+ fixed_interval: `${intervalMs}ms`,
min_doc_count: 1,
},
aggs: {
diff --git a/x-pack/plugins/ml/public/application/services/results_service/results_service.d.ts b/x-pack/plugins/ml/public/application/services/results_service/results_service.d.ts
index b26528b76037b..aae0cb51aa81d 100644
--- a/x-pack/plugins/ml/public/application/services/results_service/results_service.d.ts
+++ b/x-pack/plugins/ml/public/application/services/results_service/results_service.d.ts
@@ -13,7 +13,7 @@ export function resultsServiceProvider(
jobIds: string[],
earliestMs: number,
latestMs: number,
- interval: string | number,
+ intervalMs: number,
perPage?: number,
fromPage?: number
): Promise;
@@ -41,7 +41,7 @@ export function resultsServiceProvider(
influencerFieldValues: string[],
earliestMs: number,
latestMs: number,
- interval: string,
+ intervalMs: number,
maxResults: number,
perPage: number,
fromPage: number,
@@ -57,8 +57,25 @@ export function resultsServiceProvider(
timeFieldName: string,
earliestMs: number,
latestMs: number,
- interval: string | number
+ intervalMs: number
+ ): Promise;
+ getEventDistributionData(
+ index: string,
+ splitField: string,
+ filterField: string,
+ query: any,
+ metricFunction: string, // ES aggregation name
+ metricFieldName: string,
+ timeFieldName: string,
+ earliestMs: number,
+ latestMs: number,
+ intervalMs: number
+ ): Promise;
+ getRecordMaxScoreByTime(
+ jobId: string,
+ criteriaFields: any[],
+ earliestMs: number,
+ latestMs: number,
+ intervalMs: number
): Promise;
- getEventDistributionData(): Promise;
- getRecordMaxScoreByTime(): Promise;
};
diff --git a/x-pack/plugins/ml/public/application/services/results_service/results_service.js b/x-pack/plugins/ml/public/application/services/results_service/results_service.js
index ef00c9025763e..fd48845494dfd 100644
--- a/x-pack/plugins/ml/public/application/services/results_service/results_service.js
+++ b/x-pack/plugins/ml/public/application/services/results_service/results_service.js
@@ -28,7 +28,7 @@ export function resultsServiceProvider(mlApiServices) {
// Pass an empty array or ['*'] to search over all job IDs.
// Returned response contains a results property, with a key for job
// which has results for the specified time range.
- getScoresByBucket(jobIds, earliestMs, latestMs, interval, perPage = 10, fromPage = 1) {
+ getScoresByBucket(jobIds, earliestMs, latestMs, intervalMs, perPage = 10, fromPage = 1) {
return new Promise((resolve, reject) => {
const obj = {
success: true,
@@ -116,7 +116,7 @@ export function resultsServiceProvider(mlApiServices) {
byTime: {
date_histogram: {
field: 'timestamp',
- interval: interval,
+ fixed_interval: `${intervalMs}ms`,
min_doc_count: 1,
extended_bounds: {
min: earliestMs,
@@ -492,7 +492,7 @@ export function resultsServiceProvider(mlApiServices) {
influencerFieldValues,
earliestMs,
latestMs,
- interval,
+ intervalMs,
maxResults = ANOMALY_SWIM_LANE_HARD_LIMIT,
perPage = SWIM_LANE_DEFAULT_PAGE_SIZE,
fromPage = 1,
@@ -615,7 +615,7 @@ export function resultsServiceProvider(mlApiServices) {
byTime: {
date_histogram: {
field: 'timestamp',
- interval,
+ fixed_interval: `${intervalMs}ms`,
min_doc_count: 1,
},
aggs: {
@@ -1033,7 +1033,7 @@ export function resultsServiceProvider(mlApiServices) {
// Extra query object can be supplied, or pass null if no additional query.
// Returned response contains a results property, which is an object
// of document counts against time (epoch millis).
- getEventRateData(index, query, timeFieldName, earliestMs, latestMs, interval) {
+ getEventRateData(index, query, timeFieldName, earliestMs, latestMs, intervalMs) {
return new Promise((resolve, reject) => {
const obj = { success: true, results: {} };
@@ -1074,7 +1074,7 @@ export function resultsServiceProvider(mlApiServices) {
eventRate: {
date_histogram: {
field: timeFieldName,
- interval: interval,
+ fixed_interval: `${intervalMs}ms`,
min_doc_count: 0,
extended_bounds: {
min: earliestMs,
@@ -1118,7 +1118,7 @@ export function resultsServiceProvider(mlApiServices) {
timeFieldName,
earliestMs,
latestMs,
- interval
+ intervalMs
) {
return new Promise((resolve, reject) => {
if (splitField === undefined) {
@@ -1187,7 +1187,7 @@ export function resultsServiceProvider(mlApiServices) {
byTime: {
date_histogram: {
field: timeFieldName,
- interval: interval,
+ fixed_interval: `${intervalMs}ms`,
min_doc_count: AGGREGATION_MIN_DOC_COUNT,
},
aggs: {
@@ -1277,7 +1277,7 @@ export function resultsServiceProvider(mlApiServices) {
// criteria, time range, and aggregation interval.
// criteriaFields parameter must be an array, with each object in the array having 'fieldName'
// 'fieldValue' properties.
- getRecordMaxScoreByTime(jobId, criteriaFields, earliestMs, latestMs, interval) {
+ getRecordMaxScoreByTime(jobId, criteriaFields, earliestMs, latestMs, intervalMs) {
return new Promise((resolve, reject) => {
const obj = {
success: true,
@@ -1331,7 +1331,7 @@ export function resultsServiceProvider(mlApiServices) {
times: {
date_histogram: {
field: 'timestamp',
- interval: interval,
+ fixed_interval: `${intervalMs}ms`,
min_doc_count: 1,
},
aggs: {
diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseries_search_service.ts b/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseries_search_service.ts
index d1e959b33e5dc..5149fecb0ec26 100644
--- a/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseries_search_service.ts
+++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseries_search_service.ts
@@ -29,7 +29,7 @@ function getMetricData(
entityFields: EntityField[],
earliestMs: number,
latestMs: number,
- interval: string
+ intervalMs: number
): Observable {
if (
isModelPlotChartableForDetector(job, detectorIndex) &&
@@ -76,7 +76,7 @@ function getMetricData(
criteriaFields,
earliestMs,
latestMs,
- interval
+ intervalMs
);
} else {
const obj: ModelPlotOutput = {
@@ -96,7 +96,7 @@ function getMetricData(
chartConfig.timeField,
earliestMs,
latestMs,
- interval
+ intervalMs
)
.pipe(
map((resp) => {
diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.js b/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.js
index 0e99d64cf202f..7d173e161a1cb 100644
--- a/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.js
+++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.js
@@ -629,7 +629,7 @@ export class TimeSeriesExplorer extends React.Component {
nonBlankEntities,
searchBounds.min.valueOf(),
searchBounds.max.valueOf(),
- stateUpdate.contextAggregationInterval.expression
+ stateUpdate.contextAggregationInterval.asMilliseconds()
)
.toPromise()
.then((resp) => {
@@ -652,7 +652,7 @@ export class TimeSeriesExplorer extends React.Component {
this.getCriteriaFields(detectorIndex, entityControls),
searchBounds.min.valueOf(),
searchBounds.max.valueOf(),
- stateUpdate.contextAggregationInterval.expression
+ stateUpdate.contextAggregationInterval.asMilliseconds()
)
.then((resp) => {
const fullRangeRecordScoreData = processRecordScoreResults(resp.results);
@@ -703,7 +703,7 @@ export class TimeSeriesExplorer extends React.Component {
nonBlankEntities,
searchBounds.min.valueOf(),
searchBounds.max.valueOf(),
- stateUpdate.contextAggregationInterval.expression,
+ stateUpdate.contextAggregationInterval.asMilliseconds(),
aggType
)
.toPromise()
diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_utils/get_focus_data.ts b/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_utils/get_focus_data.ts
index 23d1e3f7cc904..ce0d7b0abc3e0 100644
--- a/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_utils/get_focus_data.ts
+++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_utils/get_focus_data.ts
@@ -61,7 +61,7 @@ export function getFocusData(
nonBlankEntities,
searchBounds.min.valueOf(),
searchBounds.max.valueOf(),
- focusAggregationInterval.expression
+ focusAggregationInterval.asMilliseconds()
),
// Query 2 - load all the records across selected time range for the chart anomaly markers.
mlResultsService.getRecordsForCriteria(
@@ -77,7 +77,7 @@ export function getFocusData(
[selectedJob.job_id],
searchBounds.min.valueOf(),
searchBounds.max.valueOf(),
- focusAggregationInterval.expression,
+ focusAggregationInterval.asMilliseconds(),
1,
MAX_SCHEDULED_EVENTS
),
@@ -123,7 +123,7 @@ export function getFocusData(
nonBlankEntities,
searchBounds.min.valueOf(),
searchBounds.max.valueOf(),
- focusAggregationInterval.expression,
+ focusAggregationInterval.asMilliseconds(),
aggType
);
})()
diff --git a/x-pack/plugins/ml/server/models/bucket_span_estimator/polled_data_checker.js b/x-pack/plugins/ml/server/models/bucket_span_estimator/polled_data_checker.js
index fd0cab7c0625d..981ffe9618d9f 100644
--- a/x-pack/plugins/ml/server/models/bucket_span_estimator/polled_data_checker.js
+++ b/x-pack/plugins/ml/server/models/bucket_span_estimator/polled_data_checker.js
@@ -56,7 +56,7 @@ export function polledDataCheckerFactory({ asCurrentUser }) {
date_histogram: {
min_doc_count: 1,
field: this.timeField,
- interval: `${intervalMs}ms`,
+ fixed_interval: `${intervalMs}ms`,
},
},
},
diff --git a/x-pack/plugins/ml/server/models/bucket_span_estimator/single_series_checker.js b/x-pack/plugins/ml/server/models/bucket_span_estimator/single_series_checker.js
index 750f0cfc0b4a8..5dd0a5ff563d6 100644
--- a/x-pack/plugins/ml/server/models/bucket_span_estimator/single_series_checker.js
+++ b/x-pack/plugins/ml/server/models/bucket_span_estimator/single_series_checker.js
@@ -166,7 +166,7 @@ export function singleSeriesCheckerFactory({ asCurrentUser }) {
non_empty_buckets: {
date_histogram: {
field: this.timeField,
- interval: `${intervalMs}ms`,
+ fixed_interval: `${intervalMs}ms`,
},
},
},
diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/sample_data_weblogs/ml/datafeed_low_request_rate.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/sample_data_weblogs/ml/datafeed_low_request_rate.json
index a9865183320d5..084aa08455405 100644
--- a/x-pack/plugins/ml/server/models/data_recognizer/modules/sample_data_weblogs/ml/datafeed_low_request_rate.json
+++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/sample_data_weblogs/ml/datafeed_low_request_rate.json
@@ -10,7 +10,7 @@
"buckets": {
"date_histogram": {
"field": "timestamp",
- "interval": 3600000
+ "fixed_interval": "1h"
},
"aggregations": {
"timestamp": {
diff --git a/x-pack/plugins/ml/server/models/data_visualizer/data_visualizer.ts b/x-pack/plugins/ml/server/models/data_visualizer/data_visualizer.ts
index dbfa4b5656e5f..95c4e79150059 100644
--- a/x-pack/plugins/ml/server/models/data_visualizer/data_visualizer.ts
+++ b/x-pack/plugins/ml/server/models/data_visualizer/data_visualizer.ts
@@ -468,7 +468,7 @@ export class DataVisualizer {
timeFieldName: string,
earliestMs: number,
latestMs: number,
- interval: number,
+ intervalMs: number,
maxExamples: number
): Promise {
// Batch up fields by type, getting stats for multiple fields at a time.
@@ -526,7 +526,7 @@ export class DataVisualizer {
timeFieldName,
earliestMs,
latestMs,
- interval
+ intervalMs
);
batchStats.push(stats);
}
@@ -710,7 +710,7 @@ export class DataVisualizer {
timeFieldName: string,
earliestMs: number,
latestMs: number,
- interval: number
+ intervalMs: number
): Promise {
const index = indexPatternTitle;
const size = 0;
@@ -718,11 +718,12 @@ export class DataVisualizer {
// Don't use the sampler aggregation as this can lead to some potentially
// confusing date histogram results depending on the date range of data amongst shards.
+
const aggs = {
eventRate: {
date_histogram: {
field: timeFieldName,
- interval,
+ fixed_interval: `${intervalMs}ms`,
min_doc_count: 1,
},
},
@@ -756,7 +757,7 @@ export class DataVisualizer {
return {
documentCounts: {
- interval,
+ interval: intervalMs,
buckets,
},
};
diff --git a/x-pack/plugins/ml/server/models/job_service/new_job/line_chart.ts b/x-pack/plugins/ml/server/models/job_service/new_job/line_chart.ts
index 9eea1ea2a28ae..128b28a223445 100644
--- a/x-pack/plugins/ml/server/models/job_service/new_job/line_chart.ts
+++ b/x-pack/plugins/ml/server/models/job_service/new_job/line_chart.ts
@@ -114,7 +114,7 @@ function getSearchJsonFromConfig(
times: {
date_histogram: {
field: timeField,
- interval: intervalMs,
+ fixed_interval: `${intervalMs}ms`,
min_doc_count: 0,
extended_bounds: {
min: start,
diff --git a/x-pack/plugins/ml/server/models/job_service/new_job/population_chart.ts b/x-pack/plugins/ml/server/models/job_service/new_job/population_chart.ts
index 567afec809405..71e81158d8885 100644
--- a/x-pack/plugins/ml/server/models/job_service/new_job/population_chart.ts
+++ b/x-pack/plugins/ml/server/models/job_service/new_job/population_chart.ts
@@ -142,7 +142,7 @@ function getPopulationSearchJsonFromConfig(
times: {
date_histogram: {
field: timeField,
- interval: intervalMs,
+ fixed_interval: `${intervalMs}ms`,
min_doc_count: 0,
extended_bounds: {
min: start,
diff --git a/x-pack/plugins/ml/server/models/job_service/new_job_caps/__mocks__/responses/kibana_saved_objects.json b/x-pack/plugins/ml/server/models/job_service/new_job_caps/__mocks__/responses/kibana_saved_objects.json
index ca356b2bede22..9e2af76264231 100644
--- a/x-pack/plugins/ml/server/models/job_service/new_job_caps/__mocks__/responses/kibana_saved_objects.json
+++ b/x-pack/plugins/ml/server/models/job_service/new_job_caps/__mocks__/responses/kibana_saved_objects.json
@@ -20,7 +20,7 @@
"type": "index-pattern",
"id": "be14ceb0-66b1-11e9-91c9-ffa52374d341",
"attributes": {
- "typeMeta": "{\"params\":{\"rollup_index\":\"cloud_roll_index\"},\"aggs\":{\"histogram\":{\"NetworkOut\":{\"agg\":\"histogram\",\"interval\":5},\"CPUUtilization\":{\"agg\":\"histogram\",\"interval\":5},\"NetworkIn\":{\"agg\":\"histogram\",\"interval\":5}},\"avg\":{\"NetworkOut\":{\"agg\":\"avg\"},\"CPUUtilization\":{\"agg\":\"avg\"},\"NetworkIn\":{\"agg\":\"avg\"},\"DiskReadBytes\":{\"agg\":\"avg\"}},\"min\":{\"NetworkOut\":{\"agg\":\"min\"},\"NetworkIn\":{\"agg\":\"min\"}},\"value_count\":{\"NetworkOut\":{\"agg\":\"value_count\"},\"DiskReadBytes\":{\"agg\":\"value_count\"},\"CPUUtilization\":{\"agg\":\"value_count\"},\"NetworkIn\":{\"agg\":\"value_count\"}},\"max\":{\"CPUUtilization\":{\"agg\":\"max\"},\"DiskReadBytes\":{\"agg\":\"max\"}},\"date_histogram\":{\"@timestamp\":{\"agg\":\"date_histogram\",\"delay\":\"1d\",\"interval\":\"5m\",\"time_zone\":\"UTC\"}},\"terms\":{\"instance\":{\"agg\":\"terms\"},\"sourcetype.keyword\":{\"agg\":\"terms\"},\"region\":{\"agg\":\"terms\"}},\"sum\":{\"DiskReadBytes\":{\"agg\":\"sum\"},\"NetworkOut\":{\"agg\":\"sum\"}}}}",
+ "typeMeta": "{\"params\":{\"rollup_index\":\"cloud_roll_index\"},\"aggs\":{\"histogram\":{\"NetworkOut\":{\"agg\":\"histogram\",\"interval\":5},\"CPUUtilization\":{\"agg\":\"histogram\",\"interval\":5},\"NetworkIn\":{\"agg\":\"histogram\",\"interval\":5}},\"avg\":{\"NetworkOut\":{\"agg\":\"avg\"},\"CPUUtilization\":{\"agg\":\"avg\"},\"NetworkIn\":{\"agg\":\"avg\"},\"DiskReadBytes\":{\"agg\":\"avg\"}},\"min\":{\"NetworkOut\":{\"agg\":\"min\"},\"NetworkIn\":{\"agg\":\"min\"}},\"value_count\":{\"NetworkOut\":{\"agg\":\"value_count\"},\"DiskReadBytes\":{\"agg\":\"value_count\"},\"CPUUtilization\":{\"agg\":\"value_count\"},\"NetworkIn\":{\"agg\":\"value_count\"}},\"max\":{\"CPUUtilization\":{\"agg\":\"max\"},\"DiskReadBytes\":{\"agg\":\"max\"}},\"date_histogram\":{\"@timestamp\":{\"agg\":\"date_histogram\",\"delay\":\"1d\",\"fixed_interval\":\"5m\",\"time_zone\":\"UTC\"}},\"terms\":{\"instance\":{\"agg\":\"terms\"},\"sourcetype.keyword\":{\"agg\":\"terms\"},\"region\":{\"agg\":\"terms\"}},\"sum\":{\"DiskReadBytes\":{\"agg\":\"sum\"},\"NetworkOut\":{\"agg\":\"sum\"}}}}",
"title": "cloud_roll_index",
"type": "rollup"
},
diff --git a/x-pack/plugins/ml/server/models/job_service/new_job_caps/__mocks__/responses/rollup_caps.json b/x-pack/plugins/ml/server/models/job_service/new_job_caps/__mocks__/responses/rollup_caps.json
index 2b2f8576d6769..b62bce700413a 100644
--- a/x-pack/plugins/ml/server/models/job_service/new_job_caps/__mocks__/responses/rollup_caps.json
+++ b/x-pack/plugins/ml/server/models/job_service/new_job_caps/__mocks__/responses/rollup_caps.json
@@ -37,7 +37,7 @@
{
"agg": "date_histogram",
"delay": "1d",
- "interval": "5m",
+ "fixed_interval": "5m",
"time_zone": "UTC"
}
],
@@ -123,7 +123,7 @@
{
"agg": "date_histogram",
"delay": "1d",
- "interval": "5m",
+ "fixed_interval": "5m",
"time_zone": "UTC"
}
],
@@ -174,7 +174,7 @@
{
"agg": "date_histogram",
"delay": "1d",
- "interval": "5m",
+ "fixed_interval": "5m",
"time_zone": "UTC"
}
]
diff --git a/x-pack/plugins/ml/server/routes/apidoc.json b/x-pack/plugins/ml/server/routes/apidoc.json
index 86a62b28abb5e..dab00a03b5468 100644
--- a/x-pack/plugins/ml/server/routes/apidoc.json
+++ b/x-pack/plugins/ml/server/routes/apidoc.json
@@ -20,6 +20,7 @@
"DataVisualizer",
"GetOverallStats",
"GetStatsForFields",
+ "GetHistogramsForFields",
"AnomalyDetectors",
"CreateAnomalyDetectors",
diff --git a/x-pack/plugins/ml/server/routes/data_visualizer.ts b/x-pack/plugins/ml/server/routes/data_visualizer.ts
index a697fe017f192..50d9be1be4230 100644
--- a/x-pack/plugins/ml/server/routes/data_visualizer.ts
+++ b/x-pack/plugins/ml/server/routes/data_visualizer.ts
@@ -84,7 +84,7 @@ export function dataVisualizerRoutes({ router, mlLicense }: RouteInitialization)
/**
* @apiGroup DataVisualizer
*
- * @api {post} /api/ml/data_visualizer/get_field_stats/:indexPatternTitle Get histograms for fields
+ * @api {post} /api/ml/data_visualizer/get_field_histograms/:indexPatternTitle Get histograms for fields
* @apiName GetHistogramsForFields
* @apiDescription Returns the histograms on a list fields in the specified index pattern.
*
diff --git a/x-pack/plugins/ml/server/routes/schemas/data_visualizer_schema.ts b/x-pack/plugins/ml/server/routes/schemas/data_visualizer_schema.ts
index 24e45514e1efc..57bc5578e92c5 100644
--- a/x-pack/plugins/ml/server/routes/schemas/data_visualizer_schema.ts
+++ b/x-pack/plugins/ml/server/routes/schemas/data_visualizer_schema.ts
@@ -32,8 +32,8 @@ export const dataVisualizerFieldStatsSchema = schema.object({
earliest: schema.maybe(schema.number()),
/** Latest timestamp for search, as epoch ms (optional). */
latest: schema.maybe(schema.number()),
- /** Aggregation interval to use for obtaining document counts over time (optional). */
- interval: schema.maybe(schema.string()),
+ /** Aggregation interval, in milliseconds, to use for obtaining document counts over time (optional). */
+ interval: schema.maybe(schema.number()),
/** Maximum number of examples to return for text type fields. */
maxExamples: schema.number(),
});
diff --git a/x-pack/plugins/monitoring/server/config.test.ts b/x-pack/plugins/monitoring/server/config.test.ts
index 32b8691bd6049..2efc325a3edec 100644
--- a/x-pack/plugins/monitoring/server/config.test.ts
+++ b/x-pack/plugins/monitoring/server/config.test.ts
@@ -86,6 +86,9 @@ describe('config schema', () => {
"index": "filebeat-*",
},
"max_bucket_size": 10000,
+ "metricbeat": Object {
+ "index": "metricbeat-*",
+ },
"min_interval_seconds": 10,
"show_license_expiration": true,
},
diff --git a/x-pack/plugins/monitoring/server/config.ts b/x-pack/plugins/monitoring/server/config.ts
index 789211c43db31..6ae99e3d16d64 100644
--- a/x-pack/plugins/monitoring/server/config.ts
+++ b/x-pack/plugins/monitoring/server/config.ts
@@ -29,6 +29,9 @@ export const configSchema = schema.object({
logs: schema.object({
index: schema.string({ defaultValue: 'filebeat-*' }),
}),
+ metricbeat: schema.object({
+ index: schema.string({ defaultValue: 'metricbeat-*' }),
+ }),
max_bucket_size: schema.number({ defaultValue: 10000 }),
elasticsearch: monitoringElasticsearchConfigSchema,
container: schema.object({
diff --git a/x-pack/plugins/monitoring/server/lib/ccs_utils.js b/x-pack/plugins/monitoring/server/lib/ccs_utils.js
index dab1e87435c86..bef07124fb430 100644
--- a/x-pack/plugins/monitoring/server/lib/ccs_utils.js
+++ b/x-pack/plugins/monitoring/server/lib/ccs_utils.js
@@ -5,6 +5,21 @@
*/
import { isFunction, get } from 'lodash';
+export function appendMetricbeatIndex(config, indexPattern) {
+ // Leverage this function to also append the dynamic metricbeat index too
+ let mbIndex = null;
+ // TODO: NP
+ // This function is called with both NP config and LP config
+ if (isFunction(config.get)) {
+ mbIndex = config.get('monitoring.ui.metricbeat.index');
+ } else {
+ mbIndex = get(config, 'monitoring.ui.metricbeat.index');
+ }
+
+ const newIndexPattern = `${indexPattern},${mbIndex}`;
+ return newIndexPattern;
+}
+
/**
* Prefix all comma separated index patterns within the original {@code indexPattern}.
*
@@ -27,7 +42,7 @@ export function prefixIndexPattern(config, indexPattern, ccs) {
}
if (!ccsEnabled || !ccs) {
- return indexPattern;
+ return appendMetricbeatIndex(config, indexPattern);
}
const patterns = indexPattern.split(',');
@@ -35,10 +50,10 @@ export function prefixIndexPattern(config, indexPattern, ccs) {
// if a wildcard is used, then we also want to search the local indices
if (ccs === '*') {
- return `${prefixedPattern},${indexPattern}`;
+ return appendMetricbeatIndex(config, `${prefixedPattern},${indexPattern}`);
}
- return prefixedPattern;
+ return appendMetricbeatIndex(config, prefixedPattern);
}
/**
diff --git a/x-pack/plugins/monitoring/server/lib/create_query.js b/x-pack/plugins/monitoring/server/lib/create_query.js
index 04e0d7642ec58..1983dc3dcf9af 100644
--- a/x-pack/plugins/monitoring/server/lib/create_query.js
+++ b/x-pack/plugins/monitoring/server/lib/create_query.js
@@ -57,7 +57,7 @@ export function createQuery(options) {
let typeFilter;
if (type) {
- typeFilter = { term: { type } };
+ typeFilter = { bool: { should: [{ term: { type } }, { term: { 'metricset.name': type } }] } };
}
let clusterUuidFilter;
diff --git a/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_node_summary.js b/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_node_summary.js
index 6abb392e58818..84384021a3593 100644
--- a/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_node_summary.js
+++ b/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_node_summary.js
@@ -17,15 +17,23 @@ export function handleResponse(clusterState, shardStats, nodeUuid) {
return (response) => {
let nodeSummary = {};
const nodeStatsHits = get(response, 'hits.hits', []);
- const nodes = nodeStatsHits.map((hit) => hit._source.source_node); // using [0] value because query results are sorted desc per timestamp
+ const nodes = nodeStatsHits.map((hit) =>
+ get(hit, '_source.elasticsearch.node', hit._source.source_node)
+ ); // using [0] value because query results are sorted desc per timestamp
const node = nodes[0] || getDefaultNodeFromId(nodeUuid);
- const sourceStats = get(response, 'hits.hits[0]._source.node_stats');
+ const sourceStats =
+ get(response, 'hits.hits[0]._source.elasticsearch.node.stats') ||
+ get(response, 'hits.hits[0]._source.node_stats');
const clusterNode = get(clusterState, ['nodes', nodeUuid]);
const stats = {
resolver: nodeUuid,
- node_ids: nodes.map((node) => node.uuid),
+ node_ids: nodes.map((node) => node.id || node.uuid),
attributes: node.attributes,
- transport_address: node.transport_address,
+ transport_address: get(
+ response,
+ 'hits.hits[0]._source.service.address',
+ node.transport_address
+ ),
name: node.name,
type: node.type,
};
@@ -45,10 +53,17 @@ export function handleResponse(clusterState, shardStats, nodeUuid) {
totalShards: _shardStats.shardCount,
indexCount: _shardStats.indexCount,
documents: get(sourceStats, 'indices.docs.count'),
- dataSize: get(sourceStats, 'indices.store.size_in_bytes'),
- freeSpace: get(sourceStats, 'fs.total.available_in_bytes'),
- totalSpace: get(sourceStats, 'fs.total.total_in_bytes'),
- usedHeap: get(sourceStats, 'jvm.mem.heap_used_percent'),
+ dataSize:
+ get(sourceStats, 'indices.store.size_in_bytes') ||
+ get(sourceStats, 'indices.store.size.bytes'),
+ freeSpace:
+ get(sourceStats, 'fs.total.available_in_bytes') ||
+ get(sourceStats, 'fs.summary.available.bytes'),
+ totalSpace:
+ get(sourceStats, 'fs.total.total_in_bytes') || get(sourceStats, 'fs.summary.total.bytes'),
+ usedHeap:
+ get(sourceStats, 'jvm.mem.heap_used_percent') ||
+ get(sourceStats, 'jvm.mem.heap.used.pct'),
status: i18n.translate('xpack.monitoring.es.nodes.onlineStatusLabel', {
defaultMessage: 'Online',
}),
diff --git a/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/get_node_ids.js b/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/get_node_ids.js
index 573f1792e5f8a..68bca96e2911b 100644
--- a/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/get_node_ids.js
+++ b/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/get_node_ids.js
@@ -19,6 +19,7 @@ export async function getNodeIds(req, indexPattern, { clusterUuid }, size) {
filterPath: ['aggregations.composite_data.buckets'],
body: {
query: createQuery({
+ type: 'node_stats',
start,
end,
metric: ElasticsearchMetric.getMetricFields(),
diff --git a/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/get_nodes.js b/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/get_nodes.js
index 682da324ee72f..c2794b7e7fa44 100644
--- a/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/get_nodes.js
+++ b/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/get_nodes.js
@@ -96,6 +96,7 @@ export async function getNodes(req, esIndexPattern, pageOfNodes, clusterStats, n
},
filterPath: [
'hits.hits._source.source_node',
+ 'hits.hits._source.elasticsearch.node',
'aggregations.nodes.buckets.key',
...LISTING_METRICS_PATHS,
],
diff --git a/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/map_nodes_info.js b/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/map_nodes_info.js
index 3c719c2ddfbf8..317c1cddf57ae 100644
--- a/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/map_nodes_info.js
+++ b/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/map_nodes_info.js
@@ -17,25 +17,29 @@ export function mapNodesInfo(nodeHits, clusterStats, nodesShardCount) {
const clusterState = get(clusterStats, 'cluster_state', { nodes: {} });
return nodeHits.reduce((prev, node) => {
- const sourceNode = get(node, '_source.source_node');
+ const sourceNode = get(node, '_source.source_node') || get(node, '_source.elasticsearch.node');
const calculatedNodeType = calculateNodeType(sourceNode, get(clusterState, 'master_node'));
const { nodeType, nodeTypeLabel, nodeTypeClass } = getNodeTypeClassLabel(
sourceNode,
calculatedNodeType
);
- const isOnline = !isUndefined(get(clusterState, ['nodes', sourceNode.uuid]));
+ const isOnline = !isUndefined(get(clusterState, ['nodes', sourceNode.uuid || sourceNode.id]));
return {
...prev,
- [sourceNode.uuid]: {
+ [sourceNode.uuid || sourceNode.id]: {
name: sourceNode.name,
transport_address: sourceNode.transport_address,
type: nodeType,
isOnline,
nodeTypeLabel: nodeTypeLabel,
nodeTypeClass: nodeTypeClass,
- shardCount: get(nodesShardCount, `nodes[${sourceNode.uuid}].shardCount`, 0),
+ shardCount: get(
+ nodesShardCount,
+ `nodes[${sourceNode.uuid || sourceNode.id}].shardCount`,
+ 0
+ ),
},
};
}, {});
diff --git a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/alerting.test.ts b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/alerting.test.ts
index 636082656f1a4..5e9c1818cad2b 100644
--- a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/alerting.test.ts
+++ b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/alerting.test.ts
@@ -74,7 +74,7 @@ describe(`feature_privilege_builder`, () => {
Array [
"alerting:1.0.0-zeta1:alert-type/my-feature/get",
"alerting:1.0.0-zeta1:alert-type/my-feature/getAlertState",
- "alerting:1.0.0-zeta1:alert-type/my-feature/getAlertStatus",
+ "alerting:1.0.0-zeta1:alert-type/my-feature/getAlertInstanceSummary",
"alerting:1.0.0-zeta1:alert-type/my-feature/find",
]
`);
@@ -111,7 +111,7 @@ describe(`feature_privilege_builder`, () => {
Array [
"alerting:1.0.0-zeta1:alert-type/my-feature/get",
"alerting:1.0.0-zeta1:alert-type/my-feature/getAlertState",
- "alerting:1.0.0-zeta1:alert-type/my-feature/getAlertStatus",
+ "alerting:1.0.0-zeta1:alert-type/my-feature/getAlertInstanceSummary",
"alerting:1.0.0-zeta1:alert-type/my-feature/find",
"alerting:1.0.0-zeta1:alert-type/my-feature/create",
"alerting:1.0.0-zeta1:alert-type/my-feature/delete",
@@ -158,7 +158,7 @@ describe(`feature_privilege_builder`, () => {
Array [
"alerting:1.0.0-zeta1:alert-type/my-feature/get",
"alerting:1.0.0-zeta1:alert-type/my-feature/getAlertState",
- "alerting:1.0.0-zeta1:alert-type/my-feature/getAlertStatus",
+ "alerting:1.0.0-zeta1:alert-type/my-feature/getAlertInstanceSummary",
"alerting:1.0.0-zeta1:alert-type/my-feature/find",
"alerting:1.0.0-zeta1:alert-type/my-feature/create",
"alerting:1.0.0-zeta1:alert-type/my-feature/delete",
@@ -172,7 +172,7 @@ describe(`feature_privilege_builder`, () => {
"alerting:1.0.0-zeta1:alert-type/my-feature/unmuteInstance",
"alerting:1.0.0-zeta1:readonly-alert-type/my-feature/get",
"alerting:1.0.0-zeta1:readonly-alert-type/my-feature/getAlertState",
- "alerting:1.0.0-zeta1:readonly-alert-type/my-feature/getAlertStatus",
+ "alerting:1.0.0-zeta1:readonly-alert-type/my-feature/getAlertInstanceSummary",
"alerting:1.0.0-zeta1:readonly-alert-type/my-feature/find",
]
`);
diff --git a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/alerting.ts b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/alerting.ts
index 540b9e5c1e56e..eb278a5755204 100644
--- a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/alerting.ts
+++ b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/alerting.ts
@@ -8,7 +8,7 @@ import { uniq } from 'lodash';
import { Feature, FeatureKibanaPrivileges } from '../../../../../features/server';
import { BaseFeaturePrivilegeBuilder } from './feature_privilege_builder';
-const readOperations: string[] = ['get', 'getAlertState', 'getAlertStatus', 'find'];
+const readOperations: string[] = ['get', 'getAlertState', 'getAlertInstanceSummary', 'find'];
const writeOperations: string[] = [
'create',
'delete',
diff --git a/x-pack/plugins/security_solution/common/endpoint/constants.ts b/x-pack/plugins/security_solution/common/endpoint/constants.ts
index 366bf7a1df1f2..a6018837fa4fe 100644
--- a/x-pack/plugins/security_solution/common/endpoint/constants.ts
+++ b/x-pack/plugins/security_solution/common/endpoint/constants.ts
@@ -7,6 +7,7 @@
export const eventsIndexPattern = 'logs-endpoint.events.*';
export const alertsIndexPattern = 'logs-endpoint.alerts-*';
export const metadataIndexPattern = 'metrics-endpoint.metadata-*';
+export const metadataCurrentIndexPattern = 'metrics-endpoint.metadata_current-*';
export const policyIndexPattern = 'metrics-endpoint.policy-*';
export const telemetryIndexPattern = 'metrics-endpoint.telemetry-*';
export const LIMITED_CONCURRENCY_ENDPOINT_ROUTE_TAG = 'endpoint:limited-concurrency';
diff --git a/x-pack/plugins/security_solution/common/endpoint/types/index.ts b/x-pack/plugins/security_solution/common/endpoint/types/index.ts
index 8e507cbc921a2..e0bd916103a28 100644
--- a/x-pack/plugins/security_solution/common/endpoint/types/index.ts
+++ b/x-pack/plugins/security_solution/common/endpoint/types/index.ts
@@ -445,6 +445,13 @@ export type HostInfo = Immutable<{
host_status: HostStatus;
}>;
+export type HostMetadataDetails = Immutable<{
+ agent: {
+ id: string;
+ };
+ HostDetails: HostMetadata;
+}>;
+
export type HostMetadata = Immutable<{
'@timestamp': number;
event: {
diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/index.ts
index b7d905d22e839..35fcc3b07fd05 100644
--- a/x-pack/plugins/security_solution/common/search_strategy/security_solution/index.ts
+++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/index.ts
@@ -23,6 +23,8 @@ import {
} from './hosts';
import {
NetworkQueries,
+ NetworkDetailsStrategyResponse,
+ NetworkDetailsRequestOptions,
NetworkDnsStrategyResponse,
NetworkDnsRequestOptions,
NetworkTlsStrategyResponse,
@@ -35,6 +37,8 @@ import {
NetworkTopCountriesRequestOptions,
NetworkTopNFlowStrategyResponse,
NetworkTopNFlowRequestOptions,
+ NetworkUsersStrategyResponse,
+ NetworkUsersRequestOptions,
} from './network';
import {
MatrixHistogramQuery,
@@ -87,6 +91,8 @@ export type StrategyResponseType = T extends HostsQ
? HostFirstLastSeenStrategyResponse
: T extends HostsQueries.uncommonProcesses
? HostUncommonProcessesStrategyResponse
+ : T extends NetworkQueries.details
+ ? NetworkDetailsStrategyResponse
: T extends NetworkQueries.dns
? NetworkDnsStrategyResponse
: T extends NetworkQueries.http
@@ -99,6 +105,8 @@ export type StrategyResponseType = T extends HostsQ
? NetworkTopCountriesStrategyResponse
: T extends NetworkQueries.topNFlow
? NetworkTopNFlowStrategyResponse
+ : T extends NetworkQueries.users
+ ? NetworkUsersStrategyResponse
: T extends typeof MatrixHistogramQuery
? MatrixHistogramStrategyResponse
: never;
@@ -115,6 +123,8 @@ export type StrategyRequestType = T extends HostsQu
? HostFirstLastSeenRequestOptions
: T extends HostsQueries.uncommonProcesses
? HostUncommonProcessesRequestOptions
+ : T extends NetworkQueries.details
+ ? NetworkDetailsRequestOptions
: T extends NetworkQueries.dns
? NetworkDnsRequestOptions
: T extends NetworkQueries.http
@@ -127,6 +137,8 @@ export type StrategyRequestType = T extends HostsQu
? NetworkTopCountriesRequestOptions
: T extends NetworkQueries.topNFlow
? NetworkTopNFlowRequestOptions
+ : T extends NetworkQueries.users
+ ? NetworkUsersRequestOptions
: T extends typeof MatrixHistogramQuery
? MatrixHistogramRequestOptions
: never;
diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/network/common/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/network/common/index.ts
index 66676569b3c9e..19521741c5f66 100644
--- a/x-pack/plugins/security_solution/common/search_strategy/security_solution/network/common/index.ts
+++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/network/common/index.ts
@@ -15,6 +15,13 @@ export enum NetworkTopTablesFields {
source_ips = 'source_ips',
}
+export enum FlowTarget {
+ client = 'client',
+ destination = 'destination',
+ server = 'server',
+ source = 'source',
+}
+
export enum FlowTargetSourceDest {
destination = 'destination',
source = 'source',
diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/network/details/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/network/details/index.ts
new file mode 100644
index 0000000000000..920d7cf8c5eaf
--- /dev/null
+++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/network/details/index.ts
@@ -0,0 +1,109 @@
+/*
+ * 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 { IEsSearchResponse } from '../../../../../../../../src/plugins/data/common';
+import { HostEcs } from '../../../../ecs/host';
+import { GeoEcs } from '../../../../ecs/geo';
+import { Inspect, Maybe, TotalValue, Hit, ShardsResponse } from '../../../common';
+import { RequestBasicOptions } from '../..';
+
+export interface NetworkDetailsRequestOptions extends Omit {
+ ip: string;
+}
+
+export interface NetworkDetailsStrategyResponse extends IEsSearchResponse {
+ networkDetails: {
+ client?: Maybe;
+ destination?: Maybe;
+ host?: HostEcs;
+ server?: Maybe;
+ source?: Maybe;
+ };
+ inspect?: Maybe;
+}
+
+export interface NetworkDetails {
+ firstSeen?: Maybe;
+ lastSeen?: Maybe;
+ autonomousSystem: AutonomousSystem;
+ geo: GeoEcs;
+}
+
+export interface AutonomousSystem {
+ number?: Maybe;
+ organization?: Maybe;
+}
+
+export interface AutonomousSystemOrganization {
+ name?: Maybe;
+}
+
+interface ResultHit {
+ doc_count: number;
+ results: {
+ hits: {
+ total: TotalValue | number;
+ max_score: number | null;
+ hits: Array<{
+ _source: T;
+ sort?: [number];
+ _index?: string;
+ _type?: string;
+ _id?: string;
+ _score?: number | null;
+ }>;
+ };
+ };
+}
+
+export interface NetworkHit {
+ took?: number;
+ timed_out?: boolean;
+ _scroll_id?: string;
+ _shards?: ShardsResponse;
+ timeout?: number;
+ hits?: {
+ total: number;
+ hits: Hit[];
+ };
+ doc_count: number;
+ geo: ResultHit