mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
[ResponseOps][OnWeek] Connector logs view (#148291)
Resolves https://github.com/elastic/kibana/issues/147795 ## Summary Adds a connectors event log tab that gives the ability to see historical activity of connectors. ### Checklist Delete any items that are not applicable to this PR. - [x] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md) - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios ### To verify - Create a rule with a connector, create a few to help test the search capability - Go to http://localhost:5601/app/management/insightsAndAlerting/triggersActionsConnectors/logs - Verify that you can see the execution logs for the connector - Verify that you can search - Verify that you can filter by error/success Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Ying Mao <ying.mao@elastic.co>
This commit is contained in:
parent
f7b25f5e46
commit
77742b8a9e
73 changed files with 5273 additions and 1050 deletions
61
x-pack/plugins/actions/common/execution_log_types.ts
Normal file
61
x-pack/plugins/actions/common/execution_log_types.ts
Normal file
|
@ -0,0 +1,61 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { estypes } from '@elastic/elasticsearch';
|
||||
|
||||
export interface IExecutionLog {
|
||||
id: string;
|
||||
timestamp: string;
|
||||
duration_ms: number;
|
||||
status: string;
|
||||
message: string;
|
||||
version: string;
|
||||
schedule_delay_ms: number;
|
||||
space_ids: string[];
|
||||
connector_name: string;
|
||||
connector_id: string;
|
||||
timed_out: boolean;
|
||||
}
|
||||
|
||||
export interface IExecutionLogResult {
|
||||
total: number;
|
||||
data: IExecutionLog[];
|
||||
}
|
||||
|
||||
export interface GetGlobalExecutionLogParams {
|
||||
dateStart: string;
|
||||
dateEnd?: string;
|
||||
filter?: string;
|
||||
page: number;
|
||||
perPage: number;
|
||||
sort: estypes.Sort;
|
||||
namespaces?: Array<string | undefined>;
|
||||
}
|
||||
|
||||
export interface GetGlobalExecutionKPIParams {
|
||||
dateStart: string;
|
||||
dateEnd?: string;
|
||||
filter?: string;
|
||||
namespaces?: Array<string | undefined>;
|
||||
}
|
||||
|
||||
export const EMPTY_EXECUTION_KPI_RESULT = {
|
||||
success: 0,
|
||||
unknown: 0,
|
||||
failure: 0,
|
||||
warning: 0,
|
||||
};
|
||||
|
||||
export type IExecutionKPIResult = typeof EMPTY_EXECUTION_KPI_RESULT;
|
||||
|
||||
export const executionLogSortableColumns = [
|
||||
'timestamp',
|
||||
'execution_duration',
|
||||
'schedule_delay',
|
||||
] as const;
|
||||
|
||||
export type ExecutionLogSortFields = typeof executionLogSortableColumns[number];
|
|
@ -14,6 +14,7 @@ export * from './rewrite_request_case';
|
|||
export * from './mustache_template';
|
||||
export * from './validate_email_addresses';
|
||||
export * from './connector_feature_config';
|
||||
export * from './execution_log_types';
|
||||
|
||||
export const BASE_ACTION_API_PATH = '/api/actions';
|
||||
export const INTERNAL_BASE_ACTION_API_PATH = '/internal/actions';
|
||||
|
|
|
@ -27,6 +27,8 @@ const createActionsClientMock = () => {
|
|||
listTypes: jest.fn(),
|
||||
isActionTypeEnabled: jest.fn(),
|
||||
isPreconfigured: jest.fn(),
|
||||
getGlobalExecutionKpiWithAuth: jest.fn(),
|
||||
getGlobalExecutionLogWithAuth: jest.fn(),
|
||||
};
|
||||
return mocked;
|
||||
};
|
||||
|
|
|
@ -44,6 +44,8 @@ import { inMemoryMetricsMock } from './monitoring/in_memory_metrics.mock';
|
|||
import { getOAuthJwtAccessToken } from './lib/get_oauth_jwt_access_token';
|
||||
import { getOAuthClientCredentialsAccessToken } from './lib/get_oauth_client_credentials_access_token';
|
||||
import { OAuthParams } from './routes/get_oauth_access_token';
|
||||
import { eventLogClientMock } from '@kbn/event-log-plugin/server/event_log_client.mock';
|
||||
import { GetGlobalExecutionKPIParams, GetGlobalExecutionLogParams } from '../common';
|
||||
|
||||
jest.mock('@kbn/core-saved-objects-utils-server', () => {
|
||||
const actual = jest.requireActual('@kbn/core-saved-objects-utils-server');
|
||||
|
@ -81,6 +83,10 @@ jest.mock('./lib/get_oauth_client_credentials_access_token', () => ({
|
|||
getOAuthClientCredentialsAccessToken: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('uuid', () => ({
|
||||
v4: () => 'uuidv4',
|
||||
}));
|
||||
|
||||
const defaultKibanaIndex = '.kibana';
|
||||
const unsecuredSavedObjectsClient = savedObjectsClientMock.create();
|
||||
const scopedClusterClient = elasticsearchServiceMock.createScopedClusterClient();
|
||||
|
@ -96,6 +102,8 @@ const mockUsageCounter = mockUsageCountersSetup.createUsageCounter('test');
|
|||
const logger = loggingSystemMock.create().get() as jest.Mocked<Logger>;
|
||||
const mockTaskManager = taskManagerMock.createSetup();
|
||||
const configurationUtilities = actionsConfigMock.create();
|
||||
const eventLogClient = eventLogClientMock.create();
|
||||
const getEventLogClient = jest.fn();
|
||||
|
||||
let actionsClient: ActionsClient;
|
||||
let mockedLicenseState: jest.Mocked<ILicenseState>;
|
||||
|
@ -139,11 +147,13 @@ beforeEach(() => {
|
|||
auditLogger,
|
||||
usageCounter: mockUsageCounter,
|
||||
connectorTokenClient,
|
||||
getEventLogClient,
|
||||
});
|
||||
(getOAuthJwtAccessToken as jest.Mock).mockResolvedValue(`Bearer jwttokentokentoken`);
|
||||
(getOAuthClientCredentialsAccessToken as jest.Mock).mockResolvedValue(
|
||||
`Bearer clienttokentokentoken`
|
||||
);
|
||||
getEventLogClient.mockResolvedValue(eventLogClient);
|
||||
});
|
||||
|
||||
describe('create()', () => {
|
||||
|
@ -566,6 +576,7 @@ describe('create()', () => {
|
|||
request,
|
||||
authorization: authorization as unknown as ActionsAuthorization,
|
||||
connectorTokenClient: connectorTokenClientMock.create(),
|
||||
getEventLogClient,
|
||||
});
|
||||
|
||||
const savedObjectCreateResult = {
|
||||
|
@ -687,6 +698,7 @@ describe('get()', () => {
|
|||
},
|
||||
],
|
||||
connectorTokenClient: connectorTokenClientMock.create(),
|
||||
getEventLogClient,
|
||||
});
|
||||
|
||||
await actionsClient.get({ id: 'testPreconfigured' });
|
||||
|
@ -747,6 +759,7 @@ describe('get()', () => {
|
|||
},
|
||||
],
|
||||
connectorTokenClient: connectorTokenClientMock.create(),
|
||||
getEventLogClient,
|
||||
});
|
||||
|
||||
authorization.ensureAuthorized.mockRejectedValue(
|
||||
|
@ -869,6 +882,7 @@ describe('get()', () => {
|
|||
},
|
||||
],
|
||||
connectorTokenClient: connectorTokenClientMock.create(),
|
||||
getEventLogClient,
|
||||
});
|
||||
|
||||
const result = await actionsClient.get({ id: 'testPreconfigured' });
|
||||
|
@ -942,6 +956,7 @@ describe('getAll()', () => {
|
|||
},
|
||||
],
|
||||
connectorTokenClient: connectorTokenClientMock.create(),
|
||||
getEventLogClient,
|
||||
});
|
||||
return actionsClient.getAll();
|
||||
}
|
||||
|
@ -1084,6 +1099,7 @@ describe('getAll()', () => {
|
|||
},
|
||||
],
|
||||
connectorTokenClient: connectorTokenClientMock.create(),
|
||||
getEventLogClient,
|
||||
});
|
||||
const result = await actionsClient.getAll();
|
||||
expect(result).toEqual([
|
||||
|
@ -1166,6 +1182,7 @@ describe('getBulk()', () => {
|
|||
},
|
||||
],
|
||||
connectorTokenClient: connectorTokenClientMock.create(),
|
||||
getEventLogClient,
|
||||
});
|
||||
return actionsClient.getBulk(['1', 'testPreconfigured']);
|
||||
}
|
||||
|
@ -1302,6 +1319,7 @@ describe('getBulk()', () => {
|
|||
},
|
||||
],
|
||||
connectorTokenClient: connectorTokenClientMock.create(),
|
||||
getEventLogClient,
|
||||
});
|
||||
const result = await actionsClient.getBulk(['1', 'testPreconfigured']);
|
||||
expect(result).toEqual([
|
||||
|
@ -1361,6 +1379,7 @@ describe('getOAuthAccessToken()', () => {
|
|||
},
|
||||
],
|
||||
connectorTokenClient: connectorTokenClientMock.create(),
|
||||
getEventLogClient,
|
||||
});
|
||||
return actionsClient.getOAuthAccessToken(requestBody, configurationUtilities);
|
||||
}
|
||||
|
@ -2194,6 +2213,7 @@ describe('execute()', () => {
|
|||
|
||||
test('calls the actionExecutor with the appropriate parameters', async () => {
|
||||
const actionId = uuidv4();
|
||||
const actionExecutionId = uuidv4();
|
||||
actionExecutor.execute.mockResolvedValue({ status: 'ok', actionId });
|
||||
await expect(
|
||||
actionsClient.execute({
|
||||
|
@ -2210,6 +2230,7 @@ describe('execute()', () => {
|
|||
params: {
|
||||
name: 'my name',
|
||||
},
|
||||
actionExecutionId,
|
||||
});
|
||||
|
||||
await expect(
|
||||
|
@ -2241,6 +2262,7 @@ describe('execute()', () => {
|
|||
type: 'some-type',
|
||||
},
|
||||
],
|
||||
actionExecutionId,
|
||||
});
|
||||
|
||||
await expect(
|
||||
|
@ -2274,6 +2296,7 @@ describe('execute()', () => {
|
|||
namespace: 'some-namespace',
|
||||
},
|
||||
],
|
||||
actionExecutionId,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -2525,6 +2548,7 @@ describe('isPreconfigured()', () => {
|
|||
encryptedSavedObjectsClient: encryptedSavedObjectsMock.createClient(),
|
||||
logger,
|
||||
}),
|
||||
getEventLogClient,
|
||||
});
|
||||
|
||||
expect(actionsClient.isPreconfigured('testPreconfigured')).toEqual(true);
|
||||
|
@ -2563,8 +2587,125 @@ describe('isPreconfigured()', () => {
|
|||
encryptedSavedObjectsClient: encryptedSavedObjectsMock.createClient(),
|
||||
logger,
|
||||
}),
|
||||
getEventLogClient,
|
||||
});
|
||||
|
||||
expect(actionsClient.isPreconfigured(uuidv4())).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getGlobalExecutionLogWithAuth()', () => {
|
||||
const opts: GetGlobalExecutionLogParams = {
|
||||
dateStart: '2023-01-09T08:55:56-08:00',
|
||||
dateEnd: '2023-01-10T08:55:56-08:00',
|
||||
page: 1,
|
||||
perPage: 50,
|
||||
sort: [{ timestamp: { order: 'desc' } }],
|
||||
};
|
||||
const results = {
|
||||
aggregations: {
|
||||
executionLogAgg: {
|
||||
doc_count: 5,
|
||||
executionUuid: {
|
||||
doc_count_error_upper_bound: 0,
|
||||
sum_other_doc_count: 0,
|
||||
buckets: [],
|
||||
},
|
||||
executionUuidCardinality: { doc_count: 5, executionUuidCardinality: { value: 5 } },
|
||||
},
|
||||
},
|
||||
};
|
||||
describe('authorization', () => {
|
||||
test('ensures user is authorised to access logs', async () => {
|
||||
eventLogClient.aggregateEventsWithAuthFilter.mockResolvedValue(results);
|
||||
|
||||
(getAuthorizationModeBySource as jest.Mock).mockImplementationOnce(() => {
|
||||
return AuthorizationMode.RBAC;
|
||||
});
|
||||
await actionsClient.getGlobalExecutionLogWithAuth(opts);
|
||||
expect(authorization.ensureAuthorized).toHaveBeenCalledWith('get');
|
||||
});
|
||||
|
||||
test('throws when user is not authorised to access logs', async () => {
|
||||
(getAuthorizationModeBySource as jest.Mock).mockImplementationOnce(() => {
|
||||
return AuthorizationMode.RBAC;
|
||||
});
|
||||
authorization.ensureAuthorized.mockRejectedValue(new Error(`Unauthorized to access logs`));
|
||||
|
||||
await expect(actionsClient.getGlobalExecutionLogWithAuth(opts)).rejects.toMatchInlineSnapshot(
|
||||
`[Error: Unauthorized to access logs]`
|
||||
);
|
||||
|
||||
expect(authorization.ensureAuthorized).toHaveBeenCalledWith('get');
|
||||
});
|
||||
});
|
||||
|
||||
test('calls the eventLogClient with the appropriate parameters', async () => {
|
||||
eventLogClient.aggregateEventsWithAuthFilter.mockResolvedValue(results);
|
||||
|
||||
await expect(actionsClient.getGlobalExecutionLogWithAuth(opts)).resolves.toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"data": Array [],
|
||||
"total": 5,
|
||||
}
|
||||
`);
|
||||
expect(eventLogClient.aggregateEventsWithAuthFilter).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getGlobalExecutionKpiWithAuth()', () => {
|
||||
const opts: GetGlobalExecutionKPIParams = {
|
||||
dateStart: '2023-01-09T08:55:56-08:00',
|
||||
dateEnd: '2023-01-10T08:55:56-08:00',
|
||||
};
|
||||
const results = {
|
||||
aggregations: {
|
||||
executionKpiAgg: {
|
||||
doc_count: 5,
|
||||
executionUuid: {
|
||||
doc_count_error_upper_bound: 0,
|
||||
sum_other_doc_count: 0,
|
||||
buckets: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
describe('authorization', () => {
|
||||
test('ensures user is authorised to access kpi', async () => {
|
||||
eventLogClient.aggregateEventsWithAuthFilter.mockResolvedValue(results);
|
||||
|
||||
(getAuthorizationModeBySource as jest.Mock).mockImplementationOnce(() => {
|
||||
return AuthorizationMode.RBAC;
|
||||
});
|
||||
await actionsClient.getGlobalExecutionKpiWithAuth(opts);
|
||||
expect(authorization.ensureAuthorized).toHaveBeenCalledWith('get');
|
||||
});
|
||||
|
||||
test('throws when user is not authorised to access kpi', async () => {
|
||||
(getAuthorizationModeBySource as jest.Mock).mockImplementationOnce(() => {
|
||||
return AuthorizationMode.RBAC;
|
||||
});
|
||||
authorization.ensureAuthorized.mockRejectedValue(new Error(`Unauthorized to access kpi`));
|
||||
|
||||
await expect(actionsClient.getGlobalExecutionKpiWithAuth(opts)).rejects.toMatchInlineSnapshot(
|
||||
`[Error: Unauthorized to access kpi]`
|
||||
);
|
||||
|
||||
expect(authorization.ensureAuthorized).toHaveBeenCalledWith('get');
|
||||
});
|
||||
});
|
||||
|
||||
test('calls the eventLogClient with the appropriate parameters', async () => {
|
||||
eventLogClient.aggregateEventsWithAuthFilter.mockResolvedValue(results);
|
||||
|
||||
await expect(actionsClient.getGlobalExecutionKpiWithAuth(opts)).resolves.toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"failure": 0,
|
||||
"success": 0,
|
||||
"unknown": 0,
|
||||
"warning": 0,
|
||||
}
|
||||
`);
|
||||
expect(eventLogClient.aggregateEventsWithAuthFilter).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import Boom from '@hapi/boom';
|
||||
import url from 'url';
|
||||
import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
|
||||
|
@ -23,7 +24,14 @@ import {
|
|||
} from '@kbn/core/server';
|
||||
import { AuditLogger } from '@kbn/security-plugin/server';
|
||||
import { RunNowResult } from '@kbn/task-manager-plugin/server';
|
||||
import { ActionType } from '../common';
|
||||
import { IEventLogClient } from '@kbn/event-log-plugin/server';
|
||||
import { KueryNode } from '@kbn/es-query';
|
||||
import {
|
||||
ActionType,
|
||||
GetGlobalExecutionKPIParams,
|
||||
GetGlobalExecutionLogParams,
|
||||
IExecutionLogResult,
|
||||
} from '../common';
|
||||
import { ActionTypeRegistry } from './action_type_registry';
|
||||
import {
|
||||
validateConfig,
|
||||
|
@ -31,6 +39,7 @@ import {
|
|||
ActionExecutorContract,
|
||||
validateConnector,
|
||||
ActionExecutionSource,
|
||||
parseDate,
|
||||
} from './lib';
|
||||
import {
|
||||
ActionResult,
|
||||
|
@ -72,6 +81,13 @@ import {
|
|||
GetOAuthClientCredentialsConfig,
|
||||
GetOAuthClientCredentialsSecrets,
|
||||
} from './lib/get_oauth_client_credentials_access_token';
|
||||
import {
|
||||
ACTION_FILTER,
|
||||
formatExecutionKPIResult,
|
||||
formatExecutionLogResult,
|
||||
getExecutionKPIAggregation,
|
||||
getExecutionLogAggregation,
|
||||
} from './lib/get_execution_log_aggregation';
|
||||
|
||||
// We are assuming there won't be many actions. This is why we will load
|
||||
// all the actions in advance and assume the total count to not go over 10000.
|
||||
|
@ -108,6 +124,7 @@ interface ConstructorOptions {
|
|||
auditLogger?: AuditLogger;
|
||||
usageCounter?: UsageCounter;
|
||||
connectorTokenClient: ConnectorTokenClientContract;
|
||||
getEventLogClient: () => Promise<IEventLogClient>;
|
||||
}
|
||||
|
||||
export interface UpdateOptions {
|
||||
|
@ -131,6 +148,7 @@ export class ActionsClient {
|
|||
private readonly auditLogger?: AuditLogger;
|
||||
private readonly usageCounter?: UsageCounter;
|
||||
private readonly connectorTokenClient: ConnectorTokenClientContract;
|
||||
private readonly getEventLogClient: () => Promise<IEventLogClient>;
|
||||
|
||||
constructor({
|
||||
logger,
|
||||
|
@ -148,6 +166,7 @@ export class ActionsClient {
|
|||
auditLogger,
|
||||
usageCounter,
|
||||
connectorTokenClient,
|
||||
getEventLogClient,
|
||||
}: ConstructorOptions) {
|
||||
this.logger = logger;
|
||||
this.actionTypeRegistry = actionTypeRegistry;
|
||||
|
@ -164,6 +183,7 @@ export class ActionsClient {
|
|||
this.auditLogger = auditLogger;
|
||||
this.usageCounter = usageCounter;
|
||||
this.connectorTokenClient = connectorTokenClient;
|
||||
this.getEventLogClient = getEventLogClient;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -647,7 +667,9 @@ export class ActionsClient {
|
|||
params,
|
||||
source,
|
||||
relatedSavedObjects,
|
||||
}: Omit<ExecuteOptions, 'request'>): Promise<ActionTypeExecutorResult<unknown>> {
|
||||
}: Omit<ExecuteOptions, 'request' | 'actionExecutionId'>): Promise<
|
||||
ActionTypeExecutorResult<unknown>
|
||||
> {
|
||||
if (
|
||||
(await getAuthorizationModeBySource(this.unsecuredSavedObjectsClient, source)) ===
|
||||
AuthorizationMode.RBAC
|
||||
|
@ -656,12 +678,14 @@ export class ActionsClient {
|
|||
} else {
|
||||
trackLegacyRBACExemption('execute', this.usageCounter);
|
||||
}
|
||||
|
||||
return this.actionExecutor.execute({
|
||||
actionId,
|
||||
params,
|
||||
source,
|
||||
request: this.request,
|
||||
relatedSavedObjects,
|
||||
actionExecutionId: uuidv4(),
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -729,6 +753,126 @@ export class ActionsClient {
|
|||
public isPreconfigured(connectorId: string): boolean {
|
||||
return !!this.preconfiguredActions.find((preconfigured) => preconfigured.id === connectorId);
|
||||
}
|
||||
|
||||
public async getGlobalExecutionLogWithAuth({
|
||||
dateStart,
|
||||
dateEnd,
|
||||
filter,
|
||||
page,
|
||||
perPage,
|
||||
sort,
|
||||
namespaces,
|
||||
}: GetGlobalExecutionLogParams): Promise<IExecutionLogResult> {
|
||||
this.logger.debug(`getGlobalExecutionLogWithAuth(): getting global execution log`);
|
||||
|
||||
const authorizationTuple = {} as KueryNode;
|
||||
try {
|
||||
await this.authorization.ensureAuthorized('get');
|
||||
} catch (error) {
|
||||
this.auditLogger?.log(
|
||||
connectorAuditEvent({
|
||||
action: ConnectorAuditAction.GET_GLOBAL_EXECUTION_LOG,
|
||||
error,
|
||||
})
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
|
||||
this.auditLogger?.log(
|
||||
connectorAuditEvent({
|
||||
action: ConnectorAuditAction.GET_GLOBAL_EXECUTION_LOG,
|
||||
})
|
||||
);
|
||||
|
||||
const dateNow = new Date();
|
||||
const parsedDateStart = parseDate(dateStart, 'dateStart', dateNow);
|
||||
const parsedDateEnd = parseDate(dateEnd, 'dateEnd', dateNow);
|
||||
|
||||
const eventLogClient = await this.getEventLogClient();
|
||||
|
||||
try {
|
||||
const aggResult = await eventLogClient.aggregateEventsWithAuthFilter(
|
||||
'action',
|
||||
authorizationTuple,
|
||||
{
|
||||
start: parsedDateStart.toISOString(),
|
||||
end: parsedDateEnd.toISOString(),
|
||||
aggs: getExecutionLogAggregation({
|
||||
filter: filter ? `${filter} AND (${ACTION_FILTER})` : ACTION_FILTER,
|
||||
page,
|
||||
perPage,
|
||||
sort,
|
||||
}),
|
||||
},
|
||||
namespaces,
|
||||
true
|
||||
);
|
||||
|
||||
return formatExecutionLogResult(aggResult);
|
||||
} catch (err) {
|
||||
this.logger.debug(
|
||||
`actionsClient.getGlobalExecutionLogWithAuth(): error searching global event log: ${err.message}`
|
||||
);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
public async getGlobalExecutionKpiWithAuth({
|
||||
dateStart,
|
||||
dateEnd,
|
||||
filter,
|
||||
namespaces,
|
||||
}: GetGlobalExecutionKPIParams) {
|
||||
this.logger.debug(`getGlobalExecutionKpiWithAuth(): getting global execution KPI`);
|
||||
|
||||
const authorizationTuple = {} as KueryNode;
|
||||
try {
|
||||
await this.authorization.ensureAuthorized('get');
|
||||
} catch (error) {
|
||||
this.auditLogger?.log(
|
||||
connectorAuditEvent({
|
||||
action: ConnectorAuditAction.GET_GLOBAL_EXECUTION_KPI,
|
||||
error,
|
||||
})
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
|
||||
this.auditLogger?.log(
|
||||
connectorAuditEvent({
|
||||
action: ConnectorAuditAction.GET_GLOBAL_EXECUTION_KPI,
|
||||
})
|
||||
);
|
||||
|
||||
const dateNow = new Date();
|
||||
const parsedDateStart = parseDate(dateStart, 'dateStart', dateNow);
|
||||
const parsedDateEnd = parseDate(dateEnd, 'dateEnd', dateNow);
|
||||
|
||||
const eventLogClient = await this.getEventLogClient();
|
||||
|
||||
try {
|
||||
const aggResult = await eventLogClient.aggregateEventsWithAuthFilter(
|
||||
'action',
|
||||
authorizationTuple,
|
||||
{
|
||||
start: parsedDateStart.toISOString(),
|
||||
end: parsedDateEnd.toISOString(),
|
||||
aggs: getExecutionKPIAggregation(
|
||||
filter ? `${filter} AND (${ACTION_FILTER})` : ACTION_FILTER
|
||||
),
|
||||
},
|
||||
namespaces,
|
||||
true
|
||||
);
|
||||
|
||||
return formatExecutionKPIResult(aggResult);
|
||||
} catch (err) {
|
||||
this.logger.debug(
|
||||
`actionsClient.getGlobalExecutionKpiWithAuth(): error searching global execution KPI: ${err.message}`
|
||||
);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function actionFromSavedObject(
|
||||
|
|
|
@ -32,6 +32,7 @@ const executeParams = {
|
|||
},
|
||||
executionId: '123abc',
|
||||
request: {} as KibanaRequest,
|
||||
actionExecutionId: '2',
|
||||
};
|
||||
|
||||
const spacesMock = spacesServiceMock.createStartContract();
|
||||
|
@ -139,6 +140,13 @@ test('successfully executes', async () => {
|
|||
"kind": "action",
|
||||
},
|
||||
"kibana": Object {
|
||||
"action": Object {
|
||||
"execution": Object {
|
||||
"uuid": "2",
|
||||
},
|
||||
"id": "1",
|
||||
"name": "1",
|
||||
},
|
||||
"alert": Object {
|
||||
"rule": Object {
|
||||
"execution": Object {
|
||||
|
@ -170,6 +178,13 @@ test('successfully executes', async () => {
|
|||
"outcome": "success",
|
||||
},
|
||||
"kibana": Object {
|
||||
"action": Object {
|
||||
"execution": Object {
|
||||
"uuid": "2",
|
||||
},
|
||||
"id": "1",
|
||||
"name": "1",
|
||||
},
|
||||
"alert": Object {
|
||||
"rule": Object {
|
||||
"execution": Object {
|
||||
|
@ -240,6 +255,13 @@ test('successfully executes with preconfigured connector', async () => {
|
|||
"kind": "action",
|
||||
},
|
||||
"kibana": Object {
|
||||
"action": Object {
|
||||
"execution": Object {
|
||||
"uuid": "2",
|
||||
},
|
||||
"id": "preconfigured",
|
||||
"name": "Preconfigured",
|
||||
},
|
||||
"alert": Object {
|
||||
"rule": Object {
|
||||
"execution": Object {
|
||||
|
@ -252,6 +274,7 @@ test('successfully executes with preconfigured connector', async () => {
|
|||
"id": "preconfigured",
|
||||
"namespace": "some-namespace",
|
||||
"rel": "primary",
|
||||
"space_agnostic": true,
|
||||
"type": "action",
|
||||
"type_id": "test",
|
||||
},
|
||||
|
@ -271,6 +294,13 @@ test('successfully executes with preconfigured connector', async () => {
|
|||
"outcome": "success",
|
||||
},
|
||||
"kibana": Object {
|
||||
"action": Object {
|
||||
"execution": Object {
|
||||
"uuid": "2",
|
||||
},
|
||||
"id": "preconfigured",
|
||||
"name": "Preconfigured",
|
||||
},
|
||||
"alert": Object {
|
||||
"rule": Object {
|
||||
"execution": Object {
|
||||
|
@ -283,6 +313,7 @@ test('successfully executes with preconfigured connector', async () => {
|
|||
"id": "preconfigured",
|
||||
"namespace": "some-namespace",
|
||||
"rel": "primary",
|
||||
"space_agnostic": true,
|
||||
"type": "action",
|
||||
"type_id": "test",
|
||||
},
|
||||
|
@ -692,6 +723,13 @@ test('should not throw error if action is preconfigured and isESOCanEncrypt is f
|
|||
"kind": "action",
|
||||
},
|
||||
"kibana": Object {
|
||||
"action": Object {
|
||||
"execution": Object {
|
||||
"uuid": "2",
|
||||
},
|
||||
"id": "preconfigured",
|
||||
"name": "Preconfigured",
|
||||
},
|
||||
"alert": Object {
|
||||
"rule": Object {
|
||||
"execution": Object {
|
||||
|
@ -704,6 +742,7 @@ test('should not throw error if action is preconfigured and isESOCanEncrypt is f
|
|||
"id": "preconfigured",
|
||||
"namespace": "some-namespace",
|
||||
"rel": "primary",
|
||||
"space_agnostic": true,
|
||||
"type": "action",
|
||||
"type_id": "test",
|
||||
},
|
||||
|
@ -723,6 +762,13 @@ test('should not throw error if action is preconfigured and isESOCanEncrypt is f
|
|||
"outcome": "success",
|
||||
},
|
||||
"kibana": Object {
|
||||
"action": Object {
|
||||
"execution": Object {
|
||||
"uuid": "2",
|
||||
},
|
||||
"id": "preconfigured",
|
||||
"name": "Preconfigured",
|
||||
},
|
||||
"alert": Object {
|
||||
"rule": Object {
|
||||
"execution": Object {
|
||||
|
@ -735,6 +781,7 @@ test('should not throw error if action is preconfigured and isESOCanEncrypt is f
|
|||
"id": "preconfigured",
|
||||
"namespace": "some-namespace",
|
||||
"rel": "primary",
|
||||
"space_agnostic": true,
|
||||
"type": "action",
|
||||
"type_id": "test",
|
||||
},
|
||||
|
@ -815,6 +862,7 @@ test('writes to event log for execute timeout', async () => {
|
|||
consumer: 'test-consumer',
|
||||
relatedSavedObjects: [],
|
||||
request: {} as KibanaRequest,
|
||||
actionExecutionId: '2',
|
||||
});
|
||||
expect(eventLogger.logEvent).toHaveBeenCalledTimes(1);
|
||||
expect(eventLogger.logEvent).toHaveBeenNthCalledWith(1, {
|
||||
|
@ -823,6 +871,13 @@ test('writes to event log for execute timeout', async () => {
|
|||
kind: 'action',
|
||||
},
|
||||
kibana: {
|
||||
action: {
|
||||
execution: {
|
||||
uuid: '2',
|
||||
},
|
||||
name: undefined,
|
||||
id: 'action1',
|
||||
},
|
||||
alert: {
|
||||
rule: {
|
||||
consumer: 'test-consumer',
|
||||
|
@ -833,16 +888,17 @@ test('writes to event log for execute timeout', async () => {
|
|||
},
|
||||
saved_objects: [
|
||||
{
|
||||
id: 'action1',
|
||||
namespace: 'some-namespace',
|
||||
rel: 'primary',
|
||||
type: 'action',
|
||||
id: 'action1',
|
||||
type_id: 'test',
|
||||
namespace: 'some-namespace',
|
||||
},
|
||||
],
|
||||
space_ids: ['some-namespace'],
|
||||
},
|
||||
message: `action: test:action1: 'action-1' execution cancelled due to timeout - exceeded default timeout of "5m"`,
|
||||
message:
|
||||
'action: test:action1: \'action-1\' execution cancelled due to timeout - exceeded default timeout of "5m"',
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -860,6 +916,13 @@ test('writes to event log for execute and execute start', async () => {
|
|||
kind: 'action',
|
||||
},
|
||||
kibana: {
|
||||
action: {
|
||||
execution: {
|
||||
uuid: '2',
|
||||
},
|
||||
name: 'action-1',
|
||||
id: '1',
|
||||
},
|
||||
alert: {
|
||||
rule: {
|
||||
execution: {
|
||||
|
@ -869,44 +932,17 @@ test('writes to event log for execute and execute start', async () => {
|
|||
},
|
||||
saved_objects: [
|
||||
{
|
||||
id: '1',
|
||||
namespace: 'some-namespace',
|
||||
rel: 'primary',
|
||||
type: 'action',
|
||||
id: '1',
|
||||
type_id: 'test',
|
||||
namespace: 'some-namespace',
|
||||
},
|
||||
],
|
||||
space_ids: ['some-namespace'],
|
||||
},
|
||||
message: 'action started: test:1: action-1',
|
||||
});
|
||||
expect(eventLogger.logEvent).toHaveBeenNthCalledWith(2, {
|
||||
event: {
|
||||
action: 'execute',
|
||||
kind: 'action',
|
||||
outcome: 'success',
|
||||
},
|
||||
kibana: {
|
||||
alert: {
|
||||
rule: {
|
||||
execution: {
|
||||
uuid: '123abc',
|
||||
},
|
||||
},
|
||||
},
|
||||
saved_objects: [
|
||||
{
|
||||
rel: 'primary',
|
||||
type: 'action',
|
||||
id: '1',
|
||||
type_id: 'test',
|
||||
namespace: 'some-namespace',
|
||||
},
|
||||
],
|
||||
space_ids: ['some-namespace'],
|
||||
},
|
||||
message: 'action executed: test:1: action-1',
|
||||
});
|
||||
});
|
||||
|
||||
test('writes to event log for execute and execute start when consumer and related saved object are defined', async () => {
|
||||
|
@ -933,6 +969,13 @@ test('writes to event log for execute and execute start when consumer and relate
|
|||
kind: 'action',
|
||||
},
|
||||
kibana: {
|
||||
action: {
|
||||
execution: {
|
||||
uuid: '2',
|
||||
},
|
||||
name: 'action-1',
|
||||
id: '1',
|
||||
},
|
||||
alert: {
|
||||
rule: {
|
||||
consumer: 'test-consumer',
|
||||
|
@ -944,16 +987,17 @@ test('writes to event log for execute and execute start when consumer and relate
|
|||
},
|
||||
saved_objects: [
|
||||
{
|
||||
id: '1',
|
||||
namespace: 'some-namespace',
|
||||
rel: 'primary',
|
||||
type: 'action',
|
||||
id: '1',
|
||||
type_id: 'test',
|
||||
namespace: 'some-namespace',
|
||||
},
|
||||
{
|
||||
id: '12',
|
||||
namespace: undefined,
|
||||
rel: 'primary',
|
||||
type: 'alert',
|
||||
id: '12',
|
||||
type_id: '.rule-type',
|
||||
},
|
||||
],
|
||||
|
@ -961,41 +1005,6 @@ test('writes to event log for execute and execute start when consumer and relate
|
|||
},
|
||||
message: 'action started: test:1: action-1',
|
||||
});
|
||||
expect(eventLogger.logEvent).toHaveBeenNthCalledWith(2, {
|
||||
event: {
|
||||
action: 'execute',
|
||||
kind: 'action',
|
||||
outcome: 'success',
|
||||
},
|
||||
kibana: {
|
||||
alert: {
|
||||
rule: {
|
||||
consumer: 'test-consumer',
|
||||
execution: {
|
||||
uuid: '123abc',
|
||||
},
|
||||
rule_type_id: '.rule-type',
|
||||
},
|
||||
},
|
||||
saved_objects: [
|
||||
{
|
||||
rel: 'primary',
|
||||
type: 'action',
|
||||
id: '1',
|
||||
type_id: 'test',
|
||||
namespace: 'some-namespace',
|
||||
},
|
||||
{
|
||||
rel: 'primary',
|
||||
type: 'alert',
|
||||
id: '12',
|
||||
type_id: '.rule-type',
|
||||
},
|
||||
],
|
||||
space_ids: ['some-namespace'],
|
||||
},
|
||||
message: 'action executed: test:1: action-1',
|
||||
});
|
||||
});
|
||||
|
||||
function setupActionExecutorMock() {
|
||||
|
|
|
@ -59,6 +59,7 @@ export interface TaskInfo {
|
|||
|
||||
export interface ExecuteOptions<Source = unknown> {
|
||||
actionId: string;
|
||||
actionExecutionId: string;
|
||||
isEphemeral?: boolean;
|
||||
request: KibanaRequest;
|
||||
params: Record<string, unknown>;
|
||||
|
@ -100,6 +101,7 @@ export class ActionExecutor {
|
|||
executionId,
|
||||
consumer,
|
||||
relatedSavedObjects,
|
||||
actionExecutionId,
|
||||
}: ExecuteOptions): Promise<ActionTypeExecutorResult<unknown>> {
|
||||
if (!this.isInitialized) {
|
||||
throw new Error('ActionExecutor not initialized');
|
||||
|
@ -189,6 +191,9 @@ export class ActionExecutor {
|
|||
},
|
||||
],
|
||||
relatedSavedObjects,
|
||||
name,
|
||||
actionExecutionId,
|
||||
isPreconfigured: this.actionInfo.isPreconfigured,
|
||||
});
|
||||
|
||||
eventLogger.startTiming(event);
|
||||
|
@ -297,8 +302,10 @@ export class ActionExecutor {
|
|||
executionId,
|
||||
taskInfo,
|
||||
consumer,
|
||||
actionExecutionId,
|
||||
}: {
|
||||
actionId: string;
|
||||
actionExecutionId: string;
|
||||
request: KibanaRequest;
|
||||
taskInfo?: TaskInfo;
|
||||
executionId?: string;
|
||||
|
@ -357,6 +364,8 @@ export class ActionExecutor {
|
|||
},
|
||||
],
|
||||
relatedSavedObjects,
|
||||
actionExecutionId,
|
||||
isPreconfigured: this.actionInfo.isPreconfigured,
|
||||
});
|
||||
|
||||
eventLogger.logEvent(event);
|
||||
|
@ -369,6 +378,7 @@ interface ActionInfo {
|
|||
config: unknown;
|
||||
secrets: unknown;
|
||||
actionId: string;
|
||||
isPreconfigured?: boolean;
|
||||
}
|
||||
|
||||
async function getActionInfoInternal<Source = unknown>(
|
||||
|
@ -395,6 +405,7 @@ async function getActionInfoInternal<Source = unknown>(
|
|||
config: pcAction.config,
|
||||
secrets: pcAction.secrets,
|
||||
actionId,
|
||||
isPreconfigured: true,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -15,6 +15,8 @@ export enum ConnectorAuditAction {
|
|||
DELETE = 'connector_delete',
|
||||
FIND = 'connector_find',
|
||||
EXECUTE = 'connector_execute',
|
||||
GET_GLOBAL_EXECUTION_LOG = 'connector_get_global_execution_log',
|
||||
GET_GLOBAL_EXECUTION_KPI = 'connector_get_global_execution_kpi',
|
||||
}
|
||||
|
||||
type VerbsTuple = [string, string, string];
|
||||
|
@ -26,6 +28,8 @@ const eventVerbs: Record<ConnectorAuditAction, VerbsTuple> = {
|
|||
connector_delete: ['delete', 'deleting', 'deleted'],
|
||||
connector_find: ['access', 'accessing', 'accessed'],
|
||||
connector_execute: ['execute', 'executing', 'executed'],
|
||||
connector_get_global_execution_log: ['access', 'accessing', 'accessed'],
|
||||
connector_get_global_execution_kpi: ['access', 'accessing', 'accessed'],
|
||||
};
|
||||
|
||||
const eventTypes: Record<ConnectorAuditAction, EcsEventType | undefined> = {
|
||||
|
@ -35,6 +39,8 @@ const eventTypes: Record<ConnectorAuditAction, EcsEventType | undefined> = {
|
|||
connector_delete: 'deletion',
|
||||
connector_find: 'access',
|
||||
connector_execute: undefined,
|
||||
connector_get_global_execution_log: 'access',
|
||||
connector_get_global_execution_kpi: 'access',
|
||||
};
|
||||
|
||||
export interface ConnectorAuditEventParams {
|
||||
|
|
|
@ -29,6 +29,8 @@ describe('createActionEventLogRecordObject', () => {
|
|||
},
|
||||
],
|
||||
spaceId: 'default',
|
||||
name: 'test name',
|
||||
actionExecutionId: '123abc',
|
||||
})
|
||||
).toStrictEqual({
|
||||
'@timestamp': '1970-01-01T00:00:00.000Z',
|
||||
|
@ -58,6 +60,13 @@ describe('createActionEventLogRecordObject', () => {
|
|||
schedule_delay: 0,
|
||||
scheduled: '1970-01-01T00:00:00.000Z',
|
||||
},
|
||||
action: {
|
||||
name: 'test name',
|
||||
id: '1',
|
||||
execution: {
|
||||
uuid: '123abc',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
@ -80,6 +89,7 @@ describe('createActionEventLogRecordObject', () => {
|
|||
relation: 'primary',
|
||||
},
|
||||
],
|
||||
actionExecutionId: '123abc',
|
||||
})
|
||||
).toStrictEqual({
|
||||
event: {
|
||||
|
@ -104,6 +114,13 @@ describe('createActionEventLogRecordObject', () => {
|
|||
type_id: '.email',
|
||||
},
|
||||
],
|
||||
action: {
|
||||
name: 'test name',
|
||||
id: '1',
|
||||
execution: {
|
||||
uuid: '123abc',
|
||||
},
|
||||
},
|
||||
},
|
||||
message: 'action execution start',
|
||||
});
|
||||
|
@ -125,6 +142,7 @@ describe('createActionEventLogRecordObject', () => {
|
|||
relation: 'primary',
|
||||
},
|
||||
],
|
||||
actionExecutionId: '123abc',
|
||||
})
|
||||
).toStrictEqual({
|
||||
event: {
|
||||
|
@ -141,6 +159,13 @@ describe('createActionEventLogRecordObject', () => {
|
|||
type_id: '.email',
|
||||
},
|
||||
],
|
||||
action: {
|
||||
name: 'test name',
|
||||
id: '1',
|
||||
execution: {
|
||||
uuid: '123abc',
|
||||
},
|
||||
},
|
||||
},
|
||||
message: 'action execution start',
|
||||
});
|
||||
|
@ -163,6 +188,8 @@ describe('createActionEventLogRecordObject', () => {
|
|||
relation: 'primary',
|
||||
},
|
||||
],
|
||||
name: 'test name',
|
||||
actionExecutionId: '123abc',
|
||||
})
|
||||
).toStrictEqual({
|
||||
event: {
|
||||
|
@ -189,6 +216,13 @@ describe('createActionEventLogRecordObject', () => {
|
|||
schedule_delay: undefined,
|
||||
scheduled: '1970-01-01T00:00:00.000Z',
|
||||
},
|
||||
action: {
|
||||
name: 'test name',
|
||||
id: '1',
|
||||
execution: {
|
||||
uuid: '123abc',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
@ -218,6 +252,7 @@ describe('createActionEventLogRecordObject', () => {
|
|||
id: '123',
|
||||
},
|
||||
],
|
||||
actionExecutionId: '123abc',
|
||||
})
|
||||
).toStrictEqual({
|
||||
event: {
|
||||
|
@ -250,6 +285,70 @@ describe('createActionEventLogRecordObject', () => {
|
|||
type_id: '.rule-type',
|
||||
},
|
||||
],
|
||||
action: {
|
||||
name: 'test name',
|
||||
id: '1',
|
||||
execution: {
|
||||
uuid: '123abc',
|
||||
},
|
||||
},
|
||||
},
|
||||
message: 'action execution start',
|
||||
});
|
||||
});
|
||||
|
||||
test('created action event "execute" for preconfigured connector with space_agnostic true', async () => {
|
||||
expect(
|
||||
createActionEventLogRecordObject({
|
||||
actionId: '1',
|
||||
name: 'test name',
|
||||
action: 'execute',
|
||||
message: 'action execution start',
|
||||
namespace: 'default',
|
||||
executionId: '123abc',
|
||||
consumer: 'test-consumer',
|
||||
savedObjects: [
|
||||
{
|
||||
id: '2',
|
||||
type: 'action',
|
||||
typeId: '.email',
|
||||
relation: 'primary',
|
||||
},
|
||||
],
|
||||
actionExecutionId: '123abc',
|
||||
isPreconfigured: true,
|
||||
})
|
||||
).toStrictEqual({
|
||||
event: {
|
||||
action: 'execute',
|
||||
kind: 'action',
|
||||
},
|
||||
kibana: {
|
||||
alert: {
|
||||
rule: {
|
||||
consumer: 'test-consumer',
|
||||
execution: {
|
||||
uuid: '123abc',
|
||||
},
|
||||
},
|
||||
},
|
||||
saved_objects: [
|
||||
{
|
||||
id: '2',
|
||||
namespace: 'default',
|
||||
rel: 'primary',
|
||||
type: 'action',
|
||||
type_id: '.email',
|
||||
space_agnostic: true,
|
||||
},
|
||||
],
|
||||
action: {
|
||||
name: 'test name',
|
||||
id: '1',
|
||||
execution: {
|
||||
uuid: '123abc',
|
||||
},
|
||||
},
|
||||
},
|
||||
message: 'action execution start',
|
||||
});
|
||||
|
|
|
@ -14,6 +14,7 @@ export type Event = Exclude<IEvent, undefined>;
|
|||
interface CreateActionEventLogRecordParams {
|
||||
actionId: string;
|
||||
action: string;
|
||||
actionExecutionId: string;
|
||||
name?: string;
|
||||
message?: string;
|
||||
namespace?: string;
|
||||
|
@ -32,11 +33,24 @@ interface CreateActionEventLogRecordParams {
|
|||
relation?: string;
|
||||
}>;
|
||||
relatedSavedObjects?: RelatedSavedObjects;
|
||||
isPreconfigured?: boolean;
|
||||
}
|
||||
|
||||
export function createActionEventLogRecordObject(params: CreateActionEventLogRecordParams): Event {
|
||||
const { action, message, task, namespace, executionId, spaceId, consumer, relatedSavedObjects } =
|
||||
params;
|
||||
const {
|
||||
action,
|
||||
message,
|
||||
task,
|
||||
namespace,
|
||||
executionId,
|
||||
spaceId,
|
||||
consumer,
|
||||
relatedSavedObjects,
|
||||
name,
|
||||
actionExecutionId,
|
||||
isPreconfigured,
|
||||
actionId,
|
||||
} = params;
|
||||
|
||||
const kibanaAlertRule = {
|
||||
...(consumer ? { consumer } : {}),
|
||||
|
@ -62,10 +76,19 @@ export function createActionEventLogRecordObject(params: CreateActionEventLogRec
|
|||
type: so.type,
|
||||
id: so.id,
|
||||
type_id: so.typeId,
|
||||
// set space_agnostic to true for preconfigured connectors
|
||||
...(so.type === 'action' && isPreconfigured ? { space_agnostic: isPreconfigured } : {}),
|
||||
...(namespace ? { namespace } : {}),
|
||||
})),
|
||||
...(spaceId ? { space_ids: [spaceId] } : {}),
|
||||
...(task ? { task: { scheduled: task.scheduled, schedule_delay: task.scheduleDelay } } : {}),
|
||||
action: {
|
||||
...(name ? { name } : {}),
|
||||
id: actionId,
|
||||
execution: {
|
||||
uuid: actionExecutionId,
|
||||
},
|
||||
},
|
||||
},
|
||||
...(message ? { message } : {}),
|
||||
};
|
||||
|
|
|
@ -0,0 +1,945 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { fromKueryExpression } from '@kbn/es-query';
|
||||
import {
|
||||
getExecutionLogAggregation,
|
||||
formatExecutionLogResult,
|
||||
ExecutionUuidAggResult,
|
||||
getExecutionKPIAggregation,
|
||||
formatExecutionKPIResult,
|
||||
} from './get_execution_log_aggregation';
|
||||
|
||||
describe('getExecutionLogAggregation', () => {
|
||||
test('should throw error when given bad sort field', () => {
|
||||
expect(() => {
|
||||
getExecutionLogAggregation({
|
||||
page: 1,
|
||||
perPage: 10,
|
||||
sort: [{ notsortable: { order: 'asc' } }],
|
||||
});
|
||||
}).toThrowErrorMatchingInlineSnapshot(
|
||||
`"Invalid sort field \\"notsortable\\" - must be one of [timestamp,execution_duration,schedule_delay]"`
|
||||
);
|
||||
});
|
||||
|
||||
test('should throw error when given one bad sort field', () => {
|
||||
expect(() => {
|
||||
getExecutionLogAggregation({
|
||||
page: 1,
|
||||
perPage: 10,
|
||||
sort: [{ notsortable: { order: 'asc' } }, { timestamp: { order: 'asc' } }],
|
||||
});
|
||||
}).toThrowErrorMatchingInlineSnapshot(
|
||||
`"Invalid sort field \\"notsortable\\" - must be one of [timestamp,execution_duration,schedule_delay]"`
|
||||
);
|
||||
});
|
||||
|
||||
test('should throw error when given bad page field', () => {
|
||||
expect(() => {
|
||||
getExecutionLogAggregation({
|
||||
page: 0,
|
||||
perPage: 10,
|
||||
sort: [{ timestamp: { order: 'asc' } }],
|
||||
});
|
||||
}).toThrowErrorMatchingInlineSnapshot(`"Invalid page field \\"0\\" - must be greater than 0"`);
|
||||
});
|
||||
|
||||
test('should throw error when given bad perPage field', () => {
|
||||
expect(() => {
|
||||
getExecutionLogAggregation({
|
||||
page: 1,
|
||||
perPage: 0,
|
||||
sort: [{ timestamp: { order: 'asc' } }],
|
||||
});
|
||||
}).toThrowErrorMatchingInlineSnapshot(
|
||||
`"Invalid perPage field \\"0\\" - must be greater than 0"`
|
||||
);
|
||||
});
|
||||
|
||||
test('should correctly generate aggregation', () => {
|
||||
expect(
|
||||
getExecutionLogAggregation({
|
||||
page: 2,
|
||||
perPage: 10,
|
||||
sort: [{ timestamp: { order: 'asc' } }, { execution_duration: { order: 'desc' } }],
|
||||
})
|
||||
).toEqual({
|
||||
executionLogAgg: {
|
||||
aggs: {
|
||||
executionUuid: {
|
||||
aggs: {
|
||||
actionExecution: {
|
||||
aggs: {
|
||||
executeStartTime: {
|
||||
min: {
|
||||
field: 'event.start',
|
||||
},
|
||||
},
|
||||
executionDuration: {
|
||||
max: {
|
||||
field: 'event.duration',
|
||||
},
|
||||
},
|
||||
outcomeAndMessage: {
|
||||
top_hits: {
|
||||
_source: {
|
||||
includes: [
|
||||
'event.outcome',
|
||||
'message',
|
||||
'error.message',
|
||||
'kibana.version',
|
||||
'kibana.space_ids',
|
||||
'kibana.action.name',
|
||||
'kibana.action.id',
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
scheduleDelay: {
|
||||
max: {
|
||||
field: 'kibana.task.schedule_delay',
|
||||
},
|
||||
},
|
||||
},
|
||||
filter: {
|
||||
bool: {
|
||||
must: [
|
||||
{
|
||||
match: {
|
||||
'event.action': 'execute',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
executionUuidSorted: {
|
||||
bucket_sort: {
|
||||
from: 10,
|
||||
gap_policy: 'insert_zeros',
|
||||
size: 10,
|
||||
sort: [
|
||||
{
|
||||
'actionExecution>executeStartTime': {
|
||||
order: 'asc',
|
||||
},
|
||||
},
|
||||
{
|
||||
'actionExecution>executionDuration': {
|
||||
order: 'desc',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
timeoutMessage: {
|
||||
filter: {
|
||||
bool: {
|
||||
must: [
|
||||
{
|
||||
match: {
|
||||
'event.action': 'execute-timeout',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
terms: {
|
||||
field: 'kibana.action.execution.uuid',
|
||||
order: [
|
||||
{
|
||||
'actionExecution>executeStartTime': 'asc',
|
||||
},
|
||||
{
|
||||
'actionExecution>executionDuration': 'desc',
|
||||
},
|
||||
],
|
||||
size: 1000,
|
||||
},
|
||||
},
|
||||
executionUuidCardinality: {
|
||||
aggs: {
|
||||
executionUuidCardinality: {
|
||||
cardinality: {
|
||||
field: 'kibana.action.execution.uuid',
|
||||
},
|
||||
},
|
||||
},
|
||||
filter: {
|
||||
bool: {
|
||||
must: [
|
||||
{
|
||||
match: {
|
||||
'event.action': 'execute',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('should correctly generate aggregation with a defined filter in the form of a string', () => {
|
||||
expect(
|
||||
getExecutionLogAggregation({
|
||||
page: 2,
|
||||
perPage: 10,
|
||||
sort: [{ timestamp: { order: 'asc' } }, { execution_duration: { order: 'desc' } }],
|
||||
filter: 'test:test',
|
||||
})
|
||||
).toEqual({
|
||||
executionLogAgg: {
|
||||
aggs: {
|
||||
executionUuid: {
|
||||
aggs: {
|
||||
actionExecution: {
|
||||
aggs: {
|
||||
executeStartTime: {
|
||||
min: {
|
||||
field: 'event.start',
|
||||
},
|
||||
},
|
||||
executionDuration: {
|
||||
max: {
|
||||
field: 'event.duration',
|
||||
},
|
||||
},
|
||||
outcomeAndMessage: {
|
||||
top_hits: {
|
||||
_source: {
|
||||
includes: [
|
||||
'event.outcome',
|
||||
'message',
|
||||
'error.message',
|
||||
'kibana.version',
|
||||
'kibana.space_ids',
|
||||
'kibana.action.name',
|
||||
'kibana.action.id',
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
scheduleDelay: {
|
||||
max: {
|
||||
field: 'kibana.task.schedule_delay',
|
||||
},
|
||||
},
|
||||
},
|
||||
filter: {
|
||||
bool: {
|
||||
must: [
|
||||
{
|
||||
match: {
|
||||
'event.action': 'execute',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
executionUuidSorted: {
|
||||
bucket_sort: {
|
||||
from: 10,
|
||||
gap_policy: 'insert_zeros',
|
||||
size: 10,
|
||||
sort: [
|
||||
{
|
||||
'actionExecution>executeStartTime': {
|
||||
order: 'asc',
|
||||
},
|
||||
},
|
||||
{
|
||||
'actionExecution>executionDuration': {
|
||||
order: 'desc',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
timeoutMessage: {
|
||||
filter: {
|
||||
bool: {
|
||||
must: [
|
||||
{
|
||||
match: {
|
||||
'event.action': 'execute-timeout',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
terms: {
|
||||
field: 'kibana.action.execution.uuid',
|
||||
order: [
|
||||
{
|
||||
'actionExecution>executeStartTime': 'asc',
|
||||
},
|
||||
{
|
||||
'actionExecution>executionDuration': 'desc',
|
||||
},
|
||||
],
|
||||
size: 1000,
|
||||
},
|
||||
},
|
||||
executionUuidCardinality: {
|
||||
aggs: {
|
||||
executionUuidCardinality: {
|
||||
cardinality: {
|
||||
field: 'kibana.action.execution.uuid',
|
||||
},
|
||||
},
|
||||
},
|
||||
filter: {
|
||||
bool: {
|
||||
must: [
|
||||
{
|
||||
match: {
|
||||
'event.action': 'execute',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
filter: {
|
||||
bool: {
|
||||
filter: {
|
||||
bool: {
|
||||
minimum_should_match: 1,
|
||||
should: [
|
||||
{
|
||||
match: {
|
||||
test: 'test',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('should correctly generate aggregation with a defined filter in the form of a KueryNode', () => {
|
||||
expect(
|
||||
getExecutionLogAggregation({
|
||||
page: 2,
|
||||
perPage: 10,
|
||||
sort: [{ timestamp: { order: 'asc' } }, { execution_duration: { order: 'desc' } }],
|
||||
filter: fromKueryExpression('test:test'),
|
||||
})
|
||||
).toEqual({
|
||||
executionLogAgg: {
|
||||
aggs: {
|
||||
executionUuid: {
|
||||
aggs: {
|
||||
actionExecution: {
|
||||
aggs: {
|
||||
executeStartTime: {
|
||||
min: {
|
||||
field: 'event.start',
|
||||
},
|
||||
},
|
||||
executionDuration: {
|
||||
max: {
|
||||
field: 'event.duration',
|
||||
},
|
||||
},
|
||||
outcomeAndMessage: {
|
||||
top_hits: {
|
||||
_source: {
|
||||
includes: [
|
||||
'event.outcome',
|
||||
'message',
|
||||
'error.message',
|
||||
'kibana.version',
|
||||
'kibana.space_ids',
|
||||
'kibana.action.name',
|
||||
'kibana.action.id',
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
scheduleDelay: {
|
||||
max: {
|
||||
field: 'kibana.task.schedule_delay',
|
||||
},
|
||||
},
|
||||
},
|
||||
filter: {
|
||||
bool: {
|
||||
must: [
|
||||
{
|
||||
match: {
|
||||
'event.action': 'execute',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
executionUuidSorted: {
|
||||
bucket_sort: {
|
||||
from: 10,
|
||||
gap_policy: 'insert_zeros',
|
||||
size: 10,
|
||||
sort: [
|
||||
{
|
||||
'actionExecution>executeStartTime': {
|
||||
order: 'asc',
|
||||
},
|
||||
},
|
||||
{
|
||||
'actionExecution>executionDuration': {
|
||||
order: 'desc',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
timeoutMessage: {
|
||||
filter: {
|
||||
bool: {
|
||||
must: [
|
||||
{
|
||||
match: {
|
||||
'event.action': 'execute-timeout',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
terms: {
|
||||
field: 'kibana.action.execution.uuid',
|
||||
order: [
|
||||
{
|
||||
'actionExecution>executeStartTime': 'asc',
|
||||
},
|
||||
{
|
||||
'actionExecution>executionDuration': 'desc',
|
||||
},
|
||||
],
|
||||
size: 1000,
|
||||
},
|
||||
},
|
||||
executionUuidCardinality: {
|
||||
aggs: {
|
||||
executionUuidCardinality: {
|
||||
cardinality: {
|
||||
field: 'kibana.action.execution.uuid',
|
||||
},
|
||||
},
|
||||
},
|
||||
filter: {
|
||||
bool: {
|
||||
must: [
|
||||
{
|
||||
match: {
|
||||
'event.action': 'execute',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
filter: {
|
||||
bool: {
|
||||
filter: {
|
||||
bool: {
|
||||
minimum_should_match: 1,
|
||||
should: [
|
||||
{
|
||||
match: {
|
||||
test: 'test',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('formatExecutionLogResult', () => {
|
||||
test('should return empty results if aggregations are undefined', () => {
|
||||
expect(formatExecutionLogResult({ aggregations: undefined })).toEqual({
|
||||
total: 0,
|
||||
data: [],
|
||||
});
|
||||
});
|
||||
test('should return empty results if aggregations.executionLogAgg are undefined', () => {
|
||||
expect(
|
||||
formatExecutionLogResult({
|
||||
aggregations: { executionLogAgg: undefined as unknown as ExecutionUuidAggResult },
|
||||
})
|
||||
).toEqual({
|
||||
total: 0,
|
||||
data: [],
|
||||
});
|
||||
});
|
||||
test('should format results correctly', () => {
|
||||
const results = {
|
||||
aggregations: {
|
||||
executionLogAgg: {
|
||||
doc_count: 5,
|
||||
executionUuid: {
|
||||
doc_count_error_upper_bound: 0,
|
||||
sum_other_doc_count: 0,
|
||||
buckets: [
|
||||
{
|
||||
key: '8b3af07e-7593-4c40-b704-9c06d3b06e58',
|
||||
doc_count: 1,
|
||||
actionExecution: {
|
||||
doc_count: 1,
|
||||
scheduleDelay: { value: 2783000000 },
|
||||
outcomeAndMessage: {
|
||||
hits: {
|
||||
total: { value: 1, relation: 'eq' },
|
||||
max_score: 3.033605,
|
||||
hits: [
|
||||
{
|
||||
_index: '.kibana-event-log-8.7.0-000001',
|
||||
_id: '5SmlgoUBAOza-PIJVKGD',
|
||||
_score: 3.033605,
|
||||
_source: {
|
||||
event: { outcome: 'success' },
|
||||
kibana: {
|
||||
space_ids: ['default'],
|
||||
version: '8.7.0',
|
||||
action: { name: 'test connector', id: '1' },
|
||||
},
|
||||
message:
|
||||
'action executed: .server-log:6709f660-8d11-11ed-bae5-bd32cbc9eaaa: test connector',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
executionDuration: { value: 1000000 },
|
||||
executeStartTime: {
|
||||
value: 1672934150495,
|
||||
value_as_string: '2023-01-05T15:55:50.495Z',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
executionUuidCardinality: { doc_count: 1, executionUuidCardinality: { value: 1 } },
|
||||
},
|
||||
},
|
||||
};
|
||||
expect(formatExecutionLogResult(results)).toEqual({
|
||||
data: [
|
||||
{
|
||||
connector_name: 'test connector',
|
||||
connector_id: '1',
|
||||
duration_ms: 1,
|
||||
id: '8b3af07e-7593-4c40-b704-9c06d3b06e58',
|
||||
message:
|
||||
'action executed: .server-log:6709f660-8d11-11ed-bae5-bd32cbc9eaaa: test connector',
|
||||
schedule_delay_ms: 2783,
|
||||
space_ids: ['default'],
|
||||
status: 'success',
|
||||
timestamp: '2023-01-05T15:55:50.495Z',
|
||||
version: '8.7.0',
|
||||
timed_out: false,
|
||||
},
|
||||
],
|
||||
total: 1,
|
||||
});
|
||||
});
|
||||
|
||||
test('should format results correctly with action execution errors', () => {
|
||||
const results = {
|
||||
aggregations: {
|
||||
executionLogAgg: {
|
||||
doc_count: 10,
|
||||
executionUuid: {
|
||||
doc_count_error_upper_bound: 0,
|
||||
sum_other_doc_count: 0,
|
||||
buckets: [
|
||||
{
|
||||
key: 'fdf9cadb-4568-4d22-afd2-437e4efbe767',
|
||||
doc_count: 1,
|
||||
actionExecution: {
|
||||
doc_count: 1,
|
||||
scheduleDelay: { value: 2946000000 },
|
||||
outcomeAndMessage: {
|
||||
hits: {
|
||||
total: { value: 1, relation: 'eq' },
|
||||
max_score: 3.1420498,
|
||||
hits: [
|
||||
{
|
||||
_index: '.kibana-event-log-8.7.0-000001',
|
||||
_id: 'CSm_goUBAOza-PIJBaIn',
|
||||
_score: 3.1420498,
|
||||
_source: {
|
||||
event: { outcome: 'failure' },
|
||||
kibana: {
|
||||
space_ids: ['default'],
|
||||
version: '8.7.0',
|
||||
action: { name: 'test', id: '1' },
|
||||
},
|
||||
message:
|
||||
'action execution failure: .email:e020c620-8d14-11ed-bae5-bd32cbc9eaaa: test',
|
||||
error: {
|
||||
message:
|
||||
'action execution failure: .email:e020c620-8d14-11ed-bae5-bd32cbc9eaaa: test',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
executionDuration: { value: 441000000 },
|
||||
executeStartTime: {
|
||||
value: 1672935833813,
|
||||
value_as_string: '2023-01-05T16:23:53.813Z',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
key: '8b3af07e-7593-4c40-b704-9c06d3b06e58',
|
||||
doc_count: 1,
|
||||
actionExecution: {
|
||||
doc_count: 1,
|
||||
scheduleDelay: { value: 2783000000 },
|
||||
outcomeAndMessage: {
|
||||
hits: {
|
||||
total: { value: 1, relation: 'eq' },
|
||||
max_score: 3.1420498,
|
||||
hits: [
|
||||
{
|
||||
_index: '.kibana-event-log-8.7.0-000001',
|
||||
_id: '5SmlgoUBAOza-PIJVKGD',
|
||||
_score: 3.1420498,
|
||||
_source: {
|
||||
event: { outcome: 'success' },
|
||||
kibana: {
|
||||
space_ids: ['default'],
|
||||
version: '8.7.0',
|
||||
action: { name: 'test connector', id: '1' },
|
||||
},
|
||||
message:
|
||||
'action executed: .server-log:6709f660-8d11-11ed-bae5-bd32cbc9eaaa: test connector',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
executionDuration: { value: 1000000 },
|
||||
executeStartTime: {
|
||||
value: 1672934150495,
|
||||
value_as_string: '2023-01-05T15:55:50.495Z',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
executionUuidCardinality: { doc_count: 2, executionUuidCardinality: { value: 2 } },
|
||||
},
|
||||
},
|
||||
};
|
||||
expect(formatExecutionLogResult(results)).toEqual({
|
||||
data: [
|
||||
{
|
||||
connector_name: 'test',
|
||||
connector_id: '1',
|
||||
duration_ms: 441,
|
||||
id: 'fdf9cadb-4568-4d22-afd2-437e4efbe767',
|
||||
message:
|
||||
'action execution failure: .email:e020c620-8d14-11ed-bae5-bd32cbc9eaaa: test - action execution failure: .email:e020c620-8d14-11ed-bae5-bd32cbc9eaaa: test',
|
||||
schedule_delay_ms: 2946,
|
||||
space_ids: ['default'],
|
||||
status: 'failure',
|
||||
timestamp: '2023-01-05T16:23:53.813Z',
|
||||
version: '8.7.0',
|
||||
timed_out: false,
|
||||
},
|
||||
{
|
||||
connector_name: 'test connector',
|
||||
connector_id: '1',
|
||||
duration_ms: 1,
|
||||
id: '8b3af07e-7593-4c40-b704-9c06d3b06e58',
|
||||
message:
|
||||
'action executed: .server-log:6709f660-8d11-11ed-bae5-bd32cbc9eaaa: test connector',
|
||||
schedule_delay_ms: 2783,
|
||||
space_ids: ['default'],
|
||||
status: 'success',
|
||||
timestamp: '2023-01-05T15:55:50.495Z',
|
||||
version: '8.7.0',
|
||||
timed_out: false,
|
||||
},
|
||||
],
|
||||
total: 2,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getExecutionKPIAggregation', () => {
|
||||
test('should correctly generate aggregation', () => {
|
||||
expect(getExecutionKPIAggregation()).toEqual({
|
||||
executionKpiAgg: {
|
||||
aggs: {
|
||||
executionUuid: {
|
||||
aggs: {
|
||||
actionExecution: {
|
||||
aggs: {
|
||||
actionExecutionOutcomes: {
|
||||
terms: {
|
||||
field: 'event.outcome',
|
||||
size: 3,
|
||||
},
|
||||
},
|
||||
executeStartTime: {
|
||||
min: {
|
||||
field: 'event.start',
|
||||
},
|
||||
},
|
||||
},
|
||||
filter: {
|
||||
bool: {
|
||||
must: [
|
||||
{
|
||||
match: {
|
||||
'event.action': 'execute',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
executionUuidSorted: {
|
||||
bucket_sort: {
|
||||
from: 0,
|
||||
gap_policy: 'insert_zeros',
|
||||
size: 10000,
|
||||
},
|
||||
},
|
||||
},
|
||||
terms: {
|
||||
field: 'kibana.action.execution.uuid',
|
||||
order: [
|
||||
{
|
||||
'actionExecution>executeStartTime': 'desc',
|
||||
},
|
||||
],
|
||||
size: 10000,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('should correctly generate aggregation with a defined filter in the form of a string', () => {
|
||||
expect(getExecutionKPIAggregation('test:test')).toEqual({
|
||||
executionKpiAgg: {
|
||||
aggs: {
|
||||
executionUuid: {
|
||||
aggs: {
|
||||
actionExecution: {
|
||||
aggs: {
|
||||
actionExecutionOutcomes: {
|
||||
terms: {
|
||||
field: 'event.outcome',
|
||||
size: 3,
|
||||
},
|
||||
},
|
||||
executeStartTime: {
|
||||
min: {
|
||||
field: 'event.start',
|
||||
},
|
||||
},
|
||||
},
|
||||
filter: {
|
||||
bool: {
|
||||
must: [
|
||||
{
|
||||
match: {
|
||||
'event.action': 'execute',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
executionUuidSorted: {
|
||||
bucket_sort: {
|
||||
from: 0,
|
||||
gap_policy: 'insert_zeros',
|
||||
size: 10000,
|
||||
},
|
||||
},
|
||||
},
|
||||
terms: {
|
||||
field: 'kibana.action.execution.uuid',
|
||||
order: [
|
||||
{
|
||||
'actionExecution>executeStartTime': 'desc',
|
||||
},
|
||||
],
|
||||
size: 10000,
|
||||
},
|
||||
},
|
||||
},
|
||||
filter: {
|
||||
bool: {
|
||||
filter: {
|
||||
bool: {
|
||||
minimum_should_match: 1,
|
||||
should: [
|
||||
{
|
||||
match: {
|
||||
test: 'test',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('should correctly generate aggregation with a defined filter in the form of a KueryNode', () => {
|
||||
expect(getExecutionKPIAggregation(fromKueryExpression('test:test'))).toEqual({
|
||||
executionKpiAgg: {
|
||||
aggs: {
|
||||
executionUuid: {
|
||||
aggs: {
|
||||
actionExecution: {
|
||||
aggs: {
|
||||
actionExecutionOutcomes: {
|
||||
terms: {
|
||||
field: 'event.outcome',
|
||||
size: 3,
|
||||
},
|
||||
},
|
||||
executeStartTime: {
|
||||
min: {
|
||||
field: 'event.start',
|
||||
},
|
||||
},
|
||||
},
|
||||
filter: {
|
||||
bool: {
|
||||
must: [
|
||||
{
|
||||
match: {
|
||||
'event.action': 'execute',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
executionUuidSorted: {
|
||||
bucket_sort: {
|
||||
from: 0,
|
||||
gap_policy: 'insert_zeros',
|
||||
size: 10000,
|
||||
},
|
||||
},
|
||||
},
|
||||
terms: {
|
||||
field: 'kibana.action.execution.uuid',
|
||||
order: [
|
||||
{
|
||||
'actionExecution>executeStartTime': 'desc',
|
||||
},
|
||||
],
|
||||
size: 10000,
|
||||
},
|
||||
},
|
||||
},
|
||||
filter: {
|
||||
bool: {
|
||||
filter: {
|
||||
bool: {
|
||||
minimum_should_match: 1,
|
||||
should: [
|
||||
{
|
||||
match: {
|
||||
test: 'test',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('formatExecutionKPIAggBuckets', () => {
|
||||
test('should return empty results if aggregations are undefined', () => {
|
||||
expect(
|
||||
formatExecutionKPIResult({
|
||||
aggregations: undefined,
|
||||
})
|
||||
).toEqual({ failure: 0, success: 0, unknown: 0, warning: 0 });
|
||||
});
|
||||
|
||||
test('should format results correctly', () => {
|
||||
const results = {
|
||||
aggregations: {
|
||||
executionKpiAgg: {
|
||||
doc_count: 21,
|
||||
executionUuid: {
|
||||
doc_count_error_upper_bound: 0,
|
||||
sum_other_doc_count: 0,
|
||||
buckets: [
|
||||
{
|
||||
key: '8b3af07e-7593-4c40-b704-9c06d3b06e58',
|
||||
doc_count: 1,
|
||||
actionExecution: {
|
||||
doc_count: 1,
|
||||
actionExecutionOutcomes: {
|
||||
doc_count_error_upper_bound: 0,
|
||||
sum_other_doc_count: 0,
|
||||
buckets: [{ key: 'success', doc_count: 1 }],
|
||||
},
|
||||
executeStartTime: {
|
||||
value: 1672934150495,
|
||||
value_as_string: '2023-01-05T15:55:50.495Z',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
expect(formatExecutionKPIResult(results)).toEqual({
|
||||
failure: 0,
|
||||
success: 1,
|
||||
unknown: 0,
|
||||
warning: 0,
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,424 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { KueryNode } from '@kbn/es-query';
|
||||
import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
|
||||
import Boom from '@hapi/boom';
|
||||
import { flatMap, get, isEmpty } from 'lodash';
|
||||
import { AggregateEventsBySavedObjectResult } from '@kbn/event-log-plugin/server';
|
||||
import { fromKueryExpression, toElasticsearchQuery } from '@kbn/es-query';
|
||||
import { IExecutionLog, IExecutionLogResult, EMPTY_EXECUTION_KPI_RESULT } from '../../common';
|
||||
|
||||
const DEFAULT_MAX_BUCKETS_LIMIT = 1000; // do not retrieve more than this number of executions
|
||||
const DEFAULT_MAX_KPI_BUCKETS_LIMIT = 10000;
|
||||
|
||||
const SPACE_ID_FIELD = 'kibana.space_ids';
|
||||
const ACTION_NAME_FIELD = 'kibana.action.name';
|
||||
const ACTION_ID_FIELD = 'kibana.action.id';
|
||||
const START_FIELD = 'event.start';
|
||||
const ACTION_FIELD = 'event.action';
|
||||
const OUTCOME_FIELD = 'event.outcome';
|
||||
const DURATION_FIELD = 'event.duration';
|
||||
const MESSAGE_FIELD = 'message';
|
||||
const VERSION_FIELD = 'kibana.version';
|
||||
const ERROR_MESSAGE_FIELD = 'error.message';
|
||||
const SCHEDULE_DELAY_FIELD = 'kibana.task.schedule_delay';
|
||||
const EXECUTION_UUID_FIELD = 'kibana.action.execution.uuid';
|
||||
|
||||
const Millis2Nanos = 1000 * 1000;
|
||||
|
||||
export const ACTION_FILTER = 'event.provider: actions AND NOT event.action: execute-start';
|
||||
export const EMPTY_EXECUTION_LOG_RESULT = {
|
||||
total: 0,
|
||||
data: [],
|
||||
};
|
||||
|
||||
interface IActionExecution
|
||||
extends estypes.AggregationsTermsAggregateBase<{ key: string; doc_count: number }> {
|
||||
buckets: Array<{ key: string; doc_count: number }>;
|
||||
}
|
||||
|
||||
interface IExecutionUuidKpiAggBucket extends estypes.AggregationsStringTermsBucketKeys {
|
||||
actionExecution: {
|
||||
doc_count: number;
|
||||
actionExecutionOutcomes: IActionExecution;
|
||||
};
|
||||
}
|
||||
interface IExecutionUuidAggBucket extends estypes.AggregationsStringTermsBucketKeys {
|
||||
timeoutMessage: estypes.AggregationsMultiBucketBase;
|
||||
actionExecution: {
|
||||
executeStartTime: estypes.AggregationsMinAggregate;
|
||||
executionDuration: estypes.AggregationsMaxAggregate;
|
||||
scheduleDelay: estypes.AggregationsMaxAggregate;
|
||||
outcomeAndMessage: estypes.AggregationsTopHitsAggregate;
|
||||
};
|
||||
}
|
||||
|
||||
export interface ExecutionUuidAggResult extends estypes.AggregationsAggregateBase {
|
||||
buckets: IExecutionUuidAggBucket[];
|
||||
}
|
||||
|
||||
export interface ExecutionUuidKPIAggResult extends estypes.AggregationsAggregateBase {
|
||||
buckets: IExecutionUuidKpiAggBucket[];
|
||||
}
|
||||
|
||||
interface ExecutionLogAggResult extends estypes.AggregationsAggregateBase {
|
||||
executionUuid: ExecutionUuidAggResult;
|
||||
executionUuidCardinality: {
|
||||
executionUuidCardinality: estypes.AggregationsCardinalityAggregate;
|
||||
};
|
||||
}
|
||||
|
||||
interface ExecutionKpiAggResult extends estypes.AggregationsAggregateBase {
|
||||
executionUuid: ExecutionUuidKPIAggResult;
|
||||
}
|
||||
|
||||
export interface IExecutionLogAggOptions {
|
||||
filter?: string | KueryNode;
|
||||
page: number;
|
||||
perPage: number;
|
||||
sort: estypes.Sort;
|
||||
}
|
||||
|
||||
const ExecutionLogSortFields: Record<string, string> = {
|
||||
timestamp: 'actionExecution>executeStartTime',
|
||||
execution_duration: 'actionExecution>executionDuration',
|
||||
schedule_delay: 'actionExecution>scheduleDelay',
|
||||
};
|
||||
|
||||
export const getExecutionKPIAggregation = (filter?: IExecutionLogAggOptions['filter']) => {
|
||||
const dslFilterQuery: estypes.QueryDslBoolQuery['filter'] = buildDslFilterQuery(filter);
|
||||
|
||||
return {
|
||||
executionKpiAgg: {
|
||||
...(dslFilterQuery ? { filter: { bool: { filter: dslFilterQuery } } } : {}),
|
||||
aggs: {
|
||||
executionUuid: {
|
||||
// Bucket by execution UUID
|
||||
terms: {
|
||||
field: EXECUTION_UUID_FIELD,
|
||||
size: DEFAULT_MAX_KPI_BUCKETS_LIMIT,
|
||||
order: formatSortForTermSort([{ timestamp: { order: 'desc' } }]),
|
||||
},
|
||||
aggs: {
|
||||
executionUuidSorted: {
|
||||
bucket_sort: {
|
||||
from: 0,
|
||||
size: DEFAULT_MAX_KPI_BUCKETS_LIMIT,
|
||||
gap_policy: 'insert_zeros' as estypes.AggregationsGapPolicy,
|
||||
},
|
||||
},
|
||||
actionExecution: {
|
||||
filter: {
|
||||
bool: {
|
||||
must: [
|
||||
{
|
||||
match: {
|
||||
[ACTION_FIELD]: 'execute',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
aggs: {
|
||||
executeStartTime: {
|
||||
min: {
|
||||
field: START_FIELD,
|
||||
},
|
||||
},
|
||||
actionExecutionOutcomes: {
|
||||
terms: {
|
||||
size: 3,
|
||||
field: OUTCOME_FIELD,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export function getExecutionLogAggregation({
|
||||
filter,
|
||||
page,
|
||||
perPage,
|
||||
sort,
|
||||
}: IExecutionLogAggOptions) {
|
||||
// Check if valid sort fields
|
||||
const sortFields = flatMap(sort as estypes.SortCombinations[], (s) => Object.keys(s));
|
||||
for (const field of sortFields) {
|
||||
if (!Object.keys(ExecutionLogSortFields).includes(field)) {
|
||||
throw Boom.badRequest(
|
||||
`Invalid sort field "${field}" - must be one of [${Object.keys(ExecutionLogSortFields).join(
|
||||
','
|
||||
)}]`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Check if valid page value
|
||||
if (page <= 0) {
|
||||
throw Boom.badRequest(`Invalid page field "${page}" - must be greater than 0`);
|
||||
}
|
||||
|
||||
// Check if valid page value
|
||||
if (perPage <= 0) {
|
||||
throw Boom.badRequest(`Invalid perPage field "${perPage}" - must be greater than 0`);
|
||||
}
|
||||
|
||||
const dslFilterQuery: estypes.QueryDslBoolQuery['filter'] = buildDslFilterQuery(filter);
|
||||
|
||||
return {
|
||||
executionLogAgg: {
|
||||
...(dslFilterQuery ? { filter: { bool: { filter: dslFilterQuery } } } : {}),
|
||||
aggs: {
|
||||
// Get total number of executions
|
||||
executionUuidCardinality: {
|
||||
filter: {
|
||||
bool: {
|
||||
must: [
|
||||
{
|
||||
match: {
|
||||
[ACTION_FIELD]: 'execute',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
aggs: {
|
||||
executionUuidCardinality: {
|
||||
cardinality: {
|
||||
field: EXECUTION_UUID_FIELD,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
executionUuid: {
|
||||
// Bucket by execution UUID
|
||||
terms: {
|
||||
field: EXECUTION_UUID_FIELD,
|
||||
size: DEFAULT_MAX_BUCKETS_LIMIT,
|
||||
order: formatSortForTermSort(sort),
|
||||
},
|
||||
aggs: {
|
||||
// Bucket sort to allow paging through executions
|
||||
executionUuidSorted: {
|
||||
bucket_sort: {
|
||||
sort: formatSortForBucketSort(sort),
|
||||
from: (page - 1) * perPage,
|
||||
size: perPage,
|
||||
gap_policy: 'insert_zeros' as estypes.AggregationsGapPolicy,
|
||||
},
|
||||
},
|
||||
// Filter by action execute doc and get information from this event
|
||||
actionExecution: {
|
||||
filter: {
|
||||
bool: {
|
||||
must: [
|
||||
{
|
||||
match: {
|
||||
[ACTION_FIELD]: 'execute',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
aggs: {
|
||||
executeStartTime: {
|
||||
min: {
|
||||
field: START_FIELD,
|
||||
},
|
||||
},
|
||||
scheduleDelay: {
|
||||
max: {
|
||||
field: SCHEDULE_DELAY_FIELD,
|
||||
},
|
||||
},
|
||||
executionDuration: {
|
||||
max: {
|
||||
field: DURATION_FIELD,
|
||||
},
|
||||
},
|
||||
outcomeAndMessage: {
|
||||
top_hits: {
|
||||
_source: {
|
||||
includes: [
|
||||
OUTCOME_FIELD,
|
||||
MESSAGE_FIELD,
|
||||
ERROR_MESSAGE_FIELD,
|
||||
VERSION_FIELD,
|
||||
SPACE_ID_FIELD,
|
||||
ACTION_NAME_FIELD,
|
||||
ACTION_ID_FIELD,
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
// If there was a timeout, this filter will return non-zero doc count
|
||||
timeoutMessage: {
|
||||
filter: {
|
||||
bool: {
|
||||
must: [
|
||||
{
|
||||
match: {
|
||||
[ACTION_FIELD]: 'execute-timeout',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function buildDslFilterQuery(filter: IExecutionLogAggOptions['filter']) {
|
||||
try {
|
||||
const filterKueryNode = typeof filter === 'string' ? fromKueryExpression(filter) : filter;
|
||||
return filterKueryNode ? toElasticsearchQuery(filterKueryNode) : undefined;
|
||||
} catch (err) {
|
||||
throw Boom.badRequest(`Invalid kuery syntax for filter ${filter}`);
|
||||
}
|
||||
}
|
||||
|
||||
function formatExecutionLogAggBucket(bucket: IExecutionUuidAggBucket): IExecutionLog {
|
||||
const durationUs = bucket?.actionExecution?.executionDuration?.value
|
||||
? bucket.actionExecution.executionDuration.value
|
||||
: 0;
|
||||
const scheduleDelayUs = bucket?.actionExecution?.scheduleDelay?.value
|
||||
? bucket.actionExecution.scheduleDelay.value
|
||||
: 0;
|
||||
|
||||
const outcomeAndMessage =
|
||||
bucket?.actionExecution?.outcomeAndMessage?.hits?.hits[0]?._source ?? {};
|
||||
let status = outcomeAndMessage.kibana?.alerting?.outcome ?? '';
|
||||
if (isEmpty(status)) {
|
||||
status = outcomeAndMessage.event?.outcome ?? '';
|
||||
}
|
||||
const outcomeMessage = outcomeAndMessage.message ?? '';
|
||||
const outcomeErrorMessage = outcomeAndMessage.error?.message ?? '';
|
||||
const message =
|
||||
status === 'failure' ? `${outcomeMessage} - ${outcomeErrorMessage}` : outcomeMessage;
|
||||
const version = outcomeAndMessage.kibana?.version ?? '';
|
||||
|
||||
const spaceIds = outcomeAndMessage?.kibana?.space_ids ?? [];
|
||||
const connectorName = outcomeAndMessage?.kibana?.action?.name ?? '';
|
||||
const connectorId = outcomeAndMessage?.kibana?.action?.id ?? '';
|
||||
const timedOut = (bucket?.timeoutMessage?.doc_count ?? 0) > 0;
|
||||
return {
|
||||
id: bucket?.key ?? '',
|
||||
timestamp: bucket?.actionExecution?.executeStartTime.value_as_string ?? '',
|
||||
duration_ms: durationUs / Millis2Nanos,
|
||||
status,
|
||||
message,
|
||||
version,
|
||||
schedule_delay_ms: scheduleDelayUs / Millis2Nanos,
|
||||
space_ids: spaceIds,
|
||||
connector_name: connectorName,
|
||||
connector_id: connectorId,
|
||||
timed_out: timedOut,
|
||||
};
|
||||
}
|
||||
|
||||
function formatExecutionKPIAggBuckets(buckets: IExecutionUuidKpiAggBucket[]) {
|
||||
const objToReturn = {
|
||||
success: 0,
|
||||
unknown: 0,
|
||||
failure: 0,
|
||||
warning: 0,
|
||||
};
|
||||
|
||||
buckets.forEach((bucket) => {
|
||||
const actionExecutionOutcomes = bucket?.actionExecution?.actionExecutionOutcomes?.buckets ?? [];
|
||||
|
||||
const actionExecutionCount = bucket?.actionExecution?.doc_count ?? 0;
|
||||
const outcomes = {
|
||||
successActionExecution: 0,
|
||||
failureActionExecution: 0,
|
||||
warningActionExecution: 0,
|
||||
};
|
||||
actionExecutionOutcomes.reduce((acc, subBucket) => {
|
||||
const key = subBucket.key;
|
||||
|
||||
if (key === 'success') {
|
||||
acc.successActionExecution = subBucket.doc_count ?? 0;
|
||||
} else if (key === 'failure') {
|
||||
acc.failureActionExecution = subBucket.doc_count ?? 0;
|
||||
} else if (key === 'warning') {
|
||||
acc.warningActionExecution = subBucket.doc_count ?? 0;
|
||||
}
|
||||
return acc;
|
||||
}, outcomes);
|
||||
|
||||
objToReturn.success += outcomes.successActionExecution;
|
||||
objToReturn.unknown +=
|
||||
actionExecutionCount -
|
||||
(outcomes.successActionExecution +
|
||||
outcomes.failureActionExecution +
|
||||
outcomes.warningActionExecution);
|
||||
objToReturn.failure += outcomes.failureActionExecution;
|
||||
objToReturn.warning += outcomes.warningActionExecution;
|
||||
});
|
||||
|
||||
return objToReturn;
|
||||
}
|
||||
|
||||
export function formatExecutionKPIResult(results: AggregateEventsBySavedObjectResult) {
|
||||
const { aggregations } = results;
|
||||
if (!aggregations || !aggregations.executionKpiAgg) {
|
||||
return EMPTY_EXECUTION_KPI_RESULT;
|
||||
}
|
||||
const aggs = aggregations.executionKpiAgg as ExecutionKpiAggResult;
|
||||
const buckets = aggs.executionUuid.buckets;
|
||||
return formatExecutionKPIAggBuckets(buckets);
|
||||
}
|
||||
|
||||
export function formatExecutionLogResult(
|
||||
results: AggregateEventsBySavedObjectResult
|
||||
): IExecutionLogResult {
|
||||
const { aggregations } = results;
|
||||
|
||||
if (!aggregations || !aggregations.executionLogAgg) {
|
||||
return EMPTY_EXECUTION_LOG_RESULT;
|
||||
}
|
||||
|
||||
const aggs = aggregations.executionLogAgg as ExecutionLogAggResult;
|
||||
|
||||
const total = aggs.executionUuidCardinality.executionUuidCardinality.value;
|
||||
const buckets = aggs.executionUuid.buckets;
|
||||
|
||||
return {
|
||||
total,
|
||||
data: buckets.map((bucket: IExecutionUuidAggBucket) => formatExecutionLogAggBucket(bucket)),
|
||||
};
|
||||
}
|
||||
|
||||
export function formatSortForBucketSort(sort: estypes.Sort) {
|
||||
return (sort as estypes.SortCombinations[]).map((s) =>
|
||||
Object.keys(s).reduce(
|
||||
(acc, curr) => ({ ...acc, [ExecutionLogSortFields[curr]]: get(s, curr) }),
|
||||
{}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export function formatSortForTermSort(sort: estypes.Sort) {
|
||||
return (sort as estypes.SortCombinations[]).map((s) =>
|
||||
Object.keys(s).reduce(
|
||||
(acc, curr) => ({ ...acc, [ExecutionLogSortFields[curr]]: get(s, `${curr}.order`) }),
|
||||
{}
|
||||
)
|
||||
);
|
||||
}
|
|
@ -34,3 +34,4 @@ export {
|
|||
isHttpRequestExecutionSource,
|
||||
} from './action_execution_source';
|
||||
export { validateEmptyStrings } from './validate_empty_strings';
|
||||
export { parseDate } from './parse_date';
|
||||
|
|
51
x-pack/plugins/actions/server/lib/parse_date.test.ts
Normal file
51
x-pack/plugins/actions/server/lib/parse_date.test.ts
Normal file
|
@ -0,0 +1,51 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { parseDate, parseIsoOrRelativeDate } from './parse_date';
|
||||
|
||||
describe('parseDate', () => {
|
||||
test('returns valid parsed date', () => {
|
||||
const date = new Date(Date.now() - 1 * 60 * 60 * 1000);
|
||||
const parsedDate = parseDate(date.toISOString(), 'dateStart', new Date());
|
||||
expect(parsedDate?.valueOf()).toBe(date.valueOf());
|
||||
});
|
||||
|
||||
test('returns default value if date is undefined', () => {
|
||||
const date = new Date();
|
||||
const parsedDate = parseDate(undefined, 'dateStart', date);
|
||||
expect(parsedDate?.valueOf()).toBe(date.valueOf());
|
||||
});
|
||||
|
||||
test('throws an error for invalid date strings', () => {
|
||||
expect(() =>
|
||||
parseDate('this shall not pass', 'dateStart', new Date())
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
`"Invalid date for parameter dateStart: \\"this shall not pass\\""`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseIsoOrRelativeDate', () => {
|
||||
test('handles ISO dates', () => {
|
||||
const date = new Date();
|
||||
const parsedDate = parseIsoOrRelativeDate(date.toISOString());
|
||||
expect(parsedDate?.valueOf()).toBe(date.valueOf());
|
||||
});
|
||||
|
||||
test('handles relative dates', () => {
|
||||
const hoursDiff = 1;
|
||||
const date = new Date(Date.now() - hoursDiff * 60 * 60 * 1000);
|
||||
const parsedDate = parseIsoOrRelativeDate(`${hoursDiff}h`);
|
||||
const diff = Math.abs(parsedDate!.valueOf() - date.valueOf());
|
||||
expect(diff).toBeLessThan(1000);
|
||||
});
|
||||
|
||||
test('returns undefined for invalid date strings', () => {
|
||||
const parsedDate = parseIsoOrRelativeDate('this shall not pass');
|
||||
expect(parsedDate).toBeUndefined();
|
||||
});
|
||||
});
|
90
x-pack/plugins/actions/server/lib/parse_date.ts
Normal file
90
x-pack/plugins/actions/server/lib/parse_date.ts
Normal file
|
@ -0,0 +1,90 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import Boom from '@hapi/boom';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
const SECONDS_REGEX = /^[1-9][0-9]*s$/;
|
||||
const MINUTES_REGEX = /^[1-9][0-9]*m$/;
|
||||
const HOURS_REGEX = /^[1-9][0-9]*h$/;
|
||||
const DAYS_REGEX = /^[1-9][0-9]*d$/;
|
||||
|
||||
export function parseDate(
|
||||
dateString: string | undefined,
|
||||
propertyName: string,
|
||||
defaultValue: Date
|
||||
): Date {
|
||||
if (dateString === undefined) {
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
const parsedDate = parseIsoOrRelativeDate(dateString);
|
||||
if (parsedDate === undefined) {
|
||||
throw Boom.badRequest(
|
||||
i18n.translate('xpack.actions.actionsClient.invalidDate', {
|
||||
defaultMessage: 'Invalid date for parameter {field}: "{dateValue}"',
|
||||
values: {
|
||||
field: propertyName,
|
||||
dateValue: dateString,
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
return parsedDate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse an ISO date or NNx duration string as a Date
|
||||
*
|
||||
* @param dateString an ISO date or NNx "duration" string representing now-duration
|
||||
* @returns a Date or undefined if the dateString was not valid
|
||||
*/
|
||||
export function parseIsoOrRelativeDate(dateString: string): Date | undefined {
|
||||
const epochMillis = Date.parse(dateString);
|
||||
if (!isNaN(epochMillis)) return new Date(epochMillis);
|
||||
|
||||
let millis: number;
|
||||
try {
|
||||
millis = parseDuration(dateString);
|
||||
} catch (err) {
|
||||
return;
|
||||
}
|
||||
|
||||
return new Date(Date.now() - millis);
|
||||
}
|
||||
|
||||
export function parseDuration(duration: string): number {
|
||||
const parsed = parseInt(duration, 10);
|
||||
if (isSeconds(duration)) {
|
||||
return parsed * 1000;
|
||||
} else if (isMinutes(duration)) {
|
||||
return parsed * 60 * 1000;
|
||||
} else if (isHours(duration)) {
|
||||
return parsed * 60 * 60 * 1000;
|
||||
} else if (isDays(duration)) {
|
||||
return parsed * 24 * 60 * 60 * 1000;
|
||||
}
|
||||
throw new Error(
|
||||
`Invalid duration "${duration}". Durations must be of the form {number}x. Example: 5s, 5m, 5h or 5d"`
|
||||
);
|
||||
}
|
||||
|
||||
function isSeconds(duration: string) {
|
||||
return SECONDS_REGEX.test(duration);
|
||||
}
|
||||
|
||||
function isMinutes(duration: string) {
|
||||
return MINUTES_REGEX.test(duration);
|
||||
}
|
||||
|
||||
function isHours(duration: string) {
|
||||
return HOURS_REGEX.test(duration);
|
||||
}
|
||||
|
||||
function isDays(duration: string) {
|
||||
return DAYS_REGEX.test(duration);
|
||||
}
|
|
@ -19,7 +19,17 @@ import { ActionTypeDisabledError } from './errors';
|
|||
import { actionsClientMock } from '../mocks';
|
||||
import { inMemoryMetricsMock } from '../monitoring/in_memory_metrics.mock';
|
||||
import { IN_MEMORY_METRICS } from '../monitoring';
|
||||
import { pick } from 'lodash';
|
||||
|
||||
const executeParamsFields = [
|
||||
'actionId',
|
||||
'isEphemeral',
|
||||
'params',
|
||||
'relatedSavedObjects',
|
||||
'executionId',
|
||||
'request.headers',
|
||||
'taskInfo',
|
||||
];
|
||||
const spaceIdToNamespace = jest.fn();
|
||||
const actionTypeRegistry = actionTypeRegistryMock.create();
|
||||
const mockedEncryptedSavedObjectsClient = encryptedSavedObjectsMock.createClient();
|
||||
|
@ -136,25 +146,25 @@ test('executes the task by calling the executor with proper parameters, using gi
|
|||
{ namespace: 'namespace-test' }
|
||||
);
|
||||
|
||||
expect(mockedActionExecutor.execute).toHaveBeenCalledWith({
|
||||
const [executeParams] = mockedActionExecutor.execute.mock.calls[0];
|
||||
expect(pick(executeParams, executeParamsFields)).toEqual({
|
||||
actionId: '2',
|
||||
isEphemeral: false,
|
||||
params: { baz: true },
|
||||
relatedSavedObjects: [],
|
||||
executionId: '123abc',
|
||||
request: expect.objectContaining({
|
||||
request: {
|
||||
headers: {
|
||||
// base64 encoded "123:abc"
|
||||
authorization: 'ApiKey MTIzOmFiYw==',
|
||||
},
|
||||
}),
|
||||
},
|
||||
taskInfo: {
|
||||
scheduled: new Date(),
|
||||
attempts: 0,
|
||||
},
|
||||
});
|
||||
|
||||
const [executeParams] = mockedActionExecutor.execute.mock.calls[0];
|
||||
expect(taskRunnerFactoryInitializerParams.basePathService.set).toHaveBeenCalledWith(
|
||||
executeParams.request,
|
||||
'/s/test'
|
||||
|
@ -196,25 +206,25 @@ test('executes the task by calling the executor with proper parameters, using st
|
|||
{ namespace: 'namespace-test' }
|
||||
);
|
||||
|
||||
expect(mockedActionExecutor.execute).toHaveBeenCalledWith({
|
||||
const [executeParams] = mockedActionExecutor.execute.mock.calls[0];
|
||||
expect(pick(executeParams, executeParamsFields)).toEqual({
|
||||
actionId: '9',
|
||||
isEphemeral: false,
|
||||
params: { baz: true },
|
||||
executionId: '123abc',
|
||||
relatedSavedObjects: [],
|
||||
request: expect.objectContaining({
|
||||
request: {
|
||||
headers: {
|
||||
// base64 encoded "123:abc"
|
||||
authorization: 'ApiKey MTIzOmFiYw==',
|
||||
},
|
||||
}),
|
||||
},
|
||||
taskInfo: {
|
||||
scheduled: new Date(),
|
||||
attempts: 0,
|
||||
},
|
||||
});
|
||||
|
||||
const [executeParams] = mockedActionExecutor.execute.mock.calls[0];
|
||||
expect(taskRunnerFactoryInitializerParams.basePathService.set).toHaveBeenCalledWith(
|
||||
executeParams.request,
|
||||
'/s/test'
|
||||
|
@ -251,26 +261,26 @@ test('executes the task by calling the executor with proper parameters when cons
|
|||
{ namespace: 'namespace-test' }
|
||||
);
|
||||
|
||||
expect(mockedActionExecutor.execute).toHaveBeenCalledWith({
|
||||
const [executeParams] = mockedActionExecutor.execute.mock.calls[0];
|
||||
expect(pick(executeParams, [...executeParamsFields, 'consumer'])).toEqual({
|
||||
actionId: '2',
|
||||
consumer: 'test-consumer',
|
||||
isEphemeral: false,
|
||||
params: { baz: true },
|
||||
relatedSavedObjects: [],
|
||||
executionId: '123abc',
|
||||
request: expect.objectContaining({
|
||||
request: {
|
||||
headers: {
|
||||
// base64 encoded "123:abc"
|
||||
authorization: 'ApiKey MTIzOmFiYw==',
|
||||
},
|
||||
}),
|
||||
},
|
||||
taskInfo: {
|
||||
scheduled: new Date(),
|
||||
attempts: 0,
|
||||
},
|
||||
});
|
||||
|
||||
const [executeParams] = mockedActionExecutor.execute.mock.calls[0];
|
||||
expect(taskRunnerFactoryInitializerParams.basePathService.set).toHaveBeenCalledWith(
|
||||
executeParams.request,
|
||||
'/s/test'
|
||||
|
@ -444,25 +454,25 @@ test('uses API key when provided', async () => {
|
|||
|
||||
await taskRunner.run();
|
||||
|
||||
expect(mockedActionExecutor.execute).toHaveBeenCalledWith({
|
||||
const [executeParams] = mockedActionExecutor.execute.mock.calls[0];
|
||||
expect(pick(executeParams, executeParamsFields)).toEqual({
|
||||
actionId: '2',
|
||||
isEphemeral: false,
|
||||
params: { baz: true },
|
||||
executionId: '123abc',
|
||||
relatedSavedObjects: [],
|
||||
request: expect.objectContaining({
|
||||
request: {
|
||||
headers: {
|
||||
// base64 encoded "123:abc"
|
||||
authorization: 'ApiKey MTIzOmFiYw==',
|
||||
},
|
||||
}),
|
||||
},
|
||||
taskInfo: {
|
||||
scheduled: new Date(),
|
||||
attempts: 0,
|
||||
},
|
||||
});
|
||||
|
||||
const [executeParams] = mockedActionExecutor.execute.mock.calls[0];
|
||||
expect(taskRunnerFactoryInitializerParams.basePathService.set).toHaveBeenCalledWith(
|
||||
executeParams.request,
|
||||
'/s/test'
|
||||
|
@ -502,7 +512,8 @@ test('uses relatedSavedObjects merged with references when provided', async () =
|
|||
|
||||
await taskRunner.run();
|
||||
|
||||
expect(mockedActionExecutor.execute).toHaveBeenCalledWith({
|
||||
const [executeParams] = mockedActionExecutor.execute.mock.calls[0];
|
||||
expect(pick(executeParams, executeParamsFields)).toEqual({
|
||||
actionId: '2',
|
||||
isEphemeral: false,
|
||||
params: { baz: true },
|
||||
|
@ -513,12 +524,12 @@ test('uses relatedSavedObjects merged with references when provided', async () =
|
|||
type: 'some-type',
|
||||
},
|
||||
],
|
||||
request: expect.objectContaining({
|
||||
request: {
|
||||
headers: {
|
||||
// base64 encoded "123:abc"
|
||||
authorization: 'ApiKey MTIzOmFiYw==',
|
||||
},
|
||||
}),
|
||||
},
|
||||
taskInfo: {
|
||||
scheduled: new Date(),
|
||||
attempts: 0,
|
||||
|
@ -554,7 +565,8 @@ test('uses relatedSavedObjects as is when references are empty', async () => {
|
|||
|
||||
await taskRunner.run();
|
||||
|
||||
expect(mockedActionExecutor.execute).toHaveBeenCalledWith({
|
||||
const [executeParams] = mockedActionExecutor.execute.mock.calls[0];
|
||||
expect(pick(executeParams, executeParamsFields)).toEqual({
|
||||
actionId: '2',
|
||||
isEphemeral: false,
|
||||
params: { baz: true },
|
||||
|
@ -566,12 +578,12 @@ test('uses relatedSavedObjects as is when references are empty', async () => {
|
|||
namespace: 'yo',
|
||||
},
|
||||
],
|
||||
request: expect.objectContaining({
|
||||
request: {
|
||||
headers: {
|
||||
// base64 encoded "123:abc"
|
||||
authorization: 'ApiKey MTIzOmFiYw==',
|
||||
},
|
||||
}),
|
||||
},
|
||||
taskInfo: {
|
||||
scheduled: new Date(),
|
||||
attempts: 0,
|
||||
|
@ -611,16 +623,18 @@ test('sanitizes invalid relatedSavedObjects when provided', async () => {
|
|||
});
|
||||
|
||||
await taskRunner.run();
|
||||
expect(mockedActionExecutor.execute).toHaveBeenCalledWith({
|
||||
|
||||
const [executeParams] = mockedActionExecutor.execute.mock.calls[0];
|
||||
expect(pick(executeParams, executeParamsFields)).toEqual({
|
||||
actionId: '2',
|
||||
isEphemeral: false,
|
||||
params: { baz: true },
|
||||
request: expect.objectContaining({
|
||||
request: {
|
||||
headers: {
|
||||
// base64 encoded "123:abc"
|
||||
authorization: 'ApiKey MTIzOmFiYw==',
|
||||
},
|
||||
}),
|
||||
},
|
||||
executionId: '123abc',
|
||||
relatedSavedObjects: [],
|
||||
taskInfo: {
|
||||
|
@ -656,22 +670,22 @@ test(`doesn't use API key when not provided`, async () => {
|
|||
|
||||
await taskRunner.run();
|
||||
|
||||
expect(mockedActionExecutor.execute).toHaveBeenCalledWith({
|
||||
const [executeParams] = mockedActionExecutor.execute.mock.calls[0];
|
||||
expect(pick(executeParams, executeParamsFields)).toEqual({
|
||||
actionId: '2',
|
||||
isEphemeral: false,
|
||||
params: { baz: true },
|
||||
executionId: '123abc',
|
||||
relatedSavedObjects: [],
|
||||
request: expect.objectContaining({
|
||||
request: {
|
||||
headers: {},
|
||||
}),
|
||||
},
|
||||
taskInfo: {
|
||||
scheduled: new Date(),
|
||||
attempts: 0,
|
||||
},
|
||||
});
|
||||
|
||||
const [executeParams] = mockedActionExecutor.execute.mock.calls[0];
|
||||
expect(taskRunnerFactoryInitializerParams.basePathService.set).toHaveBeenCalledWith(
|
||||
executeParams.request,
|
||||
'/s/test'
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { pick } from 'lodash';
|
||||
import { pipe } from 'fp-ts/lib/pipeable';
|
||||
import { map, fromNullable, getOrElse } from 'fp-ts/lib/Option';
|
||||
|
@ -84,6 +85,7 @@ export class TaskRunnerFactory {
|
|||
scheduled: taskInstance.runAt,
|
||||
attempts: taskInstance.attempts,
|
||||
};
|
||||
const actionExecutionId = uuidv4();
|
||||
|
||||
return {
|
||||
async run() {
|
||||
|
@ -122,6 +124,7 @@ export class TaskRunnerFactory {
|
|||
executionId,
|
||||
consumer,
|
||||
relatedSavedObjects: validatedRelatedSavedObjects(logger, relatedSavedObjects),
|
||||
actionExecutionId,
|
||||
});
|
||||
} catch (e) {
|
||||
logger.error(
|
||||
|
@ -208,6 +211,7 @@ export class TaskRunnerFactory {
|
|||
executionId,
|
||||
relatedSavedObjects: (relatedSavedObjects || []) as RelatedSavedObjects,
|
||||
...getSourceFromReferences(references),
|
||||
actionExecutionId,
|
||||
});
|
||||
|
||||
inMemoryMetrics.increment(IN_MEMORY_METRICS.ACTION_TIMEOUTS);
|
||||
|
|
|
@ -302,6 +302,7 @@ describe('Actions Plugin', () => {
|
|||
licensing: licensingMock.createStart(),
|
||||
taskManager: taskManagerMock.createStart(),
|
||||
encryptedSavedObjects: encryptedSavedObjectsMock.createStart(),
|
||||
eventLog: eventLogMock.createStart(),
|
||||
};
|
||||
});
|
||||
|
||||
|
@ -380,6 +381,7 @@ describe('Actions Plugin', () => {
|
|||
licensing: licensingMock.createStart(),
|
||||
taskManager: taskManagerMock.createStart(),
|
||||
encryptedSavedObjects: encryptedSavedObjectsMock.createStart(),
|
||||
eventLog: eventLogMock.createStart(),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -33,7 +33,11 @@ import { LicensingPluginSetup, LicensingPluginStart } from '@kbn/licensing-plugi
|
|||
import { SpacesPluginStart, SpacesPluginSetup } from '@kbn/spaces-plugin/server';
|
||||
import { PluginSetupContract as FeaturesPluginSetup } from '@kbn/features-plugin/server';
|
||||
import { SecurityPluginSetup } from '@kbn/security-plugin/server';
|
||||
import { IEventLogger, IEventLogService } from '@kbn/event-log-plugin/server';
|
||||
import {
|
||||
IEventLogClientService,
|
||||
IEventLogger,
|
||||
IEventLogService,
|
||||
} from '@kbn/event-log-plugin/server';
|
||||
import { MonitoringCollectionSetup } from '@kbn/monitoring-collection-plugin/server';
|
||||
import {
|
||||
ensureCleanupFailedExecutionsTaskScheduled,
|
||||
|
@ -171,6 +175,7 @@ export interface ActionsPluginsStart {
|
|||
encryptedSavedObjects: EncryptedSavedObjectsPluginStart;
|
||||
taskManager: TaskManagerStartContract;
|
||||
licensing: LicensingPluginStart;
|
||||
eventLog: IEventLogClientService;
|
||||
spaces?: SpacesPluginStart;
|
||||
}
|
||||
|
||||
|
@ -458,6 +463,9 @@ export class ActionsPlugin implements Plugin<PluginSetupContract, PluginStartCon
|
|||
encryptedSavedObjectsClient,
|
||||
logger: this.logger,
|
||||
}),
|
||||
async getEventLogClient() {
|
||||
return plugins.eventLog.getClient(request);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -620,7 +628,7 @@ export class ActionsPlugin implements Plugin<PluginSetupContract, PluginStartCon
|
|||
} = this;
|
||||
|
||||
return async function actionsRouteHandlerContext(context, request) {
|
||||
const [{ savedObjects }, { taskManager, encryptedSavedObjects }] =
|
||||
const [{ savedObjects }, { taskManager, encryptedSavedObjects, eventLog }] =
|
||||
await core.getStartServices();
|
||||
const coreContext = await context.core;
|
||||
|
||||
|
@ -672,6 +680,9 @@ export class ActionsPlugin implements Plugin<PluginSetupContract, PluginStartCon
|
|||
}),
|
||||
logger,
|
||||
}),
|
||||
async getEventLogClient() {
|
||||
return eventLog.getClient(request);
|
||||
},
|
||||
});
|
||||
},
|
||||
listTypes: actionTypeRegistry!.list.bind(actionTypeRegistry!),
|
||||
|
|
|
@ -0,0 +1,68 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { httpServiceMock } from '@kbn/core/server/mocks';
|
||||
import { licenseStateMock } from '../lib/license_state.mock';
|
||||
import { mockHandlerArguments } from './legacy/_mock_handler_arguments';
|
||||
import { actionsClientMock } from '../actions_client.mock';
|
||||
import { getGlobalExecutionKPIRoute } from './get_global_execution_kpi';
|
||||
import { verifyAccessAndContext } from './verify_access_and_context';
|
||||
|
||||
const actionsClient = actionsClientMock.create();
|
||||
jest.mock('./verify_access_and_context', () => ({
|
||||
verifyAccessAndContext: jest.fn(),
|
||||
}));
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
(verifyAccessAndContext as jest.Mock).mockImplementation((license, handler) => handler);
|
||||
});
|
||||
|
||||
describe('getGlobalExecutionKPIRoute', () => {
|
||||
const dateString = new Date().toISOString();
|
||||
const mockedExecutionLog = {
|
||||
success: 3,
|
||||
unknown: 0,
|
||||
failure: 0,
|
||||
warning: 0,
|
||||
};
|
||||
|
||||
it('gets global execution KPI', async () => {
|
||||
const licenseState = licenseStateMock.create();
|
||||
const router = httpServiceMock.createRouter();
|
||||
|
||||
getGlobalExecutionKPIRoute(router, licenseState);
|
||||
|
||||
const [config, handler] = router.post.mock.calls[0];
|
||||
|
||||
expect(config.path).toMatchInlineSnapshot(
|
||||
`"/internal/actions/_global_connector_execution_kpi"`
|
||||
);
|
||||
|
||||
actionsClient.getGlobalExecutionKpiWithAuth.mockResolvedValue(mockedExecutionLog);
|
||||
|
||||
const [context, req, res] = mockHandlerArguments(
|
||||
{ actionsClient },
|
||||
{
|
||||
body: {
|
||||
date_start: dateString,
|
||||
},
|
||||
},
|
||||
['ok']
|
||||
);
|
||||
|
||||
await handler(context, req, res);
|
||||
|
||||
expect(actionsClient.getGlobalExecutionKpiWithAuth).toHaveBeenCalledTimes(1);
|
||||
expect(actionsClient.getGlobalExecutionKpiWithAuth.mock.calls[0]).toEqual([
|
||||
{
|
||||
dateStart: dateString,
|
||||
},
|
||||
]);
|
||||
|
||||
expect(res.ok).toHaveBeenCalled();
|
||||
});
|
||||
});
|
|
@ -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
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import { IRouter } from '@kbn/core/server';
|
||||
import { schema } from '@kbn/config-schema';
|
||||
import {
|
||||
GetGlobalExecutionKPIParams,
|
||||
INTERNAL_BASE_ACTION_API_PATH,
|
||||
RewriteRequestCase,
|
||||
} from '../../common';
|
||||
import { verifyAccessAndContext } from './verify_access_and_context';
|
||||
import { ActionsRequestHandlerContext } from '../types';
|
||||
import { ILicenseState } from '../lib';
|
||||
import { rewriteNamespaces } from './rewrite_namespaces';
|
||||
|
||||
const bodySchema = schema.object({
|
||||
date_start: schema.string(),
|
||||
date_end: schema.maybe(schema.string()),
|
||||
filter: schema.maybe(schema.string()),
|
||||
namespaces: schema.maybe(schema.arrayOf(schema.string())),
|
||||
});
|
||||
|
||||
const rewriteReq: RewriteRequestCase<GetGlobalExecutionKPIParams> = ({
|
||||
date_start: dateStart,
|
||||
date_end: dateEnd,
|
||||
namespaces,
|
||||
...rest
|
||||
}) => ({
|
||||
...rest,
|
||||
namespaces: rewriteNamespaces(namespaces),
|
||||
dateStart,
|
||||
dateEnd,
|
||||
});
|
||||
|
||||
export const getGlobalExecutionKPIRoute = (
|
||||
router: IRouter<ActionsRequestHandlerContext>,
|
||||
licenseState: ILicenseState
|
||||
) => {
|
||||
router.post(
|
||||
{
|
||||
path: `${INTERNAL_BASE_ACTION_API_PATH}/_global_connector_execution_kpi`,
|
||||
validate: {
|
||||
body: bodySchema,
|
||||
},
|
||||
},
|
||||
router.handleLegacyErrors(
|
||||
verifyAccessAndContext(licenseState, async function (context, req, res) {
|
||||
const actionsClient = (await context.actions).getActionsClient();
|
||||
return res.ok({
|
||||
body: await actionsClient.getGlobalExecutionKpiWithAuth(rewriteReq(req.body)),
|
||||
});
|
||||
})
|
||||
)
|
||||
);
|
||||
};
|
|
@ -0,0 +1,88 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { getGlobalExecutionLogRoute } from './get_global_execution_logs';
|
||||
import { httpServiceMock } from '@kbn/core/server/mocks';
|
||||
import { licenseStateMock } from '../lib/license_state.mock';
|
||||
import { mockHandlerArguments } from './legacy/_mock_handler_arguments';
|
||||
import { actionsClientMock } from '../actions_client.mock';
|
||||
import { IExecutionLogResult } from '../../common';
|
||||
import { verifyAccessAndContext } from './verify_access_and_context';
|
||||
|
||||
const actionsClient = actionsClientMock.create();
|
||||
jest.mock('./verify_access_and_context', () => ({
|
||||
verifyAccessAndContext: jest.fn(),
|
||||
}));
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
(verifyAccessAndContext as jest.Mock).mockImplementation((license, handler) => handler);
|
||||
});
|
||||
|
||||
describe('getRuleExecutionLogRoute', () => {
|
||||
const dateString = new Date().toISOString();
|
||||
const mockedExecutionLog: IExecutionLogResult = {
|
||||
data: [
|
||||
{
|
||||
connector_name: 'test connector',
|
||||
connector_id: '1',
|
||||
duration_ms: 1,
|
||||
id: '8b3af07e-7593-4c40-b704-9c06d3b06e58',
|
||||
message:
|
||||
'action executed: .server-log:6709f660-8d11-11ed-bae5-bd32cbc9eaaa: test connector',
|
||||
schedule_delay_ms: 2783,
|
||||
space_ids: ['default'],
|
||||
status: 'success',
|
||||
timestamp: '2023-01-05T15:55:50.495Z',
|
||||
version: '8.7.0',
|
||||
timed_out: false,
|
||||
},
|
||||
],
|
||||
total: 1,
|
||||
};
|
||||
|
||||
it('gets global execution logs', async () => {
|
||||
const licenseState = licenseStateMock.create();
|
||||
const router = httpServiceMock.createRouter();
|
||||
|
||||
getGlobalExecutionLogRoute(router, licenseState);
|
||||
|
||||
const [config, handler] = router.post.mock.calls[0];
|
||||
|
||||
expect(config.path).toMatchInlineSnapshot(
|
||||
`"/internal/actions/_global_connector_execution_logs"`
|
||||
);
|
||||
|
||||
actionsClient.getGlobalExecutionLogWithAuth.mockResolvedValue(mockedExecutionLog);
|
||||
|
||||
const [context, req, res] = mockHandlerArguments(
|
||||
{ actionsClient },
|
||||
{
|
||||
body: {
|
||||
date_start: dateString,
|
||||
per_page: 10,
|
||||
page: 1,
|
||||
sort: [{ timestamp: { order: 'desc' } }],
|
||||
},
|
||||
},
|
||||
['ok']
|
||||
);
|
||||
|
||||
await handler(context, req, res);
|
||||
|
||||
expect(actionsClient.getGlobalExecutionLogWithAuth).toHaveBeenCalledTimes(1);
|
||||
expect(actionsClient.getGlobalExecutionLogWithAuth.mock.calls[0]).toEqual([
|
||||
{
|
||||
dateStart: dateString,
|
||||
page: 1,
|
||||
perPage: 10,
|
||||
sort: [{ timestamp: { order: 'desc' } }],
|
||||
},
|
||||
]);
|
||||
|
||||
expect(res.ok).toHaveBeenCalled();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,70 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { IRouter } from '@kbn/core/server';
|
||||
import { schema } from '@kbn/config-schema';
|
||||
import { ILicenseState } from '../lib';
|
||||
import { ActionsRequestHandlerContext } from '../types';
|
||||
import {
|
||||
GetGlobalExecutionLogParams,
|
||||
INTERNAL_BASE_ACTION_API_PATH,
|
||||
RewriteRequestCase,
|
||||
} from '../../common';
|
||||
import { verifyAccessAndContext } from './verify_access_and_context';
|
||||
import { rewriteNamespaces } from './rewrite_namespaces';
|
||||
|
||||
const sortOrderSchema = schema.oneOf([schema.literal('asc'), schema.literal('desc')]);
|
||||
|
||||
const sortFieldSchema = schema.oneOf([
|
||||
schema.object({ timestamp: schema.object({ order: sortOrderSchema }) }),
|
||||
schema.object({ execution_duration: schema.object({ order: sortOrderSchema }) }),
|
||||
schema.object({ schedule_delay: schema.object({ order: sortOrderSchema }) }),
|
||||
]);
|
||||
|
||||
const sortFieldsSchema = schema.arrayOf(sortFieldSchema, {
|
||||
defaultValue: [{ timestamp: { order: 'desc' } }],
|
||||
});
|
||||
|
||||
const bodySchema = schema.object({
|
||||
date_start: schema.string(),
|
||||
date_end: schema.maybe(schema.string()),
|
||||
filter: schema.maybe(schema.string()),
|
||||
per_page: schema.number({ defaultValue: 10, min: 1 }),
|
||||
page: schema.number({ defaultValue: 1, min: 1 }),
|
||||
sort: sortFieldsSchema,
|
||||
namespaces: schema.maybe(schema.arrayOf(schema.string())),
|
||||
});
|
||||
|
||||
const rewriteBodyReq: RewriteRequestCase<GetGlobalExecutionLogParams> = ({
|
||||
date_start: dateStart,
|
||||
date_end: dateEnd,
|
||||
per_page: perPage,
|
||||
namespaces,
|
||||
...rest
|
||||
}) => ({ ...rest, namespaces: rewriteNamespaces(namespaces), dateStart, dateEnd, perPage });
|
||||
|
||||
export const getGlobalExecutionLogRoute = (
|
||||
router: IRouter<ActionsRequestHandlerContext>,
|
||||
licenseState: ILicenseState
|
||||
) => {
|
||||
router.post(
|
||||
{
|
||||
path: `${INTERNAL_BASE_ACTION_API_PATH}/_global_connector_execution_logs`,
|
||||
validate: {
|
||||
body: bodySchema,
|
||||
},
|
||||
},
|
||||
router.handleLegacyErrors(
|
||||
verifyAccessAndContext(licenseState, async function (context, req, res) {
|
||||
const actionsClient = (await context.actions).getActionsClient();
|
||||
return res.ok({
|
||||
body: await actionsClient.getGlobalExecutionLogWithAuth(rewriteBodyReq(req.body)),
|
||||
});
|
||||
})
|
||||
)
|
||||
);
|
||||
};
|
|
@ -19,6 +19,8 @@ import { updateActionRoute } from './update';
|
|||
import { getOAuthAccessToken } from './get_oauth_access_token';
|
||||
import { defineLegacyRoutes } from './legacy';
|
||||
import { ActionsConfigurationUtilities } from '../actions_config';
|
||||
import { getGlobalExecutionLogRoute } from './get_global_execution_logs';
|
||||
import { getGlobalExecutionKPIRoute } from './get_global_execution_kpi';
|
||||
|
||||
export interface RouteOptions {
|
||||
router: IRouter<ActionsRequestHandlerContext>;
|
||||
|
@ -39,6 +41,8 @@ export function defineRoutes(opts: RouteOptions) {
|
|||
updateActionRoute(router, licenseState);
|
||||
connectorTypesRoute(router, licenseState);
|
||||
executeActionRoute(router, licenseState);
|
||||
getGlobalExecutionLogRoute(router, licenseState);
|
||||
getGlobalExecutionKPIRoute(router, licenseState);
|
||||
|
||||
getOAuthAccessToken(router, licenseState, actionsConfigUtils);
|
||||
}
|
||||
|
|
11
x-pack/plugins/actions/server/routes/rewrite_namespaces.ts
Normal file
11
x-pack/plugins/actions/server/routes/rewrite_namespaces.ts
Normal file
|
@ -0,0 +1,11 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
export const rewriteNamespaces = (namespaces?: Array<string | undefined>) =>
|
||||
namespaces
|
||||
? namespaces.map((id: string | undefined) => (id === 'default' ? undefined : id))
|
||||
: undefined;
|
|
@ -31,7 +31,7 @@
|
|||
"@kbn/std",
|
||||
"@kbn/logging",
|
||||
"@kbn/logging-mocks",
|
||||
"@kbn/core-elasticsearch-client-server-mocks",
|
||||
"@kbn/core-elasticsearch-client-server-mocks"
|
||||
],
|
||||
"exclude": [
|
||||
"target/**/*",
|
||||
|
|
|
@ -392,6 +392,9 @@
|
|||
"type_id": {
|
||||
"type": "keyword",
|
||||
"ignore_above": 1024
|
||||
},
|
||||
"space_agnostic": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -404,6 +407,26 @@
|
|||
},
|
||||
"version": {
|
||||
"type": "version"
|
||||
},
|
||||
"action": {
|
||||
"properties": {
|
||||
"name": {
|
||||
"ignore_above": 1024,
|
||||
"type": "keyword"
|
||||
},
|
||||
"id": {
|
||||
"type": "keyword",
|
||||
"ignore_above": 1024
|
||||
},
|
||||
"execution": {
|
||||
"properties": {
|
||||
"uuid": {
|
||||
"ignore_above": 1024,
|
||||
"type": "keyword"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -170,11 +170,23 @@ export const EventSchema = schema.maybe(
|
|||
id: ecsString(),
|
||||
type: ecsString(),
|
||||
type_id: ecsString(),
|
||||
space_agnostic: ecsBoolean(),
|
||||
})
|
||||
)
|
||||
),
|
||||
space_ids: ecsStringMulti(),
|
||||
version: ecsVersion(),
|
||||
action: schema.maybe(
|
||||
schema.object({
|
||||
name: ecsString(),
|
||||
id: ecsString(),
|
||||
execution: schema.maybe(
|
||||
schema.object({
|
||||
uuid: ecsString(),
|
||||
})
|
||||
),
|
||||
})
|
||||
),
|
||||
})
|
||||
),
|
||||
})
|
||||
|
|
|
@ -178,6 +178,9 @@ exports.EcsCustomPropertyMappings = {
|
|||
type: 'keyword',
|
||||
ignore_above: 1024,
|
||||
},
|
||||
space_agnostic: {
|
||||
type: 'boolean',
|
||||
},
|
||||
},
|
||||
},
|
||||
space_ids: {
|
||||
|
@ -187,6 +190,26 @@ exports.EcsCustomPropertyMappings = {
|
|||
version: {
|
||||
type: 'version',
|
||||
},
|
||||
action: {
|
||||
properties: {
|
||||
name: {
|
||||
ignore_above: 1024,
|
||||
type: 'keyword',
|
||||
},
|
||||
id: {
|
||||
type: 'keyword',
|
||||
ignore_above: 1024,
|
||||
},
|
||||
execution: {
|
||||
properties: {
|
||||
uuid: {
|
||||
ignore_above: 1024,
|
||||
type: 'keyword',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -827,6 +827,154 @@ describe('aggregateEventsWithAuthFilter', () => {
|
|||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('should call cluster with correct options when includeSpaceAgnostic is true', async () => {
|
||||
clusterClient.search.mockResponse({
|
||||
aggregations: {
|
||||
genericAgg: {
|
||||
buckets: [
|
||||
{
|
||||
key: 'execute',
|
||||
doc_count: 10,
|
||||
},
|
||||
{
|
||||
key: 'execute-start',
|
||||
doc_count: 10,
|
||||
},
|
||||
{
|
||||
key: 'new-instance',
|
||||
doc_count: 2,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
hits: {
|
||||
hits: [],
|
||||
total: { relation: 'eq', value: 0 },
|
||||
},
|
||||
took: 0,
|
||||
timed_out: false,
|
||||
_shards: {
|
||||
failed: 0,
|
||||
successful: 0,
|
||||
total: 0,
|
||||
skipped: 0,
|
||||
},
|
||||
});
|
||||
const options: AggregateEventsWithAuthFilter = {
|
||||
index: 'index-name',
|
||||
namespaces: ['namespace'],
|
||||
type: 'saved-object-type',
|
||||
aggregateOptions: DEFAULT_OPTIONS as AggregateOptionsType,
|
||||
authFilter: fromKueryExpression('test:test'),
|
||||
includeSpaceAgnostic: true,
|
||||
};
|
||||
const result = await clusterClientAdapter.aggregateEventsWithAuthFilter(options);
|
||||
|
||||
const [query] = clusterClient.search.mock.calls[0];
|
||||
expect(query).toEqual({
|
||||
index: 'index-name',
|
||||
body: {
|
||||
size: 0,
|
||||
query: {
|
||||
bool: {
|
||||
filter: {
|
||||
bool: {
|
||||
minimum_should_match: 1,
|
||||
should: [
|
||||
{
|
||||
match: {
|
||||
test: 'test',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
must: [
|
||||
{
|
||||
nested: {
|
||||
path: 'kibana.saved_objects',
|
||||
query: {
|
||||
bool: {
|
||||
must: [
|
||||
{
|
||||
term: {
|
||||
'kibana.saved_objects.rel': {
|
||||
value: 'primary',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
term: {
|
||||
'kibana.saved_objects.type': {
|
||||
value: 'saved-object-type',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
bool: {
|
||||
should: [
|
||||
{
|
||||
bool: {
|
||||
should: [
|
||||
{
|
||||
term: {
|
||||
'kibana.saved_objects.namespace': {
|
||||
value: 'namespace',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
match: {
|
||||
'kibana.saved_objects.space_agnostic': true,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
aggs: {
|
||||
genericAgg: {
|
||||
term: {
|
||||
field: 'event.action',
|
||||
size: 10,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(result).toEqual({
|
||||
aggregations: {
|
||||
genericAgg: {
|
||||
buckets: [
|
||||
{
|
||||
key: 'execute',
|
||||
doc_count: 10,
|
||||
},
|
||||
{
|
||||
key: 'execute-start',
|
||||
doc_count: 10,
|
||||
},
|
||||
{
|
||||
key: 'new-instance',
|
||||
doc_count: 2,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getQueryBody', () => {
|
||||
|
|
|
@ -64,6 +64,7 @@ export interface AggregateEventsWithAuthFilter {
|
|||
type: string;
|
||||
authFilter: KueryNode;
|
||||
aggregateOptions: AggregateOptionsType;
|
||||
includeSpaceAgnostic?: boolean;
|
||||
}
|
||||
|
||||
export type FindEventsOptionsWithAuthFilter = QueryOptionsEventsWithAuthFilter & {
|
||||
|
@ -85,6 +86,7 @@ export interface AggregateEventsBySavedObjectResult {
|
|||
type GetQueryBodyWithAuthFilterOpts =
|
||||
| (FindEventsOptionsWithAuthFilter & {
|
||||
namespaces: AggregateEventsWithAuthFilter['namespaces'];
|
||||
includeSpaceAgnostic?: AggregateEventsWithAuthFilter['includeSpaceAgnostic'];
|
||||
})
|
||||
| AggregateEventsWithAuthFilter;
|
||||
|
||||
|
@ -527,7 +529,7 @@ export function getQueryBodyWithAuthFilter(
|
|||
opts: GetQueryBodyWithAuthFilterOpts,
|
||||
queryOptions: QueryOptionsType
|
||||
) {
|
||||
const { namespaces, type, authFilter } = opts;
|
||||
const { namespaces, type, authFilter, includeSpaceAgnostic } = opts;
|
||||
const { start, end, filter } = queryOptions ?? {};
|
||||
const ids = 'ids' in opts ? opts.ids : [];
|
||||
|
||||
|
@ -568,7 +570,22 @@ export function getQueryBodyWithAuthFilter(
|
|||
},
|
||||
{
|
||||
bool: {
|
||||
should: namespaceQuery,
|
||||
...(includeSpaceAgnostic
|
||||
? {
|
||||
should: [
|
||||
{
|
||||
bool: {
|
||||
should: namespaceQuery,
|
||||
},
|
||||
},
|
||||
{
|
||||
match: {
|
||||
['kibana.saved_objects.space_agnostic']: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
: { should: namespaceQuery }),
|
||||
},
|
||||
},
|
||||
];
|
||||
|
|
|
@ -251,9 +251,15 @@ describe('EventLogStart', () => {
|
|||
});
|
||||
test('calls aggregateEventsWithAuthFilter with given aggregation', async () => {
|
||||
savedObjectGetter.mockResolvedValueOnce(expectedSavedObject);
|
||||
await eventLogClient.aggregateEventsWithAuthFilter('saved-object-type', testAuthFilter, {
|
||||
aggs: { myAgg: {} },
|
||||
});
|
||||
await eventLogClient.aggregateEventsWithAuthFilter(
|
||||
'saved-object-type',
|
||||
testAuthFilter,
|
||||
{
|
||||
aggs: { myAgg: {} },
|
||||
},
|
||||
undefined,
|
||||
true
|
||||
);
|
||||
expect(esContext.esAdapter.aggregateEventsWithAuthFilter).toHaveBeenCalledWith({
|
||||
index: esContext.esNames.indexPattern,
|
||||
namespaces: [undefined],
|
||||
|
@ -270,6 +276,7 @@ describe('EventLogStart', () => {
|
|||
},
|
||||
],
|
||||
},
|
||||
includeSpaceAgnostic: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -168,7 +168,8 @@ export class EventLogClient implements IEventLogClient {
|
|||
type: string,
|
||||
authFilter: KueryNode,
|
||||
options?: AggregateOptionsType,
|
||||
namespaces?: Array<string | undefined>
|
||||
namespaces?: Array<string | undefined>,
|
||||
includeSpaceAgnostic?: boolean
|
||||
) {
|
||||
if (!authFilter) {
|
||||
throw new Error('No authorization filter defined!');
|
||||
|
@ -188,6 +189,7 @@ export class EventLogClient implements IEventLogClient {
|
|||
type,
|
||||
authFilter,
|
||||
aggregateOptions: { ...aggregateOptions, aggs } as AggregateOptionsType,
|
||||
includeSpaceAgnostic,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -74,7 +74,8 @@ export interface IEventLogClient {
|
|||
type: string,
|
||||
authFilter: KueryNode,
|
||||
options?: Partial<AggregateOptionsType>,
|
||||
namespaces?: Array<string | undefined>
|
||||
namespaces?: Array<string | undefined>,
|
||||
includeSpaceAgnostic?: boolean
|
||||
): Promise<AggregateEventsBySavedObjectResult>;
|
||||
}
|
||||
|
||||
|
|
|
@ -34858,15 +34858,9 @@
|
|||
"xpack.triggersActionsUI.sections.ruleApi.bulkEditResponse.failure": "Impossible de mettre à jour {property} pour {failure, plural, one {# règle} other {# règles}}.",
|
||||
"xpack.triggersActionsUI.sections.ruleApi.bulkEditResponse.someSuccess": "{property} mise à jour pour {success, plural, one {# règle} other {# règles}}, erreurs rencontrées pour {failure, plural, one {# règle} other {# règles}}.",
|
||||
"xpack.triggersActionsUI.sections.ruleApi.bulkEditResponse.success": "{property} mise à jour pour {total, plural, one {# règle} other {# règles}}.",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.refineSearchPrompt.prompt": "Voici les {visibleDocumentSize} premiers documents correspondant à votre recherche. Veuillez l'affiner pour en voir plus.",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.rule.statusPanel.totalExecutions": "{executions, plural, one {# exécution} other {# exécutions}} au cours des dernières 24 heures",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.ruleActionErrorLogFlyout.actionErrorsPlural": "{value, plural, one {action avec erreur} other {actions avec erreur}}",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.ruleDetailsTitle": "{ruleName}",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.ruleEventLogDataGrid.erroredActionsCellPopover": "{value, plural, one {action avec erreur} other {actions avec erreur}}",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.ruleEventLogDataGrid.erroredActionsTooltip": "{value, plural, one {# action avec erreur} other {# actions avec erreur}}",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.ruleEventLogPaginationStatus.paginationResults": "Affichage de {range} sur {total, number} {type}",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.ruleEventLogPaginationStatus.paginationResultsRange": "{start, number} - {end, number}",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.ruleEventLogPaginationStatus.paginationResultsType": "{total, plural, one {entrée} other {entrées}} de log",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.ruleExecutionSummaryAndChart.loadSummaryError": "Impossible de charger le résumé de la règle : {message}",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.unableToLoadRuleMessage": "Impossible de charger la règle : {message}",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.unableToLoadRulesMessage": "Impossible de charger les règles : {message}",
|
||||
|
@ -35181,37 +35175,26 @@
|
|||
"xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.activeAlerts": "Alertes actives",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.apiError": "Impossible de récupérer l'historique d'exécution",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.duration": "Durée",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.erroredActions": "Actions comportant des erreurs",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.erroredActionsToolTip": "Nombre d'actions ayant échoué.",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.esSearchDuration": "Durée de la recherche ES",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.id": "Id",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.message": "Message",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.newAlerts": "Nouvelles alertes",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.openActionErrorsFlyout": "Ouvrir le menu volant des erreurs d’action",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.recoveredAlerts": "Alertes récupérées",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.response": "Réponse",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.ruleId": "ID règle",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.ruleName": "Règle",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.scheduledActions": "Actions générées",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.scheduledActionsToolTip": "Nombre total d'actions générées lors de l'exécution de la règle.",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.scheduleDelay": "Retard sur la planification",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.searchPlaceholder": "Rechercher message de log d’événements",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.showAll": "Afficher tout",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.showOnlyFailures": "Afficher les échecs uniquement",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.spaceIds": "Espace",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.succeededActions": "Actions ayant réussi",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.succeededActionsToolTip": "Nombre d'actions ayant entièrement réussi.",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.timedOut": "Expiré",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.timestamp": "Horodatage",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.totalSearchDuration": "Durée de la recherche totale",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.triggeredActions": "Actions déclenchées",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.triggeredActionsToolTip": "Sous-ensemble des actions générées qui seront exécutées.",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.viewActionErrors": "Afficher les erreurs des actions",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.eventLogStatusFilterLabel": "Réponse",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.manageLicensePlanBannerLinkTitle": "Gérer la licence",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.popoverButtonTitle": "Actions",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.redirectObjectNoun": "règle",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.refineSearchPrompt.backToTop": "Revenir en haut de la page.",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.rule.alertsTabText": "Alertes",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.rule.eventLogTabText": "Historique",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.rule.ruleSummary.recoveredLabel": "Récupéré",
|
||||
|
@ -35223,7 +35206,6 @@
|
|||
"xpack.triggersActionsUI.sections.ruleDetails.ruleEventLogListKpi.alertsTooltip": "Statuts des alertes pour un maximum de 10 000 exécutions de règles les plus récentes.",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.ruleEventLogListKpi.apiError": "Impossible de récupérer le KPI du log d'événements.",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.ruleEventLogListKpi.responseTooltip": "Réponses pour un maximum de 10 000 exécutions de règles les plus récentes.",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.ruleEventLogPaginationStatus.paginationResultsRangeNoResult": "0",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.rulesList.ruleLastExecutionDescription": "Dernière réponse",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.rulesList.status.active": "Actif",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.rulesList.status.inactive": "Récupéré",
|
||||
|
|
|
@ -34828,15 +34828,9 @@
|
|||
"xpack.triggersActionsUI.sections.ruleApi.bulkEditResponse.failure": "{failure, plural, other {# ルール}}の{property}を更新できませんでした。",
|
||||
"xpack.triggersActionsUI.sections.ruleApi.bulkEditResponse.someSuccess": "{success, plural, other {# ルール}}, {failure, plural, other {# ルール}}エラーの{property}が更新されました。",
|
||||
"xpack.triggersActionsUI.sections.ruleApi.bulkEditResponse.success": "{total, plural, other {# ルール}}の{property}が更新されました。",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.refineSearchPrompt.prompt": "これらは検索条件に一致した初めの{visibleDocumentSize}件のドキュメントです。他の結果を表示するには検索条件を絞ってください。",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.rule.statusPanel.totalExecutions": "過去24時間で{executions, plural, other {# 件の実行}}",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.ruleActionErrorLogFlyout.actionErrorsPlural": "{value, plural, other {個のエラーが発生したアクション}}",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.ruleDetailsTitle": "{ruleName}",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.ruleEventLogDataGrid.erroredActionsCellPopover": "{value, plural, other {個のエラーが発生したアクション}}",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.ruleEventLogDataGrid.erroredActionsTooltip": "{value, plural, other {# 個のエラーが発生したアクション}}",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.ruleEventLogPaginationStatus.paginationResults": "{total, number} {type}件中{range}を表示しています",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.ruleEventLogPaginationStatus.paginationResultsRange": "{start, number} - {end, number}",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.ruleEventLogPaginationStatus.paginationResultsType": "ログ{total, plural, other {エントリ}}",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.ruleExecutionSummaryAndChart.loadSummaryError": "ルール概要を読み込めません:{message}",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.unableToLoadRuleMessage": "ルールを読み込めません:{message}",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.unableToLoadRulesMessage": "ルールを読み込めません:{message}",
|
||||
|
@ -35150,37 +35144,26 @@
|
|||
"xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.activeAlerts": "アクティブアラート",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.apiError": "実行履歴を取得できませんでした",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.duration": "期間",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.erroredActions": "エラーのアクション",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.erroredActionsToolTip": "失敗したアクションの数。",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.esSearchDuration": "ES検索期間",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.id": "Id",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.message": "メッセージ",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.newAlerts": "新しいアラート",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.openActionErrorsFlyout": "アクションエラーフライアウトを開く",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.recoveredAlerts": "回復されたアラート",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.response": "応答",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.ruleId": "ルールID",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.ruleName": "ルール",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.scheduledActions": "生成されたアクション",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.scheduledActionsToolTip": "ルールの実行時に生成されたアクションの合計数。",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.scheduleDelay": "遅延をスケジュール",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.searchPlaceholder": "検索イベントログメッセージ",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.showAll": "すべて表示",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.showOnlyFailures": "失敗のみを表示",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.spaceIds": "スペース",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.succeededActions": "成功したアクション",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.succeededActionsToolTip": "正常に完了したアクションの数。",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.timedOut": "タイムアウトしました",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.timestamp": "タイムスタンプ",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.totalSearchDuration": "合計検索期間",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.triggeredActions": "トリガーされたアクション",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.triggeredActionsToolTip": "実行される生成済みアクションのサブセット。",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.viewActionErrors": "アクションエラーを表示",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.eventLogStatusFilterLabel": "応答",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.manageLicensePlanBannerLinkTitle": "ライセンスの管理",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.popoverButtonTitle": "アクション",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.redirectObjectNoun": "ルール",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.refineSearchPrompt.backToTop": "最上部へ戻る。",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.rule.alertsTabText": "アラート",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.rule.eventLogTabText": "履歴",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.rule.ruleSummary.recoveredLabel": "回復済み",
|
||||
|
@ -35192,7 +35175,6 @@
|
|||
"xpack.triggersActionsUI.sections.ruleDetails.ruleEventLogListKpi.alertsTooltip": "最大10,000件の直近のルール実行のアラートステータス。",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.ruleEventLogListKpi.apiError": "イベントログKPIを取得できませんでした。",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.ruleEventLogListKpi.responseTooltip": "最大10,000件の直近のルール実行の応答。",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.ruleEventLogPaginationStatus.paginationResultsRangeNoResult": "0",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.rulesList.ruleLastExecutionDescription": "前回の応答",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.rulesList.status.active": "アクティブ",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.rulesList.status.inactive": "回復済み",
|
||||
|
|
|
@ -34863,15 +34863,9 @@
|
|||
"xpack.triggersActionsUI.sections.ruleApi.bulkEditResponse.failure": "无法更新 {failure, plural, other {# 个规则}}的 {property}。",
|
||||
"xpack.triggersActionsUI.sections.ruleApi.bulkEditResponse.someSuccess": "已更新 {success, plural, other {# 规则}}的 {property},{failure, plural, other {# 个规则}}遇到错误。",
|
||||
"xpack.triggersActionsUI.sections.ruleApi.bulkEditResponse.success": "已更新 {total, plural, other {# 个规则}}的 {property}。",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.refineSearchPrompt.prompt": "下面是与您的搜索匹配的前 {visibleDocumentSize} 个文档,请优化您的搜索以查看其他文档。",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.rule.statusPanel.totalExecutions": "过去 24 小时中的 {executions, plural, other {# 次执行}}",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.ruleActionErrorLogFlyout.actionErrorsPlural": "{value, plural, other {个错误操作}}",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.ruleDetailsTitle": "{ruleName}",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.ruleEventLogDataGrid.erroredActionsCellPopover": "{value, plural, other {个错误操作}}",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.ruleEventLogDataGrid.erroredActionsTooltip": "{value, plural, other {# 个错误操作}}",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.ruleEventLogPaginationStatus.paginationResults": "正在显示第 {range} 个(共 {total, number} 个){type}",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.ruleEventLogPaginationStatus.paginationResultsRange": "{start, number} - {end, number}",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.ruleEventLogPaginationStatus.paginationResultsType": "日志{total, plural, other {条目}}",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.ruleExecutionSummaryAndChart.loadSummaryError": "无法加载规则摘要:{message}",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.unableToLoadRuleMessage": "无法加载规则:{message}",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.unableToLoadRulesMessage": "无法加载规则:{message}",
|
||||
|
@ -35186,37 +35180,26 @@
|
|||
"xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.activeAlerts": "活动告警",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.apiError": "无法提取执行历史记录",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.duration": "持续时间",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.erroredActions": "错误操作",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.erroredActionsToolTip": "失败的操作数。",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.esSearchDuration": "ES 搜索持续时间",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.id": "ID",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.message": "消息",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.newAlerts": "新告警",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.openActionErrorsFlyout": "打开操作错误浮出控件",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.recoveredAlerts": "已恢复告警",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.response": "响应",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.ruleId": "规则 ID",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.ruleName": "规则",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.scheduledActions": "生成的操作",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.scheduledActionsToolTip": "运行规则时生成的总操作数。",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.scheduleDelay": "计划延迟",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.searchPlaceholder": "搜索事件日志消息",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.showAll": "全部显示",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.showOnlyFailures": "仅显示失败次数",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.spaceIds": "工作区",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.succeededActions": "成功的操作",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.succeededActionsToolTip": "已成功完成的操作数。",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.timedOut": "已超时",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.timestamp": "时间戳",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.totalSearchDuration": "搜索持续时间总计",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.triggeredActions": "已触发操作",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.triggeredActionsToolTip": "将运行的所生成操作的子集。",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.viewActionErrors": "查看操作错误",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.eventLogStatusFilterLabel": "响应",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.manageLicensePlanBannerLinkTitle": "管理许可证",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.popoverButtonTitle": "操作",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.redirectObjectNoun": "规则",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.refineSearchPrompt.backToTop": "返回顶部。",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.rule.alertsTabText": "告警",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.rule.eventLogTabText": "历史记录",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.rule.ruleSummary.recoveredLabel": "已恢复",
|
||||
|
@ -35228,7 +35211,6 @@
|
|||
"xpack.triggersActionsUI.sections.ruleDetails.ruleEventLogListKpi.alertsTooltip": "多达 10,000 次最近规则运行的告警状态。",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.ruleEventLogListKpi.apiError": "无法提取事件日志 KPI。",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.ruleEventLogListKpi.responseTooltip": "多达 10,000 次最近规则运行的响应。",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.ruleEventLogPaginationStatus.paginationResultsRangeNoResult": "0",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.rulesList.ruleLastExecutionDescription": "上次响应",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.rulesList.status.active": "活动",
|
||||
"xpack.triggersActionsUI.sections.ruleDetails.rulesList.status.inactive": "已恢复",
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
*/
|
||||
|
||||
import React, { lazy } from 'react';
|
||||
import { Switch, Route, Router } from 'react-router-dom';
|
||||
import { Switch, Route, Router, Redirect } from 'react-router-dom';
|
||||
import { ChromeBreadcrumb, CoreStart, CoreTheme, ScopedHistory } from '@kbn/core/public';
|
||||
import { render, unmountComponentAtNode } from 'react-dom';
|
||||
import { I18nProvider } from '@kbn/i18n-react';
|
||||
|
@ -35,9 +35,10 @@ import {
|
|||
import { setDataViewsService } from '../common/lib/data_apis';
|
||||
import { KibanaContextProvider, useKibana } from '../common/lib/kibana';
|
||||
import { ConnectorProvider } from './context/connector_context';
|
||||
import { Section } from './constants';
|
||||
|
||||
const ActionsConnectorsList = lazy(
|
||||
() => import('./sections/actions_connectors_list/components/actions_connectors_list')
|
||||
const ActionsConnectorsHome = lazy(
|
||||
() => import('./sections/actions_connectors_list/components/actions_connectors_home')
|
||||
);
|
||||
|
||||
export interface TriggersAndActionsUiServices extends CoreStart {
|
||||
|
@ -72,6 +73,8 @@ export const renderApp = (deps: TriggersAndActionsUiServices) => {
|
|||
export const App = ({ deps }: { deps: TriggersAndActionsUiServices }) => {
|
||||
const { dataViews, uiSettings, theme$ } = deps;
|
||||
const isDarkMode = useObservable<boolean>(uiSettings.get$('theme:darkMode'));
|
||||
const sections: Section[] = ['connectors', 'logs'];
|
||||
const sectionsRegex = sections.join('|');
|
||||
|
||||
setDataViewsService(dataViews);
|
||||
return (
|
||||
|
@ -80,7 +83,7 @@ export const App = ({ deps }: { deps: TriggersAndActionsUiServices }) => {
|
|||
<KibanaThemeProvider theme$={theme$}>
|
||||
<KibanaContextProvider services={{ ...deps }}>
|
||||
<Router history={deps.history}>
|
||||
<AppWithoutRouter />
|
||||
<AppWithoutRouter sectionsRegex={sectionsRegex} />
|
||||
</Router>
|
||||
</KibanaContextProvider>
|
||||
</KibanaThemeProvider>
|
||||
|
@ -89,7 +92,7 @@ export const App = ({ deps }: { deps: TriggersAndActionsUiServices }) => {
|
|||
);
|
||||
};
|
||||
|
||||
export const AppWithoutRouter = () => {
|
||||
export const AppWithoutRouter = ({ sectionsRegex }: { sectionsRegex: string }) => {
|
||||
const {
|
||||
actions: { validateEmailAddresses },
|
||||
} = useKibana().services;
|
||||
|
@ -97,7 +100,12 @@ export const AppWithoutRouter = () => {
|
|||
return (
|
||||
<ConnectorProvider value={{ services: { validateEmailAddresses } }}>
|
||||
<Switch>
|
||||
<Route path={'/'} component={suspendedComponentWithProps(ActionsConnectorsList, 'xl')} />
|
||||
<Route
|
||||
path={`/:section(${sectionsRegex})`}
|
||||
component={suspendedComponentWithProps(ActionsConnectorsHome, 'xl')}
|
||||
/>
|
||||
|
||||
<Redirect from={'/'} to="connectors" />
|
||||
</Switch>
|
||||
</ConnectorProvider>
|
||||
);
|
||||
|
|
|
@ -88,3 +88,22 @@ export const LOCKED_COLUMNS = [
|
|||
export const RULE_EXECUTION_DEFAULT_INITIAL_VISIBLE_COLUMNS = [...LOCKED_COLUMNS.slice(1)];
|
||||
export const GLOBAL_EXECUTION_DEFAULT_INITIAL_VISIBLE_COLUMNS = ['rule_name', ...LOCKED_COLUMNS];
|
||||
export const DEFAULT_NUMBER_FORMAT = 'format:number:defaultPattern';
|
||||
|
||||
export const CONNECTOR_EXECUTION_LOG_COLUMN_IDS = [
|
||||
'connector_id',
|
||||
'space_ids',
|
||||
'id',
|
||||
'timestamp',
|
||||
'status',
|
||||
'connector_name',
|
||||
'message',
|
||||
'execution_duration',
|
||||
'schedule_delay',
|
||||
'timed_out',
|
||||
] as const;
|
||||
|
||||
export const CONNECTOR_LOCKED_COLUMNS = ['timestamp', 'status', 'connector_name', 'message'];
|
||||
|
||||
export const GLOBAL_CONNECTOR_EXECUTION_DEFAULT_INITIAL_VISIBLE_COLUMNS = [
|
||||
...CONNECTOR_LOCKED_COLUMNS,
|
||||
];
|
||||
|
|
|
@ -0,0 +1,60 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { useSpacesData } from '../../common/lib/kibana';
|
||||
|
||||
interface UseMultipleSpacesProps {
|
||||
setShowFromAllSpaces: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
showFromAllSpaces: boolean;
|
||||
visibleColumns: string[];
|
||||
setVisibleColumns: React.Dispatch<React.SetStateAction<string[]>>;
|
||||
}
|
||||
|
||||
export const useMultipleSpaces = (props: UseMultipleSpacesProps) => {
|
||||
const { setShowFromAllSpaces, showFromAllSpaces, visibleColumns, setVisibleColumns } = props;
|
||||
|
||||
const spacesData = useSpacesData();
|
||||
|
||||
const onShowAllSpacesChange = useCallback(() => {
|
||||
setShowFromAllSpaces((prev) => !prev);
|
||||
const nextShowFromAllSpaces = !showFromAllSpaces;
|
||||
|
||||
if (nextShowFromAllSpaces && !visibleColumns.includes('space_ids')) {
|
||||
const connectorNameIndex = visibleColumns.findIndex((c) => c === 'connector_name');
|
||||
const newVisibleColumns = [...visibleColumns];
|
||||
newVisibleColumns.splice(connectorNameIndex + 1, 0, 'space_ids');
|
||||
setVisibleColumns(newVisibleColumns);
|
||||
} else if (!nextShowFromAllSpaces && visibleColumns.includes('space_ids')) {
|
||||
setVisibleColumns(visibleColumns.filter((c) => c !== 'space_ids'));
|
||||
}
|
||||
}, [setShowFromAllSpaces, showFromAllSpaces, visibleColumns, setVisibleColumns]);
|
||||
|
||||
const accessibleSpaceIds = useMemo(
|
||||
() => (spacesData ? [...spacesData.spacesMap.values()].map((e) => e.id) : []),
|
||||
[spacesData]
|
||||
);
|
||||
const canAccessMultipleSpaces = useMemo(
|
||||
() => accessibleSpaceIds.length > 1,
|
||||
[accessibleSpaceIds]
|
||||
);
|
||||
const namespaces = useMemo(
|
||||
() => (showFromAllSpaces && spacesData ? accessibleSpaceIds : undefined),
|
||||
[showFromAllSpaces, spacesData, accessibleSpaceIds]
|
||||
);
|
||||
const activeSpace = useMemo(
|
||||
() => spacesData?.spacesMap.get(spacesData?.activeSpaceId),
|
||||
[spacesData]
|
||||
);
|
||||
|
||||
return {
|
||||
onShowAllSpacesChange,
|
||||
canAccessMultipleSpaces,
|
||||
namespaces,
|
||||
activeSpace,
|
||||
};
|
||||
};
|
|
@ -11,3 +11,7 @@ export { createActionConnector } from './create';
|
|||
export { deleteActions } from './delete';
|
||||
export { executeAction } from './execute';
|
||||
export { updateActionConnector } from './update';
|
||||
export type { LoadGlobalConnectorExecutionLogAggregationsProps } from './load_execution_log_aggregations';
|
||||
export { loadGlobalConnectorExecutionLogAggregations } from './load_execution_log_aggregations';
|
||||
export type { LoadGlobalConnectorExecutionKPIAggregationsProps } from './load_execution_kpi_aggregations';
|
||||
export { loadGlobalConnectorExecutionKPIAggregations } from './load_execution_kpi_aggregations';
|
||||
|
|
|
@ -0,0 +1,45 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { httpServiceMock } from '@kbn/core/public/mocks';
|
||||
import { loadGlobalConnectorExecutionKPIAggregations } from './load_execution_kpi_aggregations';
|
||||
|
||||
const http = httpServiceMock.createStartContract();
|
||||
|
||||
const mockResponse = {
|
||||
failure: 0,
|
||||
success: 1,
|
||||
unknown: 0,
|
||||
warning: 0,
|
||||
};
|
||||
|
||||
describe('loadGlobalConnectorExecutionKPIAggregations', () => {
|
||||
test('should call load execution kpi aggregation API', async () => {
|
||||
http.post.mockResolvedValueOnce(mockResponse);
|
||||
|
||||
const result = await loadGlobalConnectorExecutionKPIAggregations({
|
||||
dateStart: '2022-03-23T16:17:53.482Z',
|
||||
dateEnd: '2022-03-23T16:17:53.482Z',
|
||||
outcomeFilter: ['success', 'warning'],
|
||||
message: 'test-message',
|
||||
http,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
...mockResponse,
|
||||
});
|
||||
|
||||
expect(http.post.mock.calls[0]).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
"/internal/actions/_global_connector_execution_kpi",
|
||||
Object {
|
||||
"body": "{\\"filter\\":\\"(message: \\\\\\"test-message\\\\\\" OR error.message: \\\\\\"test-message\\\\\\") and (kibana.alerting.outcome:success OR (event.outcome: success AND NOT kibana.alerting.outcome:*) OR kibana.alerting.outcome: warning)\\",\\"date_start\\":\\"2022-03-23T16:17:53.482Z\\",\\"date_end\\":\\"2022-03-23T16:17:53.482Z\\"}",
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
});
|
|
@ -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
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { HttpSetup } from '@kbn/core/public';
|
||||
import { IExecutionKPIResult } from '@kbn/actions-plugin/common';
|
||||
import { INTERNAL_BASE_ACTION_API_PATH } from '../../constants';
|
||||
import { getFilter } from '../rule_api';
|
||||
|
||||
export interface LoadGlobalConnectorExecutionKPIAggregationsProps {
|
||||
outcomeFilter?: string[];
|
||||
message?: string;
|
||||
dateStart: string;
|
||||
dateEnd?: string;
|
||||
namespaces?: Array<string | undefined>;
|
||||
}
|
||||
|
||||
export const loadGlobalConnectorExecutionKPIAggregations = ({
|
||||
http,
|
||||
outcomeFilter,
|
||||
message,
|
||||
dateStart,
|
||||
dateEnd,
|
||||
namespaces,
|
||||
}: LoadGlobalConnectorExecutionKPIAggregationsProps & { http: HttpSetup }) => {
|
||||
const filter = getFilter({ outcomeFilter, message });
|
||||
|
||||
return http.post<IExecutionKPIResult>(
|
||||
`${INTERNAL_BASE_ACTION_API_PATH}/_global_connector_execution_kpi`,
|
||||
{
|
||||
body: JSON.stringify({
|
||||
filter: filter.length ? filter.join(' and ') : undefined,
|
||||
date_start: dateStart,
|
||||
date_end: dateEnd,
|
||||
namespaces: namespaces ? JSON.stringify(namespaces) : namespaces,
|
||||
}),
|
||||
}
|
||||
);
|
||||
};
|
|
@ -0,0 +1,81 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { httpServiceMock } from '@kbn/core/public/mocks';
|
||||
import {
|
||||
loadGlobalConnectorExecutionLogAggregations,
|
||||
SortField,
|
||||
} from './load_execution_log_aggregations';
|
||||
|
||||
const http = httpServiceMock.createStartContract();
|
||||
|
||||
const mockResponse = {
|
||||
data: [
|
||||
{
|
||||
connector_name: 'test connector',
|
||||
duration_ms: 1,
|
||||
id: '8b3af07e-7593-4c40-b704-9c06d3b06e58',
|
||||
message: 'action executed: .server-log:6709f660-8d11-11ed-bae5-bd32cbc9eaaa: test connector',
|
||||
schedule_delay_ms: 2783,
|
||||
space_ids: ['default'],
|
||||
status: 'success',
|
||||
timestamp: '2023-01-05T15:55:50.495Z',
|
||||
version: '8.7.0',
|
||||
},
|
||||
],
|
||||
total: 1,
|
||||
};
|
||||
|
||||
describe('loadGlobalConnectorExecutionLogAggregations', () => {
|
||||
test('should call load execution log aggregation API', async () => {
|
||||
http.post.mockResolvedValueOnce(mockResponse);
|
||||
|
||||
const sortTimestamp = {
|
||||
timestamp: {
|
||||
order: 'asc',
|
||||
},
|
||||
} as SortField;
|
||||
|
||||
const result = await loadGlobalConnectorExecutionLogAggregations({
|
||||
dateStart: '2022-03-23T16:17:53.482Z',
|
||||
dateEnd: '2022-03-23T16:17:53.482Z',
|
||||
outcomeFilter: ['success', 'warning'],
|
||||
message: 'test-message',
|
||||
perPage: 10,
|
||||
page: 0,
|
||||
sort: [sortTimestamp],
|
||||
http,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
...mockResponse,
|
||||
data: [
|
||||
{
|
||||
connector_name: 'test connector',
|
||||
execution_duration: 1,
|
||||
id: '8b3af07e-7593-4c40-b704-9c06d3b06e58',
|
||||
message:
|
||||
'action executed: .server-log:6709f660-8d11-11ed-bae5-bd32cbc9eaaa: test connector',
|
||||
schedule_delay: 2783,
|
||||
space_ids: ['default'],
|
||||
status: 'success',
|
||||
timestamp: '2023-01-05T15:55:50.495Z',
|
||||
version: '8.7.0',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(http.post.mock.calls[0]).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
"/internal/actions/_global_connector_execution_logs",
|
||||
Object {
|
||||
"body": "{\\"date_start\\":\\"2022-03-23T16:17:53.482Z\\",\\"date_end\\":\\"2022-03-23T16:17:53.482Z\\",\\"filter\\":\\"(message: \\\\\\"test-message\\\\\\" OR error.message: \\\\\\"test-message\\\\\\") and (kibana.alerting.outcome:success OR (event.outcome: success AND NOT kibana.alerting.outcome:*) OR kibana.alerting.outcome: warning)\\",\\"per_page\\":10,\\"page\\":1,\\"sort\\":\\"[{\\\\\\"timestamp\\\\\\":{\\\\\\"order\\\\\\":\\\\\\"asc\\\\\\"}}]\\"}",
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,87 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
/* eslint-disable @typescript-eslint/naming-convention */
|
||||
|
||||
import { HttpSetup } from '@kbn/core/public';
|
||||
import type { SortOrder } from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
|
||||
import {
|
||||
IExecutionLog,
|
||||
ExecutionLogSortFields,
|
||||
IExecutionLogResult,
|
||||
AsApiContract,
|
||||
INTERNAL_BASE_ACTION_API_PATH,
|
||||
RewriteRequestCase,
|
||||
} from '@kbn/actions-plugin/common';
|
||||
import { getFilter } from '../rule_api';
|
||||
|
||||
const getRenamedLog = (data: IExecutionLog) => {
|
||||
const { duration_ms, schedule_delay_ms, ...rest } = data;
|
||||
|
||||
return {
|
||||
execution_duration: data.duration_ms,
|
||||
schedule_delay: data.schedule_delay_ms,
|
||||
...rest,
|
||||
};
|
||||
};
|
||||
|
||||
const rewriteBodyRes: RewriteRequestCase<IExecutionLogResult> = ({ data, ...rest }: any) => ({
|
||||
data: data.map((log: IExecutionLog) => getRenamedLog(log)),
|
||||
...rest,
|
||||
});
|
||||
|
||||
export type SortField = Record<
|
||||
ExecutionLogSortFields,
|
||||
{
|
||||
order: SortOrder;
|
||||
}
|
||||
>;
|
||||
|
||||
export interface LoadGlobalConnectorExecutionLogAggregationsProps {
|
||||
dateStart: string;
|
||||
dateEnd?: string;
|
||||
outcomeFilter?: string[];
|
||||
message?: string;
|
||||
perPage?: number;
|
||||
page?: number;
|
||||
sort?: SortField[];
|
||||
namespaces?: Array<string | undefined>;
|
||||
}
|
||||
|
||||
export const loadGlobalConnectorExecutionLogAggregations = async ({
|
||||
http,
|
||||
dateStart,
|
||||
dateEnd,
|
||||
outcomeFilter,
|
||||
message,
|
||||
perPage = 10,
|
||||
page = 0,
|
||||
sort = [],
|
||||
namespaces,
|
||||
}: LoadGlobalConnectorExecutionLogAggregationsProps & { http: HttpSetup }) => {
|
||||
const sortField: any[] = sort;
|
||||
const filter = getFilter({ outcomeFilter, message });
|
||||
|
||||
const result = await http.post<AsApiContract<IExecutionLogResult>>(
|
||||
`${INTERNAL_BASE_ACTION_API_PATH}/_global_connector_execution_logs`,
|
||||
{
|
||||
body: JSON.stringify({
|
||||
date_start: dateStart,
|
||||
date_end: dateEnd,
|
||||
filter: filter.length ? filter.join(' and ') : undefined,
|
||||
per_page: perPage,
|
||||
// Need to add the + 1 for pages because APIs are 1 indexed,
|
||||
// whereas data grid sorts are 0 indexed.
|
||||
page: page + 1,
|
||||
sort: sortField.length ? JSON.stringify(sortField) : undefined,
|
||||
namespaces: namespaces ? JSON.stringify(namespaces) : undefined,
|
||||
}),
|
||||
}
|
||||
);
|
||||
|
||||
return rewriteBodyRes(result);
|
||||
};
|
|
@ -46,5 +46,6 @@ export { runSoon } from './run_soon';
|
|||
export { bulkDeleteRules } from './bulk_delete';
|
||||
export { bulkEnableRules } from './bulk_enable';
|
||||
export { bulkDisableRules } from './bulk_disable';
|
||||
export { getFilter } from './get_filter';
|
||||
export { getFlappingSettings } from './get_flapping_settings';
|
||||
export { updateFlappingSettings } from './update_flapping_settings';
|
||||
|
|
|
@ -0,0 +1,133 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { mountWithIntl, nextTick } from '@kbn/test-jest-helpers';
|
||||
import { loadGlobalConnectorExecutionKPIAggregations } from '../../../lib/action_connector_api/load_execution_kpi_aggregations';
|
||||
import { ConnectorEventLogListKPI } from './actions_connectors_event_log_list_kpi';
|
||||
import { getIsExperimentalFeatureEnabled } from '../../../../common/get_experimental_features';
|
||||
|
||||
jest.mock('../../../../common/lib/kibana', () => ({
|
||||
useKibana: jest.fn().mockReturnValue({
|
||||
services: {
|
||||
notifications: { toast: { addDanger: jest.fn() } },
|
||||
},
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('../../../lib/action_connector_api/load_execution_kpi_aggregations', () => ({
|
||||
loadGlobalConnectorExecutionKPIAggregations: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('../../../../common/get_experimental_features', () => ({
|
||||
getIsExperimentalFeatureEnabled: jest.fn(),
|
||||
}));
|
||||
|
||||
const mockKpiResponse = {
|
||||
success: 4,
|
||||
unknown: 0,
|
||||
failure: 60,
|
||||
warning: 10,
|
||||
};
|
||||
|
||||
const loadGlobalExecutionKPIAggregationsMock =
|
||||
loadGlobalConnectorExecutionKPIAggregations as unknown as jest.MockedFunction<any>;
|
||||
|
||||
describe('actions_connectors_event_log_list_kpi', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
(getIsExperimentalFeatureEnabled as jest.Mock<any, any>).mockImplementation(() => false);
|
||||
loadGlobalExecutionKPIAggregationsMock.mockResolvedValue(mockKpiResponse);
|
||||
});
|
||||
|
||||
it('renders correctly', async () => {
|
||||
const wrapper = mountWithIntl(
|
||||
<ConnectorEventLogListKPI
|
||||
dateStart="now-24h"
|
||||
dateEnd="now"
|
||||
loadGlobalConnectorExecutionKPIAggregations={loadGlobalExecutionKPIAggregationsMock}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(
|
||||
wrapper
|
||||
.find('[data-test-subj="connectorEventLogKpi-successOutcome"] .euiStat__title')
|
||||
.first()
|
||||
.text()
|
||||
).toEqual('--');
|
||||
expect(
|
||||
wrapper
|
||||
.find('[data-test-subj="connectorEventLogKpi-warningOutcome"] .euiStat__title')
|
||||
.first()
|
||||
.text()
|
||||
).toEqual('--');
|
||||
expect(
|
||||
wrapper
|
||||
.find('[data-test-subj="connectorEventLogKpi-failureOutcome"] .euiStat__title')
|
||||
.first()
|
||||
.text()
|
||||
).toEqual('--');
|
||||
|
||||
// Let the load resolve
|
||||
await act(async () => {
|
||||
await nextTick();
|
||||
wrapper.update();
|
||||
});
|
||||
|
||||
expect(loadGlobalConnectorExecutionKPIAggregations).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
message: undefined,
|
||||
outcomeFilter: undefined,
|
||||
})
|
||||
);
|
||||
|
||||
expect(
|
||||
wrapper
|
||||
.find('[data-test-subj="connectorEventLogKpi-successOutcome"] .euiStat__title')
|
||||
.first()
|
||||
.text()
|
||||
).toEqual(`${mockKpiResponse.success}`);
|
||||
expect(
|
||||
wrapper
|
||||
.find('[data-test-subj="connectorEventLogKpi-warningOutcome"] .euiStat__title')
|
||||
.first()
|
||||
.text()
|
||||
).toEqual(`${mockKpiResponse.warning}`);
|
||||
expect(
|
||||
wrapper
|
||||
.find('[data-test-subj="connectorEventLogKpi-failureOutcome"] .euiStat__title')
|
||||
.first()
|
||||
.text()
|
||||
).toEqual(`${mockKpiResponse.failure}`);
|
||||
});
|
||||
|
||||
it('calls KPI API with filters', async () => {
|
||||
const wrapper = mountWithIntl(
|
||||
<ConnectorEventLogListKPI
|
||||
dateStart="now-24h"
|
||||
dateEnd="now"
|
||||
message="test"
|
||||
outcomeFilter={['status: 123', 'test:456']}
|
||||
loadGlobalConnectorExecutionKPIAggregations={loadGlobalExecutionKPIAggregationsMock}
|
||||
/>
|
||||
);
|
||||
|
||||
// Let the load resolve
|
||||
await act(async () => {
|
||||
await nextTick();
|
||||
wrapper.update();
|
||||
});
|
||||
|
||||
expect(loadGlobalExecutionKPIAggregationsMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
message: 'test',
|
||||
outcomeFilter: ['status: 123', 'test:456'],
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,187 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useEffect, useState, useMemo, useRef } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import datemath from '@kbn/datemath';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiStat, EuiSpacer } from '@elastic/eui';
|
||||
import { IExecutionKPIResult } from '@kbn/actions-plugin/common';
|
||||
import {
|
||||
ComponentOpts as ConnectorApis,
|
||||
withActionOperations,
|
||||
} from '../../common/components/with_actions_api_operations';
|
||||
import { getIsExperimentalFeatureEnabled } from '../../../../common/get_experimental_features';
|
||||
import { useKibana } from '../../../../common/lib/kibana';
|
||||
import { EventLogListStatus, EventLogStat } from '../../common/components/event_log';
|
||||
|
||||
const getParsedDate = (date: string) => {
|
||||
if (date.includes('now')) {
|
||||
return datemath.parse(date)?.format() || date;
|
||||
}
|
||||
return date;
|
||||
};
|
||||
|
||||
const API_FAILED_MESSAGE = i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.connectorEventLogListKpi.apiError',
|
||||
{
|
||||
defaultMessage: 'Failed to fetch event log KPI.',
|
||||
}
|
||||
);
|
||||
|
||||
const RESPONSE_TOOLTIP = i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.connectorEventLogListKpi.responseTooltip',
|
||||
{
|
||||
defaultMessage: 'The responses for up to 10,000 most recent actions triggered.',
|
||||
}
|
||||
);
|
||||
|
||||
export type ConnectorEventLogListKPIProps = {
|
||||
dateStart: string;
|
||||
dateEnd: string;
|
||||
outcomeFilter?: string[];
|
||||
message?: string;
|
||||
refreshToken?: number;
|
||||
namespaces?: Array<string | undefined>;
|
||||
} & Pick<ConnectorApis, 'loadGlobalConnectorExecutionKPIAggregations'>;
|
||||
|
||||
export const ConnectorEventLogListKPI = (props: ConnectorEventLogListKPIProps) => {
|
||||
const {
|
||||
dateStart,
|
||||
dateEnd,
|
||||
outcomeFilter,
|
||||
message,
|
||||
refreshToken,
|
||||
namespaces,
|
||||
loadGlobalConnectorExecutionKPIAggregations,
|
||||
} = props;
|
||||
const {
|
||||
notifications: { toasts },
|
||||
} = useKibana().services;
|
||||
|
||||
const isInitialized = useRef(false);
|
||||
const isUsingExecutionStatus = getIsExperimentalFeatureEnabled('ruleUseExecutionStatus');
|
||||
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||
const [kpi, setKpi] = useState<IExecutionKPIResult>();
|
||||
|
||||
const loadKPIFn = useMemo(() => {
|
||||
return loadGlobalConnectorExecutionKPIAggregations;
|
||||
}, [loadGlobalConnectorExecutionKPIAggregations]);
|
||||
|
||||
const loadKPIs = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const newKpi = await loadKPIFn({
|
||||
dateStart: getParsedDate(dateStart),
|
||||
dateEnd: getParsedDate(dateEnd),
|
||||
outcomeFilter,
|
||||
message,
|
||||
...(namespaces ? { namespaces } : {}),
|
||||
});
|
||||
setKpi(newKpi);
|
||||
} catch (e) {
|
||||
toasts.addDanger({
|
||||
title: API_FAILED_MESSAGE,
|
||||
text: e.body?.message ?? e,
|
||||
});
|
||||
}
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
loadKPIs();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [dateStart, dateEnd, outcomeFilter, message, namespaces]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isInitialized.current) {
|
||||
loadKPIs();
|
||||
}
|
||||
isInitialized.current = true;
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [refreshToken]);
|
||||
|
||||
const isLoadingData = useMemo(() => isLoading || !kpi, [isLoading, kpi]);
|
||||
|
||||
const getStatDescription = (element: React.ReactNode) => {
|
||||
return (
|
||||
<>
|
||||
{element}
|
||||
<EuiSpacer size="s" />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem grow={4}>
|
||||
<EventLogStat title="Responses" tooltip={RESPONSE_TOOLTIP}>
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<EuiStat
|
||||
data-test-subj="connectorEventLogKpi-successOutcome"
|
||||
description={getStatDescription(
|
||||
<EventLogListStatus
|
||||
status="success"
|
||||
useExecutionStatus={isUsingExecutionStatus}
|
||||
/>
|
||||
)}
|
||||
titleSize="s"
|
||||
title={kpi?.success ?? 0}
|
||||
isLoading={isLoadingData}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiStat
|
||||
data-test-subj="connectorEventLogKpi-warningOutcome"
|
||||
description={getStatDescription(
|
||||
<EventLogListStatus
|
||||
status="warning"
|
||||
useExecutionStatus={isUsingExecutionStatus}
|
||||
/>
|
||||
)}
|
||||
titleSize="s"
|
||||
title={kpi?.warning ?? 0}
|
||||
isLoading={isLoadingData}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiStat
|
||||
data-test-subj="connectorEventLogKpi-failureOutcome"
|
||||
description={getStatDescription(
|
||||
<EventLogListStatus
|
||||
status="failure"
|
||||
useExecutionStatus={isUsingExecutionStatus}
|
||||
/>
|
||||
)}
|
||||
titleSize="s"
|
||||
title={kpi?.failure ?? 0}
|
||||
isLoading={isLoadingData}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiStat
|
||||
data-test-subj="connectorEventLogKpi-unknownOutcome"
|
||||
description={getStatDescription(
|
||||
<EventLogListStatus
|
||||
status="unknown"
|
||||
useExecutionStatus={isUsingExecutionStatus}
|
||||
/>
|
||||
)}
|
||||
titleSize="s"
|
||||
title={kpi?.unknown ?? 0}
|
||||
isLoading={isLoadingData}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EventLogStat>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
};
|
||||
|
||||
export const ConnectorEventLogListKPIWithApi = withActionOperations(ConnectorEventLogListKPI);
|
|
@ -0,0 +1,119 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { mountWithIntl, nextTick } from '@kbn/test-jest-helpers';
|
||||
import { loadGlobalConnectorExecutionLogAggregations } from '../../../lib/action_connector_api/load_execution_log_aggregations';
|
||||
import { ConnectorEventLogListTable } from './actions_connectors_event_log_list_table';
|
||||
import { getIsExperimentalFeatureEnabled } from '../../../../common/get_experimental_features';
|
||||
|
||||
jest.mock('../../../../common/lib/kibana', () => ({
|
||||
...jest.requireActual('../../../../common/lib/kibana'),
|
||||
useKibana: jest.fn().mockReturnValue({
|
||||
services: {
|
||||
notifications: { toasts: { addDanger: jest.fn() } },
|
||||
},
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('../../../../common/get_experimental_features', () => ({
|
||||
getIsExperimentalFeatureEnabled: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('../../../lib/action_connector_api/load_execution_log_aggregations', () => ({
|
||||
loadGlobalConnectorExecutionLogAggregations: jest.fn(),
|
||||
}));
|
||||
|
||||
const mockResponse = {
|
||||
total: 1,
|
||||
data: [
|
||||
{
|
||||
connector_id: '86020b10-9b3b-11ed-8422-2f5a388a317d',
|
||||
connector_name: 'test connector',
|
||||
duration_ms: 0,
|
||||
id: '3895f21e-8de8-416f-9ab2-6ca5f8a4b294',
|
||||
message: 'action executed: .server-log:86020b10-9b3b-11ed-8422-2f5a388a317d: test',
|
||||
schedule_delay_ms: 2923,
|
||||
space_ids: ['default'],
|
||||
status: 'success',
|
||||
timed_out: false,
|
||||
timestamp: '2023-01-23T16:41:01.260Z',
|
||||
version: '8.7.0',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const loadGlobalExecutionLogAggregationsMock =
|
||||
loadGlobalConnectorExecutionLogAggregations as unknown as jest.MockedFunction<any>;
|
||||
|
||||
describe('actions_connectors_event_log_list_table', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
(getIsExperimentalFeatureEnabled as jest.Mock<any, any>).mockImplementation(() => false);
|
||||
loadGlobalExecutionLogAggregationsMock.mockResolvedValue(mockResponse);
|
||||
});
|
||||
|
||||
it('renders correctly', async () => {
|
||||
const wrapper = mountWithIntl(
|
||||
<ConnectorEventLogListTable
|
||||
refreshToken={0}
|
||||
initialPageSize={50}
|
||||
hasConnectorNames={true}
|
||||
hasAllSpaceSwitch={true}
|
||||
loadGlobalConnectorExecutionLogAggregations={loadGlobalExecutionLogAggregationsMock}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(wrapper.find('[data-test-subj="connectorEventLogListProgressBar"]')).toBeTruthy();
|
||||
expect(wrapper.find('[data-test-subj="connectorEventLogListDatePicker"]')).toBeTruthy();
|
||||
expect(wrapper.find('[data-test-subj="connectorEventLogListKpi"]')).toBeTruthy();
|
||||
|
||||
// Let the load resolve
|
||||
await act(async () => {
|
||||
await nextTick();
|
||||
wrapper.update();
|
||||
});
|
||||
|
||||
expect(loadGlobalConnectorExecutionLogAggregations).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
message: '',
|
||||
namespaces: undefined,
|
||||
outcomeFilter: [],
|
||||
page: 0,
|
||||
perPage: 50,
|
||||
sort: [],
|
||||
})
|
||||
);
|
||||
|
||||
expect(wrapper.find('[data-test-subj="connectorEventLogListProgressBar"]')).toEqual({});
|
||||
expect(
|
||||
wrapper
|
||||
.find('[data-gridcell-column-id="timestamp"] .euiDataGridRowCell__truncate')
|
||||
.first()
|
||||
.text()
|
||||
).toBeTruthy();
|
||||
expect(
|
||||
wrapper
|
||||
.find('[data-gridcell-column-id="status"] .euiDataGridRowCell__truncate')
|
||||
.first()
|
||||
.text()
|
||||
).toEqual('succeeded');
|
||||
expect(
|
||||
wrapper
|
||||
.find('[data-gridcell-column-id="connector_name"] .euiDataGridRowCell__truncate')
|
||||
.first()
|
||||
.text()
|
||||
).toEqual('test connector');
|
||||
expect(
|
||||
wrapper
|
||||
.find('[data-gridcell-column-id="message"] .euiDataGridRowCell__truncate')
|
||||
.first()
|
||||
.text()
|
||||
).toEqual('action executed: .server-log:86020b10-9b3b-11ed-8422-2f5a388a317d: test');
|
||||
});
|
||||
});
|
|
@ -0,0 +1,597 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useCallback, useEffect, useState, useMemo, useRef } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import datemath from '@kbn/datemath';
|
||||
import {
|
||||
EuiFieldSearch,
|
||||
EuiFlexItem,
|
||||
EuiFlexGroup,
|
||||
EuiProgress,
|
||||
EuiSpacer,
|
||||
EuiDataGridSorting,
|
||||
Pagination,
|
||||
EuiSuperDatePicker,
|
||||
OnTimeChangeProps,
|
||||
EuiSwitch,
|
||||
EuiDataGridColumn,
|
||||
} from '@elastic/eui';
|
||||
import { SpacesContextProps } from '@kbn/spaces-plugin/public';
|
||||
import { IExecutionLog } from '@kbn/actions-plugin/common';
|
||||
import { useKibana } from '../../../../common/lib/kibana';
|
||||
import {
|
||||
GLOBAL_CONNECTOR_EXECUTION_DEFAULT_INITIAL_VISIBLE_COLUMNS,
|
||||
CONNECTOR_LOCKED_COLUMNS,
|
||||
} from '../../../constants';
|
||||
import { CenterJustifiedSpinner } from '../../../components/center_justified_spinner';
|
||||
import { LoadGlobalConnectorExecutionLogAggregationsProps } from '../../../lib/action_connector_api';
|
||||
import {
|
||||
ComponentOpts as ConnectorApis,
|
||||
withActionOperations,
|
||||
} from '../../common/components/with_actions_api_operations';
|
||||
import { RefineSearchPrompt } from '../../common/components/refine_search_prompt';
|
||||
import { ConnectorEventLogListKPIWithApi as ConnectorEventLogListKPI } from './actions_connectors_event_log_list_kpi';
|
||||
import {
|
||||
EventLogDataGrid,
|
||||
EventLogListStatusFilter,
|
||||
getIsColumnSortable,
|
||||
} from '../../common/components/event_log';
|
||||
import { useMultipleSpaces } from '../../../hooks/use_multiple_spaces';
|
||||
|
||||
const getEmptyFunctionComponent: React.FC<SpacesContextProps> = ({ children }) => <>{children}</>;
|
||||
|
||||
const getParsedDate = (date: string) => {
|
||||
if (date.includes('now')) {
|
||||
return datemath.parse(date)?.format() || date;
|
||||
}
|
||||
return date;
|
||||
};
|
||||
|
||||
const API_FAILED_MESSAGE = i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.connectorEventLogList.eventLogColumn.apiError',
|
||||
{
|
||||
defaultMessage: 'Failed to fetch execution history',
|
||||
}
|
||||
);
|
||||
|
||||
const SEARCH_PLACEHOLDER = i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.connectorEventLogList.eventLogColumn.searchPlaceholder',
|
||||
{
|
||||
defaultMessage: 'Search event log message',
|
||||
}
|
||||
);
|
||||
|
||||
const CONNECTOR_EVENT_LOG_LIST_STORAGE_KEY =
|
||||
'xpack.triggersActionsUI.connectorEventLogList.initialColumns';
|
||||
|
||||
const getDefaultColumns = (columns: string[]) => {
|
||||
const columnsWithoutLockedColumn = columns.filter(
|
||||
(column) => !CONNECTOR_LOCKED_COLUMNS.includes(column)
|
||||
);
|
||||
return [...CONNECTOR_LOCKED_COLUMNS, ...columnsWithoutLockedColumn];
|
||||
};
|
||||
|
||||
const ALL_SPACES_LABEL = i18n.translate(
|
||||
'xpack.triggersActionsUI.connectorEventLogList.showAllSpacesToggle',
|
||||
{
|
||||
defaultMessage: 'Show connectors from all spaces',
|
||||
}
|
||||
);
|
||||
|
||||
const updateButtonProps = {
|
||||
iconOnly: true,
|
||||
fill: false,
|
||||
};
|
||||
|
||||
const MAX_RESULTS = 1000;
|
||||
|
||||
export type ConnectorEventLogListOptions = 'stackManagement' | 'default';
|
||||
|
||||
export type ConnectorEventLogListCommonProps = {
|
||||
localStorageKey?: string;
|
||||
refreshToken?: number;
|
||||
initialPageSize?: number;
|
||||
hasConnectorNames?: boolean;
|
||||
hasAllSpaceSwitch?: boolean;
|
||||
} & Pick<ConnectorApis, 'loadGlobalConnectorExecutionLogAggregations'>;
|
||||
|
||||
export type ConnectorEventLogListTableProps<T extends ConnectorEventLogListOptions = 'default'> =
|
||||
T extends 'default'
|
||||
? ConnectorEventLogListCommonProps
|
||||
: T extends 'stackManagement'
|
||||
? ConnectorEventLogListCommonProps
|
||||
: never;
|
||||
|
||||
export const ConnectorEventLogListTable = <T extends ConnectorEventLogListOptions>(
|
||||
props: ConnectorEventLogListTableProps<T>
|
||||
) => {
|
||||
const {
|
||||
localStorageKey = CONNECTOR_EVENT_LOG_LIST_STORAGE_KEY,
|
||||
refreshToken,
|
||||
loadGlobalConnectorExecutionLogAggregations,
|
||||
initialPageSize = 10,
|
||||
hasConnectorNames = false,
|
||||
hasAllSpaceSwitch = false,
|
||||
} = props;
|
||||
|
||||
const { uiSettings, notifications } = useKibana().services;
|
||||
|
||||
const [searchText, setSearchText] = useState<string>('');
|
||||
const [search, setSearch] = useState<string>('');
|
||||
const [internalRefreshToken, setInternalRefreshToken] = useState<number | undefined>(
|
||||
refreshToken
|
||||
);
|
||||
const [showFromAllSpaces, setShowFromAllSpaces] = useState(false);
|
||||
|
||||
// Data grid states
|
||||
const [logs, setLogs] = useState<IExecutionLog[]>();
|
||||
const [visibleColumns, setVisibleColumns] = useState<string[]>(() => {
|
||||
return getDefaultColumns(GLOBAL_CONNECTOR_EXECUTION_DEFAULT_INITIAL_VISIBLE_COLUMNS);
|
||||
});
|
||||
const [sortingColumns, setSortingColumns] = useState<EuiDataGridSorting['columns']>([]);
|
||||
const [filter, setFilter] = useState<string[]>([]);
|
||||
const [actualTotalItemCount, setActualTotalItemCount] = useState<number>(0);
|
||||
const [pagination, setPagination] = useState<Pagination>({
|
||||
pageIndex: 0,
|
||||
pageSize: initialPageSize,
|
||||
totalItemCount: 0,
|
||||
});
|
||||
|
||||
// Date related states
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||
const [dateStart, setDateStart] = useState<string>('now-24h');
|
||||
const [dateEnd, setDateEnd] = useState<string>('now');
|
||||
const [dateFormat] = useState(() => uiSettings?.get('dateFormat'));
|
||||
const [commonlyUsedRanges] = useState(() => {
|
||||
return (
|
||||
uiSettings
|
||||
?.get('timepicker:quickRanges')
|
||||
?.map(({ from, to, display }: { from: string; to: string; display: string }) => ({
|
||||
start: from,
|
||||
end: to,
|
||||
label: display,
|
||||
})) || []
|
||||
);
|
||||
});
|
||||
|
||||
const { onShowAllSpacesChange, canAccessMultipleSpaces, namespaces } = useMultipleSpaces({
|
||||
setShowFromAllSpaces,
|
||||
showFromAllSpaces,
|
||||
visibleColumns,
|
||||
setVisibleColumns,
|
||||
});
|
||||
|
||||
const isInitialized = useRef(false);
|
||||
|
||||
const isOnLastPage = useMemo(() => {
|
||||
const { pageIndex, pageSize } = pagination;
|
||||
return (pageIndex + 1) * pageSize >= MAX_RESULTS;
|
||||
}, [pagination]);
|
||||
|
||||
// Formats the sort columns to be consumed by the API endpoint
|
||||
const formattedSort = useMemo(() => {
|
||||
return sortingColumns.map(({ id: sortId, direction }) => ({
|
||||
[sortId]: {
|
||||
order: direction,
|
||||
},
|
||||
}));
|
||||
}, [sortingColumns]);
|
||||
|
||||
const loadEventLogs = async () => {
|
||||
if (!loadGlobalConnectorExecutionLogAggregations) {
|
||||
return;
|
||||
}
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const result = await loadGlobalConnectorExecutionLogAggregations({
|
||||
sort: formattedSort as LoadGlobalConnectorExecutionLogAggregationsProps['sort'],
|
||||
outcomeFilter: filter,
|
||||
message: searchText,
|
||||
dateStart: getParsedDate(dateStart),
|
||||
dateEnd: getParsedDate(dateEnd),
|
||||
page: pagination.pageIndex,
|
||||
perPage: pagination.pageSize,
|
||||
namespaces,
|
||||
});
|
||||
setLogs(result.data);
|
||||
setPagination({
|
||||
...pagination,
|
||||
totalItemCount: Math.min(result.total, MAX_RESULTS),
|
||||
});
|
||||
setActualTotalItemCount(result.total);
|
||||
} catch (e) {
|
||||
notifications.toasts.addDanger({
|
||||
title: API_FAILED_MESSAGE,
|
||||
text: e.body?.message ?? e,
|
||||
});
|
||||
}
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
const onChangeItemsPerPage = useCallback(
|
||||
(pageSize: number) => {
|
||||
setPagination((prevPagination) => ({
|
||||
...prevPagination,
|
||||
pageIndex: 0,
|
||||
pageSize,
|
||||
}));
|
||||
},
|
||||
[setPagination]
|
||||
);
|
||||
|
||||
const onChangePage = useCallback(
|
||||
(pageIndex: number) => {
|
||||
setPagination((prevPagination) => ({
|
||||
...prevPagination,
|
||||
pageIndex,
|
||||
}));
|
||||
},
|
||||
[setPagination]
|
||||
);
|
||||
|
||||
const onTimeChange = useCallback(
|
||||
({ start, end, isInvalid }: OnTimeChangeProps) => {
|
||||
if (isInvalid) {
|
||||
return;
|
||||
}
|
||||
setDateStart(start);
|
||||
setDateEnd(end);
|
||||
},
|
||||
[setDateStart, setDateEnd]
|
||||
);
|
||||
|
||||
const onRefresh = () => {
|
||||
setInternalRefreshToken(Date.now());
|
||||
loadEventLogs();
|
||||
};
|
||||
|
||||
const onFilterChange = useCallback(
|
||||
(newFilter: string[]) => {
|
||||
setPagination((prevPagination) => ({
|
||||
...prevPagination,
|
||||
pageIndex: 0,
|
||||
}));
|
||||
setFilter(newFilter);
|
||||
},
|
||||
[setPagination, setFilter]
|
||||
);
|
||||
|
||||
const onSearchChange = useCallback(
|
||||
(e) => {
|
||||
if (e.target.value === '') {
|
||||
setSearchText('');
|
||||
}
|
||||
setSearch(e.target.value);
|
||||
},
|
||||
[setSearchText, setSearch]
|
||||
);
|
||||
|
||||
const onKeyUp = useCallback(
|
||||
(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
setSearchText(search);
|
||||
}
|
||||
},
|
||||
[search, setSearchText]
|
||||
);
|
||||
|
||||
const columns: EuiDataGridColumn[] = useMemo(
|
||||
() => [
|
||||
{
|
||||
id: 'connector_id',
|
||||
displayAsText: i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.connectorEventLogList.eventLogColumn.connectorId',
|
||||
{
|
||||
defaultMessage: 'Connector Id',
|
||||
}
|
||||
),
|
||||
isSortable: getIsColumnSortable('connector_id'),
|
||||
},
|
||||
{
|
||||
id: 'id',
|
||||
displayAsText: i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.connectorEventLogList.eventLogColumn.id',
|
||||
{
|
||||
defaultMessage: 'Execution Id',
|
||||
}
|
||||
),
|
||||
isSortable: getIsColumnSortable('id'),
|
||||
},
|
||||
{
|
||||
id: 'timestamp',
|
||||
displayAsText: i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.connectorEventLogList.eventLogColumn.timestamp',
|
||||
{
|
||||
defaultMessage: 'Timestamp',
|
||||
}
|
||||
),
|
||||
isSortable: getIsColumnSortable('timestamp'),
|
||||
isResizable: false,
|
||||
actions: {
|
||||
showHide: false,
|
||||
},
|
||||
initialWidth: 250,
|
||||
},
|
||||
{
|
||||
id: 'status',
|
||||
displayAsText: i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.connectorEventLogList.eventLogColumn.response',
|
||||
{
|
||||
defaultMessage: 'Response',
|
||||
}
|
||||
),
|
||||
actions: {
|
||||
showHide: false,
|
||||
showSortAsc: false,
|
||||
showSortDesc: false,
|
||||
additional: [
|
||||
{
|
||||
iconType: 'annotation',
|
||||
label: i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.connectorEventLogList.eventLogColumn.showOnlyFailures',
|
||||
{
|
||||
defaultMessage: 'Show only failures',
|
||||
}
|
||||
),
|
||||
onClick: () => onFilterChange(['failure']),
|
||||
size: 'xs',
|
||||
},
|
||||
{
|
||||
iconType: 'annotation',
|
||||
label: i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.connectorEventLogList.eventLogColumn.showAll',
|
||||
{
|
||||
defaultMessage: 'Show all',
|
||||
}
|
||||
),
|
||||
onClick: () => onFilterChange([]),
|
||||
size: 'xs',
|
||||
},
|
||||
],
|
||||
},
|
||||
isSortable: getIsColumnSortable('status'),
|
||||
isResizable: false,
|
||||
initialWidth: 150,
|
||||
},
|
||||
...(hasConnectorNames
|
||||
? [
|
||||
{
|
||||
id: 'connector_name',
|
||||
displayAsText: i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.connectorEventLogList.eventLogColumn.connectorName',
|
||||
{
|
||||
defaultMessage: 'Connector',
|
||||
}
|
||||
),
|
||||
isSortable: getIsColumnSortable('connector_name'),
|
||||
actions: {
|
||||
showSortAsc: false,
|
||||
showSortDesc: false,
|
||||
showHide: false,
|
||||
},
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
id: 'message',
|
||||
actions: {
|
||||
showSortAsc: false,
|
||||
showSortDesc: false,
|
||||
},
|
||||
displayAsText: i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.connectorEventLogList.eventLogColumn.message',
|
||||
{
|
||||
defaultMessage: 'Message',
|
||||
}
|
||||
),
|
||||
isSortable: getIsColumnSortable('message'),
|
||||
cellActions: [],
|
||||
},
|
||||
{
|
||||
id: 'execution_duration',
|
||||
displayAsText: i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.connectorEventLogList.eventLogColumn.duration',
|
||||
{
|
||||
defaultMessage: 'Duration',
|
||||
}
|
||||
),
|
||||
isSortable: getIsColumnSortable('execution_duration'),
|
||||
isResizable: false,
|
||||
actions: {
|
||||
showHide: false,
|
||||
},
|
||||
initialWidth: 100,
|
||||
},
|
||||
{
|
||||
id: 'schedule_delay',
|
||||
displayAsText: i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.connectorEventLogList.eventLogColumn.scheduleDelay',
|
||||
{
|
||||
defaultMessage: 'Schedule delay',
|
||||
}
|
||||
),
|
||||
isSortable: getIsColumnSortable('schedule_delay'),
|
||||
},
|
||||
{
|
||||
id: 'timed_out',
|
||||
displayAsText: i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.connectorEventLogList.eventLogColumn.timedOut',
|
||||
{
|
||||
defaultMessage: 'Timed out',
|
||||
}
|
||||
),
|
||||
isSortable: getIsColumnSortable('timed_out'),
|
||||
},
|
||||
...(showFromAllSpaces
|
||||
? [
|
||||
{
|
||||
id: 'space_ids',
|
||||
displayAsText: i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.connectorEventLogList.eventLogColumn.spaceIds',
|
||||
{
|
||||
defaultMessage: 'Space',
|
||||
}
|
||||
),
|
||||
isSortable: getIsColumnSortable('space_ids'),
|
||||
actions: {
|
||||
showSortAsc: false,
|
||||
showSortDesc: false,
|
||||
showHide: false,
|
||||
},
|
||||
},
|
||||
]
|
||||
: []),
|
||||
],
|
||||
[onFilterChange, hasConnectorNames, showFromAllSpaces]
|
||||
);
|
||||
|
||||
const renderList = () => {
|
||||
if (!logs) {
|
||||
return <CenterJustifiedSpinner />;
|
||||
}
|
||||
return (
|
||||
<>
|
||||
{isLoading && (
|
||||
<EuiProgress size="xs" color="accent" data-test-subj="connectorEventLogListProgressBar" />
|
||||
)}
|
||||
<EventLogDataGrid
|
||||
columns={columns}
|
||||
logs={logs}
|
||||
pagination={pagination}
|
||||
sortingColumns={sortingColumns}
|
||||
visibleColumns={visibleColumns}
|
||||
dateFormat={dateFormat}
|
||||
onChangeItemsPerPage={onChangeItemsPerPage}
|
||||
onChangePage={onChangePage}
|
||||
setVisibleColumns={setVisibleColumns}
|
||||
setSortingColumns={setSortingColumns}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
loadEventLogs();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [
|
||||
sortingColumns,
|
||||
dateStart,
|
||||
dateEnd,
|
||||
filter,
|
||||
pagination.pageIndex,
|
||||
pagination.pageSize,
|
||||
searchText,
|
||||
showFromAllSpaces,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isInitialized.current) {
|
||||
loadEventLogs();
|
||||
}
|
||||
isInitialized.current = true;
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [refreshToken]);
|
||||
|
||||
useEffect(() => {
|
||||
localStorage.setItem(localStorageKey, JSON.stringify(visibleColumns));
|
||||
}, [localStorageKey, visibleColumns]);
|
||||
|
||||
useEffect(() => {
|
||||
setInternalRefreshToken(refreshToken);
|
||||
}, [refreshToken]);
|
||||
|
||||
return (
|
||||
<EuiFlexGroup gutterSize="none" direction="column">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFlexGroup alignItems="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFieldSearch
|
||||
fullWidth
|
||||
isClearable
|
||||
value={search}
|
||||
onChange={onSearchChange}
|
||||
onKeyUp={onKeyUp}
|
||||
placeholder={SEARCH_PLACEHOLDER}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EventLogListStatusFilter selectedOptions={filter} onChange={onFilterChange} />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiSuperDatePicker
|
||||
data-test-subj="connectorEventLogListDatePicker"
|
||||
width="auto"
|
||||
isLoading={isLoading}
|
||||
start={dateStart}
|
||||
end={dateEnd}
|
||||
onTimeChange={onTimeChange}
|
||||
onRefresh={onRefresh}
|
||||
dateFormat={dateFormat}
|
||||
commonlyUsedRanges={commonlyUsedRanges}
|
||||
updateButtonProps={updateButtonProps}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
{hasAllSpaceSwitch && canAccessMultipleSpaces && (
|
||||
<EuiFlexItem data-test-subj="showAllSpacesSwitch">
|
||||
<EuiSwitch
|
||||
label={ALL_SPACES_LABEL}
|
||||
checked={showFromAllSpaces}
|
||||
onChange={onShowAllSpacesChange}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<ConnectorEventLogListKPI
|
||||
data-test-subj="connectorEventLogListKpi"
|
||||
dateStart={dateStart}
|
||||
dateEnd={dateEnd}
|
||||
outcomeFilter={filter}
|
||||
message={searchText}
|
||||
refreshToken={internalRefreshToken}
|
||||
namespaces={namespaces}
|
||||
/>
|
||||
<EuiSpacer />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
{renderList()}
|
||||
{isOnLastPage && (
|
||||
<RefineSearchPrompt
|
||||
documentSize={actualTotalItemCount}
|
||||
visibleDocumentSize={MAX_RESULTS}
|
||||
backToTopAnchor="logs"
|
||||
/>
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
};
|
||||
|
||||
const ConnectorEventLogListTableWithSpaces: React.FC<ConnectorEventLogListTableProps> = (props) => {
|
||||
const { spaces } = useKibana().services;
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
const SpacesContextWrapper = useCallback(
|
||||
spaces ? spaces.ui.components.getSpacesContextProvider : getEmptyFunctionComponent,
|
||||
[spaces]
|
||||
);
|
||||
return (
|
||||
<SpacesContextWrapper feature="triggersActions">
|
||||
<ConnectorEventLogListTable {...props} />
|
||||
</SpacesContextWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export const ConnectorEventLogListTableWithApi = withActionOperations(
|
||||
ConnectorEventLogListTableWithSpaces
|
||||
);
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export { ConnectorEventLogListTableWithApi as default };
|
|
@ -0,0 +1,137 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { lazy, useCallback, useEffect } from 'react';
|
||||
import { Route, RouteComponentProps, Switch } from 'react-router-dom';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { EuiSpacer, EuiButtonEmpty, EuiPageHeader, EuiPageTemplate } from '@elastic/eui';
|
||||
import { routeToConnectors, routeToLogs, Section } from '../../../constants';
|
||||
import { getAlertingSectionBreadcrumb } from '../../../lib/breadcrumb';
|
||||
import { getCurrentDocTitle } from '../../../lib/doc_title';
|
||||
import { suspendedComponentWithProps } from '../../../lib/suspended_component_with_props';
|
||||
import { HealthContextProvider } from '../../../context/health_context';
|
||||
import { HealthCheck } from '../../../components/health_check';
|
||||
import { useKibana } from '../../../../common/lib/kibana';
|
||||
import ConnectorEventLogListTableWithApi from './actions_connectors_event_log_list_table';
|
||||
|
||||
const ConnectorsList = lazy(() => import('./actions_connectors_list'));
|
||||
|
||||
export interface MatchParams {
|
||||
section: Section;
|
||||
}
|
||||
|
||||
export const ActionsConnectorsHome: React.FunctionComponent<RouteComponentProps<MatchParams>> = ({
|
||||
match: {
|
||||
params: { section },
|
||||
},
|
||||
history,
|
||||
}) => {
|
||||
const { chrome, setBreadcrumbs, docLinks } = useKibana().services;
|
||||
|
||||
const tabs: Array<{
|
||||
id: Section;
|
||||
name: React.ReactNode;
|
||||
}> = [];
|
||||
tabs.push({
|
||||
id: 'connectors',
|
||||
name: (
|
||||
<FormattedMessage
|
||||
id="xpack.triggersActionsUI.connectors.home.connectorsTabTitle"
|
||||
defaultMessage="Connectors"
|
||||
/>
|
||||
),
|
||||
});
|
||||
tabs.push({
|
||||
id: 'logs',
|
||||
name: (
|
||||
<FormattedMessage
|
||||
id="xpack.triggersActionsUI.connectors.home.logsTabTitle"
|
||||
defaultMessage="Logs"
|
||||
/>
|
||||
),
|
||||
});
|
||||
|
||||
const onSectionChange = (newSection: Section) => {
|
||||
history.push(`/${newSection}`);
|
||||
};
|
||||
|
||||
// Set breadcrumb and page title
|
||||
useEffect(() => {
|
||||
setBreadcrumbs([getAlertingSectionBreadcrumb(section || 'connectors')]);
|
||||
chrome.docTitle.change(getCurrentDocTitle(section || 'connectors'));
|
||||
}, [section, chrome, setBreadcrumbs]);
|
||||
|
||||
const renderLogsList = useCallback(() => {
|
||||
return (
|
||||
<EuiPageTemplate.Section grow={false} paddingSize="none">
|
||||
{suspendedComponentWithProps(
|
||||
ConnectorEventLogListTableWithApi,
|
||||
'xl'
|
||||
)({
|
||||
refreshToken: 0,
|
||||
initialPageSize: 50,
|
||||
hasConnectorNames: true,
|
||||
hasAllSpaceSwitch: true,
|
||||
})}
|
||||
</EuiPageTemplate.Section>
|
||||
);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiPageHeader
|
||||
bottomBorder
|
||||
paddingSize="none"
|
||||
pageTitle={i18n.translate('xpack.triggersActionsUI.connectors.home.appTitle', {
|
||||
defaultMessage: 'Connectors',
|
||||
})}
|
||||
description={i18n.translate('xpack.triggersActionsUI.connectors.home.description', {
|
||||
defaultMessage: 'Connect third-party software with your alerting data.',
|
||||
})}
|
||||
rightSideItems={[
|
||||
<EuiButtonEmpty
|
||||
data-test-subj="documentationButton"
|
||||
key="documentation-button"
|
||||
href={docLinks.links.alerting.actionTypes}
|
||||
iconType="help"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.triggersActionsUI.connectors.home.documentationButtonLabel"
|
||||
defaultMessage="Documentation"
|
||||
/>
|
||||
</EuiButtonEmpty>,
|
||||
]}
|
||||
tabs={tabs.map((tab) => ({
|
||||
label: tab.name,
|
||||
onClick: () => onSectionChange(tab.id),
|
||||
isSelected: tab.id === section,
|
||||
key: tab.id,
|
||||
'data-test-subj': `${tab.id}Tab`,
|
||||
}))}
|
||||
/>
|
||||
|
||||
<EuiSpacer size="l" />
|
||||
|
||||
<HealthContextProvider>
|
||||
<HealthCheck waitForCheck={true}>
|
||||
<Switch>
|
||||
<Route exact path={routeToLogs} component={renderLogsList} />
|
||||
<Route
|
||||
exact
|
||||
path={routeToConnectors}
|
||||
component={suspendedComponentWithProps(ConnectorsList, 'xl')}
|
||||
/>
|
||||
</Switch>
|
||||
</HealthCheck>
|
||||
</HealthContextProvider>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export { ActionsConnectorsHome as default };
|
|
@ -9,7 +9,6 @@ import { ClassNames } from '@emotion/react';
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
EuiInMemoryTable,
|
||||
EuiSpacer,
|
||||
EuiButton,
|
||||
EuiLink,
|
||||
EuiIconTip,
|
||||
|
@ -412,57 +411,41 @@ const ActionsConnectorsList: React.FunctionComponent = () => {
|
|||
options: actionTypesList,
|
||||
},
|
||||
],
|
||||
toolsLeft:
|
||||
selectedItems.length === 0 || !canDelete
|
||||
? []
|
||||
: [
|
||||
<EuiButton
|
||||
key="delete"
|
||||
iconType="trash"
|
||||
color="danger"
|
||||
data-test-subj="bulkDelete"
|
||||
onClick={() => onDelete(selectedItems)}
|
||||
title={
|
||||
canDelete
|
||||
? undefined
|
||||
: i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.actionsConnectorsList.buttons.deleteDisabledTitle',
|
||||
{ defaultMessage: 'Unable to delete connectors' }
|
||||
)
|
||||
}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.triggersActionsUI.sections.actionsConnectorsList.buttons.deleteLabel"
|
||||
defaultMessage="Delete {count}"
|
||||
values={{
|
||||
count: selectedItems.length,
|
||||
}}
|
||||
/>
|
||||
</EuiButton>,
|
||||
],
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{actionConnectorTableItems.length !== 0 && (
|
||||
<EuiPageTemplate.Header
|
||||
paddingSize="none"
|
||||
pageTitle={i18n.translate('xpack.triggersActionsUI.connectors.home.appTitle', {
|
||||
defaultMessage: 'Connectors',
|
||||
})}
|
||||
description={i18n.translate('xpack.triggersActionsUI.connectors.home.description', {
|
||||
defaultMessage: 'Connect third-party software with your alerting data.',
|
||||
})}
|
||||
rightSideItems={(canSave
|
||||
toolsLeft: (selectedItems.length === 0 || !canDelete
|
||||
? []
|
||||
: [
|
||||
<EuiButton
|
||||
key="delete"
|
||||
iconType="trash"
|
||||
color="danger"
|
||||
data-test-subj="bulkDelete"
|
||||
onClick={() => onDelete(selectedItems)}
|
||||
title={
|
||||
canDelete
|
||||
? undefined
|
||||
: i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.actionsConnectorsList.buttons.deleteDisabledTitle',
|
||||
{ defaultMessage: 'Unable to delete connectors' }
|
||||
)
|
||||
}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.triggersActionsUI.sections.actionsConnectorsList.buttons.deleteLabel"
|
||||
defaultMessage="Delete {count}"
|
||||
values={{
|
||||
count: selectedItems.length,
|
||||
}}
|
||||
/>
|
||||
</EuiButton>,
|
||||
]
|
||||
).concat(
|
||||
canSave
|
||||
? [
|
||||
<EuiButton
|
||||
data-test-subj="createActionButton"
|
||||
key="create-action"
|
||||
fill
|
||||
onClick={() => setAddFlyoutVisibility(true)}
|
||||
iconType="plusInCircle"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.triggersActionsUI.sections.actionsConnectorsList.addActionButtonLabel"
|
||||
|
@ -471,21 +454,13 @@ const ActionsConnectorsList: React.FunctionComponent = () => {
|
|||
</EuiButton>,
|
||||
]
|
||||
: []
|
||||
).concat([
|
||||
<EuiButtonEmpty
|
||||
data-test-subj="documentationButton"
|
||||
key="documentation-button"
|
||||
href={docLinks.links.alerting.connectors}
|
||||
iconType="help"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.triggersActionsUI.sections.actionsConnectorsList.documentationButtonLabel"
|
||||
defaultMessage="Documentation"
|
||||
/>
|
||||
</EuiButtonEmpty>,
|
||||
])}
|
||||
/>
|
||||
)}
|
||||
),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiPageTemplate.Section
|
||||
paddingSize="none"
|
||||
data-test-subj="actionsList"
|
||||
|
@ -535,7 +510,6 @@ const ActionsConnectorsList: React.FunctionComponent = () => {
|
|||
setIsLoadingState={(isLoading: boolean) => setIsLoadingActionTypes(isLoading)}
|
||||
/>
|
||||
|
||||
<EuiSpacer size="m" />
|
||||
{/* Render the view based on if there's data or if they can save */}
|
||||
{(isLoadingActions || isLoadingActionTypes) && <CenterJustifiedSpinner />}
|
||||
{actionConnectorTableItems.length !== 0 && table}
|
||||
|
|
|
@ -0,0 +1,373 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useMemo, useCallback } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import {
|
||||
EuiDataGrid,
|
||||
EuiDataGridStyle,
|
||||
Pagination,
|
||||
EuiDataGridCellValueElementProps,
|
||||
EuiDataGridSorting,
|
||||
EuiDataGridColumn,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiSpacer,
|
||||
EuiBadge,
|
||||
EuiDataGridCellPopoverElementProps,
|
||||
useEuiTheme,
|
||||
EuiToolTip,
|
||||
EuiText,
|
||||
EuiIcon,
|
||||
} from '@elastic/eui';
|
||||
import {
|
||||
IExecutionLog,
|
||||
executionLogSortableColumns,
|
||||
ExecutionLogSortFields,
|
||||
} from '@kbn/alerting-plugin/common';
|
||||
import { IExecutionLog as IConnectorsExecutionLog } from '@kbn/actions-plugin/common';
|
||||
import { get } from 'lodash';
|
||||
import { getIsExperimentalFeatureEnabled } from '../../../../../common/get_experimental_features';
|
||||
import { EventLogListCellRenderer, ColumnId, EventLogPaginationStatus } from '.';
|
||||
import { RuleActionErrorBadge } from '../../../rule_details/components/rule_action_error_badge';
|
||||
import './event_log_list.scss';
|
||||
|
||||
export const getIsColumnSortable = (columnId: string) => {
|
||||
return executionLogSortableColumns.includes(columnId as ExecutionLogSortFields);
|
||||
};
|
||||
|
||||
const getErroredActionsTranslation = (errors: number) => {
|
||||
return i18n.translate('xpack.triggersActionsUI.sections.eventLogDataGrid.erroredActionsTooltip', {
|
||||
defaultMessage: '{value, plural, one {# errored action} other {# errored actions}}',
|
||||
values: { value: errors },
|
||||
});
|
||||
};
|
||||
|
||||
const PAGE_SIZE_OPTIONS = [10, 50, 100];
|
||||
|
||||
type ExecutionLog = IExecutionLog | IConnectorsExecutionLog;
|
||||
|
||||
export interface EventLogDataGrid {
|
||||
columns: EuiDataGridColumn[];
|
||||
logs: ExecutionLog[];
|
||||
pagination: Pagination;
|
||||
sortingColumns: EuiDataGridSorting['columns'];
|
||||
visibleColumns: string[];
|
||||
dateFormat: string;
|
||||
pageSizeOptions?: number[];
|
||||
selectedRunLog?: ExecutionLog;
|
||||
onChangeItemsPerPage: (pageSize: number) => void;
|
||||
onChangePage: (pageIndex: number) => void;
|
||||
onFlyoutOpen?: (runLog: IExecutionLog) => void;
|
||||
setVisibleColumns: (visibleColumns: string[]) => void;
|
||||
setSortingColumns: (sortingColumns: EuiDataGridSorting['columns']) => void;
|
||||
}
|
||||
|
||||
export const numTriggeredActionsDisplay = i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.eventLogColumn.triggeredActions',
|
||||
{
|
||||
defaultMessage: 'Triggered actions',
|
||||
}
|
||||
);
|
||||
const numTriggeredActionsToolTip = i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.eventLogColumn.triggeredActionsToolTip',
|
||||
{
|
||||
defaultMessage: 'The subset of generated actions that will run.',
|
||||
}
|
||||
);
|
||||
|
||||
export const numGeneratedActionsDisplay = i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.eventLogColumn.scheduledActions',
|
||||
{
|
||||
defaultMessage: 'Generated actions',
|
||||
}
|
||||
);
|
||||
const numGeneratedActionsToolTip = i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.eventLogColumn.scheduledActionsToolTip',
|
||||
{
|
||||
defaultMessage: 'The total number of actions generated when the rule ran.',
|
||||
}
|
||||
);
|
||||
|
||||
export const numSucceededActionsDisplay = i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.eventLogColumn.succeededActions',
|
||||
{
|
||||
defaultMessage: 'Succeeded actions',
|
||||
}
|
||||
);
|
||||
const numSucceededActionsToolTip = i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.eventLogColumn.succeededActionsToolTip',
|
||||
{
|
||||
defaultMessage: 'The number of actions that were completed successfully.',
|
||||
}
|
||||
);
|
||||
|
||||
export const numErroredActionsDisplay = i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.eventLogColumn.erroredActions',
|
||||
{
|
||||
defaultMessage: 'Errored actions',
|
||||
}
|
||||
);
|
||||
const numErroredActionsToolTip = i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.eventLogColumn.erroredActionsToolTip',
|
||||
{
|
||||
defaultMessage: 'The number of failed actions.',
|
||||
}
|
||||
);
|
||||
|
||||
const columnsWithToolTipMap: Record<string, Record<string, string>> = {
|
||||
num_triggered_actions: {
|
||||
display: numTriggeredActionsDisplay,
|
||||
toolTip: numTriggeredActionsToolTip,
|
||||
},
|
||||
num_generated_actions: {
|
||||
display: numGeneratedActionsDisplay,
|
||||
toolTip: numGeneratedActionsToolTip,
|
||||
},
|
||||
num_succeeded_actions: {
|
||||
display: numSucceededActionsDisplay,
|
||||
toolTip: numSucceededActionsToolTip,
|
||||
},
|
||||
num_errored_actions: {
|
||||
display: numErroredActionsDisplay,
|
||||
toolTip: numErroredActionsToolTip,
|
||||
},
|
||||
};
|
||||
|
||||
export const ColumnHeaderWithToolTip = ({ id }: { id: string }) => {
|
||||
return (
|
||||
<EuiToolTip content={columnsWithToolTipMap[id].toolTip}>
|
||||
<EuiFlexGroup gutterSize="xs" alignItems="center">
|
||||
<EuiFlexItem>{columnsWithToolTipMap[id].display}</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiIcon size="s" color="subdued" type="questionInCircle" className="eui-alignTop" />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiToolTip>
|
||||
);
|
||||
};
|
||||
|
||||
export const EventLogDataGrid = (props: EventLogDataGrid) => {
|
||||
const {
|
||||
columns,
|
||||
logs = [],
|
||||
sortingColumns,
|
||||
pageSizeOptions = PAGE_SIZE_OPTIONS,
|
||||
pagination,
|
||||
dateFormat,
|
||||
visibleColumns,
|
||||
selectedRunLog,
|
||||
setVisibleColumns,
|
||||
setSortingColumns,
|
||||
onChangeItemsPerPage,
|
||||
onChangePage,
|
||||
onFlyoutOpen,
|
||||
} = props;
|
||||
|
||||
const { euiTheme } = useEuiTheme();
|
||||
|
||||
const isRuleUsingExecutionStatus = getIsExperimentalFeatureEnabled('ruleUseExecutionStatus');
|
||||
|
||||
const getPaginatedRowIndex = useCallback(
|
||||
(rowIndex: number) => {
|
||||
const { pageIndex, pageSize } = pagination;
|
||||
return rowIndex - pageIndex * pageSize;
|
||||
},
|
||||
[pagination]
|
||||
);
|
||||
|
||||
const columnVisibilityProps = useMemo(() => {
|
||||
return {
|
||||
visibleColumns,
|
||||
setVisibleColumns,
|
||||
};
|
||||
}, [visibleColumns, setVisibleColumns]);
|
||||
|
||||
const sortingProps = useMemo(
|
||||
() => ({
|
||||
onSort: setSortingColumns,
|
||||
columns: sortingColumns,
|
||||
}),
|
||||
[setSortingColumns, sortingColumns]
|
||||
);
|
||||
|
||||
const paginationProps = useMemo(
|
||||
() => ({
|
||||
...pagination,
|
||||
pageSizeOptions,
|
||||
onChangeItemsPerPage,
|
||||
onChangePage,
|
||||
}),
|
||||
[pagination, pageSizeOptions, onChangeItemsPerPage, onChangePage]
|
||||
);
|
||||
|
||||
const rowClasses = useMemo(() => {
|
||||
if (!selectedRunLog) {
|
||||
return {};
|
||||
}
|
||||
const index = logs.findIndex((log) => log.id === selectedRunLog.id);
|
||||
return {
|
||||
[index]: 'ruleEventLogDataGrid--rowClassSelected',
|
||||
};
|
||||
}, [selectedRunLog, logs]);
|
||||
|
||||
const gridStyles: EuiDataGridStyle = useMemo(() => {
|
||||
return {
|
||||
border: 'horizontal',
|
||||
header: 'underline',
|
||||
rowClasses,
|
||||
};
|
||||
}, [rowClasses]);
|
||||
|
||||
const renderMessageWithActionError = (
|
||||
columnId: string,
|
||||
errors: number,
|
||||
showTooltip: boolean = false
|
||||
) => {
|
||||
if (columnId !== 'message') {
|
||||
return null;
|
||||
}
|
||||
if (!errors) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<EuiFlexItem grow={false}>
|
||||
{showTooltip ? (
|
||||
<EuiToolTip content={getErroredActionsTranslation(errors)}>
|
||||
<RuleActionErrorBadge totalErrors={errors} showIcon />
|
||||
</EuiToolTip>
|
||||
) : (
|
||||
<RuleActionErrorBadge totalErrors={errors} showIcon />
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
);
|
||||
};
|
||||
|
||||
// Renders the cell popover for runs with errored actions
|
||||
const renderCellPopover = (cellPopoverProps: EuiDataGridCellPopoverElementProps) => {
|
||||
const { columnId, rowIndex, cellActions, DefaultCellPopover } = cellPopoverProps;
|
||||
|
||||
if (columnId !== 'message') {
|
||||
return <DefaultCellPopover {...cellPopoverProps} />;
|
||||
}
|
||||
|
||||
const pagedRowIndex = getPaginatedRowIndex(rowIndex);
|
||||
const runLog = logs[pagedRowIndex];
|
||||
|
||||
if (!runLog) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const value = runLog[columnId as keyof ExecutionLog] as string;
|
||||
const actionErrors = get(runLog, 'num_errored_actions', 0 as number);
|
||||
|
||||
return (
|
||||
<div style={{ width: '100%' }}>
|
||||
<EuiSpacer size="s" />
|
||||
<div>
|
||||
<EuiText size="m">{value}</EuiText>
|
||||
</div>
|
||||
<EuiSpacer size="s" />
|
||||
{actionErrors > 0 && (
|
||||
<>
|
||||
<EuiSpacer size="l" />
|
||||
<EuiFlexGroup gutterSize="none" alignItems="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
{renderMessageWithActionError(columnId, actionErrors)}
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
|
||||
<FormattedMessage
|
||||
id="xpack.triggersActionsUI.sections.eventLogDataGrid.erroredActionsCellPopover"
|
||||
defaultMessage="{value, plural, one {errored action} other {errored actions}}"
|
||||
values={{
|
||||
value: actionErrors,
|
||||
}}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer size="s" />
|
||||
{cellActions}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Main cell renderer, renders durations, statuses, etc.
|
||||
const renderCell = ({ rowIndex, columnId }: EuiDataGridCellValueElementProps) => {
|
||||
const pagedRowIndex = getPaginatedRowIndex(rowIndex);
|
||||
|
||||
const runLog = logs[pagedRowIndex];
|
||||
const value = logs[pagedRowIndex]?.[columnId as keyof ExecutionLog] as string;
|
||||
const actionErrors = get(logs[pagedRowIndex], 'num_errored_actions', 0 as number);
|
||||
const version = logs?.[pagedRowIndex]?.version;
|
||||
const ruleId = get(runLog, 'rule_id');
|
||||
const spaceIds = runLog?.space_ids;
|
||||
|
||||
if (columnId === 'num_errored_actions' && runLog && onFlyoutOpen) {
|
||||
return (
|
||||
<EuiBadge
|
||||
data-test-subj="eventLogDataGridErroredActionBadge"
|
||||
style={{
|
||||
cursor: 'pointer',
|
||||
borderRadius: euiTheme.border.radius.medium,
|
||||
}}
|
||||
color="hollow"
|
||||
onClick={() => onFlyoutOpen(runLog as IExecutionLog)}
|
||||
onClickAriaLabel={i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.eventLogColumn.openActionErrorsFlyout',
|
||||
{
|
||||
defaultMessage: 'Open action errors flyout',
|
||||
}
|
||||
)}
|
||||
>
|
||||
{value}
|
||||
</EuiBadge>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<EuiFlexGroup gutterSize="s" alignItems="center">
|
||||
{renderMessageWithActionError(columnId, actionErrors, true)}
|
||||
<EuiFlexItem>
|
||||
<EventLogListCellRenderer
|
||||
columnId={columnId as ColumnId}
|
||||
value={value}
|
||||
version={version}
|
||||
dateFormat={dateFormat}
|
||||
ruleId={ruleId}
|
||||
spaceIds={spaceIds}
|
||||
useExecutionStatus={isRuleUsingExecutionStatus}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<EventLogPaginationStatus
|
||||
pageIndex={pagination.pageIndex}
|
||||
pageSize={pagination.pageSize}
|
||||
totalItemCount={pagination.totalItemCount}
|
||||
/>
|
||||
<EuiDataGrid
|
||||
aria-label="event log"
|
||||
data-test-subj="eventLogList"
|
||||
columns={columns}
|
||||
rowCount={pagination.totalItemCount}
|
||||
renderCellValue={renderCell}
|
||||
columnVisibility={columnVisibilityProps}
|
||||
sorting={sortingProps}
|
||||
pagination={paginationProps}
|
||||
gridStyle={gridStyles}
|
||||
renderCellPopover={renderCellPopover}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -9,12 +9,9 @@ import React from 'react';
|
|||
import moment from 'moment';
|
||||
import { EuiIcon, EuiLink } from '@elastic/eui';
|
||||
import { shallow, mount } from 'enzyme';
|
||||
import {
|
||||
RuleEventLogListCellRenderer,
|
||||
DEFAULT_DATE_FORMAT,
|
||||
} from './rule_event_log_list_cell_renderer';
|
||||
import { RuleEventLogListStatus } from './rule_event_log_list_status';
|
||||
import { RuleDurationFormat } from '../../rules_list/components/rule_duration_format';
|
||||
import { EventLogListCellRenderer, DEFAULT_DATE_FORMAT } from './event_log_list_cell_renderer';
|
||||
import { EventLogListStatus } from './event_log_list_status';
|
||||
import { RuleDurationFormat } from '../../../rules_list/components/rule_duration_format';
|
||||
|
||||
jest.mock('react-router-dom', () => ({
|
||||
useHistory: () => ({
|
||||
|
@ -24,7 +21,7 @@ jest.mock('react-router-dom', () => ({
|
|||
}),
|
||||
}));
|
||||
|
||||
jest.mock('../../../../common/lib/kibana', () => ({
|
||||
jest.mock('../../../../../common/lib/kibana', () => ({
|
||||
useSpacesData: () => ({
|
||||
spacesMap: new Map([
|
||||
['space1', { id: 'space1' }],
|
||||
|
@ -64,20 +61,20 @@ describe('rule_event_log_list_cell_renderer', () => {
|
|||
});
|
||||
|
||||
it('renders primitive values correctly', () => {
|
||||
const wrapper = mount(<RuleEventLogListCellRenderer columnId="message" value="test" />);
|
||||
const wrapper = mount(<EventLogListCellRenderer columnId="message" value="test" />);
|
||||
|
||||
expect(wrapper.text()).toEqual('test');
|
||||
});
|
||||
|
||||
it('renders undefined correctly', () => {
|
||||
const wrapper = shallow(<RuleEventLogListCellRenderer columnId="message" />);
|
||||
const wrapper = shallow(<EventLogListCellRenderer columnId="message" />);
|
||||
|
||||
expect(wrapper.text()).toBeFalsy();
|
||||
});
|
||||
|
||||
it('renders date duration correctly', () => {
|
||||
const wrapper = shallow(
|
||||
<RuleEventLogListCellRenderer columnId="execution_duration" value="100000" />
|
||||
<EventLogListCellRenderer columnId="execution_duration" value="100000" />
|
||||
);
|
||||
|
||||
expect(wrapper.find(RuleDurationFormat).exists()).toBeTruthy();
|
||||
|
@ -86,7 +83,7 @@ describe('rule_event_log_list_cell_renderer', () => {
|
|||
|
||||
it('renders alert count correctly', () => {
|
||||
const wrapper = shallow(
|
||||
<RuleEventLogListCellRenderer columnId="num_new_alerts" value="3" version="8.3.0" />
|
||||
<EventLogListCellRenderer columnId="num_new_alerts" value="3" version="8.3.0" />
|
||||
);
|
||||
|
||||
expect(wrapper.text()).toEqual('3');
|
||||
|
@ -94,29 +91,29 @@ describe('rule_event_log_list_cell_renderer', () => {
|
|||
|
||||
it('renders timestamps correctly', () => {
|
||||
const time = '2022-03-20T07:40:44-07:00';
|
||||
const wrapper = shallow(<RuleEventLogListCellRenderer columnId="timestamp" value={time} />);
|
||||
const wrapper = shallow(<EventLogListCellRenderer columnId="timestamp" value={time} />);
|
||||
|
||||
expect(wrapper.text()).toEqual(moment(time).format(DEFAULT_DATE_FORMAT));
|
||||
});
|
||||
|
||||
it('renders alert status correctly', () => {
|
||||
const wrapper = shallow(<RuleEventLogListCellRenderer columnId="status" value="success" />);
|
||||
const wrapper = shallow(<EventLogListCellRenderer columnId="status" value="success" />);
|
||||
|
||||
expect(wrapper.find(RuleEventLogListStatus).exists()).toBeTruthy();
|
||||
expect(wrapper.find(RuleEventLogListStatus).props().status).toEqual('success');
|
||||
expect(wrapper.find(EventLogListStatus).exists()).toBeTruthy();
|
||||
expect(wrapper.find(EventLogListStatus).props().status).toEqual('success');
|
||||
});
|
||||
|
||||
it('unaccounted status will still render, but with the unknown color', () => {
|
||||
const wrapper = mount(<RuleEventLogListCellRenderer columnId="status" value="newOutcome" />);
|
||||
const wrapper = mount(<EventLogListCellRenderer columnId="status" value="newOutcome" />);
|
||||
|
||||
expect(wrapper.find(RuleEventLogListStatus).exists()).toBeTruthy();
|
||||
expect(wrapper.find(RuleEventLogListStatus).text()).toEqual('newOutcome');
|
||||
expect(wrapper.find(EventLogListStatus).exists()).toBeTruthy();
|
||||
expect(wrapper.find(EventLogListStatus).text()).toEqual('newOutcome');
|
||||
expect(wrapper.find(EuiIcon).props().color).toEqual('gray');
|
||||
});
|
||||
|
||||
it('links to rules on the correct space', () => {
|
||||
const wrapper1 = shallow(
|
||||
<RuleEventLogListCellRenderer
|
||||
<EventLogListCellRenderer
|
||||
columnId="rule_name"
|
||||
value="Rule"
|
||||
ruleId="1"
|
||||
|
@ -126,7 +123,7 @@ describe('rule_event_log_list_cell_renderer', () => {
|
|||
// @ts-ignore data-href is not a native EuiLink prop
|
||||
expect(wrapper1.find(EuiLink).props()['data-href']).toEqual('/rule/1');
|
||||
const wrapper2 = shallow(
|
||||
<RuleEventLogListCellRenderer
|
||||
<EventLogListCellRenderer
|
||||
columnId="rule_name"
|
||||
value="Rule"
|
||||
ruleId="1"
|
|
@ -11,21 +11,24 @@ import { EuiLink } from '@elastic/eui';
|
|||
import { RuleAlertingOutcome } from '@kbn/alerting-plugin/common';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { getRuleDetailsRoute } from '@kbn/rule-data-utils';
|
||||
import { formatRuleAlertCount } from '../../../../common/lib/format_rule_alert_count';
|
||||
import { useKibana, useSpacesData } from '../../../../common/lib/kibana';
|
||||
import { RuleEventLogListStatus } from './rule_event_log_list_status';
|
||||
import { RuleDurationFormat } from '../../rules_list/components/rule_duration_format';
|
||||
import { formatRuleAlertCount } from '../../../../../common/lib/format_rule_alert_count';
|
||||
import { useKibana, useSpacesData } from '../../../../../common/lib/kibana';
|
||||
import { EventLogListStatus } from './event_log_list_status';
|
||||
import { RuleDurationFormat } from '../../../rules_list/components/rule_duration_format';
|
||||
import {
|
||||
RULE_EXECUTION_LOG_COLUMN_IDS,
|
||||
RULE_EXECUTION_LOG_DURATION_COLUMNS,
|
||||
RULE_EXECUTION_LOG_ALERT_COUNT_COLUMNS,
|
||||
} from '../../../constants';
|
||||
CONNECTOR_EXECUTION_LOG_COLUMN_IDS,
|
||||
} from '../../../../constants';
|
||||
|
||||
export const DEFAULT_DATE_FORMAT = 'MMM D, YYYY @ HH:mm:ss.SSS';
|
||||
|
||||
export type ColumnId = typeof RULE_EXECUTION_LOG_COLUMN_IDS[number];
|
||||
export type ColumnId =
|
||||
| typeof RULE_EXECUTION_LOG_COLUMN_IDS[number]
|
||||
| typeof CONNECTOR_EXECUTION_LOG_COLUMN_IDS[number];
|
||||
|
||||
interface RuleEventLogListCellRendererProps {
|
||||
interface EventLogListCellRendererProps {
|
||||
columnId: ColumnId;
|
||||
version?: string;
|
||||
value?: string | string[];
|
||||
|
@ -35,7 +38,7 @@ interface RuleEventLogListCellRendererProps {
|
|||
useExecutionStatus?: boolean;
|
||||
}
|
||||
|
||||
export const RuleEventLogListCellRenderer = (props: RuleEventLogListCellRendererProps) => {
|
||||
export const EventLogListCellRenderer = (props: EventLogListCellRendererProps) => {
|
||||
const {
|
||||
columnId,
|
||||
value,
|
||||
|
@ -97,7 +100,7 @@ export const RuleEventLogListCellRenderer = (props: RuleEventLogListCellRenderer
|
|||
|
||||
if (columnId === 'status') {
|
||||
return (
|
||||
<RuleEventLogListStatus
|
||||
<EventLogListStatus
|
||||
status={value as RuleAlertingOutcome}
|
||||
useExecutionStatus={useExecutionStatus}
|
||||
/>
|
||||
|
@ -129,5 +132,9 @@ export const RuleEventLogListCellRenderer = (props: RuleEventLogListCellRenderer
|
|||
return <RuleDurationFormat duration={parseInt(value as string, 10)} />;
|
||||
}
|
||||
|
||||
if (columnId === 'timed_out') {
|
||||
return <>{value ? 'true' : 'false'}</>;
|
||||
}
|
||||
|
||||
return <>{value}</>;
|
||||
};
|
|
@ -13,9 +13,9 @@ import {
|
|||
RULE_LAST_RUN_OUTCOME_FAILED,
|
||||
RULE_LAST_RUN_OUTCOME_WARNING,
|
||||
ALERT_STATUS_UNKNOWN,
|
||||
} from '../../rules_list/translations';
|
||||
} from '../../../rules_list/translations';
|
||||
|
||||
interface RuleEventLogListStatusProps {
|
||||
interface EventLogListStatusProps {
|
||||
status: RuleAlertingOutcome;
|
||||
useExecutionStatus?: boolean;
|
||||
}
|
||||
|
@ -44,7 +44,7 @@ const STATUS_TO_OUTCOME: Record<RuleAlertingOutcome, string> = {
|
|||
unknown: ALERT_STATUS_UNKNOWN,
|
||||
};
|
||||
|
||||
export const RuleEventLogListStatus = (props: RuleEventLogListStatusProps) => {
|
||||
export const EventLogListStatus = (props: EventLogListStatusProps) => {
|
||||
const { status, useExecutionStatus = true } = props;
|
||||
const color = STATUS_TO_COLOR[status] || 'gray';
|
||||
|
|
@ -8,10 +8,10 @@
|
|||
import React from 'react';
|
||||
import { mountWithIntl } from '@kbn/test-jest-helpers';
|
||||
import { EuiFilterButton, EuiFilterSelectItem } from '@elastic/eui';
|
||||
import { RuleEventLogListStatusFilter } from './rule_event_log_list_status_filter';
|
||||
import { getIsExperimentalFeatureEnabled } from '../../../../common/get_experimental_features';
|
||||
import { EventLogListStatusFilter } from './event_log_list_status_filter';
|
||||
import { getIsExperimentalFeatureEnabled } from '../../../../../common/get_experimental_features';
|
||||
|
||||
jest.mock('../../../../common/get_experimental_features', () => ({
|
||||
jest.mock('../../../../../common/get_experimental_features', () => ({
|
||||
getIsExperimentalFeatureEnabled: jest.fn(),
|
||||
}));
|
||||
|
||||
|
@ -21,14 +21,14 @@ beforeEach(() => {
|
|||
|
||||
const onChangeMock = jest.fn();
|
||||
|
||||
describe('rule_event_log_list_status_filter', () => {
|
||||
describe('event_log_list_status_filter', () => {
|
||||
beforeEach(() => {
|
||||
onChangeMock.mockReset();
|
||||
});
|
||||
|
||||
it('renders correctly', () => {
|
||||
const wrapper = mountWithIntl(
|
||||
<RuleEventLogListStatusFilter selectedOptions={[]} onChange={onChangeMock} />
|
||||
<EventLogListStatusFilter selectedOptions={[]} onChange={onChangeMock} />
|
||||
);
|
||||
|
||||
expect(wrapper.find(EuiFilterSelectItem).exists()).toBeFalsy();
|
||||
|
@ -39,7 +39,7 @@ describe('rule_event_log_list_status_filter', () => {
|
|||
|
||||
it('can open the popover correctly', () => {
|
||||
const wrapper = mountWithIntl(
|
||||
<RuleEventLogListStatusFilter selectedOptions={[]} onChange={onChangeMock} />
|
||||
<EventLogListStatusFilter selectedOptions={[]} onChange={onChangeMock} />
|
||||
);
|
||||
|
||||
wrapper.find(EuiFilterButton).simulate('click');
|
|
@ -9,17 +9,17 @@ import React, { useState, useCallback } from 'react';
|
|||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { RuleAlertingOutcome } from '@kbn/alerting-plugin/common';
|
||||
import { EuiFilterButton, EuiPopover, EuiFilterGroup, EuiFilterSelectItem } from '@elastic/eui';
|
||||
import { getIsExperimentalFeatureEnabled } from '../../../../common/get_experimental_features';
|
||||
import { RuleEventLogListStatus } from './rule_event_log_list_status';
|
||||
import { getIsExperimentalFeatureEnabled } from '../../../../../common/get_experimental_features';
|
||||
import { EventLogListStatus } from './event_log_list_status';
|
||||
|
||||
const statusFilters: RuleAlertingOutcome[] = ['success', 'failure', 'warning', 'unknown'];
|
||||
|
||||
interface RuleEventLogListStatusFilterProps {
|
||||
interface EventLogListStatusFilterProps {
|
||||
selectedOptions: string[];
|
||||
onChange: (selectedValues: string[]) => void;
|
||||
}
|
||||
|
||||
export const RuleEventLogListStatusFilter = (props: RuleEventLogListStatusFilterProps) => {
|
||||
export const EventLogListStatusFilter = (props: EventLogListStatusFilterProps) => {
|
||||
const { selectedOptions = [], onChange = () => {} } = props;
|
||||
|
||||
const isRuleUsingExecutionStatus = getIsExperimentalFeatureEnabled('ruleUseExecutionStatus');
|
||||
|
@ -48,7 +48,7 @@ export const RuleEventLogListStatusFilter = (props: RuleEventLogListStatusFilter
|
|||
closePopover={() => setIsPopoverOpen(false)}
|
||||
button={
|
||||
<EuiFilterButton
|
||||
data-test-subj="ruleEventLogStatusFilterButton"
|
||||
data-test-subj="eventLogStatusFilterButton"
|
||||
iconType="arrowDown"
|
||||
hasActiveFilters={selectedOptions.length > 0}
|
||||
numActiveFilters={selectedOptions.length}
|
||||
|
@ -56,7 +56,7 @@ export const RuleEventLogListStatusFilter = (props: RuleEventLogListStatusFilter
|
|||
onClick={onClick}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.triggersActionsUI.sections.ruleDetails.eventLogStatusFilterLabel"
|
||||
id="xpack.triggersActionsUI.sections.eventLogStatusFilterLabel"
|
||||
defaultMessage="Response"
|
||||
/>
|
||||
</EuiFilterButton>
|
||||
|
@ -67,11 +67,11 @@ export const RuleEventLogListStatusFilter = (props: RuleEventLogListStatusFilter
|
|||
return (
|
||||
<EuiFilterSelectItem
|
||||
key={status}
|
||||
data-test-subj={`ruleEventLogStatusFilter-${status}`}
|
||||
data-test-subj={`eventLogStatusFilter-${status}`}
|
||||
onClick={onFilterItemClick(status)}
|
||||
checked={selectedOptions.includes(status) ? 'on' : undefined}
|
||||
>
|
||||
<RuleEventLogListStatus
|
||||
<EventLogListStatus
|
||||
status={status}
|
||||
useExecutionStatus={isRuleUsingExecutionStatus}
|
||||
/>
|
|
@ -9,12 +9,12 @@ import React, { useMemo } from 'react';
|
|||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { Pagination, EuiText } from '@elastic/eui';
|
||||
|
||||
export type RuleEventLogPaginationStatusProps = Pick<
|
||||
export type EventLogPaginationStatusProps = Pick<
|
||||
Pagination,
|
||||
'pageIndex' | 'pageSize' | 'totalItemCount'
|
||||
>;
|
||||
|
||||
export const RuleEventLogPaginationStatus = (props: RuleEventLogPaginationStatusProps) => {
|
||||
export const EventLogPaginationStatus = (props: EventLogPaginationStatusProps) => {
|
||||
const { pageIndex, pageSize, totalItemCount } = props;
|
||||
|
||||
const paginationStatusRange = useMemo(() => {
|
||||
|
@ -22,7 +22,7 @@ export const RuleEventLogPaginationStatus = (props: RuleEventLogPaginationStatus
|
|||
return (
|
||||
<strong>
|
||||
<FormattedMessage
|
||||
id="xpack.triggersActionsUI.sections.ruleDetails.ruleEventLogPaginationStatus.paginationResultsRangeNoResult"
|
||||
id="xpack.triggersActionsUI.sections.eventLogPaginationStatus.paginationResultsRangeNoResult"
|
||||
defaultMessage="0"
|
||||
/>
|
||||
</strong>
|
||||
|
@ -33,7 +33,7 @@ export const RuleEventLogPaginationStatus = (props: RuleEventLogPaginationStatus
|
|||
return (
|
||||
<strong>
|
||||
<FormattedMessage
|
||||
id="xpack.triggersActionsUI.sections.ruleDetails.ruleEventLogPaginationStatus.paginationResultsRange"
|
||||
id="xpack.triggersActionsUI.sections.eventLogPaginationStatus.paginationResultsRange"
|
||||
defaultMessage="{start, number} - {end, number}"
|
||||
values={{
|
||||
start: pageIndex * pageSize + 1,
|
||||
|
@ -45,9 +45,9 @@ export const RuleEventLogPaginationStatus = (props: RuleEventLogPaginationStatus
|
|||
}, [pageSize, pageIndex, totalItemCount]);
|
||||
|
||||
return (
|
||||
<EuiText data-test-subj="ruleEventLogPaginationStatus" size="xs">
|
||||
<EuiText data-test-subj="eventLogPaginationStatus" size="xs">
|
||||
<FormattedMessage
|
||||
id="xpack.triggersActionsUI.sections.ruleDetails.ruleEventLogPaginationStatus.paginationResults"
|
||||
id="xpack.triggersActionsUI.sections.eventLogPaginationStatus.paginationResults"
|
||||
defaultMessage="Showing {range} of {total, number} {type}"
|
||||
values={{
|
||||
range: paginationStatusRange,
|
||||
|
@ -55,7 +55,7 @@ export const RuleEventLogPaginationStatus = (props: RuleEventLogPaginationStatus
|
|||
type: (
|
||||
<strong>
|
||||
<FormattedMessage
|
||||
id="xpack.triggersActionsUI.sections.ruleDetails.ruleEventLogPaginationStatus.paginationResultsType"
|
||||
id="xpack.triggersActionsUI.sections.eventLogPaginationStatus.paginationResultsType"
|
||||
defaultMessage="log {total, plural, one {entry} other {entries}}"
|
||||
values={{ total: totalItemCount }}
|
||||
/>
|
|
@ -0,0 +1,34 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { EuiPanel, EuiFlexGroup, EuiFlexItem, EuiIconTip, EuiSpacer } from '@elastic/eui';
|
||||
|
||||
export const EventLogStat = ({
|
||||
title,
|
||||
tooltip,
|
||||
children,
|
||||
}: {
|
||||
title: string;
|
||||
tooltip: string;
|
||||
children?: JSX.Element;
|
||||
}) => {
|
||||
return (
|
||||
<EuiPanel color="subdued">
|
||||
<EuiFlexGroup gutterSize="xs">
|
||||
<EuiFlexItem grow={false}>
|
||||
<b>{title}</b>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiIconTip content={tooltip} position="top" />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer />
|
||||
{children}
|
||||
</EuiPanel>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
export { EventLogListCellRenderer } from './event_log_list_cell_renderer';
|
||||
export type { ColumnId } from './event_log_list_cell_renderer';
|
||||
export { EventLogListStatusFilter } from './event_log_list_status_filter';
|
||||
export { EventLogListStatus } from './event_log_list_status';
|
||||
export { EventLogPaginationStatus } from './event_log_pagination_status';
|
||||
export {
|
||||
EventLogDataGrid,
|
||||
getIsColumnSortable,
|
||||
ColumnHeaderWithToolTip,
|
||||
numTriggeredActionsDisplay,
|
||||
numGeneratedActionsDisplay,
|
||||
numSucceededActionsDisplay,
|
||||
numErroredActionsDisplay,
|
||||
} from './event_log_data_grid';
|
||||
export { EventLogStat } from './event_log_stat';
|
|
@ -42,14 +42,14 @@ export const RefineSearchPrompt = (props: RefineSearchFooterProps) => {
|
|||
return (
|
||||
<EuiText style={textStyles} textAlign="center" size="s">
|
||||
<FormattedMessage
|
||||
id="xpack.triggersActionsUI.sections.ruleDetails.refineSearchPrompt.prompt"
|
||||
id="xpack.triggersActionsUI.sections.refineSearchPrompt.prompt"
|
||||
defaultMessage="These are the first {visibleDocumentSize} documents matching your search, refine your search to see others."
|
||||
values={{ visibleDocumentSize }}
|
||||
/>
|
||||
|
||||
<EuiLink href={`#${backToTopAnchor}`}>
|
||||
<FormattedMessage
|
||||
id="xpack.triggersActionsUI.sections.ruleDetails.refineSearchPrompt.backToTop"
|
||||
id="xpack.triggersActionsUI.sections.refineSearchPrompt.backToTop"
|
||||
defaultMessage="Back to top."
|
||||
/>
|
||||
</EuiLink>
|
|
@ -7,12 +7,25 @@
|
|||
|
||||
import React from 'react';
|
||||
|
||||
import { IExecutionLogResult, IExecutionKPIResult } from '@kbn/actions-plugin/common';
|
||||
import { ActionType } from '../../../../types';
|
||||
import { loadActionTypes } from '../../../lib/action_connector_api';
|
||||
import {
|
||||
loadActionTypes,
|
||||
LoadGlobalConnectorExecutionLogAggregationsProps,
|
||||
loadGlobalConnectorExecutionLogAggregations,
|
||||
LoadGlobalConnectorExecutionKPIAggregationsProps,
|
||||
loadGlobalConnectorExecutionKPIAggregations,
|
||||
} from '../../../lib/action_connector_api';
|
||||
import { useKibana } from '../../../../common/lib/kibana';
|
||||
|
||||
export interface ComponentOpts {
|
||||
loadActionTypes: () => Promise<ActionType[]>;
|
||||
loadGlobalConnectorExecutionLogAggregations: (
|
||||
props: LoadGlobalConnectorExecutionLogAggregationsProps
|
||||
) => Promise<IExecutionLogResult>;
|
||||
loadGlobalConnectorExecutionKPIAggregations: (
|
||||
props: LoadGlobalConnectorExecutionKPIAggregationsProps
|
||||
) => Promise<IExecutionKPIResult>;
|
||||
}
|
||||
|
||||
export type PropsWithOptionalApiHandlers<T> = Omit<T, keyof ComponentOpts> & Partial<ComponentOpts>;
|
||||
|
@ -23,7 +36,26 @@ export function withActionOperations<T>(
|
|||
return (props: PropsWithOptionalApiHandlers<T>) => {
|
||||
const { http } = useKibana().services;
|
||||
return (
|
||||
<WrappedComponent {...(props as T)} loadActionTypes={async () => loadActionTypes({ http })} />
|
||||
<WrappedComponent
|
||||
{...(props as T)}
|
||||
loadActionTypes={async () => loadActionTypes({ http })}
|
||||
loadGlobalConnectorExecutionLogAggregations={async (
|
||||
loadProps: LoadGlobalConnectorExecutionLogAggregationsProps
|
||||
) =>
|
||||
loadGlobalConnectorExecutionLogAggregations({
|
||||
...loadProps,
|
||||
http,
|
||||
})
|
||||
}
|
||||
loadGlobalConnectorExecutionKPIAggregations={async (
|
||||
loadGlobalExecutionKPIAggregationsProps: LoadGlobalConnectorExecutionKPIAggregationsProps
|
||||
) =>
|
||||
loadGlobalConnectorExecutionKPIAggregations({
|
||||
...loadGlobalExecutionKPIAggregationsProps,
|
||||
http,
|
||||
})
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
|
|
@ -12,7 +12,7 @@ import { useKibana } from '../../../../common/lib/kibana';
|
|||
|
||||
import { EuiSuperDatePicker } from '@elastic/eui';
|
||||
import { Rule } from '../../../../types';
|
||||
import { RefineSearchPrompt } from '../refine_search_prompt';
|
||||
import { RefineSearchPrompt } from '../../common/components/refine_search_prompt';
|
||||
import { RuleErrorLog } from './rule_error_log';
|
||||
|
||||
const useKibanaMock = useKibana as jest.Mocked<typeof useKibana>;
|
||||
|
|
|
@ -24,12 +24,12 @@ import {
|
|||
import { IExecutionErrors } from '@kbn/alerting-plugin/common';
|
||||
import { useKibana } from '../../../../common/lib/kibana';
|
||||
|
||||
import { RefineSearchPrompt } from '../refine_search_prompt';
|
||||
import { RefineSearchPrompt } from '../../common/components/refine_search_prompt';
|
||||
import {
|
||||
ComponentOpts as RuleApis,
|
||||
withBulkRuleOperations,
|
||||
} from '../../common/components/with_bulk_rule_api_operations';
|
||||
import { RuleEventLogListCellRenderer } from './rule_event_log_list_cell_renderer';
|
||||
import { EventLogListCellRenderer } from '../../common/components/event_log';
|
||||
|
||||
const getParsedDate = (date: string) => {
|
||||
if (date.includes('now')) {
|
||||
|
@ -203,7 +203,7 @@ export const RuleErrorLog = (props: RuleErrorLogProps) => {
|
|||
}
|
||||
),
|
||||
render: (date: string) => (
|
||||
<RuleEventLogListCellRenderer columnId="timestamp" value={date} dateFormat={dateFormat} />
|
||||
<EventLogListCellRenderer columnId="timestamp" value={date} dateFormat={dateFormat} />
|
||||
),
|
||||
sortable: true,
|
||||
width: '250px',
|
||||
|
|
|
@ -1,655 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useMemo, useCallback } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import {
|
||||
EuiDataGrid,
|
||||
EuiDataGridStyle,
|
||||
Pagination,
|
||||
EuiDataGridCellValueElementProps,
|
||||
EuiDataGridSorting,
|
||||
EuiDataGridColumn,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiSpacer,
|
||||
EuiBadge,
|
||||
EuiDataGridCellPopoverElementProps,
|
||||
useEuiTheme,
|
||||
EuiToolTip,
|
||||
EuiText,
|
||||
EuiIcon,
|
||||
} from '@elastic/eui';
|
||||
import {
|
||||
IExecutionLog,
|
||||
executionLogSortableColumns,
|
||||
ExecutionLogSortFields,
|
||||
} from '@kbn/alerting-plugin/common';
|
||||
import { getIsExperimentalFeatureEnabled } from '../../../../common/get_experimental_features';
|
||||
import { RuleEventLogListCellRenderer, ColumnId } from './rule_event_log_list_cell_renderer';
|
||||
import { RuleEventLogPaginationStatus } from './rule_event_log_pagination_status';
|
||||
import { RuleActionErrorBadge } from './rule_action_error_badge';
|
||||
import './rule_event_log_list.scss';
|
||||
|
||||
const getIsColumnSortable = (columnId: string) => {
|
||||
return executionLogSortableColumns.includes(columnId as ExecutionLogSortFields);
|
||||
};
|
||||
|
||||
const getErroredActionsTranslation = (errors: number) => {
|
||||
return i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.ruleDetails.ruleEventLogDataGrid.erroredActionsTooltip',
|
||||
{
|
||||
defaultMessage: '{value, plural, one {# errored action} other {# errored actions}}',
|
||||
values: { value: errors },
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const PAGE_SIZE_OPTIONS = [10, 50, 100];
|
||||
|
||||
export interface RuleEventLogDataGrid {
|
||||
logs: IExecutionLog[];
|
||||
pagination: Pagination;
|
||||
sortingColumns: EuiDataGridSorting['columns'];
|
||||
visibleColumns: string[];
|
||||
dateFormat: string;
|
||||
pageSizeOptions?: number[];
|
||||
selectedRunLog?: IExecutionLog;
|
||||
showRuleNameAndIdColumns?: boolean;
|
||||
showSpaceColumns?: boolean;
|
||||
onChangeItemsPerPage: (pageSize: number) => void;
|
||||
onChangePage: (pageIndex: number) => void;
|
||||
onFilterChange: (filter: string[]) => void;
|
||||
onFlyoutOpen: (runLog: IExecutionLog) => void;
|
||||
setVisibleColumns: (visibleColumns: string[]) => void;
|
||||
setSortingColumns: (sortingColumns: EuiDataGridSorting['columns']) => void;
|
||||
}
|
||||
|
||||
const numTriggeredActionsDisplay = i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.triggeredActions',
|
||||
{
|
||||
defaultMessage: 'Triggered actions',
|
||||
}
|
||||
);
|
||||
const numTriggeredActionsToolTip = i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.triggeredActionsToolTip',
|
||||
{
|
||||
defaultMessage: 'The subset of generated actions that will run.',
|
||||
}
|
||||
);
|
||||
|
||||
const numGeneratedActionsDisplay = i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.scheduledActions',
|
||||
{
|
||||
defaultMessage: 'Generated actions',
|
||||
}
|
||||
);
|
||||
const numGeneratedActionsToolTip = i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.scheduledActionsToolTip',
|
||||
{
|
||||
defaultMessage: 'The total number of actions generated when the rule ran.',
|
||||
}
|
||||
);
|
||||
|
||||
const numSucceededActionsDisplay = i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.succeededActions',
|
||||
{
|
||||
defaultMessage: 'Succeeded actions',
|
||||
}
|
||||
);
|
||||
const numSucceededActionsToolTip = i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.succeededActionsToolTip',
|
||||
{
|
||||
defaultMessage: 'The number of actions that were completed successfully.',
|
||||
}
|
||||
);
|
||||
|
||||
const numErroredActionsDisplay = i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.erroredActions',
|
||||
{
|
||||
defaultMessage: 'Errored actions',
|
||||
}
|
||||
);
|
||||
const numErroredActionsToolTip = i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.erroredActionsToolTip',
|
||||
{
|
||||
defaultMessage: 'The number of failed actions.',
|
||||
}
|
||||
);
|
||||
|
||||
const columnsWithToolTipMap: Record<string, Record<string, string>> = {
|
||||
num_triggered_actions: {
|
||||
display: numTriggeredActionsDisplay,
|
||||
toolTip: numTriggeredActionsToolTip,
|
||||
},
|
||||
num_generated_actions: {
|
||||
display: numGeneratedActionsDisplay,
|
||||
toolTip: numGeneratedActionsToolTip,
|
||||
},
|
||||
num_succeeded_actions: {
|
||||
display: numSucceededActionsDisplay,
|
||||
toolTip: numSucceededActionsToolTip,
|
||||
},
|
||||
num_errored_actions: {
|
||||
display: numErroredActionsDisplay,
|
||||
toolTip: numErroredActionsToolTip,
|
||||
},
|
||||
};
|
||||
|
||||
const ColumnHeaderWithToolTip = ({ id }: { id: string }) => {
|
||||
return (
|
||||
<EuiToolTip content={columnsWithToolTipMap[id].toolTip}>
|
||||
<EuiFlexGroup gutterSize="xs" alignItems="center">
|
||||
<EuiFlexItem>{columnsWithToolTipMap[id].display}</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiIcon size="s" color="subdued" type="questionInCircle" className="eui-alignTop" />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiToolTip>
|
||||
);
|
||||
};
|
||||
|
||||
export const RuleEventLogDataGrid = (props: RuleEventLogDataGrid) => {
|
||||
const {
|
||||
logs = [],
|
||||
sortingColumns,
|
||||
pageSizeOptions = PAGE_SIZE_OPTIONS,
|
||||
pagination,
|
||||
dateFormat,
|
||||
visibleColumns,
|
||||
selectedRunLog,
|
||||
showRuleNameAndIdColumns = false,
|
||||
showSpaceColumns = false,
|
||||
setVisibleColumns,
|
||||
setSortingColumns,
|
||||
onChangeItemsPerPage,
|
||||
onChangePage,
|
||||
onFilterChange,
|
||||
onFlyoutOpen,
|
||||
} = props;
|
||||
|
||||
const { euiTheme } = useEuiTheme();
|
||||
|
||||
const isRuleUsingExecutionStatus = getIsExperimentalFeatureEnabled('ruleUseExecutionStatus');
|
||||
|
||||
const getPaginatedRowIndex = useCallback(
|
||||
(rowIndex: number) => {
|
||||
const { pageIndex, pageSize } = pagination;
|
||||
return rowIndex - pageIndex * pageSize;
|
||||
},
|
||||
[pagination]
|
||||
);
|
||||
|
||||
const columns: EuiDataGridColumn[] = useMemo(
|
||||
() => [
|
||||
...(showRuleNameAndIdColumns
|
||||
? [
|
||||
{
|
||||
id: 'rule_id',
|
||||
displayAsText: i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.ruleId',
|
||||
{
|
||||
defaultMessage: 'Rule Id',
|
||||
}
|
||||
),
|
||||
isSortable: getIsColumnSortable('rule_id'),
|
||||
actions: {
|
||||
showSortAsc: false,
|
||||
showSortDesc: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'rule_name',
|
||||
displayAsText: i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.ruleName',
|
||||
{
|
||||
defaultMessage: 'Rule',
|
||||
}
|
||||
),
|
||||
isSortable: getIsColumnSortable('rule_name'),
|
||||
actions: {
|
||||
showSortAsc: false,
|
||||
showSortDesc: false,
|
||||
showHide: false,
|
||||
},
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(showSpaceColumns
|
||||
? [
|
||||
{
|
||||
id: 'space_ids',
|
||||
displayAsText: i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.spaceIds',
|
||||
{
|
||||
defaultMessage: 'Space',
|
||||
}
|
||||
),
|
||||
isSortable: getIsColumnSortable('space_ids'),
|
||||
actions: {
|
||||
showSortAsc: false,
|
||||
showSortDesc: false,
|
||||
showHide: false,
|
||||
},
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
id: 'id',
|
||||
displayAsText: i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.id',
|
||||
{
|
||||
defaultMessage: 'Id',
|
||||
}
|
||||
),
|
||||
isSortable: getIsColumnSortable('id'),
|
||||
},
|
||||
{
|
||||
id: 'timestamp',
|
||||
displayAsText: i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.timestamp',
|
||||
{
|
||||
defaultMessage: 'Timestamp',
|
||||
}
|
||||
),
|
||||
isSortable: getIsColumnSortable('timestamp'),
|
||||
isResizable: false,
|
||||
actions: {
|
||||
showHide: false,
|
||||
},
|
||||
initialWidth: 250,
|
||||
},
|
||||
{
|
||||
id: 'execution_duration',
|
||||
displayAsText: i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.duration',
|
||||
{
|
||||
defaultMessage: 'Duration',
|
||||
}
|
||||
),
|
||||
isSortable: getIsColumnSortable('execution_duration'),
|
||||
isResizable: false,
|
||||
actions: {
|
||||
showHide: false,
|
||||
},
|
||||
initialWidth: 100,
|
||||
},
|
||||
{
|
||||
id: 'status',
|
||||
displayAsText: i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.response',
|
||||
{
|
||||
defaultMessage: 'Response',
|
||||
}
|
||||
),
|
||||
actions: {
|
||||
showHide: false,
|
||||
showSortAsc: false,
|
||||
showSortDesc: false,
|
||||
additional: [
|
||||
{
|
||||
iconType: 'annotation',
|
||||
label: i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.showOnlyFailures',
|
||||
{
|
||||
defaultMessage: 'Show only failures',
|
||||
}
|
||||
),
|
||||
onClick: () => onFilterChange(['failure']),
|
||||
size: 'xs',
|
||||
},
|
||||
{
|
||||
iconType: 'annotation',
|
||||
label: i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.showAll',
|
||||
{
|
||||
defaultMessage: 'Show all',
|
||||
}
|
||||
),
|
||||
onClick: () => onFilterChange([]),
|
||||
size: 'xs',
|
||||
},
|
||||
],
|
||||
},
|
||||
isSortable: getIsColumnSortable('status'),
|
||||
isResizable: false,
|
||||
initialWidth: 150,
|
||||
},
|
||||
{
|
||||
id: 'message',
|
||||
actions: {
|
||||
showSortAsc: false,
|
||||
showSortDesc: false,
|
||||
},
|
||||
displayAsText: i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.message',
|
||||
{
|
||||
defaultMessage: 'Message',
|
||||
}
|
||||
),
|
||||
isSortable: getIsColumnSortable('message'),
|
||||
cellActions: [
|
||||
({ rowIndex, Component }) => {
|
||||
const pagedRowIndex = getPaginatedRowIndex(rowIndex);
|
||||
const runLog = logs[pagedRowIndex];
|
||||
const actionErrors = runLog?.num_errored_actions as number;
|
||||
if (actionErrors) {
|
||||
return (
|
||||
<Component onClick={() => onFlyoutOpen(runLog)} iconType="alert">
|
||||
<FormattedMessage
|
||||
id="xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.viewActionErrors"
|
||||
defaultMessage="View action errors"
|
||||
/>
|
||||
</Component>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'num_active_alerts',
|
||||
displayAsText: i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.activeAlerts',
|
||||
{
|
||||
defaultMessage: 'Active alerts',
|
||||
}
|
||||
),
|
||||
initialWidth: 140,
|
||||
isSortable: getIsColumnSortable('num_active_alerts'),
|
||||
},
|
||||
{
|
||||
id: 'num_new_alerts',
|
||||
displayAsText: i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.newAlerts',
|
||||
{
|
||||
defaultMessage: 'New alerts',
|
||||
}
|
||||
),
|
||||
initialWidth: 140,
|
||||
isSortable: getIsColumnSortable('num_new_alerts'),
|
||||
},
|
||||
{
|
||||
id: 'num_recovered_alerts',
|
||||
displayAsText: i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.recoveredAlerts',
|
||||
{
|
||||
defaultMessage: 'Recovered alerts',
|
||||
}
|
||||
),
|
||||
isSortable: getIsColumnSortable('num_recovered_alerts'),
|
||||
},
|
||||
{
|
||||
id: 'num_triggered_actions',
|
||||
displayAsText: numTriggeredActionsDisplay,
|
||||
display: <ColumnHeaderWithToolTip id="num_triggered_actions" />,
|
||||
isSortable: getIsColumnSortable('num_triggered_actions'),
|
||||
},
|
||||
{
|
||||
id: 'num_generated_actions',
|
||||
displayAsText: numGeneratedActionsDisplay,
|
||||
display: <ColumnHeaderWithToolTip id="num_generated_actions" />,
|
||||
isSortable: getIsColumnSortable('num_generated_actions'),
|
||||
},
|
||||
{
|
||||
id: 'num_succeeded_actions',
|
||||
displayAsText: numSucceededActionsDisplay,
|
||||
display: <ColumnHeaderWithToolTip id="num_succeeded_actions" />,
|
||||
isSortable: getIsColumnSortable('num_succeeded_actions'),
|
||||
},
|
||||
{
|
||||
id: 'num_errored_actions',
|
||||
actions: {
|
||||
showSortAsc: false,
|
||||
showSortDesc: false,
|
||||
},
|
||||
displayAsText: numErroredActionsDisplay,
|
||||
display: <ColumnHeaderWithToolTip id="num_errored_actions" />,
|
||||
isSortable: getIsColumnSortable('num_errored_actions'),
|
||||
},
|
||||
{
|
||||
id: 'total_search_duration',
|
||||
displayAsText: i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.totalSearchDuration',
|
||||
{
|
||||
defaultMessage: 'Total search duration',
|
||||
}
|
||||
),
|
||||
isSortable: getIsColumnSortable('total_search_duration'),
|
||||
},
|
||||
{
|
||||
id: 'es_search_duration',
|
||||
displayAsText: i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.esSearchDuration',
|
||||
{
|
||||
defaultMessage: 'ES search duration',
|
||||
}
|
||||
),
|
||||
isSortable: getIsColumnSortable('es_search_duration'),
|
||||
},
|
||||
{
|
||||
id: 'schedule_delay',
|
||||
displayAsText: i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.scheduleDelay',
|
||||
{
|
||||
defaultMessage: 'Schedule delay',
|
||||
}
|
||||
),
|
||||
isSortable: getIsColumnSortable('schedule_delay'),
|
||||
},
|
||||
{
|
||||
id: 'timed_out',
|
||||
displayAsText: i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.timedOut',
|
||||
{
|
||||
defaultMessage: 'Timed out',
|
||||
}
|
||||
),
|
||||
isSortable: getIsColumnSortable('timed_out'),
|
||||
},
|
||||
],
|
||||
[
|
||||
getPaginatedRowIndex,
|
||||
onFlyoutOpen,
|
||||
onFilterChange,
|
||||
showRuleNameAndIdColumns,
|
||||
showSpaceColumns,
|
||||
logs,
|
||||
]
|
||||
);
|
||||
|
||||
const columnVisibilityProps = useMemo(() => {
|
||||
return {
|
||||
visibleColumns,
|
||||
setVisibleColumns,
|
||||
};
|
||||
}, [visibleColumns, setVisibleColumns]);
|
||||
|
||||
const sortingProps = useMemo(
|
||||
() => ({
|
||||
onSort: setSortingColumns,
|
||||
columns: sortingColumns,
|
||||
}),
|
||||
[setSortingColumns, sortingColumns]
|
||||
);
|
||||
|
||||
const paginationProps = useMemo(
|
||||
() => ({
|
||||
...pagination,
|
||||
pageSizeOptions,
|
||||
onChangeItemsPerPage,
|
||||
onChangePage,
|
||||
}),
|
||||
[pagination, pageSizeOptions, onChangeItemsPerPage, onChangePage]
|
||||
);
|
||||
|
||||
const rowClasses = useMemo(() => {
|
||||
if (!selectedRunLog) {
|
||||
return {};
|
||||
}
|
||||
const index = logs.findIndex((log) => log.id === selectedRunLog.id);
|
||||
return {
|
||||
[index]: 'ruleEventLogDataGrid--rowClassSelected',
|
||||
};
|
||||
}, [selectedRunLog, logs]);
|
||||
|
||||
const gridStyles: EuiDataGridStyle = useMemo(() => {
|
||||
return {
|
||||
border: 'horizontal',
|
||||
header: 'underline',
|
||||
rowClasses,
|
||||
};
|
||||
}, [rowClasses]);
|
||||
|
||||
const renderMessageWithActionError = (
|
||||
columnId: string,
|
||||
errors: number,
|
||||
showTooltip: boolean = false
|
||||
) => {
|
||||
if (columnId !== 'message') {
|
||||
return null;
|
||||
}
|
||||
if (!errors) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<EuiFlexItem grow={false}>
|
||||
{showTooltip ? (
|
||||
<EuiToolTip content={getErroredActionsTranslation(errors)}>
|
||||
<RuleActionErrorBadge totalErrors={errors} showIcon />
|
||||
</EuiToolTip>
|
||||
) : (
|
||||
<RuleActionErrorBadge totalErrors={errors} showIcon />
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
);
|
||||
};
|
||||
|
||||
// Renders the cell popover for runs with errored actions
|
||||
const renderCellPopover = (cellPopoverProps: EuiDataGridCellPopoverElementProps) => {
|
||||
const { columnId, rowIndex, cellActions, DefaultCellPopover } = cellPopoverProps;
|
||||
|
||||
if (columnId !== 'message') {
|
||||
return <DefaultCellPopover {...cellPopoverProps} />;
|
||||
}
|
||||
|
||||
const pagedRowIndex = getPaginatedRowIndex(rowIndex);
|
||||
const runLog = logs[pagedRowIndex];
|
||||
|
||||
if (!runLog) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const value = runLog[columnId as keyof IExecutionLog] as string;
|
||||
const actionErrors = runLog.num_errored_actions || (0 as number);
|
||||
|
||||
return (
|
||||
<div style={{ width: '100%' }}>
|
||||
<EuiSpacer size="s" />
|
||||
<div>
|
||||
<EuiText size="m">{value}</EuiText>
|
||||
</div>
|
||||
<EuiSpacer size="s" />
|
||||
{actionErrors > 0 && (
|
||||
<>
|
||||
<EuiSpacer size="l" />
|
||||
<EuiFlexGroup gutterSize="none" alignItems="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
{renderMessageWithActionError(columnId, actionErrors)}
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
|
||||
<FormattedMessage
|
||||
id="xpack.triggersActionsUI.sections.ruleDetails.ruleEventLogDataGrid.erroredActionsCellPopover"
|
||||
defaultMessage="{value, plural, one {errored action} other {errored actions}}"
|
||||
values={{
|
||||
value: actionErrors,
|
||||
}}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer size="s" />
|
||||
{cellActions}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Main cell renderer, renders durations, statuses, etc.
|
||||
const renderCell = ({ rowIndex, columnId }: EuiDataGridCellValueElementProps) => {
|
||||
const pagedRowIndex = getPaginatedRowIndex(rowIndex);
|
||||
|
||||
const runLog = logs[pagedRowIndex];
|
||||
const value = logs[pagedRowIndex]?.[columnId as keyof IExecutionLog] as string;
|
||||
const actionErrors = logs[pagedRowIndex]?.num_errored_actions || (0 as number);
|
||||
const version = logs?.[pagedRowIndex]?.version;
|
||||
const ruleId = runLog?.rule_id;
|
||||
const spaceIds = runLog?.space_ids;
|
||||
|
||||
if (columnId === 'num_errored_actions' && runLog) {
|
||||
return (
|
||||
<EuiBadge
|
||||
data-test-subj="ruleEventLogDataGridErroredActionBadge"
|
||||
style={{
|
||||
cursor: 'pointer',
|
||||
borderRadius: euiTheme.border.radius.medium,
|
||||
}}
|
||||
color="hollow"
|
||||
onClick={() => onFlyoutOpen(runLog)}
|
||||
onClickAriaLabel={i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.openActionErrorsFlyout',
|
||||
{
|
||||
defaultMessage: 'Open action errors flyout',
|
||||
}
|
||||
)}
|
||||
>
|
||||
{value}
|
||||
</EuiBadge>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<EuiFlexGroup gutterSize="s" alignItems="center">
|
||||
{renderMessageWithActionError(columnId, actionErrors, true)}
|
||||
<EuiFlexItem>
|
||||
<RuleEventLogListCellRenderer
|
||||
columnId={columnId as ColumnId}
|
||||
value={value}
|
||||
version={version}
|
||||
dateFormat={dateFormat}
|
||||
ruleId={ruleId}
|
||||
spaceIds={spaceIds}
|
||||
useExecutionStatus={isRuleUsingExecutionStatus}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<RuleEventLogPaginationStatus
|
||||
pageIndex={pagination.pageIndex}
|
||||
pageSize={pagination.pageSize}
|
||||
totalItemCount={pagination.totalItemCount}
|
||||
/>
|
||||
<EuiDataGrid
|
||||
aria-label="rule event log"
|
||||
data-test-subj="ruleEventLogList"
|
||||
columns={columns}
|
||||
rowCount={pagination.totalItemCount}
|
||||
renderCellValue={renderCell}
|
||||
columnVisibility={columnVisibilityProps}
|
||||
sorting={sortingProps}
|
||||
pagination={paginationProps}
|
||||
gridStyle={gridStyles}
|
||||
renderCellPopover={renderCellPopover}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -12,9 +12,9 @@ import { mountWithIntl, nextTick } from '@kbn/test-jest-helpers';
|
|||
import { useKibana } from '../../../../common/lib/kibana';
|
||||
import { ActionGroup, ALERTS_FEATURE_ID } from '@kbn/alerting-plugin/common';
|
||||
import { EuiSuperDatePicker, EuiDataGrid } from '@elastic/eui';
|
||||
import { RuleEventLogListStatusFilter } from './rule_event_log_list_status_filter';
|
||||
import { EventLogListStatusFilter } from '../../common/components/event_log';
|
||||
import { RuleEventLogList } from './rule_event_log_list';
|
||||
import { RefineSearchPrompt } from '../refine_search_prompt';
|
||||
import { RefineSearchPrompt } from '../../common/components/refine_search_prompt';
|
||||
import {
|
||||
RULE_EXECUTION_DEFAULT_INITIAL_VISIBLE_COLUMNS,
|
||||
GLOBAL_EXECUTION_DEFAULT_INITIAL_VISIBLE_COLUMNS,
|
||||
|
@ -124,7 +124,7 @@ describe.skip('rule_event_log_list', () => {
|
|||
|
||||
expect(wrapper.find(EuiSuperDatePicker).props().isLoading).toBeFalsy();
|
||||
|
||||
expect(wrapper.find(RuleEventLogListStatusFilter).exists()).toBeTruthy();
|
||||
expect(wrapper.find(EventLogListStatusFilter).exists()).toBeTruthy();
|
||||
expect(wrapper.find('[data-gridcell-column-id="timestamp"]').length).toEqual(5);
|
||||
expect(wrapper.find(EuiDataGrid).props().rowCount).toEqual(mockLogResponse.total);
|
||||
});
|
||||
|
@ -252,9 +252,9 @@ describe.skip('rule_event_log_list', () => {
|
|||
});
|
||||
|
||||
// Filter by success
|
||||
wrapper.find('[data-test-subj="ruleEventLogStatusFilterButton"]').at(0).simulate('click');
|
||||
wrapper.find('[data-test-subj="eventLogStatusFilterButton"]').at(0).simulate('click');
|
||||
|
||||
wrapper.find('[data-test-subj="ruleEventLogStatusFilter-success"]').at(0).simulate('click');
|
||||
wrapper.find('[data-test-subj="eventLogStatusFilter-success"]').at(0).simulate('click');
|
||||
|
||||
await act(async () => {
|
||||
await nextTick();
|
||||
|
@ -272,9 +272,9 @@ describe.skip('rule_event_log_list', () => {
|
|||
);
|
||||
|
||||
// Filter by failure as well
|
||||
wrapper.find('[data-test-subj="ruleEventLogStatusFilterButton"]').at(0).simulate('click');
|
||||
wrapper.find('[data-test-subj="eventLogStatusFilterButton"]').at(0).simulate('click');
|
||||
|
||||
wrapper.find('[data-test-subj="ruleEventLogStatusFilter-failure"]').at(0).simulate('click');
|
||||
wrapper.find('[data-test-subj="eventLogStatusFilter-failure"]').at(0).simulate('click');
|
||||
|
||||
await act(async () => {
|
||||
await nextTick();
|
||||
|
@ -549,7 +549,7 @@ describe.skip('rule_event_log_list', () => {
|
|||
wrapper.update();
|
||||
});
|
||||
|
||||
expect(wrapper.find('[data-test-subj="ruleEventLogPaginationStatus"]').first().text()).toEqual(
|
||||
expect(wrapper.find('[data-test-subj="eventLogPaginationStatus"]').first().text()).toEqual(
|
||||
'Showing 0 of 0 log entries'
|
||||
);
|
||||
});
|
||||
|
@ -576,7 +576,7 @@ describe.skip('rule_event_log_list', () => {
|
|||
wrapper.update();
|
||||
});
|
||||
|
||||
expect(wrapper.find('[data-test-subj="ruleEventLogPaginationStatus"]').first().text()).toEqual(
|
||||
expect(wrapper.find('[data-test-subj="eventLogPaginationStatus"]').first().text()).toEqual(
|
||||
'Showing 1 - 1 of 1 log entry'
|
||||
);
|
||||
});
|
||||
|
@ -603,7 +603,7 @@ describe.skip('rule_event_log_list', () => {
|
|||
wrapper.update();
|
||||
});
|
||||
|
||||
expect(wrapper.find('[data-test-subj="ruleEventLogPaginationStatus"]').first().text()).toEqual(
|
||||
expect(wrapper.find('[data-test-subj="eventLogPaginationStatus"]').first().text()).toEqual(
|
||||
'Showing 1 - 10 of 85 log entries'
|
||||
);
|
||||
|
||||
|
@ -614,7 +614,7 @@ describe.skip('rule_event_log_list', () => {
|
|||
wrapper.update();
|
||||
});
|
||||
|
||||
expect(wrapper.find('[data-test-subj="ruleEventLogPaginationStatus"]').first().text()).toEqual(
|
||||
expect(wrapper.find('[data-test-subj="eventLogPaginationStatus"]').first().text()).toEqual(
|
||||
'Showing 11 - 20 of 85 log entries'
|
||||
);
|
||||
|
||||
|
@ -625,7 +625,7 @@ describe.skip('rule_event_log_list', () => {
|
|||
wrapper.update();
|
||||
});
|
||||
|
||||
expect(wrapper.find('[data-test-subj="ruleEventLogPaginationStatus"]').first().text()).toEqual(
|
||||
expect(wrapper.find('[data-test-subj="eventLogPaginationStatus"]').first().text()).toEqual(
|
||||
'Showing 81 - 85 of 85 log entries'
|
||||
);
|
||||
});
|
||||
|
@ -674,10 +674,7 @@ describe.skip('rule_event_log_list', () => {
|
|||
expect(wrapper.find('[data-test-subj="ruleActionErrorBadge"]').first().text()).toEqual('4');
|
||||
|
||||
// Click to open flyout
|
||||
wrapper
|
||||
.find('[data-test-subj="ruleEventLogDataGridErroredActionBadge"]')
|
||||
.first()
|
||||
.simulate('click');
|
||||
wrapper.find('[data-test-subj="eventLogDataGridErroredActionBadge"]').first().simulate('click');
|
||||
expect(wrapper.find('[data-test-subj="ruleActionErrorLogFlyout"]').exists()).toBeTruthy();
|
||||
});
|
||||
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
import React, { useEffect, useState, useMemo, useRef } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import datemath from '@kbn/datemath';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiPanel, EuiIconTip, EuiStat, EuiSpacer } from '@elastic/eui';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiStat, EuiSpacer } from '@elastic/eui';
|
||||
import { IExecutionKPIResult } from '@kbn/alerting-plugin/common';
|
||||
import {
|
||||
ComponentOpts as RuleApis,
|
||||
|
@ -16,7 +16,7 @@ import {
|
|||
} from '../../common/components/with_bulk_rule_api_operations';
|
||||
import { getIsExperimentalFeatureEnabled } from '../../../../common/get_experimental_features';
|
||||
import { useKibana } from '../../../../common/lib/kibana';
|
||||
import { RuleEventLogListStatus } from './rule_event_log_list_status';
|
||||
import { EventLogListStatus, EventLogStat } from '../../common/components/event_log';
|
||||
|
||||
const getParsedDate = (date: string) => {
|
||||
if (date.includes('now')) {
|
||||
|
@ -53,31 +53,6 @@ const ACTIONS_TOOLTIP = i18n.translate(
|
|||
}
|
||||
);
|
||||
|
||||
const Stat = ({
|
||||
title,
|
||||
tooltip,
|
||||
children,
|
||||
}: {
|
||||
title: string;
|
||||
tooltip: string;
|
||||
children?: JSX.Element;
|
||||
}) => {
|
||||
return (
|
||||
<EuiPanel color="subdued">
|
||||
<EuiFlexGroup justifyContent="spaceBetween">
|
||||
<EuiFlexItem grow={false}>
|
||||
<b>{title}</b>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiIconTip content={tooltip} position="top" />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer />
|
||||
{children}
|
||||
</EuiPanel>
|
||||
);
|
||||
};
|
||||
|
||||
export type RuleEventLogListKPIProps = {
|
||||
ruleId: string;
|
||||
dateStart: string;
|
||||
|
@ -165,13 +140,13 @@ export const RuleEventLogListKPI = (props: RuleEventLogListKPIProps) => {
|
|||
return (
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem grow={4}>
|
||||
<Stat title="Response" tooltip={RESPONSE_TOOLTIP}>
|
||||
<EventLogStat title="Responses" tooltip={RESPONSE_TOOLTIP}>
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<EuiStat
|
||||
data-test-subj="ruleEventLogKpi-successOutcome"
|
||||
description={getStatDescription(
|
||||
<RuleEventLogListStatus
|
||||
<EventLogListStatus
|
||||
status="success"
|
||||
useExecutionStatus={isRuleUsingExecutionStatus}
|
||||
/>
|
||||
|
@ -185,7 +160,7 @@ export const RuleEventLogListKPI = (props: RuleEventLogListKPIProps) => {
|
|||
<EuiStat
|
||||
data-test-subj="ruleEventLogKpi-warningOutcome"
|
||||
description={getStatDescription(
|
||||
<RuleEventLogListStatus
|
||||
<EventLogListStatus
|
||||
status="warning"
|
||||
useExecutionStatus={isRuleUsingExecutionStatus}
|
||||
/>
|
||||
|
@ -199,7 +174,7 @@ export const RuleEventLogListKPI = (props: RuleEventLogListKPIProps) => {
|
|||
<EuiStat
|
||||
data-test-subj="ruleEventLogKpi-failureOutcome"
|
||||
description={getStatDescription(
|
||||
<RuleEventLogListStatus
|
||||
<EventLogListStatus
|
||||
status="failure"
|
||||
useExecutionStatus={isRuleUsingExecutionStatus}
|
||||
/>
|
||||
|
@ -210,10 +185,10 @@ export const RuleEventLogListKPI = (props: RuleEventLogListKPIProps) => {
|
|||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</Stat>
|
||||
</EventLogStat>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={4}>
|
||||
<Stat title="Alerts" tooltip={ALERTS_TOOLTIP}>
|
||||
<EventLogStat title="Alerts" tooltip={ALERTS_TOOLTIP}>
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<EuiStat
|
||||
|
@ -243,10 +218,10 @@ export const RuleEventLogListKPI = (props: RuleEventLogListKPIProps) => {
|
|||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</Stat>
|
||||
</EventLogStat>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={2}>
|
||||
<Stat title="Actions" tooltip={ACTIONS_TOOLTIP}>
|
||||
<EventLogStat title="Actions" tooltip={ACTIONS_TOOLTIP}>
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<EuiStat
|
||||
|
@ -267,7 +242,7 @@ export const RuleEventLogListKPI = (props: RuleEventLogListKPIProps) => {
|
|||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</Stat>
|
||||
</EventLogStat>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
import React, { useCallback, useEffect, useState, useMemo, useRef } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import datemath from '@kbn/datemath';
|
||||
import {
|
||||
EuiFieldSearch,
|
||||
|
@ -19,20 +20,29 @@ import {
|
|||
EuiSuperDatePicker,
|
||||
OnTimeChangeProps,
|
||||
EuiSwitch,
|
||||
EuiDataGridColumn,
|
||||
} from '@elastic/eui';
|
||||
import { IExecutionLog } from '@kbn/alerting-plugin/common';
|
||||
import { SpacesContextProps } from '@kbn/spaces-plugin/public';
|
||||
import { useKibana, useSpacesData } from '../../../../common/lib/kibana';
|
||||
import { useKibana } from '../../../../common/lib/kibana';
|
||||
import {
|
||||
RULE_EXECUTION_DEFAULT_INITIAL_VISIBLE_COLUMNS,
|
||||
GLOBAL_EXECUTION_DEFAULT_INITIAL_VISIBLE_COLUMNS,
|
||||
LOCKED_COLUMNS,
|
||||
} from '../../../constants';
|
||||
import { RuleEventLogListStatusFilter } from './rule_event_log_list_status_filter';
|
||||
import { RuleEventLogDataGrid } from './rule_event_log_data_grid';
|
||||
import {
|
||||
EventLogDataGrid,
|
||||
getIsColumnSortable,
|
||||
ColumnHeaderWithToolTip,
|
||||
numTriggeredActionsDisplay,
|
||||
numGeneratedActionsDisplay,
|
||||
numSucceededActionsDisplay,
|
||||
numErroredActionsDisplay,
|
||||
EventLogListStatusFilter,
|
||||
} from '../../common/components/event_log';
|
||||
import { CenterJustifiedSpinner } from '../../../components/center_justified_spinner';
|
||||
import { RuleActionErrorLogFlyout } from './rule_action_error_log_flyout';
|
||||
import { RefineSearchPrompt } from '../refine_search_prompt';
|
||||
import { RefineSearchPrompt } from '../../common/components/refine_search_prompt';
|
||||
import { RulesListDocLink } from '../../rules_list/components/rules_list_doc_link';
|
||||
import { LoadExecutionLogAggregationsProps } from '../../../lib/rule_api';
|
||||
import { RuleEventLogListKPIWithApi as RuleEventLogListKPI } from './rule_event_log_list_kpi';
|
||||
|
@ -40,6 +50,7 @@ import {
|
|||
ComponentOpts as RuleApis,
|
||||
withBulkRuleOperations,
|
||||
} from '../../common/components/with_bulk_rule_api_operations';
|
||||
import { useMultipleSpaces } from '../../../hooks/use_multiple_spaces';
|
||||
|
||||
const getEmptyFunctionComponent: React.FC<SpacesContextProps> = ({ children }) => <>{children}</>;
|
||||
|
||||
|
@ -170,23 +181,13 @@ export const RuleEventLogListTable = <T extends RuleEventLogListOptions>(
|
|||
);
|
||||
});
|
||||
|
||||
const spacesData = useSpacesData();
|
||||
const accessibleSpaceIds = useMemo(
|
||||
() => (spacesData ? [...spacesData.spacesMap.values()].map((e) => e.id) : []),
|
||||
[spacesData]
|
||||
);
|
||||
const areMultipleSpacesAccessible = useMemo(
|
||||
() => accessibleSpaceIds.length > 1,
|
||||
[accessibleSpaceIds]
|
||||
);
|
||||
const namespaces = useMemo(
|
||||
() => (showFromAllSpaces && spacesData ? accessibleSpaceIds : undefined),
|
||||
[showFromAllSpaces, spacesData, accessibleSpaceIds]
|
||||
);
|
||||
const activeSpace = useMemo(
|
||||
() => spacesData?.spacesMap.get(spacesData?.activeSpaceId),
|
||||
[spacesData]
|
||||
);
|
||||
const { onShowAllSpacesChange, canAccessMultipleSpaces, namespaces, activeSpace } =
|
||||
useMultipleSpaces({
|
||||
setShowFromAllSpaces,
|
||||
showFromAllSpaces,
|
||||
visibleColumns,
|
||||
setVisibleColumns,
|
||||
});
|
||||
|
||||
const isInitialized = useRef(false);
|
||||
|
||||
|
@ -255,6 +256,14 @@ export const RuleEventLogListTable = <T extends RuleEventLogListOptions>(
|
|||
setIsLoading(false);
|
||||
};
|
||||
|
||||
const getPaginatedRowIndex = useCallback(
|
||||
(rowIndex: number) => {
|
||||
const { pageIndex, pageSize } = pagination;
|
||||
return rowIndex - pageIndex * pageSize;
|
||||
},
|
||||
[pagination]
|
||||
);
|
||||
|
||||
const onChangeItemsPerPage = useCallback(
|
||||
(pageSize: number) => {
|
||||
setPagination((prevPagination) => ({
|
||||
|
@ -332,19 +341,277 @@ export const RuleEventLogListTable = <T extends RuleEventLogListOptions>(
|
|||
[search, setSearchText]
|
||||
);
|
||||
|
||||
const onShowAllSpacesChange = useCallback(() => {
|
||||
setShowFromAllSpaces((prev) => !prev);
|
||||
const nextShowFromAllSpaces = !showFromAllSpaces;
|
||||
|
||||
if (nextShowFromAllSpaces && !visibleColumns.includes('space_ids')) {
|
||||
const ruleNameIndex = visibleColumns.findIndex((c) => c === 'rule_name');
|
||||
const newVisibleColumns = [...visibleColumns];
|
||||
newVisibleColumns.splice(ruleNameIndex + 1, 0, 'space_ids');
|
||||
setVisibleColumns(newVisibleColumns);
|
||||
} else if (!nextShowFromAllSpaces && visibleColumns.includes('space_ids')) {
|
||||
setVisibleColumns(visibleColumns.filter((c) => c !== 'space_ids'));
|
||||
}
|
||||
}, [setShowFromAllSpaces, showFromAllSpaces, visibleColumns]);
|
||||
const columns: EuiDataGridColumn[] = useMemo(
|
||||
() => [
|
||||
...(hasRuleNames
|
||||
? [
|
||||
{
|
||||
id: 'rule_id',
|
||||
displayAsText: i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.ruleId',
|
||||
{
|
||||
defaultMessage: 'Rule Id',
|
||||
}
|
||||
),
|
||||
isSortable: getIsColumnSortable('rule_id'),
|
||||
actions: {
|
||||
showSortAsc: false,
|
||||
showSortDesc: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'rule_name',
|
||||
displayAsText: i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.ruleName',
|
||||
{
|
||||
defaultMessage: 'Rule',
|
||||
}
|
||||
),
|
||||
isSortable: getIsColumnSortable('rule_name'),
|
||||
actions: {
|
||||
showSortAsc: false,
|
||||
showSortDesc: false,
|
||||
showHide: false,
|
||||
},
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(showFromAllSpaces
|
||||
? [
|
||||
{
|
||||
id: 'space_ids',
|
||||
displayAsText: i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.spaceIds',
|
||||
{
|
||||
defaultMessage: 'Space',
|
||||
}
|
||||
),
|
||||
isSortable: getIsColumnSortable('space_ids'),
|
||||
actions: {
|
||||
showSortAsc: false,
|
||||
showSortDesc: false,
|
||||
showHide: false,
|
||||
},
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
id: 'id',
|
||||
displayAsText: i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.id',
|
||||
{
|
||||
defaultMessage: 'Id',
|
||||
}
|
||||
),
|
||||
isSortable: getIsColumnSortable('id'),
|
||||
},
|
||||
{
|
||||
id: 'timestamp',
|
||||
displayAsText: i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.timestamp',
|
||||
{
|
||||
defaultMessage: 'Timestamp',
|
||||
}
|
||||
),
|
||||
isSortable: getIsColumnSortable('timestamp'),
|
||||
isResizable: false,
|
||||
actions: {
|
||||
showHide: false,
|
||||
},
|
||||
initialWidth: 250,
|
||||
},
|
||||
{
|
||||
id: 'execution_duration',
|
||||
displayAsText: i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.duration',
|
||||
{
|
||||
defaultMessage: 'Duration',
|
||||
}
|
||||
),
|
||||
isSortable: getIsColumnSortable('execution_duration'),
|
||||
isResizable: false,
|
||||
actions: {
|
||||
showHide: false,
|
||||
},
|
||||
initialWidth: 100,
|
||||
},
|
||||
{
|
||||
id: 'status',
|
||||
displayAsText: i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.response',
|
||||
{
|
||||
defaultMessage: 'Response',
|
||||
}
|
||||
),
|
||||
actions: {
|
||||
showHide: false,
|
||||
showSortAsc: false,
|
||||
showSortDesc: false,
|
||||
additional: [
|
||||
{
|
||||
iconType: 'annotation',
|
||||
label: i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.showOnlyFailures',
|
||||
{
|
||||
defaultMessage: 'Show only failures',
|
||||
}
|
||||
),
|
||||
onClick: () => onFilterChange(['failure']),
|
||||
size: 'xs',
|
||||
},
|
||||
{
|
||||
iconType: 'annotation',
|
||||
label: i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.showAll',
|
||||
{
|
||||
defaultMessage: 'Show all',
|
||||
}
|
||||
),
|
||||
onClick: () => onFilterChange([]),
|
||||
size: 'xs',
|
||||
},
|
||||
],
|
||||
},
|
||||
isSortable: getIsColumnSortable('status'),
|
||||
isResizable: false,
|
||||
initialWidth: 150,
|
||||
},
|
||||
{
|
||||
id: 'message',
|
||||
actions: {
|
||||
showSortAsc: false,
|
||||
showSortDesc: false,
|
||||
},
|
||||
displayAsText: i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.message',
|
||||
{
|
||||
defaultMessage: 'Message',
|
||||
}
|
||||
),
|
||||
isSortable: getIsColumnSortable('message'),
|
||||
cellActions: [
|
||||
({ rowIndex, Component }) => {
|
||||
const pagedRowIndex = getPaginatedRowIndex(rowIndex);
|
||||
const eventLog = logs || [];
|
||||
const runLog = eventLog[pagedRowIndex];
|
||||
const actionErrors = runLog?.num_errored_actions as number;
|
||||
if (actionErrors) {
|
||||
return (
|
||||
<Component onClick={() => onFlyoutOpen(runLog)} iconType="alert">
|
||||
<FormattedMessage
|
||||
id="xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.viewActionErrors"
|
||||
defaultMessage="View action errors"
|
||||
/>
|
||||
</Component>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'num_active_alerts',
|
||||
displayAsText: i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.activeAlerts',
|
||||
{
|
||||
defaultMessage: 'Active alerts',
|
||||
}
|
||||
),
|
||||
initialWidth: 140,
|
||||
isSortable: getIsColumnSortable('num_active_alerts'),
|
||||
},
|
||||
{
|
||||
id: 'num_new_alerts',
|
||||
displayAsText: i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.newAlerts',
|
||||
{
|
||||
defaultMessage: 'New alerts',
|
||||
}
|
||||
),
|
||||
initialWidth: 140,
|
||||
isSortable: getIsColumnSortable('num_new_alerts'),
|
||||
},
|
||||
{
|
||||
id: 'num_recovered_alerts',
|
||||
displayAsText: i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.recoveredAlerts',
|
||||
{
|
||||
defaultMessage: 'Recovered alerts',
|
||||
}
|
||||
),
|
||||
isSortable: getIsColumnSortable('num_recovered_alerts'),
|
||||
},
|
||||
{
|
||||
id: 'num_triggered_actions',
|
||||
displayAsText: numTriggeredActionsDisplay,
|
||||
display: <ColumnHeaderWithToolTip id="num_triggered_actions" />,
|
||||
isSortable: getIsColumnSortable('num_triggered_actions'),
|
||||
},
|
||||
{
|
||||
id: 'num_generated_actions',
|
||||
displayAsText: numGeneratedActionsDisplay,
|
||||
display: <ColumnHeaderWithToolTip id="num_generated_actions" />,
|
||||
isSortable: getIsColumnSortable('num_generated_actions'),
|
||||
},
|
||||
{
|
||||
id: 'num_succeeded_actions',
|
||||
displayAsText: numSucceededActionsDisplay,
|
||||
display: <ColumnHeaderWithToolTip id="num_succeeded_actions" />,
|
||||
isSortable: getIsColumnSortable('num_succeeded_actions'),
|
||||
},
|
||||
{
|
||||
id: 'num_errored_actions',
|
||||
actions: {
|
||||
showSortAsc: false,
|
||||
showSortDesc: false,
|
||||
},
|
||||
displayAsText: numErroredActionsDisplay,
|
||||
display: <ColumnHeaderWithToolTip id="num_errored_actions" />,
|
||||
isSortable: getIsColumnSortable('num_errored_actions'),
|
||||
},
|
||||
{
|
||||
id: 'total_search_duration',
|
||||
displayAsText: i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.totalSearchDuration',
|
||||
{
|
||||
defaultMessage: 'Total search duration',
|
||||
}
|
||||
),
|
||||
isSortable: getIsColumnSortable('total_search_duration'),
|
||||
},
|
||||
{
|
||||
id: 'es_search_duration',
|
||||
displayAsText: i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.esSearchDuration',
|
||||
{
|
||||
defaultMessage: 'ES search duration',
|
||||
}
|
||||
),
|
||||
isSortable: getIsColumnSortable('es_search_duration'),
|
||||
},
|
||||
{
|
||||
id: 'schedule_delay',
|
||||
displayAsText: i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.scheduleDelay',
|
||||
{
|
||||
defaultMessage: 'Schedule delay',
|
||||
}
|
||||
),
|
||||
isSortable: getIsColumnSortable('schedule_delay'),
|
||||
},
|
||||
{
|
||||
id: 'timed_out',
|
||||
displayAsText: i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.timedOut',
|
||||
{
|
||||
defaultMessage: 'Timed out',
|
||||
}
|
||||
),
|
||||
isSortable: getIsColumnSortable('timed_out'),
|
||||
},
|
||||
],
|
||||
[getPaginatedRowIndex, onFlyoutOpen, onFilterChange, hasRuleNames, showFromAllSpaces, logs]
|
||||
);
|
||||
|
||||
const renderList = () => {
|
||||
if (!logs) {
|
||||
|
@ -355,19 +622,17 @@ export const RuleEventLogListTable = <T extends RuleEventLogListOptions>(
|
|||
{isLoading && (
|
||||
<EuiProgress size="xs" color="accent" data-test-subj="ruleEventLogListProgressBar" />
|
||||
)}
|
||||
<RuleEventLogDataGrid
|
||||
<EventLogDataGrid
|
||||
columns={columns}
|
||||
logs={logs}
|
||||
pagination={pagination}
|
||||
sortingColumns={sortingColumns}
|
||||
visibleColumns={visibleColumns}
|
||||
dateFormat={dateFormat}
|
||||
selectedRunLog={selectedRunLog}
|
||||
showRuleNameAndIdColumns={hasRuleNames}
|
||||
showSpaceColumns={showFromAllSpaces}
|
||||
onChangeItemsPerPage={onChangeItemsPerPage}
|
||||
onChangePage={onChangePage}
|
||||
onFlyoutOpen={onFlyoutOpen}
|
||||
onFilterChange={setFilter}
|
||||
setVisibleColumns={setVisibleColumns}
|
||||
setSortingColumns={setSortingColumns}
|
||||
/>
|
||||
|
@ -420,7 +685,7 @@ export const RuleEventLogListTable = <T extends RuleEventLogListOptions>(
|
|||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<RuleEventLogListStatusFilter selectedOptions={filter} onChange={onFilterChange} />
|
||||
<EventLogListStatusFilter selectedOptions={filter} onChange={onFilterChange} />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiSuperDatePicker
|
||||
|
@ -436,7 +701,7 @@ export const RuleEventLogListTable = <T extends RuleEventLogListOptions>(
|
|||
updateButtonProps={updateButtonProps}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
{hasAllSpaceSwitch && areMultipleSpacesAccessible && (
|
||||
{hasAllSpaceSwitch && canAccessMultipleSpaces && (
|
||||
<EuiFlexItem data-test-subj="showAllSpacesSwitch">
|
||||
<EuiSwitch
|
||||
label={ALL_SPACES_LABEL}
|
||||
|
|
|
@ -5,7 +5,10 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import moment from 'moment';
|
||||
import expect from '@kbn/expect';
|
||||
import { asyncForEach } from '@kbn/std';
|
||||
import { FtrProviderContext } from '../../../ftr_provider_context';
|
||||
import { ObjectRemover } from '../../../lib/object_remover';
|
||||
import { generateUniqueKey } from '../../../lib/get_test_data';
|
||||
|
@ -13,6 +16,8 @@ import {
|
|||
getConnectorByName,
|
||||
createSlackConnectorAndObjectRemover,
|
||||
createSlackConnector,
|
||||
createRuleWithActionsAndParams,
|
||||
getAlertSummary,
|
||||
} from './utils';
|
||||
|
||||
export default ({ getPageObjects, getPageObject, getService }: FtrProviderContext) => {
|
||||
|
@ -291,6 +296,107 @@ export default ({ getPageObjects, getPageObject, getService }: FtrProviderContex
|
|||
expect(await testSubjects.exists('preconfiguredBadge')).to.be(true);
|
||||
expect(await testSubjects.exists('edit-connector-flyout-save-btn')).to.be(false);
|
||||
});
|
||||
|
||||
describe('Execution log', () => {
|
||||
const testRunUuid = uuidv4();
|
||||
let rule: any;
|
||||
|
||||
before(async () => {
|
||||
await pageObjects.common.navigateToApp('triggersActions');
|
||||
|
||||
const connectorName = generateUniqueKey();
|
||||
const createdAction = await createSlackConnector({ name: connectorName, getService });
|
||||
objectRemover.add(createdAction.id, 'action', 'actions');
|
||||
|
||||
const alerts = [{ id: 'us-central' }];
|
||||
rule = await createRuleWithActionsAndParams(
|
||||
createdAction.id,
|
||||
testRunUuid,
|
||||
{
|
||||
instances: alerts,
|
||||
},
|
||||
{
|
||||
schedule: { interval: '1s' },
|
||||
},
|
||||
supertest
|
||||
);
|
||||
objectRemover.add(rule.id, 'alert', 'alerts');
|
||||
|
||||
// refresh to see rule
|
||||
await browser.refresh();
|
||||
await pageObjects.header.waitUntilLoadingHasFinished();
|
||||
|
||||
// click on first rule
|
||||
await pageObjects.triggersActionsUI.clickOnAlertInAlertsList(rule.name);
|
||||
|
||||
// await first run to complete so we have an initial state
|
||||
await retry.try(async () => {
|
||||
const { alerts: alertInstances } = await getAlertSummary(rule.id, supertest);
|
||||
expect(Object.keys(alertInstances).length).to.eql(alerts.length);
|
||||
});
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await objectRemover.removeAll();
|
||||
});
|
||||
|
||||
it('renders the event log list and can filter/sort', async () => {
|
||||
await browser.refresh();
|
||||
await (await testSubjects.find('logsTab')).click();
|
||||
|
||||
const tabbedContentExists = await testSubjects.exists('ruleDetailsTabbedContent');
|
||||
if (!tabbedContentExists) {
|
||||
return;
|
||||
}
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 5000));
|
||||
|
||||
const refreshButton = await testSubjects.find('superDatePickerApplyTimeButton');
|
||||
await refreshButton.click();
|
||||
|
||||
// List, date picker, and status picker all exists
|
||||
await testSubjects.existOrFail('eventLogList');
|
||||
await testSubjects.existOrFail('eventLogStatusFilterButton');
|
||||
|
||||
let statusFilter = await testSubjects.find('eventLogStatusFilterButton');
|
||||
let statusNumber = await statusFilter.findByCssSelector('.euiNotificationBadge');
|
||||
|
||||
expect(statusNumber.getVisibleText()).to.eql(0);
|
||||
|
||||
await statusFilter.click();
|
||||
await testSubjects.click('eventLogStatusFilter-success');
|
||||
await statusFilter.click();
|
||||
|
||||
statusFilter = await testSubjects.find('eventLogStatusFilterButton');
|
||||
statusNumber = await statusFilter.findByCssSelector('.euiNotificationBadge');
|
||||
|
||||
expect(statusNumber.getVisibleText()).to.eql(1);
|
||||
|
||||
const eventLogList = await find.byCssSelector('.euiDataGridRow');
|
||||
const rows = await eventLogList.parseDomContent();
|
||||
expect(rows.length).to.be.greaterThan(0);
|
||||
|
||||
await pageObjects.triggersActionsUI.ensureEventLogColumnExists('timestamp');
|
||||
|
||||
const timestampCells = await find.allByCssSelector(
|
||||
'[data-gridcell-column-id="timestamp"][data-test-subj="dataGridRowCell"]'
|
||||
);
|
||||
|
||||
let validTimestamps = 0;
|
||||
await asyncForEach(timestampCells, async (cell) => {
|
||||
const text = await cell.getVisibleText();
|
||||
if (text.toLowerCase() !== 'invalid date') {
|
||||
if (moment(text).isValid()) {
|
||||
validTimestamps += 1;
|
||||
}
|
||||
}
|
||||
});
|
||||
expect(validTimestamps).to.be.greaterThan(0);
|
||||
|
||||
await pageObjects.triggersActionsUI.sortEventLogColumn('timestamp', 'asc');
|
||||
await testSubjects.existOrFail('dataGridHeaderCellSortingIcon-timestamp');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
async function createIndexConnector(connectorName: string, indexName: string) {
|
||||
|
|
|
@ -7,10 +7,10 @@
|
|||
|
||||
import type SuperTest from 'supertest';
|
||||
import { findIndex } from 'lodash';
|
||||
|
||||
import { RuleNotifyWhen } from '@kbn/alerting-plugin/common';
|
||||
import { ObjectRemover } from '../../../lib/object_remover';
|
||||
import { FtrProviderContext } from '../../../ftr_provider_context';
|
||||
import { getTestActionData } from '../../../lib/get_test_data';
|
||||
import { getTestActionData, getTestAlertData } from '../../../lib/get_test_data';
|
||||
|
||||
export const createSlackConnectorAndObjectRemover = async ({
|
||||
getService,
|
||||
|
@ -59,3 +59,62 @@ export const getConnectorByName = async (
|
|||
const i = findIndex(body, (c: any) => c.name === name);
|
||||
return body[i];
|
||||
};
|
||||
|
||||
export async function createRuleWithActionsAndParams(
|
||||
connectorId: string,
|
||||
testRunUuid: string,
|
||||
params: Record<string, any> = {},
|
||||
overwrites: Record<string, any> = {},
|
||||
supertest: SuperTest.SuperTest<SuperTest.Test>
|
||||
) {
|
||||
return await createAlwaysFiringRule(
|
||||
{
|
||||
name: `test-rule-${testRunUuid}`,
|
||||
actions: [
|
||||
{
|
||||
id: connectorId,
|
||||
group: 'default',
|
||||
params: {
|
||||
message: 'from alert 1s',
|
||||
level: 'warn',
|
||||
},
|
||||
frequency: {
|
||||
summary: false,
|
||||
notify_when: RuleNotifyWhen.THROTTLE,
|
||||
throttle: '1m',
|
||||
},
|
||||
},
|
||||
],
|
||||
params,
|
||||
...overwrites,
|
||||
},
|
||||
supertest
|
||||
);
|
||||
}
|
||||
|
||||
async function createAlwaysFiringRule(
|
||||
overwrites: Record<string, any> = {},
|
||||
supertest: SuperTest.SuperTest<SuperTest.Test>
|
||||
) {
|
||||
const { body: createdRule } = await supertest
|
||||
.post(`/api/alerting/rule`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send(
|
||||
getTestAlertData({
|
||||
rule_type_id: 'test.always-firing',
|
||||
...overwrites,
|
||||
})
|
||||
)
|
||||
.expect(200);
|
||||
return createdRule;
|
||||
}
|
||||
|
||||
export async function getAlertSummary(
|
||||
ruleId: string,
|
||||
supertest: SuperTest.SuperTest<SuperTest.Test>
|
||||
) {
|
||||
const { body: summary } = await supertest
|
||||
.get(`/internal/alerting/rule/${encodeURIComponent(ruleId)}/_alert_summary`)
|
||||
.expect(200);
|
||||
return summary;
|
||||
}
|
||||
|
|
|
@ -1048,20 +1048,20 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
|
|||
await refreshButton.click();
|
||||
|
||||
// List, date picker, and status picker all exists
|
||||
await testSubjects.existOrFail('ruleEventLogList');
|
||||
await testSubjects.existOrFail('eventLogList');
|
||||
await testSubjects.existOrFail('ruleEventLogListDatePicker');
|
||||
await testSubjects.existOrFail('ruleEventLogStatusFilterButton');
|
||||
await testSubjects.existOrFail('eventLogStatusFilterButton');
|
||||
|
||||
let statusFilter = await testSubjects.find('ruleEventLogStatusFilterButton');
|
||||
let statusFilter = await testSubjects.find('eventLogStatusFilterButton');
|
||||
let statusNumber = await statusFilter.findByCssSelector('.euiNotificationBadge');
|
||||
|
||||
expect(statusNumber.getVisibleText()).to.eql(0);
|
||||
|
||||
await statusFilter.click();
|
||||
await testSubjects.click('ruleEventLogStatusFilter-success');
|
||||
await testSubjects.click('eventLogStatusFilter-success');
|
||||
await statusFilter.click();
|
||||
|
||||
statusFilter = await testSubjects.find('ruleEventLogStatusFilterButton');
|
||||
statusFilter = await testSubjects.find('eventLogStatusFilterButton');
|
||||
statusNumber = await statusFilter.findByCssSelector('.euiNotificationBadge');
|
||||
|
||||
expect(statusNumber.getVisibleText()).to.eql(1);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue