>.
+--
-. Change the time window and observe the effect it has on the chart. Compare a 24 window to a 12 hour window. Notice the variability in the sum of bytes, due to different traffic levels during the day compared to at night. This variability would result in noisy rules, so the 24 hour window is better. The preview chart can help you find the right values for your rule.
+.. Save the rule.
+
+. Find the rule and view its details in *{stack-manage-app} > {rules-ui}*. For example, you can see the status of the rule and its alerts:
+
[role="screenshot"]
-image::user/alerting/images/rule-types-index-threshold-example-comparison.png[Comparing two time windows]
\ No newline at end of file
+image::user/alerting/images/rule-types-index-threshold-example-alerts.png[View the list of alerts for the rule]
+
+. Delete or disable this example rule when it's no longer useful. In the detailed rule view, select *Delete rule* from the actions menu.
+
diff --git a/package.json b/package.json
index a363b2c162e56..508a6d1e2a1af 100644
--- a/package.json
+++ b/package.json
@@ -535,6 +535,7 @@
"@kbn/securitysolution-ecs": "link:packages/kbn-securitysolution-ecs",
"@kbn/securitysolution-es-utils": "link:packages/kbn-securitysolution-es-utils",
"@kbn/securitysolution-exception-list-components": "link:packages/kbn-securitysolution-exception-list-components",
+ "@kbn/securitysolution-grouping": "link:packages/kbn-securitysolution-grouping",
"@kbn/securitysolution-hook-utils": "link:packages/kbn-securitysolution-hook-utils",
"@kbn/securitysolution-io-ts-alerting-types": "link:packages/kbn-securitysolution-io-ts-alerting-types",
"@kbn/securitysolution-io-ts-list-types": "link:packages/kbn-securitysolution-io-ts-list-types",
@@ -856,7 +857,7 @@
"react-fast-compare": "^2.0.4",
"react-focus-on": "^3.7.0",
"react-grid-layout": "^1.3.4",
- "react-hook-form": "^7.43.1",
+ "react-hook-form": "^7.43.2",
"react-intl": "^2.8.0",
"react-is": "^17.0.2",
"react-markdown": "^6.0.3",
@@ -942,7 +943,7 @@
"@babel/eslint-plugin": "^7.19.1",
"@babel/generator": "^7.21.1",
"@babel/helper-plugin-utils": "^7.20.2",
- "@babel/parser": "^7.21.1",
+ "@babel/parser": "^7.21.2",
"@babel/plugin-proposal-class-properties": "^7.18.6",
"@babel/plugin-proposal-export-namespace-from": "^7.18.9",
"@babel/plugin-proposal-nullish-coalescing-operator": "^7.18.6",
@@ -954,8 +955,8 @@
"@babel/preset-react": "^7.18.6",
"@babel/preset-typescript": "^7.21.0",
"@babel/register": "^7.21.0",
- "@babel/traverse": "^7.21.0",
- "@babel/types": "^7.21.0",
+ "@babel/traverse": "^7.21.2",
+ "@babel/types": "^7.21.2",
"@bazel/ibazel": "^0.16.2",
"@bazel/typescript": "4.6.2",
"@cypress/code-coverage": "^3.10.0",
@@ -1448,7 +1449,7 @@
"svgo": "^2.8.0",
"tape": "^5.0.1",
"tempy": "^0.3.0",
- "terser": "^5.16.4",
+ "terser": "^5.16.5",
"terser-webpack-plugin": "^4.2.3",
"tough-cookie": "^4.1.2",
"tree-kill": "^1.2.2",
diff --git a/packages/core/http/core-http-router-server-internal/src/request.ts b/packages/core/http/core-http-router-server-internal/src/request.ts
index 7e8e927bd4671..26ac377c00bf3 100644
--- a/packages/core/http/core-http-router-server-internal/src/request.ts
+++ b/packages/core/http/core-http-router-server-internal/src/request.ts
@@ -201,6 +201,7 @@ export class CoreKibanaRequest<
xsrfRequired:
((request.route?.settings as RouteOptions)?.app as KibanaRouteOptions)?.xsrfRequired ??
true, // some places in LP call KibanaRequest.from(request) manually. remove fallback to true before v8
+ access: this.getAccess(request),
tags: request.route?.settings?.tags || [],
timeout: {
payload: payloadTimeout,
@@ -222,6 +223,13 @@ export class CoreKibanaRequest<
options,
};
}
+ /** infer route access from path if not declared */
+ private getAccess(request: RawRequest): 'internal' | 'public' {
+ return (
+ ((request.route?.settings as RouteOptions)?.app as KibanaRouteOptions)?.access ??
+ (request.path.startsWith('/internal') ? 'internal' : 'public')
+ );
+ }
private getAuthRequired(request: RawRequest): boolean | 'optional' {
if (isFakeRawRequest(request)) {
diff --git a/packages/core/http/core-http-router-server-mocks/src/router.mock.ts b/packages/core/http/core-http-router-server-mocks/src/router.mock.ts
index 1a3262fdf1f80..9b2d90e18640b 100644
--- a/packages/core/http/core-http-router-server-mocks/src/router.mock.ts
+++ b/packages/core/http/core-http-router-server-mocks/src/router.mock.ts
@@ -71,7 +71,7 @@ function createKibanaRequestMock({
routeTags,
routeAuthRequired,
validation = {},
- kibanaRouteOptions = { xsrfRequired: true },
+ kibanaRouteOptions = { xsrfRequired: true, access: 'public' },
kibanaRequestState = {
requestId: '123',
requestUuid: '123e4567-e89b-12d3-a456-426614174000',
diff --git a/packages/core/http/core-http-server-internal/src/http_server.test.ts b/packages/core/http/core-http-server-internal/src/http_server.test.ts
index 92fa63c502558..b6a120e06ab8d 100644
--- a/packages/core/http/core-http-server-internal/src/http_server.test.ts
+++ b/packages/core/http/core-http-server-internal/src/http_server.test.ts
@@ -817,6 +817,56 @@ test('allows attaching metadata to attach meta-data tag strings to a route', asy
await supertest(innerServer.listener).get('/without-tags').expect(200, { tags: [] });
});
+test('allows declaring route access to flag a route as public or internal', async () => {
+ const access = 'internal';
+ const { registerRouter, server: innerServer } = await server.setup(config);
+
+ const router = new Router('', logger, enhanceWithContext);
+ router.get({ path: '/with-access', validate: false, options: { access } }, (context, req, res) =>
+ res.ok({ body: { access: req.route.options.access } })
+ );
+ router.get({ path: '/without-access', validate: false }, (context, req, res) =>
+ res.ok({ body: { access: req.route.options.access } })
+ );
+ registerRouter(router);
+
+ await server.start();
+ await supertest(innerServer.listener).get('/with-access').expect(200, { access });
+
+ await supertest(innerServer.listener).get('/without-access').expect(200, { access: 'public' });
+});
+
+test('infers access flag from path if not defined', async () => {
+ const { registerRouter, server: innerServer } = await server.setup(config);
+
+ const router = new Router('', logger, enhanceWithContext);
+ router.get({ path: '/internal/foo', validate: false }, (context, req, res) =>
+ res.ok({ body: { access: req.route.options.access } })
+ );
+ router.get({ path: '/random/foo', validate: false }, (context, req, res) =>
+ res.ok({ body: { access: req.route.options.access } })
+ );
+ router.get({ path: '/random/internal/foo', validate: false }, (context, req, res) =>
+ res.ok({ body: { access: req.route.options.access } })
+ );
+
+ router.get({ path: '/api/foo/internal/my-foo', validate: false }, (context, req, res) =>
+ res.ok({ body: { access: req.route.options.access } })
+ );
+ registerRouter(router);
+
+ await server.start();
+ await supertest(innerServer.listener).get('/internal/foo').expect(200, { access: 'internal' });
+
+ await supertest(innerServer.listener).get('/random/foo').expect(200, { access: 'public' });
+ await supertest(innerServer.listener)
+ .get('/random/internal/foo')
+ .expect(200, { access: 'public' });
+ await supertest(innerServer.listener)
+ .get('/api/foo/internal/my-foo')
+ .expect(200, { access: 'public' });
+});
+
test('exposes route details of incoming request to a route handler', async () => {
const { registerRouter, server: innerServer } = await server.setup(config);
@@ -833,6 +883,7 @@ test('exposes route details of incoming request to a route handler', async () =>
options: {
authRequired: true,
xsrfRequired: false,
+ access: 'public',
tags: [],
timeout: {},
},
@@ -1010,6 +1061,7 @@ test('exposes route details of incoming request to a route handler (POST + paylo
options: {
authRequired: true,
xsrfRequired: true,
+ access: 'public',
tags: [],
timeout: {
payload: 10000,
diff --git a/packages/core/http/core-http-server-internal/src/http_server.ts b/packages/core/http/core-http-server-internal/src/http_server.ts
index fb19795d77dce..1ef5be6c67a54 100644
--- a/packages/core/http/core-http-server-internal/src/http_server.ts
+++ b/packages/core/http/core-http-server-internal/src/http_server.ts
@@ -524,6 +524,7 @@ export class HttpServer {
const kibanaRouteOptions: KibanaRouteOptions = {
xsrfRequired: route.options.xsrfRequired ?? !isSafeMethod(route.method),
+ access: route.options.access ?? (route.path.startsWith('/internal') ? 'internal' : 'public'),
};
this.server!.route({
diff --git a/packages/core/http/core-http-server-internal/src/lifecycle_handlers.test.ts b/packages/core/http/core-http-server-internal/src/lifecycle_handlers.test.ts
index 5e182005fd40c..d13bd001bbbb9 100644
--- a/packages/core/http/core-http-server-internal/src/lifecycle_handlers.test.ts
+++ b/packages/core/http/core-http-server-internal/src/lifecycle_handlers.test.ts
@@ -167,6 +167,7 @@ describe('xsrf post-auth handler', () => {
path: '/some-path',
kibanaRouteOptions: {
xsrfRequired: false,
+ access: 'public',
},
});
diff --git a/packages/core/http/core-http-server-internal/src/lifecycle_handlers.ts b/packages/core/http/core-http-server-internal/src/lifecycle_handlers.ts
index 3fe9c8ac727ff..af148413265e8 100644
--- a/packages/core/http/core-http-server-internal/src/lifecycle_handlers.ts
+++ b/packages/core/http/core-http-server-internal/src/lifecycle_handlers.ts
@@ -60,6 +60,7 @@ export const createVersionCheckPostAuthHandler = (kibanaVersion: string): OnPost
};
};
+// TODO: implement header required for accessing internal routes. See https://github.com/elastic/kibana/issues/151940
export const createCustomHeadersPreResponseHandler = (config: HttpConfig): OnPreResponseHandler => {
const {
name: serverName,
diff --git a/packages/core/http/core-http-server/src/router/request.ts b/packages/core/http/core-http-server/src/router/request.ts
index ef33bec14f841..e0664cb1ea29a 100644
--- a/packages/core/http/core-http-server/src/router/request.ts
+++ b/packages/core/http/core-http-server/src/router/request.ts
@@ -19,6 +19,7 @@ import type { Headers } from './headers';
*/
export interface KibanaRouteOptions extends RouteOptionsApp {
xsrfRequired: boolean;
+ access: 'internal' | 'public';
}
/**
diff --git a/packages/core/http/core-http-server/src/router/route.ts b/packages/core/http/core-http-server/src/router/route.ts
index 78d76bb4ba7b8..e2b11aec08e1a 100644
--- a/packages/core/http/core-http-server/src/router/route.ts
+++ b/packages/core/http/core-http-server/src/router/route.ts
@@ -120,6 +120,18 @@ export interface RouteConfigOptions {
*/
xsrfRequired?: Method extends 'get' ? never : boolean;
+ /**
+ * Defines intended request origin of the route:
+ * - public. The route is public, declared stable and intended for external access.
+ * In the future, may require an incomming request to contain a specified header.
+ * - internal. The route is internal and intended for internal access only.
+ *
+ * If not declared, infers access from route path:
+ * - access =`internal` for '/internal' route path prefix
+ * - access = `public` for everything else
+ */
+ access?: 'public' | 'internal';
+
/**
* Additional metadata tag strings to attach to the route.
*/
diff --git a/packages/core/versioning/core-version-http-server/src/example.ts b/packages/core/versioning/core-version-http-server/src/example.ts
index de529ccb07d9d..b63c75e86a562 100644
--- a/packages/core/versioning/core-version-http-server/src/example.ts
+++ b/packages/core/versioning/core-version-http-server/src/example.ts
@@ -22,7 +22,7 @@ const versionedRouter = vtk.createVersionedRouter({ router });
const versionedRoute = versionedRouter
.post({
path: '/api/my-app/foo/{id?}',
- options: { timeout: { payload: 60000 } },
+ options: { timeout: { payload: 60000 }, access: 'public' },
})
.addVersion(
{
diff --git a/packages/core/versioning/core-version-http-server/src/version_http_toolkit.ts b/packages/core/versioning/core-version-http-server/src/version_http_toolkit.ts
index 719e0075c0070..7d8dd7765e476 100644
--- a/packages/core/versioning/core-version-http-server/src/version_http_toolkit.ts
+++ b/packages/core/versioning/core-version-http-server/src/version_http_toolkit.ts
@@ -13,6 +13,7 @@ import type {
RequestHandler,
RouteValidatorFullConfig,
RequestHandlerContextBase,
+ RouteConfigOptions,
} from '@kbn/core-http-server';
type RqCtx = RequestHandlerContextBase;
@@ -45,7 +46,7 @@ export interface CreateVersionedRouterArgs {
* const versionedRoute = versionedRouter
* .post({
* path: '/api/my-app/foo/{id?}',
- * options: { timeout: { payload: 60000 } },
+ * options: { timeout: { payload: 60000 }, access: 'public' },
* })
* .addVersion(
* {
@@ -99,14 +100,28 @@ export interface VersionHTTPToolkit {
): VersionedRouter;
}
+/**
+ * Converts an input property from optional to required. Needed for making RouteConfigOptions['access'] required.
+ */
+type WithRequiredProperty = Type & {
+ [Property in Key]-?: Type[Property];
+};
+
+/**
+ * Versioned route access flag, required
+ * - '/api/foo' is 'public'
+ * - '/internal/my-foo' is 'internal'
+ * Required
+ */
+type VersionedRouteConfigOptions = WithRequiredProperty, 'access'>;
/**
* Configuration for a versioned route
* @experimental
*/
export type VersionedRouteConfig = Omit<
RouteConfig,
- 'validate'
->;
+ 'validate' | 'options'
+> & { options: VersionedRouteConfigOptions };
/**
* Create an {@link VersionedRoute | versioned route}.
diff --git a/packages/kbn-apm-synthtrace-client/index.ts b/packages/kbn-apm-synthtrace-client/index.ts
index 82f8efe28b40a..1868cb188582e 100644
--- a/packages/kbn-apm-synthtrace-client/index.ts
+++ b/packages/kbn-apm-synthtrace-client/index.ts
@@ -20,6 +20,7 @@ export type {
} from './src/lib/apm/mobile_device';
export { httpExitSpan } from './src/lib/apm/span';
export { DistributedTrace } from './src/lib/dsl/distributed_trace_client';
+export { serviceMap } from './src/lib/dsl/service_map';
export type { Fields } from './src/lib/entity';
export type { Serializable } from './src/lib/serializable';
export { timerange } from './src/lib/timerange';
diff --git a/packages/kbn-apm-synthtrace-client/src/lib/dsl/service_map.test.ts b/packages/kbn-apm-synthtrace-client/src/lib/dsl/service_map.test.ts
new file mode 100644
index 0000000000000..90078f584c172
--- /dev/null
+++ b/packages/kbn-apm-synthtrace-client/src/lib/dsl/service_map.test.ts
@@ -0,0 +1,278 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import { pick } from 'lodash';
+import { ApmFields } from '../apm/apm_fields';
+import { BaseSpan } from '../apm/base_span';
+import { serviceMap, ServiceMapOpts } from './service_map';
+
+describe('serviceMap', () => {
+ const TIMESTAMP = 1677693600000;
+
+ describe('Basic definition', () => {
+ const BASIC_SERVICE_MAP_OPTS: ServiceMapOpts = {
+ services: [
+ 'frontend-rum',
+ 'frontend-node',
+ 'advertService',
+ 'checkoutService',
+ 'cartService',
+ 'paymentService',
+ 'productCatalogService',
+ ],
+ definePaths([rum, node, adv, chk, cart, pay, prod]) {
+ return [
+ [rum, node, adv, 'elasticsearch'],
+ [rum, node, cart, 'redis'],
+ [rum, node, chk, pay],
+ [chk, cart, 'redis'],
+ [rum, node, prod, 'elasticsearch'],
+ [chk, prod],
+ ];
+ },
+ };
+
+ it('should create an accurate set of trace paths', () => {
+ const serviceMapGenerator = serviceMap(BASIC_SERVICE_MAP_OPTS);
+ const transactions = serviceMapGenerator(TIMESTAMP);
+ expect(transactions.map(getTracePathLabel)).toMatchInlineSnapshot(`
+ Array [
+ "frontend-rum → frontend-node → advertService → elasticsearch",
+ "frontend-rum → frontend-node → cartService → redis",
+ "frontend-rum → frontend-node → checkoutService → paymentService",
+ "checkoutService → cartService → redis",
+ "frontend-rum → frontend-node → productCatalogService → elasticsearch",
+ "checkoutService → productCatalogService",
+ ]
+ `);
+ });
+
+ it('should use a default agent name if not defined', () => {
+ const serviceMapGenerator = serviceMap(BASIC_SERVICE_MAP_OPTS);
+ const transactions = serviceMapGenerator(TIMESTAMP);
+ const traceDocs = transactions.flatMap(getTraceDocsSubset);
+ for (const doc of traceDocs) {
+ expect(doc).toHaveProperty(['agent.name'], 'nodejs');
+ }
+ });
+
+ it('should use a default transaction/span names if not defined', () => {
+ const serviceMapGenerator = serviceMap(BASIC_SERVICE_MAP_OPTS);
+ const transactions = serviceMapGenerator(TIMESTAMP);
+ const traceDocs = transactions.map(getTraceDocsSubset);
+ for (let i = 0; i < traceDocs.length; i++) {
+ for (const doc of traceDocs[i]) {
+ const serviceName = doc['service.name'];
+ if (doc['processor.event'] === 'transaction') {
+ expect(doc).toHaveProperty(['transaction.name'], `GET /api/${serviceName}/${i}`);
+ }
+ if (doc['processor.event'] === 'span') {
+ if (doc['span.type'] === 'db') {
+ switch (doc['span.subtype']) {
+ case 'elasticsearch':
+ expect(doc).toHaveProperty(['span.name'], `GET ad-*/_search`);
+ break;
+ case 'redis':
+ expect(doc).toHaveProperty(['span.name'], `INCR item:i012345:count`);
+ break;
+ case 'sqlite':
+ expect(doc).toHaveProperty(['span.name'], `SELECT * FROM items`);
+ break;
+ }
+ } else {
+ expect(doc).toHaveProperty(['span.name'], `GET /api/${serviceName}/${i}`);
+ }
+ }
+ }
+ }
+ });
+
+ it('should create one parent transaction per trace', () => {
+ const serviceMapGenerator = serviceMap(BASIC_SERVICE_MAP_OPTS);
+ const transactions = serviceMapGenerator(TIMESTAMP);
+ const traces = transactions.map(getTraceDocsSubset);
+ for (const traceDocs of traces) {
+ const [transaction, ...spans] = traceDocs;
+ expect(transaction).toHaveProperty(['processor.event'], 'transaction');
+ expect(
+ spans.every(({ 'processor.event': processorEvent }) => processorEvent === 'span')
+ ).toBe(true);
+ }
+ });
+ });
+ describe('Detailed definition', () => {
+ const DETAILED_SERVICE_MAP_OPTS: ServiceMapOpts = {
+ services: [
+ { 'frontend-rum': 'rum-js' },
+ { 'frontend-node': 'nodejs' },
+ { advertService: 'java' },
+ { checkoutService: 'go' },
+ { cartService: 'dotnet' },
+ { paymentService: 'nodejs' },
+ { productCatalogService: 'go' },
+ ],
+ definePaths([rum, node, adv, chk, cart, pay, prod]) {
+ return [
+ [
+ [rum, 'fetchAd'],
+ [node, 'GET /nodejs/adTag'],
+ [adv, 'APIRestController#getAd'],
+ ['elasticsearch', 'GET ad-*/_search'],
+ ],
+ [
+ [rum, 'AddToCart'],
+ [node, 'POST /nodejs/addToCart'],
+ [cart, 'POST /dotnet/reserveProduct'],
+ ['redis', 'DECR inventory:i012345:stock'],
+ ],
+ {
+ path: [
+ [rum, 'Checkout'],
+ [node, 'POST /nodejs/placeOrder'],
+ [chk, 'POST /go/placeOrder'],
+ [pay, 'POST /nodejs/processPayment'],
+ ],
+ transaction: (t) => t.defaults({ 'labels.name': 'transaction hook test' }),
+ },
+ [
+ [chk, 'POST /go/clearCart'],
+ [cart, 'PUT /dotnet/cart/c12345/reset'],
+ ['redis', 'INCR inventory:i012345:stock'],
+ ],
+ [
+ [rum, 'ProductDashboard'],
+ [node, 'GET /nodejs/products'],
+ [prod, 'GET /go/product-catalog'],
+ ['elasticsearch', 'GET product-*/_search'],
+ ],
+ [
+ [chk, 'PUT /go/update-inventory'],
+ [prod, 'PUT /go/product/i012345'],
+ ],
+ [pay],
+ ];
+ },
+ };
+
+ const SERVICE_AGENT_MAP: Record = {
+ 'frontend-rum': 'rum-js',
+ 'frontend-node': 'nodejs',
+ advertService: 'java',
+ checkoutService: 'go',
+ cartService: 'dotnet',
+ paymentService: 'nodejs',
+ productCatalogService: 'go',
+ };
+
+ it('should use the defined agent name for a given service', () => {
+ const serviceMapGenerator = serviceMap(DETAILED_SERVICE_MAP_OPTS);
+ const transactions = serviceMapGenerator(TIMESTAMP);
+ const traceDocs = transactions.flatMap(getTraceDocsSubset);
+ for (const doc of traceDocs) {
+ if (!(doc['service.name']! in SERVICE_AGENT_MAP)) {
+ throw new Error(`Unexpected service name '${doc['service.name']}' found`);
+ }
+
+ expect(doc).toHaveProperty(['agent.name'], SERVICE_AGENT_MAP[doc['service.name']!]);
+ }
+ });
+
+ it('should use the defined transaction/span names for each trace document', () => {
+ const serviceMapGenerator = serviceMap(DETAILED_SERVICE_MAP_OPTS);
+ const transactions = serviceMapGenerator(TIMESTAMP);
+ const traceDocs = transactions.map((transaction) => {
+ return getTraceDocsSubset(transaction).map(
+ ({ 'span.name': spanName, 'transaction.name': transactionName }) =>
+ transactionName || spanName
+ );
+ });
+ expect(traceDocs).toMatchInlineSnapshot(`
+ Array [
+ Array [
+ "fetchAd",
+ "fetchAd",
+ "GET /nodejs/adTag",
+ "APIRestController#getAd",
+ "GET ad-*/_search",
+ ],
+ Array [
+ "AddToCart",
+ "AddToCart",
+ "POST /nodejs/addToCart",
+ "POST /dotnet/reserveProduct",
+ "DECR inventory:i012345:stock",
+ ],
+ Array [
+ "Checkout",
+ "Checkout",
+ "POST /nodejs/placeOrder",
+ "POST /go/placeOrder",
+ "POST /nodejs/processPayment",
+ ],
+ Array [
+ "POST /go/clearCart",
+ "POST /go/clearCart",
+ "PUT /dotnet/cart/c12345/reset",
+ "INCR inventory:i012345:stock",
+ ],
+ Array [
+ "ProductDashboard",
+ "ProductDashboard",
+ "GET /nodejs/products",
+ "GET /go/product-catalog",
+ "GET product-*/_search",
+ ],
+ Array [
+ "PUT /go/update-inventory",
+ "PUT /go/update-inventory",
+ "PUT /go/product/i012345",
+ ],
+ Array [
+ "GET /api/paymentService/6",
+ "GET /api/paymentService/6",
+ ],
+ ]
+ `);
+ });
+
+ it('should apply the transaction hook function if defined', () => {
+ const serviceMapGenerator = serviceMap(DETAILED_SERVICE_MAP_OPTS);
+ const transactions = serviceMapGenerator(TIMESTAMP);
+ expect(transactions[2].fields['labels.name']).toBe('transaction hook test');
+ });
+ });
+});
+
+function getTraceDocsSubset(transaction: BaseSpan): ApmFields[] {
+ const subsetFields = pick(transaction.fields, [
+ 'processor.event',
+ 'service.name',
+ 'agent.name',
+ 'transaction.name',
+ 'span.name',
+ 'span.type',
+ 'span.subtype',
+ 'span.destination.service.resource',
+ ]);
+
+ const children = transaction.getChildren();
+ if (children) {
+ const childFields = children.flatMap((child) => getTraceDocsSubset(child));
+ return [subsetFields, ...childFields];
+ }
+ return [subsetFields];
+}
+
+function getTracePathLabel(transaction: BaseSpan) {
+ const traceDocs = getTraceDocsSubset(transaction);
+ const traceSpans = traceDocs.filter((doc) => doc['processor.event'] === 'span');
+ const spanLabels = traceSpans.map((span) =>
+ span['span.type'] === 'db' ? span['span.subtype'] : span['service.name']
+ );
+ return spanLabels.join(' → ');
+}
diff --git a/packages/kbn-apm-synthtrace-client/src/lib/dsl/service_map.ts b/packages/kbn-apm-synthtrace-client/src/lib/dsl/service_map.ts
new file mode 100644
index 0000000000000..4c91352b11d02
--- /dev/null
+++ b/packages/kbn-apm-synthtrace-client/src/lib/dsl/service_map.ts
@@ -0,0 +1,156 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import { AgentName } from '../../types/agent_names';
+import { apm } from '../apm';
+import { Instance } from '../apm/instance';
+import { elasticsearchSpan, redisSpan, sqliteSpan, Span } from '../apm/span';
+import { Transaction } from '../apm/transaction';
+
+const ENVIRONMENT = 'Synthtrace: service_map';
+
+function service(serviceName: string, agentName: AgentName, environment?: string) {
+ return apm
+ .service({ name: serviceName, environment: environment || ENVIRONMENT, agentName })
+ .instance(serviceName);
+}
+
+type DbSpan = 'elasticsearch' | 'redis' | 'sqlite';
+type ServiceMapNode = Instance | DbSpan;
+type TransactionName = string;
+type TraceItem = ServiceMapNode | [ServiceMapNode, TransactionName];
+type TracePath = TraceItem[];
+
+function getTraceItem(traceItem: TraceItem) {
+ if (Array.isArray(traceItem)) {
+ const transactionName = traceItem[1];
+ if (typeof traceItem[0] === 'string') {
+ const dbSpan = traceItem[0];
+ return { dbSpan, transactionName, serviceInstance: undefined };
+ } else {
+ const serviceInstance = traceItem[0];
+ return { dbSpan: undefined, transactionName, serviceInstance };
+ }
+ } else if (typeof traceItem === 'string') {
+ const dbSpan = traceItem;
+ return { dbSpan, transactionName: undefined, serviceInstance: undefined };
+ } else {
+ const serviceInstance = traceItem;
+ return { dbSpan: undefined, transactionName: undefined, serviceInstance };
+ }
+}
+
+function getTransactionName(
+ transactionName: string | undefined,
+ serviceInstance: Instance,
+ index: number
+) {
+ return transactionName || `GET /api/${serviceInstance.fields['service.name']}/${index}`;
+}
+
+function getChildren(
+ childTraceItems: TracePath,
+ parentServiceInstance: Instance,
+ timestamp: number,
+ index: number
+): Span[] {
+ if (childTraceItems.length === 0) {
+ return [];
+ }
+ const [first, ...rest] = childTraceItems;
+ const { dbSpan, serviceInstance, transactionName } = getTraceItem(first);
+ if (dbSpan) {
+ switch (dbSpan) {
+ case 'elasticsearch':
+ return [
+ parentServiceInstance
+ .span(elasticsearchSpan(transactionName || 'GET ad-*/_search'))
+ .timestamp(timestamp)
+ .duration(1000),
+ ];
+ case 'redis':
+ return [
+ parentServiceInstance
+ .span(redisSpan(transactionName || 'INCR item:i012345:count'))
+ .timestamp(timestamp)
+ .duration(1000),
+ ];
+ case 'sqlite':
+ return [
+ parentServiceInstance
+ .span(sqliteSpan(transactionName || 'SELECT * FROM items'))
+ .timestamp(timestamp)
+ .duration(1000),
+ ];
+ }
+ }
+ const childSpan = serviceInstance
+ .span({
+ spanName: getTransactionName(transactionName, serviceInstance, index),
+ spanType: 'app',
+ })
+ .timestamp(timestamp)
+ .duration(1000)
+ .children(...getChildren(rest, serviceInstance, timestamp, index));
+ if (rest[0]) {
+ const next = getTraceItem(rest[0]);
+ if (next.serviceInstance) {
+ return [childSpan.destination(next.serviceInstance.fields['service.name']!)];
+ }
+ }
+ return [childSpan];
+}
+
+interface TracePathOpts {
+ path: TracePath;
+ transaction?: (transaction: Transaction) => Transaction;
+}
+type PathDef = TracePath | TracePathOpts;
+export interface ServiceMapOpts {
+ services: Array;
+ definePaths: (services: Instance[]) => PathDef[];
+ environment?: string;
+}
+
+export function serviceMap(options: ServiceMapOpts) {
+ const serviceInstances = options.services.map((s) => {
+ if (typeof s === 'string') {
+ return service(s, 'nodejs', options.environment);
+ }
+ return service(Object.keys(s)[0], Object.values(s)[0], options.environment);
+ });
+ return (timestamp: number) => {
+ const tracePaths = options.definePaths(serviceInstances);
+ return tracePaths.map((traceDef, index) => {
+ const tracePath = 'path' in traceDef ? traceDef.path : traceDef;
+ const [first] = tracePath;
+
+ const firstTraceItem = getTraceItem(first);
+ if (firstTraceItem.serviceInstance === undefined) {
+ throw new Error('First trace item must be a service instance');
+ }
+ const transactionName = getTransactionName(
+ firstTraceItem.transactionName,
+ firstTraceItem.serviceInstance,
+ index
+ );
+
+ const transaction = firstTraceItem.serviceInstance
+ .transaction({ transactionName, transactionType: 'request' })
+ .timestamp(timestamp)
+ .duration(1000)
+ .children(...getChildren(tracePath, firstTraceItem.serviceInstance, timestamp, index));
+
+ if ('transaction' in traceDef && traceDef.transaction) {
+ return traceDef.transaction(transaction);
+ }
+
+ return transaction;
+ });
+ };
+}
diff --git a/packages/kbn-apm-synthtrace-client/src/types/agent_names.ts b/packages/kbn-apm-synthtrace-client/src/types/agent_names.ts
new file mode 100644
index 0000000000000..d9e3a371e0e87
--- /dev/null
+++ b/packages/kbn-apm-synthtrace-client/src/types/agent_names.ts
@@ -0,0 +1,37 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+type ElasticAgentName =
+ | 'go'
+ | 'java'
+ | 'js-base'
+ | 'iOS/swift'
+ | 'rum-js'
+ | 'nodejs'
+ | 'python'
+ | 'dotnet'
+ | 'ruby'
+ | 'php'
+ | 'android/java';
+
+type OpenTelemetryAgentName =
+ | 'otlp'
+ | 'opentelemetry/cpp'
+ | 'opentelemetry/dotnet'
+ | 'opentelemetry/erlang'
+ | 'opentelemetry/go'
+ | 'opentelemetry/java'
+ | 'opentelemetry/nodejs'
+ | 'opentelemetry/php'
+ | 'opentelemetry/python'
+ | 'opentelemetry/ruby'
+ | 'opentelemetry/swift'
+ | 'opentelemetry/webjs';
+
+// Unable to reference AgentName from '@kbn/apm-plugin/typings/es_schemas/ui/fields/agent' due to circular reference
+export type AgentName = ElasticAgentName | OpenTelemetryAgentName;
diff --git a/packages/kbn-apm-synthtrace/src/scenarios/service_map.ts b/packages/kbn-apm-synthtrace/src/scenarios/service_map.ts
index 41e499320fd50..f39d4bdd1e221 100644
--- a/packages/kbn-apm-synthtrace/src/scenarios/service_map.ts
+++ b/packages/kbn-apm-synthtrace/src/scenarios/service_map.ts
@@ -6,116 +6,71 @@
* Side Public License, v 1.
*/
-import { apm, ApmFields, Instance } from '@kbn/apm-synthtrace-client';
-import { Transaction } from '@kbn/apm-synthtrace-client/src/lib/apm/transaction';
-import { AgentName } from '@kbn/apm-plugin/typings/es_schemas/ui/fields/agent';
+import { ApmFields, serviceMap } from '@kbn/apm-synthtrace-client';
import { Scenario } from '../cli/scenario';
import { RunOptions } from '../cli/utils/parse_run_cli_flags';
import { getSynthtraceEnvironment } from '../lib/utils/get_synthtrace_environment';
-const ENVIRONMENT = getSynthtraceEnvironment(__filename);
-
-function generateTrace(
- timestamp: number,
- transactionName: string,
- order: Instance[],
- db?: 'elasticsearch' | 'redis'
-) {
- return order
- .concat()
- .reverse()
- .reduce((prev, instance, index) => {
- const invertedIndex = order.length - index - 1;
-
- const duration = 50;
- const time = timestamp + invertedIndex * 10;
-
- const transaction: Transaction = instance
- .transaction({ transactionName })
- .timestamp(time)
- .duration(duration);
-
- if (prev) {
- const next = order[invertedIndex + 1].fields['service.name']!;
- transaction.children(
- instance
- .span({ spanName: `GET ${next}/api`, spanType: 'external', spanSubtype: 'http' })
- .destination(next)
- .duration(duration)
- .timestamp(time + 1)
- .children(prev)
- );
- } else if (db) {
- transaction.children(
- instance
- .span({ spanName: db, spanType: 'db', spanSubtype: db })
- .destination(db)
- .duration(duration)
- .timestamp(time + 1)
- );
- }
-
- return transaction;
- }, undefined)!;
-}
-
-function service(serviceName: string, agentName: AgentName) {
- return apm
- .service({ name: serviceName, environment: ENVIRONMENT, agentName })
- .instance(serviceName);
-}
+const environment = getSynthtraceEnvironment(__filename);
const scenario: Scenario = async (runOptions: RunOptions) => {
return {
generate: ({ range }) => {
- const frontendRum = service('frontend-rum', 'rum-js');
- const frontendNode = service('frontend-node', 'nodejs');
- const advertService = service('advertService', 'java');
- const checkoutService = service('checkoutService', 'go');
- const cartService = service('cartService', 'dotnet');
- const paymentService = service('paymentService', 'nodejs');
- const productCatalogService = service('productCatalogService', 'go');
return range
.interval('1s')
.rate(3)
- .generator((timestamp) => {
- return [
- generateTrace(
- timestamp,
- 'GET /api/adTag',
- [frontendRum, frontendNode, advertService],
- 'elasticsearch'
- ),
- generateTrace(
- timestamp,
- 'POST /api/addToCart',
- [frontendRum, frontendNode, cartService],
- 'redis'
- ),
- generateTrace(timestamp, 'POST /api/checkout', [
- frontendRum,
- frontendNode,
- checkoutService,
- paymentService,
- ]),
- generateTrace(
- timestamp,
- 'DELETE /api/clearCart',
- [checkoutService, cartService],
- 'redis'
- ),
- generateTrace(
- timestamp,
- 'GET /api/products',
- [frontendRum, frontendNode, productCatalogService],
- 'elasticsearch'
- ),
- generateTrace(timestamp, 'PUT /api/updateInventory', [
- checkoutService,
- productCatalogService,
- ]),
- ];
- });
+ .generator(
+ serviceMap({
+ services: [
+ { 'frontend-rum': 'rum-js' },
+ { 'frontend-node': 'nodejs' },
+ { advertService: 'java' },
+ { checkoutService: 'go' },
+ { cartService: 'dotnet' },
+ { paymentService: 'nodejs' },
+ { productCatalogService: 'go' },
+ ],
+ environment,
+ definePaths([rum, node, adv, chk, cart, pay, prod]) {
+ return [
+ [
+ [rum, 'fetchAd'],
+ [node, 'GET /nodejs/adTag'],
+ [adv, 'APIRestController#getAd'],
+ ['elasticsearch', 'GET ad-*/_search'],
+ ],
+ [
+ [rum, 'AddToCart'],
+ [node, 'POST /nodejs/addToCart'],
+ [cart, 'POST /dotnet/reserveProduct'],
+ ['redis', 'DECR inventory:i012345:stock'],
+ ],
+ [
+ [rum, 'Checkout'],
+ [node, 'POST /nodejs/placeOrder'],
+ [chk, 'POST /go/placeOrder'],
+ [pay, 'POST /nodejs/processPayment'],
+ ],
+ [
+ [chk, 'POST /go/clearCart'],
+ [cart, 'PUT /dotnet/cart/c12345/reset'],
+ ['redis', 'INCR inventory:i012345:stock'],
+ ],
+ [
+ [rum, 'ProductDashboard'],
+ [node, 'GET /nodejs/products'],
+ [prod, 'GET /go/product-catalog'],
+ ['elasticsearch', 'GET product-*/_search'],
+ ],
+ [
+ [chk, 'PUT /go/update-inventory'],
+ [prod, 'PUT /go/product/i012345'],
+ ],
+ [pay],
+ ];
+ },
+ })
+ );
},
};
};
diff --git a/packages/kbn-apm-synthtrace/tsconfig.json b/packages/kbn-apm-synthtrace/tsconfig.json
index 3db0ec03f6f4d..22ff0442879ab 100644
--- a/packages/kbn-apm-synthtrace/tsconfig.json
+++ b/packages/kbn-apm-synthtrace/tsconfig.json
@@ -8,7 +8,6 @@
"kbn_references": [
"@kbn/datemath",
"@kbn/apm-synthtrace-client",
- "@kbn/apm-plugin"
],
"exclude": [
"target/**/*",
diff --git a/packages/kbn-securitysolution-exception-list-components/src/list_header/__snapshots__/list_header.test.tsx.snap b/packages/kbn-securitysolution-exception-list-components/src/list_header/__snapshots__/list_header.test.tsx.snap
index 3311b524657af..e23da950c3486 100644
--- a/packages/kbn-securitysolution-exception-list-components/src/list_header/__snapshots__/list_header.test.tsx.snap
+++ b/packages/kbn-securitysolution-exception-list-components/src/list_header/__snapshots__/list_header.test.tsx.snap
@@ -185,7 +185,7 @@ Object {
- Manage rules
+ Link rules
@@ -565,7 +565,7 @@ Object {
- Manage rules
+ Link rules
@@ -848,7 +848,7 @@ Object {
- Manage rules
+ Link rules
@@ -1074,7 +1074,7 @@ Object {
- Manage rules
+ Link rules
@@ -1329,7 +1329,7 @@ Object {
- Manage rules
+ Link rules
@@ -1528,7 +1528,7 @@ Object {
- Manage rules
+ Link rules
diff --git a/packages/kbn-securitysolution-exception-list-components/src/list_header/menu_items/__snapshots__/menu_items.test.tsx.snap b/packages/kbn-securitysolution-exception-list-components/src/list_header/menu_items/__snapshots__/menu_items.test.tsx.snap
index ca6a0b1d4018c..16bda1172b17c 100644
--- a/packages/kbn-securitysolution-exception-list-components/src/list_header/menu_items/__snapshots__/menu_items.test.tsx.snap
+++ b/packages/kbn-securitysolution-exception-list-components/src/list_header/menu_items/__snapshots__/menu_items.test.tsx.snap
@@ -242,7 +242,7 @@ Object {
- Manage rules
+ Link rules
@@ -309,7 +309,7 @@ Object {
- Manage rules
+ Link rules
@@ -433,7 +433,7 @@ Object {
- Manage rules
+ Link rules
@@ -580,7 +580,7 @@ Object {
- Manage rules
+ Link rules
@@ -1022,7 +1022,7 @@ Object {
- Manage rules
+ Link rules
@@ -1117,7 +1117,7 @@ Object {
- Manage rules
+ Link rules
diff --git a/packages/kbn-securitysolution-exception-list-components/src/list_header/menu_items/index.tsx b/packages/kbn-securitysolution-exception-list-components/src/list_header/menu_items/index.tsx
index 14aa823046ff4..9062162b30a47 100644
--- a/packages/kbn-securitysolution-exception-list-components/src/list_header/menu_items/index.tsx
+++ b/packages/kbn-securitysolution-exception-list-components/src/list_header/menu_items/index.tsx
@@ -81,7 +81,7 @@ const MenuItemsComponent: FC = ({
if (typeof onManageRules === 'function') onManageRules();
}}
>
- {i18n.EXCEPTION_LIST_HEADER_MANAGE_RULES_BUTTON}
+ {i18n.EXCEPTION_LIST_HEADER_LINK_RULES_BUTTON}
)}
diff --git a/packages/kbn-securitysolution-exception-list-components/src/translations.ts b/packages/kbn-securitysolution-exception-list-components/src/translations.ts
index c80a6a07a6697..b769b9b10846a 100644
--- a/packages/kbn-securitysolution-exception-list-components/src/translations.ts
+++ b/packages/kbn-securitysolution-exception-list-components/src/translations.ts
@@ -67,10 +67,10 @@ export const EXCEPTION_LIST_HEADER_DELETE_ACTION = i18n.translate(
defaultMessage: 'Delete exception list',
}
);
-export const EXCEPTION_LIST_HEADER_MANAGE_RULES_BUTTON = i18n.translate(
- 'exceptionList-components.exception_list_header_manage_rules_button',
+export const EXCEPTION_LIST_HEADER_LINK_RULES_BUTTON = i18n.translate(
+ 'exceptionList-components.exception_list_header_link_rules_button',
{
- defaultMessage: 'Manage rules',
+ defaultMessage: 'Link rules',
}
);
diff --git a/packages/kbn-securitysolution-grouping/README.md b/packages/kbn-securitysolution-grouping/README.md
new file mode 100644
index 0000000000000..87b8047720a37
--- /dev/null
+++ b/packages/kbn-securitysolution-grouping/README.md
@@ -0,0 +1,3 @@
+# @kbn/securitysolution-grouping
+
+Grouping component and query. Currently only consumed by security solution alerts table. Package is a WIP. Refactoring to make generic https://github.com/elastic/kibana/issues/152491
diff --git a/packages/kbn-securitysolution-grouping/index.tsx b/packages/kbn-securitysolution-grouping/index.tsx
new file mode 100644
index 0000000000000..c9b970a1aed77
--- /dev/null
+++ b/packages/kbn-securitysolution-grouping/index.tsx
@@ -0,0 +1,31 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import React from 'react';
+import {
+ GroupSelectorProps,
+ Grouping,
+ GroupingProps,
+ GroupSelector,
+ RawBucket,
+ getGroupingQuery,
+ isNoneGroup,
+} from './src';
+import type { NamedAggregation, GroupingFieldTotalAggregation, GroupingAggregation } from './src';
+
+export const getGrouping = (props: GroupingProps): React.ReactElement => (
+
+);
+
+export const getGroupSelector = (
+ props: GroupSelectorProps
+): React.ReactElement => ;
+
+export { isNoneGroup, getGroupingQuery };
+
+export type { GroupingAggregation, GroupingFieldTotalAggregation, NamedAggregation, RawBucket };
diff --git a/packages/kbn-securitysolution-grouping/jest.config.js b/packages/kbn-securitysolution-grouping/jest.config.js
new file mode 100644
index 0000000000000..6ad3880e11f1a
--- /dev/null
+++ b/packages/kbn-securitysolution-grouping/jest.config.js
@@ -0,0 +1,26 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+module.exports = {
+ preset: '@kbn/test',
+ rootDir: '../..',
+ roots: ['/packages/kbn-securitysolution-grouping'],
+ coverageReporters: ['text', 'html'],
+ collectCoverageFrom: [
+ '/packages/kbn-securitysolution-grouping/**/*.{ts,tsx}',
+ '!/packages/kbn-securitysolution-grouping/**/*.test',
+ '!/packages/kbn-securitysolution-grouping/**/types/*',
+ '!/packages/kbn-securitysolution-grouping/**/*.type',
+ '!/packages/kbn-securitysolution-grouping/**/*.styles',
+ '!/packages/kbn-securitysolution-grouping/**/mocks/*',
+ '!/packages/kbn-securitysolution-grouping/**/*.config',
+ '!/packages/kbn-securitysolution-grouping/**/translations',
+ '!/packages/kbn-securitysolution-grouping/**/types/*',
+ ],
+ setupFilesAfterEnv: ['/packages/kbn-securitysolution-grouping/setup_test.ts'],
+};
diff --git a/packages/kbn-securitysolution-grouping/kibana.jsonc b/packages/kbn-securitysolution-grouping/kibana.jsonc
new file mode 100644
index 0000000000000..532eb8f883dfc
--- /dev/null
+++ b/packages/kbn-securitysolution-grouping/kibana.jsonc
@@ -0,0 +1,5 @@
+{
+ "type": "shared-common",
+ "id": "@kbn/securitysolution-grouping",
+ "owner": "@elastic/security-threat-hunting-explore"
+}
diff --git a/packages/kbn-securitysolution-grouping/package.json b/packages/kbn-securitysolution-grouping/package.json
new file mode 100644
index 0000000000000..e5baa64c3a1f4
--- /dev/null
+++ b/packages/kbn-securitysolution-grouping/package.json
@@ -0,0 +1,6 @@
+{
+ "name": "@kbn/securitysolution-grouping",
+ "private": true,
+ "version": "1.0.0",
+ "license": "SSPL-1.0 OR Elastic License 2.0"
+}
\ No newline at end of file
diff --git a/packages/kbn-securitysolution-grouping/setup_test.ts b/packages/kbn-securitysolution-grouping/setup_test.ts
new file mode 100644
index 0000000000000..bb55d97ec9302
--- /dev/null
+++ b/packages/kbn-securitysolution-grouping/setup_test.ts
@@ -0,0 +1,9 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+// eslint-disable-next-line import/no-extraneous-dependencies
+import '@testing-library/jest-dom';
diff --git a/x-pack/plugins/security_solution/public/common/components/grouping/accordion_panel/group_stats.test.tsx b/packages/kbn-securitysolution-grouping/src/components/accordion_panel/group_stats.test.tsx
similarity index 81%
rename from x-pack/plugins/security_solution/public/common/components/grouping/accordion_panel/group_stats.test.tsx
rename to packages/kbn-securitysolution-grouping/src/components/accordion_panel/group_stats.test.tsx
index 175a00ee7d2ac..ab7ab5d543f4b 100644
--- a/x-pack/plugins/security_solution/public/common/components/grouping/accordion_panel/group_stats.test.tsx
+++ b/packages/kbn-securitysolution-grouping/src/components/accordion_panel/group_stats.test.tsx
@@ -1,14 +1,14 @@
/*
* 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.
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
*/
import React from 'react';
import { fireEvent, render } from '@testing-library/react';
import { GroupStats } from './group_stats';
-import { TestProviders } from '../../../mock';
const onTakeActionsOpen = jest.fn();
const testProps = {
@@ -49,11 +49,7 @@ describe('Group stats', () => {
jest.clearAllMocks();
});
it('renders each stat item', () => {
- const { getByTestId } = render(
-
-
-
- );
+ const { getByTestId } = render();
expect(getByTestId('group-stats')).toBeInTheDocument();
testProps.badgeMetricStats.forEach(({ title: stat }) => {
expect(getByTestId(`metric-${stat}`)).toBeInTheDocument();
@@ -63,11 +59,7 @@ describe('Group stats', () => {
});
});
it('when onTakeActionsOpen is defined, call onTakeActionsOpen on popover click', () => {
- const { getByTestId, queryByTestId } = render(
-
-
-
- );
+ const { getByTestId, queryByTestId } = render();
fireEvent.click(getByTestId('take-action-button'));
expect(onTakeActionsOpen).toHaveBeenCalled();
['takeActionItem-1', 'takeActionItem-2'].forEach((actionItem) => {
@@ -75,11 +67,7 @@ describe('Group stats', () => {
});
});
it('when onTakeActionsOpen is undefined, render take actions dropdown on popover click', () => {
- const { getByTestId } = render(
-
-
-
- );
+ const { getByTestId } = render();
fireEvent.click(getByTestId('take-action-button'));
['takeActionItem-1', 'takeActionItem-2'].forEach((actionItem) => {
expect(getByTestId(actionItem)).toBeInTheDocument();
diff --git a/x-pack/plugins/security_solution/public/common/components/grouping/accordion_panel/group_stats.tsx b/packages/kbn-securitysolution-grouping/src/components/accordion_panel/group_stats.tsx
similarity index 87%
rename from x-pack/plugins/security_solution/public/common/components/grouping/accordion_panel/group_stats.tsx
rename to packages/kbn-securitysolution-grouping/src/components/accordion_panel/group_stats.tsx
index 9611019434c86..6de1bf6ef4678 100644
--- a/x-pack/plugins/security_solution/public/common/components/grouping/accordion_panel/group_stats.tsx
+++ b/packages/kbn-securitysolution-grouping/src/components/accordion_panel/group_stats.tsx
@@ -1,8 +1,9 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
*/
import {
@@ -16,7 +17,7 @@ import {
} from '@elastic/eui';
import React, { useCallback, useMemo, useState } from 'react';
import type { BadgeMetric, CustomMetric } from '.';
-import { StatsContainer } from '../styles';
+import { statsContainerCss } from '../styles';
import { TAKE_ACTION } from '../translations';
import type { RawBucket } from '../types';
@@ -46,7 +47,7 @@ const GroupStatsComponent = ({
() =>
badgeMetricStats?.map((metric) => (
-
+
<>
{metric.title}
@@ -58,7 +59,7 @@ const GroupStatsComponent = ({
>
-
+
)),
[badgeMetricStats]
@@ -68,10 +69,10 @@ const GroupStatsComponent = ({
() =>
customMetricStats?.map((customMetric) => (
-
+
{customMetric.title}
{customMetric.customStatRenderer}
-
+
)),
[customMetricStats]
diff --git a/x-pack/plugins/security_solution/public/common/components/grouping/accordion_panel/helpers.ts b/packages/kbn-securitysolution-grouping/src/components/accordion_panel/helpers.ts
similarity index 78%
rename from x-pack/plugins/security_solution/public/common/components/grouping/accordion_panel/helpers.ts
rename to packages/kbn-securitysolution-grouping/src/components/accordion_panel/helpers.ts
index d496b8d79068a..5529d5bf521c0 100644
--- a/x-pack/plugins/security_solution/public/common/components/grouping/accordion_panel/helpers.ts
+++ b/packages/kbn-securitysolution-grouping/src/components/accordion_panel/helpers.ts
@@ -1,8 +1,9 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
*/
export const createGroupFilter = (selectedGroup: string, query?: string) =>
diff --git a/x-pack/plugins/security_solution/public/common/components/grouping/accordion_panel/index.test.tsx b/packages/kbn-securitysolution-grouping/src/components/accordion_panel/index.test.tsx
similarity index 93%
rename from x-pack/plugins/security_solution/public/common/components/grouping/accordion_panel/index.test.tsx
rename to packages/kbn-securitysolution-grouping/src/components/accordion_panel/index.test.tsx
index db78f6f2e9257..9aa4b51437130 100644
--- a/x-pack/plugins/security_solution/public/common/components/grouping/accordion_panel/index.test.tsx
+++ b/packages/kbn-securitysolution-grouping/src/components/accordion_panel/index.test.tsx
@@ -1,8 +1,9 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
*/
import { fireEvent, render } from '@testing-library/react';
diff --git a/x-pack/plugins/security_solution/public/common/components/grouping/accordion_panel/index.tsx b/packages/kbn-securitysolution-grouping/src/components/accordion_panel/index.tsx
similarity index 92%
rename from x-pack/plugins/security_solution/public/common/components/grouping/accordion_panel/index.tsx
rename to packages/kbn-securitysolution-grouping/src/components/accordion_panel/index.tsx
index 548aa81272cd9..124c4ede22485 100644
--- a/x-pack/plugins/security_solution/public/common/components/grouping/accordion_panel/index.tsx
+++ b/packages/kbn-securitysolution-grouping/src/components/accordion_panel/index.tsx
@@ -1,14 +1,15 @@
/*
* 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.
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
*/
import { EuiAccordion, EuiFlexGroup, EuiFlexItem, EuiTitle } from '@elastic/eui';
import type { Filter } from '@kbn/es-query';
import React, { useCallback, useMemo } from 'react';
-import { firstNonNullValue } from '../../../../../common/endpoint/models/ecs_safety_helpers';
+import { firstNonNullValue } from '../../helpers';
import type { RawBucket } from '../types';
import { createGroupFilter } from './helpers';
diff --git a/x-pack/plugins/security_solution/public/common/components/grouping/empty_resuls_panel.tsx b/packages/kbn-securitysolution-grouping/src/components/empty_resuls_panel.tsx
similarity index 71%
rename from x-pack/plugins/security_solution/public/common/components/grouping/empty_resuls_panel.tsx
rename to packages/kbn-securitysolution-grouping/src/components/empty_resuls_panel.tsx
index b51cd14e84bab..8a706684d0c7e 100644
--- a/x-pack/plugins/security_solution/public/common/components/grouping/empty_resuls_panel.tsx
+++ b/packages/kbn-securitysolution-grouping/src/components/empty_resuls_panel.tsx
@@ -1,15 +1,15 @@
/*
* 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.
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
*/
import { EuiFlexGroup, EuiFlexItem, EuiImage, EuiPanel, EuiText, EuiTitle } from '@elastic/eui';
import React from 'react';
-import type { CoreStart } from '@kbn/core/public';
import { FormattedMessage } from '@kbn/i18n-react';
-import { useKibana } from '@kbn/kibana-react-plugin/public';
+import { noResultsIllustrationLight } from '@kbn/shared-svg';
const panelStyle = {
maxWidth: 500,
@@ -23,8 +23,6 @@ const heights = {
export const EmptyGroupingComponent: React.FC<{ height?: keyof typeof heights }> = ({
height = 'tall',
}) => {
- const { http } = useKibana().services;
-
return (
@@ -36,27 +34,21 @@ export const EmptyGroupingComponent: React.FC<{ height?: keyof typeof heights }>
-
+
diff --git a/x-pack/plugins/security_solution/public/common/components/grouping/groups_selector/custom_field_panel.tsx b/packages/kbn-securitysolution-grouping/src/components/group_selector/custom_field_panel.tsx
similarity index 81%
rename from x-pack/plugins/security_solution/public/common/components/grouping/groups_selector/custom_field_panel.tsx
rename to packages/kbn-securitysolution-grouping/src/components/group_selector/custom_field_panel.tsx
index dc45df7d7bf2e..158469f1697a2 100644
--- a/x-pack/plugins/security_solution/public/common/components/grouping/groups_selector/custom_field_panel.tsx
+++ b/packages/kbn-securitysolution-grouping/src/components/group_selector/custom_field_panel.tsx
@@ -1,8 +1,9 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
*/
import type { EuiComboBoxOptionOption } from '@elastic/eui';
@@ -50,22 +51,19 @@ export class CustomFieldPanel extends React.PureComponent {
{
fill
onClick={this.handleSubmit}
>
- {i18n.translate('xpack.securitySolution.selector.grouping.label.add', {
+ {i18n.translate('grouping.selector.grouping.label.add', {
defaultMessage: 'Add',
})}
diff --git a/x-pack/plugins/security_solution/public/common/components/grouping/groups_selector/index.test.tsx b/packages/kbn-securitysolution-grouping/src/components/group_selector/index.test.tsx
similarity index 79%
rename from x-pack/plugins/security_solution/public/common/components/grouping/groups_selector/index.test.tsx
rename to packages/kbn-securitysolution-grouping/src/components/group_selector/index.test.tsx
index a587206572f9d..4706247f2f0a7 100644
--- a/x-pack/plugins/security_solution/public/common/components/grouping/groups_selector/index.test.tsx
+++ b/packages/kbn-securitysolution-grouping/src/components/group_selector/index.test.tsx
@@ -1,13 +1,13 @@
/*
* 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.
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
*/
import { fireEvent, render } from '@testing-library/react';
-import { TestProviders } from '../../../mock';
-import { GroupsSelector } from '..';
+import { GroupSelector } from '..';
import React from 'react';
const onGroupChange = jest.fn();
@@ -69,19 +69,11 @@ describe('group selector', () => {
jest.clearAllMocks();
});
it('Sets the selected group from the groupSelected prop', () => {
- const { getByTestId } = render(
-
-
-
- );
+ const { getByTestId } = render();
expect(getByTestId('group-selector-dropdown').textContent).toBe('Group alerts by: Rule name');
});
it('Presents correct option when group selector dropdown is clicked', () => {
- const { getByTestId } = render(
-
-
-
- );
+ const { getByTestId } = render();
fireEvent.click(getByTestId('group-selector-dropdown'));
[
...testProps.options,
@@ -92,11 +84,7 @@ describe('group selector', () => {
});
});
it('Presents fields dropdown when custom field option is selected', () => {
- const { getByTestId } = render(
-
-
-
- );
+ const { getByTestId } = render();
fireEvent.click(getByTestId('group-selector-dropdown'));
fireEvent.click(getByTestId('panel-none'));
expect(onGroupChange).toHaveBeenCalled();
diff --git a/x-pack/plugins/security_solution/public/common/components/grouping/groups_selector/index.tsx b/packages/kbn-securitysolution-grouping/src/components/group_selector/index.tsx
similarity index 90%
rename from x-pack/plugins/security_solution/public/common/components/grouping/groups_selector/index.tsx
rename to packages/kbn-securitysolution-grouping/src/components/group_selector/index.tsx
index 81382406d2af4..210ffef50e381 100644
--- a/x-pack/plugins/security_solution/public/common/components/grouping/groups_selector/index.tsx
+++ b/packages/kbn-securitysolution-grouping/src/components/group_selector/index.tsx
@@ -1,8 +1,9 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
*/
import type {
@@ -16,7 +17,8 @@ import { CustomFieldPanel } from './custom_field_panel';
import * as i18n from '../translations';
import { StyledContextMenu, StyledEuiButtonEmpty } from '../styles';
-interface GroupSelectorProps {
+export interface GroupSelectorProps {
+ 'data-test-subj'?: string;
fields: FieldSpec[];
groupSelected: string;
onGroupChange: (groupSelection: string) => void;
@@ -24,7 +26,8 @@ interface GroupSelectorProps {
title?: string;
}
-const GroupsSelectorComponent = ({
+const GroupSelectorComponent = ({
+ 'data-test-subj': dataTestSubj,
fields,
groupSelected = 'none',
onGroupChange,
@@ -131,6 +134,7 @@ const GroupsSelectorComponent = ({
return (
{
});
it('Renders group counts when groupsNumber > 0', () => {
const { getByTestId, getAllByTestId, queryByTestId } = render(
-
-
-
+
+
+
);
expect(getByTestId('alert-count').textContent).toBe('2 alerts');
expect(getByTestId('groups-count').textContent).toBe('2 groups');
@@ -140,9 +142,9 @@ describe('grouping container', () => {
},
};
const { getByTestId, queryByTestId } = render(
-
-
-
+
+
+
);
expect(queryByTestId('alert-count')).not.toBeInTheDocument();
expect(queryByTestId('groups-count')).not.toBeInTheDocument();
@@ -152,9 +154,9 @@ describe('grouping container', () => {
it('Opens one group at a time when each group is clicked', () => {
const { getAllByTestId } = render(
-
-
-
+
+
+
);
const group1 = within(getAllByTestId('grouping-accordion')[0]).getAllByRole('button')[0];
const group2 = within(getAllByTestId('grouping-accordion')[1]).getAllByRole('button')[0];
diff --git a/x-pack/plugins/security_solution/public/common/components/grouping/container/index.tsx b/packages/kbn-securitysolution-grouping/src/components/grouping.tsx
similarity index 76%
rename from x-pack/plugins/security_solution/public/common/components/grouping/container/index.tsx
rename to packages/kbn-securitysolution-grouping/src/components/grouping.tsx
index 9956ba8e40c06..8d85df8710f95 100644
--- a/x-pack/plugins/security_solution/public/common/components/grouping/container/index.tsx
+++ b/packages/kbn-securitysolution-grouping/src/components/grouping.tsx
@@ -1,8 +1,9 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
*/
import {
@@ -14,31 +15,20 @@ import {
} from '@elastic/eui';
import type { Filter } from '@kbn/es-query';
import React, { useMemo, useState } from 'react';
-import { firstNonNullValue } from '../../../../../common/endpoint/models/ecs_safety_helpers';
-import { createGroupFilter } from '../accordion_panel/helpers';
-import { tableDefaults } from '../../../store/data_table/defaults';
-import { defaultUnit } from '../../toolbar/unit';
-import type { BadgeMetric, CustomMetric } from '../accordion_panel';
-import { GroupPanel } from '../accordion_panel';
-import { GroupStats } from '../accordion_panel/group_stats';
-import { EmptyGroupingComponent } from '../empty_resuls_panel';
-import { GroupingStyledContainer, GroupsUnitCount } from '../styles';
-import { GROUPS_UNIT } from '../translations';
-import type { GroupingTableAggregation, RawBucket } from '../types';
+import { defaultUnit, firstNonNullValue } from '../helpers';
+import { createGroupFilter } from './accordion_panel/helpers';
+import type { BadgeMetric, CustomMetric } from './accordion_panel';
+import { GroupPanel } from './accordion_panel';
+import { GroupStats } from './accordion_panel/group_stats';
+import { EmptyGroupingComponent } from './empty_resuls_panel';
+import { groupingContainerCss, groupsUnitCountCss } from './styles';
+import { GROUPS_UNIT } from './translations';
+import type { GroupingAggregation, GroupingFieldTotalAggregation, RawBucket } from './types';
-export interface GroupingContainerProps {
+export interface GroupingProps {
badgeMetricStats?: (fieldBucket: RawBucket) => BadgeMetric[];
customMetricStats?: (fieldBucket: RawBucket) => CustomMetric[];
- data: GroupingTableAggregation &
- Record<
- string,
- {
- value?: number | null;
- buckets?: Array<{
- doc_count?: number | null;
- }>;
- }
- >;
+ data?: GroupingAggregation & GroupingFieldTotalAggregation;
groupPanelRenderer?: (fieldBucket: RawBucket) => JSX.Element | undefined;
groupsSelector?: JSX.Element;
inspectButton?: JSX.Element;
@@ -48,6 +38,7 @@ export interface GroupingContainerProps {
pageSize: number;
onChangeItemsPerPage: (itemsPerPageNumber: number) => void;
onChangePage: (pageNumber: number) => void;
+ itemsPerPageOptions: number[];
};
renderChildComponent: (groupFilter: Filter[]) => React.ReactNode;
selectedGroup: string;
@@ -55,7 +46,7 @@ export interface GroupingContainerProps {
unit?: (n: number) => string;
}
-const GroupingContainerComponent = ({
+const GroupingComponent = ({
badgeMetricStats,
customMetricStats,
data,
@@ -68,7 +59,7 @@ const GroupingContainerComponent = ({
selectedGroup,
takeActionItems,
unit = defaultUnit,
-}: GroupingContainerProps) => {
+}: GroupingProps) => {
const [trigger, setTrigger] = useState<
Record
>({});
@@ -86,7 +77,7 @@ const GroupingContainerComponent = ({
const groupPanels = useMemo(
() =>
- data.stackByMultipleFields0?.buckets?.map((groupBucket) => {
+ data?.stackByMultipleFields0?.buckets?.map((groupBucket) => {
const group = firstNonNullValue(groupBucket.key);
const groupKey = `group0-${group}`;
@@ -128,7 +119,7 @@ const GroupingContainerComponent = ({
[
badgeMetricStats,
customMetricStats,
- data.stackByMultipleFields0?.buckets,
+ data?.stackByMultipleFields0?.buckets,
groupPanelRenderer,
isLoading,
renderChildComponent,
@@ -153,12 +144,18 @@ const GroupingContainerComponent = ({
{groupsNumber > 0 ? (
- {unitCountText}
+
+ {unitCountText}
+
-
+
{unitGroupsCountText}
-
+
) : null}
@@ -170,7 +167,7 @@ const GroupingContainerComponent = ({
-
+
{groupsNumber > 0 ? (
<>
{groupPanels}
@@ -179,7 +176,7 @@ const GroupingContainerComponent = ({
activePage={pagination.pageIndex}
data-test-subj="grouping-table-pagination"
itemsPerPage={pagination.pageSize}
- itemsPerPageOptions={tableDefaults.itemsPerPageOptions}
+ itemsPerPageOptions={pagination.itemsPerPageOptions}
onChangeItemsPerPage={pagination.onChangeItemsPerPage}
onChangePage={pagination.onChangePage}
pageCount={pageCount}
@@ -194,9 +191,9 @@ const GroupingContainerComponent = ({
>
)}
-
+
>
);
};
-export const GroupingContainer = React.memo(GroupingContainerComponent);
+export const Grouping = React.memo(GroupingComponent);
diff --git a/x-pack/plugins/security_solution/public/common/components/grouping/index.tsx b/packages/kbn-securitysolution-grouping/src/components/index.tsx
similarity index 55%
rename from x-pack/plugins/security_solution/public/common/components/grouping/index.tsx
rename to packages/kbn-securitysolution-grouping/src/components/index.tsx
index c239373acd5e2..d44dfe7cc5303 100644
--- a/x-pack/plugins/security_solution/public/common/components/grouping/index.tsx
+++ b/packages/kbn-securitysolution-grouping/src/components/index.tsx
@@ -1,16 +1,15 @@
/*
* 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.
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
*/
import { NONE_GROUP_KEY } from './types';
-export * from './container';
-export * from './query';
-export * from './query/types';
-export * from './groups_selector';
+export * from './group_selector';
export * from './types';
+export * from './grouping';
export const isNoneGroup = (groupKey: string) => groupKey === NONE_GROUP_KEY;
diff --git a/x-pack/plugins/security_solution/public/common/components/grouping/styles.tsx b/packages/kbn-securitysolution-grouping/src/components/styles.tsx
similarity index 58%
rename from x-pack/plugins/security_solution/public/common/components/grouping/styles.tsx
rename to packages/kbn-securitysolution-grouping/src/components/styles.tsx
index 80eb0183cb510..ede1b4eae164c 100644
--- a/x-pack/plugins/security_solution/public/common/components/grouping/styles.tsx
+++ b/packages/kbn-securitysolution-grouping/src/components/styles.tsx
@@ -1,26 +1,28 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
*/
import { EuiButtonEmpty, EuiContextMenu } from '@elastic/eui';
import { euiStyled } from '@kbn/kibana-react-plugin/common';
-import styled from 'styled-components';
+import { css } from '@emotion/react';
+import { euiThemeVars } from '@kbn/ui-theme';
-export const GroupsUnitCount = styled.span`
- font-size: ${({ theme }) => theme.eui.euiFontSizeXS};
- font-weight: ${({ theme }) => theme.eui.euiFontWeightSemiBold};
- border-right: ${({ theme }) => theme.eui.euiBorderThin};
+export const groupsUnitCountCss = css`
+ font-size: ${euiThemeVars.euiFontSizeXS};
+ font-weight: ${euiThemeVars.euiFontWeightSemiBold};
+ border-right: ${euiThemeVars.euiBorderThin};
margin-right: 16px;
padding-right: 16px;
`;
-export const StatsContainer = styled.span`
- font-size: ${({ theme }) => theme.eui.euiFontSizeXS};
- font-weight: ${({ theme }) => theme.eui.euiFontWeightSemiBold};
- border-right: ${({ theme }) => theme.eui.euiBorderThin};
+export const statsContainerCss = css`
+ font-size: ${euiThemeVars.euiFontSizeXS};
+ font-weight: ${euiThemeVars.euiFontWeightSemiBold};
+ border-right: ${euiThemeVars.euiBorderThin};
margin-right: 16px;
padding-right: 16px;
.smallDot {
@@ -33,26 +35,26 @@ export const StatsContainer = styled.span`
}
`;
-export const GroupingStyledContainer = styled.div`
+export const groupingContainerCss = css`
.euiAccordion__childWrapper .euiAccordion__padding--m {
margin-left: 8px;
margin-right: 8px;
- border-left: ${({ theme }) => theme.eui.euiBorderThin};
- border-right: ${({ theme }) => theme.eui.euiBorderThin};
- border-bottom: ${({ theme }) => theme.eui.euiBorderThin};
+ border-left: ${euiThemeVars.euiBorderThin};
+ border-right: ${euiThemeVars.euiBorderThin};
+ border-bottom: ${euiThemeVars.euiBorderThin};
border-radius: 0 0 6px 6px;
}
.euiAccordion__triggerWrapper {
- border-bottom: ${({ theme }) => theme.eui.euiBorderThin};
- border-left: ${({ theme }) => theme.eui.euiBorderThin};
- border-right: ${({ theme }) => theme.eui.euiBorderThin};
+ border-bottom: ${euiThemeVars.euiBorderThin};
+ border-left: ${euiThemeVars.euiBorderThin};
+ border-right: ${euiThemeVars.euiBorderThin};
border-radius: 6px;
min-height: 78px;
padding-left: 16px;
padding-right: 16px;
}
.groupingAccordionForm {
- border-top: ${({ theme }) => theme.eui.euiBorderThin};
+ border-top: ${euiThemeVars.euiBorderThin};
border-bottom: none;
border-radius: 6px;
min-width: 1090px;
@@ -75,7 +77,7 @@ export const StyledContextMenu = euiStyled(EuiContextMenu)`
text-overflow: ellipsis;
}
.euiContextMenuItem {
- border-bottom: ${({ theme }) => theme.eui.euiBorderThin};
+ border-bottom: ${euiThemeVars.euiBorderThin};
}
.euiContextMenuItem:last-child {
border: none;
diff --git a/packages/kbn-securitysolution-grouping/src/components/translations.ts b/packages/kbn-securitysolution-grouping/src/components/translations.ts
new file mode 100644
index 0000000000000..0ce0e118e7a43
--- /dev/null
+++ b/packages/kbn-securitysolution-grouping/src/components/translations.ts
@@ -0,0 +1,54 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import { i18n } from '@kbn/i18n';
+
+export const GROUPS_UNIT = (totalCount: number) =>
+ i18n.translate('grouping.total.unit', {
+ values: { totalCount },
+ defaultMessage: `{totalCount, plural, =1 {group} other {groups}}`,
+ });
+
+export const TAKE_ACTION = i18n.translate('grouping.additionalActions.takeAction', {
+ defaultMessage: 'Take actions',
+});
+
+export const BETA = i18n.translate('grouping.betaLabel', {
+ defaultMessage: 'Beta',
+});
+
+export const BETA_TOOL_TIP = i18n.translate('grouping.betaToolTip', {
+ defaultMessage:
+ 'Grouping may show only a subset of alerts while in beta. To see all alerts, use the list view by selecting "None"',
+});
+
+export const GROUP_BY = i18n.translate('grouping.alerts.label', {
+ defaultMessage: 'Group alerts by',
+});
+
+export const GROUP_BY_CUSTOM_FIELD = i18n.translate('grouping.customGroupByPanelTitle', {
+ defaultMessage: 'Group By Custom Field',
+});
+
+export const SELECT_FIELD = i18n.translate('grouping.groupByPanelTitle', {
+ defaultMessage: 'Select Field',
+});
+
+export const NONE = i18n.translate('grouping.noneGroupByOptionName', {
+ defaultMessage: 'None',
+});
+
+export const CUSTOM_FIELD = i18n.translate('grouping.customGroupByOptionName', {
+ defaultMessage: 'Custom field',
+});
+
+export const ALERTS_UNIT = (totalCount: number) =>
+ i18n.translate('grouping.eventsTab.unit', {
+ values: { totalCount },
+ defaultMessage: `{totalCount, plural, =1 {alert} other {alerts}}`,
+ });
diff --git a/x-pack/plugins/security_solution/public/common/components/grouping/types.ts b/packages/kbn-securitysolution-grouping/src/components/types.ts
similarity index 80%
rename from x-pack/plugins/security_solution/public/common/components/grouping/types.ts
rename to packages/kbn-securitysolution-grouping/src/components/types.ts
index b4685c75cab29..5a86bae394dd5 100644
--- a/x-pack/plugins/security_solution/public/common/components/grouping/types.ts
+++ b/packages/kbn-securitysolution-grouping/src/components/types.ts
@@ -1,13 +1,17 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
*/
-import type { GenericBuckets } from '../../../../common/search_strategy/common';
-
-export const DEFAULT_GROUPING_QUERY_ID = 'defaultGroupingQuery';
+// copied from common/search_strategy/common
+export interface GenericBuckets {
+ key: string | string[];
+ key_as_string?: string; // contains, for example, formatted dates
+ doc_count: number;
+}
export const NONE_GROUP_KEY = 'none';
@@ -43,7 +47,7 @@ export type RawBucket = GenericBuckets & {
};
/** Defines the shape of the aggregation returned by Elasticsearch */
-export interface GroupingTableAggregation {
+export interface GroupingAggregation {
stackByMultipleFields0?: {
buckets?: RawBucket[];
};
diff --git a/packages/kbn-securitysolution-grouping/src/containers/index.ts b/packages/kbn-securitysolution-grouping/src/containers/index.ts
new file mode 100644
index 0000000000000..acae150b85548
--- /dev/null
+++ b/packages/kbn-securitysolution-grouping/src/containers/index.ts
@@ -0,0 +1,10 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+export * from './query';
+export * from './query/types';
diff --git a/x-pack/plugins/security_solution/public/common/components/grouping/query/index.test.ts b/packages/kbn-securitysolution-grouping/src/containers/query/index.test.ts
similarity index 93%
rename from x-pack/plugins/security_solution/public/common/components/grouping/query/index.test.ts
rename to packages/kbn-securitysolution-grouping/src/containers/query/index.test.ts
index cc4075265f1f9..432f3e645ef2b 100644
--- a/x-pack/plugins/security_solution/public/common/components/grouping/query/index.test.ts
+++ b/packages/kbn-securitysolution-grouping/src/containers/query/index.test.ts
@@ -1,12 +1,13 @@
/*
* 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.
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
*/
-import type { GroupingQueryArgs } from '..';
-import { getGroupingQuery, MAX_QUERY_SIZE } from '..';
+import type { GroupingQueryArgs } from './types';
+import { getGroupingQuery, MAX_QUERY_SIZE } from '.';
const testProps: GroupingQueryArgs = {
additionalFilters: [],
diff --git a/x-pack/plugins/security_solution/public/common/components/grouping/query/index.ts b/packages/kbn-securitysolution-grouping/src/containers/query/index.ts
similarity index 95%
rename from x-pack/plugins/security_solution/public/common/components/grouping/query/index.ts
rename to packages/kbn-securitysolution-grouping/src/containers/query/index.ts
index 8a0242e8cc3ae..ab40ac554931a 100644
--- a/x-pack/plugins/security_solution/public/common/components/grouping/query/index.ts
+++ b/packages/kbn-securitysolution-grouping/src/containers/query/index.ts
@@ -1,8 +1,9 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
*/
import { isEmpty } from 'lodash/fp';
diff --git a/x-pack/plugins/security_solution/public/common/components/grouping/query/types.ts b/packages/kbn-securitysolution-grouping/src/containers/query/types.ts
similarity index 90%
rename from x-pack/plugins/security_solution/public/common/components/grouping/query/types.ts
rename to packages/kbn-securitysolution-grouping/src/containers/query/types.ts
index 4f1eb464e6fa6..d6c08c2d12514 100644
--- a/x-pack/plugins/security_solution/public/common/components/grouping/query/types.ts
+++ b/packages/kbn-securitysolution-grouping/src/containers/query/types.ts
@@ -1,8 +1,9 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
*/
import type { MappingRuntimeFields } from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
diff --git a/packages/kbn-securitysolution-grouping/src/helpers.ts b/packages/kbn-securitysolution-grouping/src/helpers.ts
new file mode 100644
index 0000000000000..f07c369e022a0
--- /dev/null
+++ b/packages/kbn-securitysolution-grouping/src/helpers.ts
@@ -0,0 +1,32 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+import * as i18n from './components/translations';
+
+/**
+ * All mappings in Elasticsearch support arrays. They can also return null values or be missing. For example, a `keyword` mapping could return `null` or `[null]` or `[]` or `'hi'`, or `['hi', 'there']`. We need to handle these cases in order to avoid throwing an error.
+ * When dealing with an value that comes from ES, wrap the underlying type in `ECSField`. For example, if you have a `keyword` or `text` value coming from ES, cast it to `ECSField`.
+ */
+export type ECSField = T | null | undefined | Array;
+/**
+ * Return first non-null value. If the field contains an array, this will return the first value that isn't null. If the field isn't an array it'll be returned unless it's null.
+ */
+export function firstNonNullValue(valueOrCollection: ECSField): T | undefined {
+ if (valueOrCollection === null) {
+ return undefined;
+ } else if (Array.isArray(valueOrCollection)) {
+ for (const value of valueOrCollection) {
+ if (value !== null) {
+ return value;
+ }
+ }
+ } else {
+ return valueOrCollection;
+ }
+}
+
+export const defaultUnit = (n: number) => i18n.ALERTS_UNIT(n);
diff --git a/packages/kbn-securitysolution-grouping/src/index.ts b/packages/kbn-securitysolution-grouping/src/index.ts
new file mode 100644
index 0000000000000..b9b10cc40f62a
--- /dev/null
+++ b/packages/kbn-securitysolution-grouping/src/index.ts
@@ -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 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+export * from './components';
+export * from './containers';
+export * from './helpers';
diff --git a/packages/kbn-securitysolution-grouping/tsconfig.json b/packages/kbn-securitysolution-grouping/tsconfig.json
new file mode 100644
index 0000000000000..9eeb0195df7ce
--- /dev/null
+++ b/packages/kbn-securitysolution-grouping/tsconfig.json
@@ -0,0 +1,28 @@
+{
+ "extends": "../../tsconfig.base.json",
+ "compilerOptions": {
+ "outDir": "target/types",
+ "types": [
+ "jest",
+ "node",
+ "react",
+ "@emotion/react/types/css-prop"
+ ]
+ },
+ "include": [
+ "**/*.ts",
+ "**/*.tsx",
+ ],
+ "exclude": [
+ "target/**/*"
+ ],
+ "kbn_references": [
+ "@kbn/data-views-plugin",
+ "@kbn/es-query",
+ "@kbn/i18n",
+ "@kbn/i18n-react",
+ "@kbn/kibana-react-plugin",
+ "@kbn/shared-svg",
+ "@kbn/ui-theme"
+ ]
+}
diff --git a/packages/kbn-securitysolution-io-ts-list-types/src/request/create_endpoint_list_item_schema/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/request/create_endpoint_list_item_schema/index.ts
index 0262e16539e9f..8c8e1d3e0db4b 100644
--- a/packages/kbn-securitysolution-io-ts-list-types/src/request/create_endpoint_list_item_schema/index.ts
+++ b/packages/kbn-securitysolution-io-ts-list-types/src/request/create_endpoint_list_item_schema/index.ts
@@ -22,7 +22,6 @@ import { description } from '../../common/description';
import { name } from '../../common/name';
import { meta } from '../../common/meta';
import { tags } from '../../common/tags';
-import { ExpireTimeOrUndefined, expireTimeOrUndefined } from '../../common';
export const createEndpointListItemSchema = t.intersection([
t.exact(
@@ -40,7 +39,6 @@ export const createEndpointListItemSchema = t.intersection([
meta, // defaults to undefined if not set during decode
os_types: osTypeArrayOrUndefined, // defaults to empty array if not set during decode
tags, // defaults to empty array if not set during decode
- expire_time: expireTimeOrUndefined, // defaults to undefined if not set during decode
})
),
]);
@@ -50,12 +48,11 @@ export type CreateEndpointListItemSchema = t.OutputOf>,
- 'tags' | 'item_id' | 'entries' | 'comments' | 'os_types' | 'expire_time'
+ 'tags' | 'item_id' | 'entries' | 'comments' | 'os_types'
> & {
comments: CreateCommentsArray;
tags: Tags;
item_id: ItemId;
entries: EntriesArray;
os_types: OsTypeArray;
- expire_time: ExpireTimeOrUndefined;
};
diff --git a/packages/kbn-securitysolution-io-ts-list-types/src/request/update_endpoint_list_item_schema/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/request/update_endpoint_list_item_schema/index.ts
index b0669b05463cf..8e5aa41e1fad2 100644
--- a/packages/kbn-securitysolution-io-ts-list-types/src/request/update_endpoint_list_item_schema/index.ts
+++ b/packages/kbn-securitysolution-io-ts-list-types/src/request/update_endpoint_list_item_schema/index.ts
@@ -21,7 +21,6 @@ import { Tags, tags } from '../../common/tags';
import { RequiredKeepUndefined } from '../../common/required_keep_undefined';
import { UpdateCommentsArray } from '../../common/update_comment';
import { EntriesArray } from '../../common/entries';
-import { ExpireTimeOrUndefined, expireTimeOrUndefined } from '../../common';
export const updateEndpointListItemSchema = t.intersection([
t.exact(
@@ -41,7 +40,6 @@ export const updateEndpointListItemSchema = t.intersection([
meta, // defaults to undefined if not set during decode
os_types: osTypeArrayOrUndefined, // defaults to empty array if not set during decode
tags, // defaults to empty array if not set during decode
- expire_time: expireTimeOrUndefined,
})
),
]);
@@ -51,11 +49,10 @@ export type UpdateEndpointListItemSchema = t.OutputOf>,
- 'tags' | 'entries' | 'comments' | 'expire_time'
+ 'tags' | 'entries' | 'comments'
> & {
comments: UpdateCommentsArray;
tags: Tags;
entries: EntriesArray;
os_types: OsTypeArray;
- expire_time: ExpireTimeOrUndefined;
};
diff --git a/src/core/server/integration_tests/saved_objects/migrations/group3/skip_reindex.test.ts b/src/core/server/integration_tests/saved_objects/migrations/group3/skip_reindex.test.ts
index 5354a958e8cb7..f239f36c9ebc4 100644
--- a/src/core/server/integration_tests/saved_objects/migrations/group3/skip_reindex.test.ts
+++ b/src/core/server/integration_tests/saved_objects/migrations/group3/skip_reindex.test.ts
@@ -22,7 +22,8 @@ import {
} from '../kibana_migrator_test_kit';
import { delay } from '../test_utils';
-describe('when migrating to a new version', () => {
+// FLAKY: https://github.com/elastic/kibana/issues/152448
+describe.skip('when migrating to a new version', () => {
let esServer: TestElasticsearchUtils['es'];
let esClient: ElasticsearchClient;
let migrator: IKibanaMigrator;
diff --git a/src/plugins/data_view_editor/kibana.jsonc b/src/plugins/data_view_editor/kibana.jsonc
index 007a7f78321db..bdec3b4f4943d 100644
--- a/src/plugins/data_view_editor/kibana.jsonc
+++ b/src/plugins/data_view_editor/kibana.jsonc
@@ -13,6 +13,7 @@
],
"requiredBundles": [
"kibanaReact",
+ "kibanaUtils",
"esUiShared"
]
}
diff --git a/src/plugins/data_view_editor/public/components/form_fields/title_docs_popover.test.tsx b/src/plugins/data_view_editor/public/components/form_fields/title_docs_popover.test.tsx
new file mode 100644
index 0000000000000..37d8f53b69ab9
--- /dev/null
+++ b/src/plugins/data_view_editor/public/components/form_fields/title_docs_popover.test.tsx
@@ -0,0 +1,27 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import React from 'react';
+import { findTestSubject } from '@elastic/eui/lib/test';
+import { mountWithIntl } from '@kbn/test-jest-helpers';
+import { TitleDocsPopover } from './title_docs_popover';
+
+describe('DataViewEditor TitleDocsPopover', () => {
+ it('should render normally', async () => {
+ const component = mountWithIntl();
+
+ expect(findTestSubject(component, 'indexPatternDocsButton').exists()).toBeTruthy();
+ expect(findTestSubject(component, 'indexPatternDocsPopoverContent').exists()).toBeFalsy();
+
+ findTestSubject(component, 'indexPatternDocsButton').simulate('click');
+
+ await component.update();
+
+ expect(findTestSubject(component, 'indexPatternDocsPopoverContent').exists()).toBeTruthy();
+ });
+});
diff --git a/src/plugins/data_view_editor/public/components/form_fields/title_docs_popover.tsx b/src/plugins/data_view_editor/public/components/form_fields/title_docs_popover.tsx
new file mode 100644
index 0000000000000..7e762a161639e
--- /dev/null
+++ b/src/plugins/data_view_editor/public/components/form_fields/title_docs_popover.tsx
@@ -0,0 +1,114 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import React, { useState } from 'react';
+import { css } from '@emotion/react';
+import { FormattedMessage } from '@kbn/i18n-react';
+import {
+ EuiButtonIcon,
+ EuiPanel,
+ EuiPopover,
+ EuiPopoverTitle,
+ EuiText,
+ EuiCode,
+} from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+
+export const TitleDocsPopover: React.FC = () => {
+ const [isOpen, setIsOpen] = useState(false);
+
+ const helpButton = (
+ setIsOpen((prev) => !prev)}
+ iconType="documentation"
+ data-test-subj="indexPatternDocsButton"
+ aria-label={i18n.translate('indexPatternEditor.titleDocsPopover.ariaLabel', {
+ defaultMessage: 'Index pattern examples',
+ })}
+ />
+ );
+
+ return (
+ setIsOpen(false)}
+ >
+
+ {i18n.translate('indexPatternEditor.titleDocsPopover.title', {
+ defaultMessage: 'Index pattern',
+ })}
+
+
+
+
+
+
+
+ -
+
+
+
+
+ filebeat-*
+
+
+ -
+
+
+
+
+ filebeat-a,filebeat-b
+
+
+ -
+
+
+
+
+ filebeat-*,-filebeat-c
+
+
+ -
+
+ {i18n.translate(
+ 'indexPatternEditor.titleDocsPopover.dontUseSpecialCharactersDescription',
+ {
+ defaultMessage: 'Spaces and the characters /?"<>| are not allowed.',
+ }
+ )}
+
+
+
+
+
+
+ );
+};
diff --git a/src/plugins/data_view_editor/public/components/form_fields/title_field.tsx b/src/plugins/data_view_editor/public/components/form_fields/title_field.tsx
index b2ea9c78e9fca..3824a6cea5258 100644
--- a/src/plugins/data_view_editor/public/components/form_fields/title_field.tsx
+++ b/src/plugins/data_view_editor/public/components/form_fields/title_field.tsx
@@ -22,6 +22,7 @@ import { canAppendWildcard } from '../../lib';
import { schema } from '../form_schema';
import { RollupIndicesCapsResponse, IndexPatternConfig, MatchedIndicesSet } from '../../types';
import { matchedIndiciesDefault } from '../../data_view_editor_service';
+import { TitleDocsPopover } from './title_docs_popover';
interface TitleFieldProps {
isRollup: boolean;
@@ -194,6 +195,8 @@ export const TitleField = ({
isLoading={field.isValidating}
fullWidth
data-test-subj="createIndexPatternTitleInput"
+ append={}
+ placeholder="example-*"
/>
);
diff --git a/src/plugins/data_view_editor/public/components/form_schema.ts b/src/plugins/data_view_editor/public/components/form_schema.ts
index 69993f17ecb35..eeadf52075440 100644
--- a/src/plugins/data_view_editor/public/components/form_schema.ts
+++ b/src/plugins/data_view_editor/public/components/form_schema.ts
@@ -28,10 +28,6 @@ export const schema = {
defaultMessage: 'Index pattern',
}),
defaultValue: '',
- helpText: i18n.translate('indexPatternEditor.validations.titleHelpText', {
- defaultMessage:
- 'Enter an index pattern that matches one or more data sources. Use an asterisk (*) to match multiple characters. Spaces and the characters , /, ?, ", <, >, | are not allowed.',
- }),
validations: [
{
validator: fieldValidators.emptyField(
@@ -84,7 +80,7 @@ export const schema = {
},
isAdHoc: {
label: i18n.translate('indexPatternEditor.editor.form.IsAdHocLabel', {
- defaultMessage: 'Creeate AdHoc DataView',
+ defaultMessage: 'Create AdHoc DataView',
}),
defaultValue: false,
type: 'hidden',
diff --git a/src/plugins/data_view_editor/public/components/preview_panel/indices_list/__snapshots__/indices_list.test.tsx.snap b/src/plugins/data_view_editor/public/components/preview_panel/indices_list/__snapshots__/indices_list.test.tsx.snap
index 609d88fac17b6..c068a7ef09447 100644
--- a/src/plugins/data_view_editor/public/components/preview_panel/indices_list/__snapshots__/indices_list.test.tsx.snap
+++ b/src/plugins/data_view_editor/public/components/preview_panel/indices_list/__snapshots__/indices_list.test.tsx.snap
@@ -201,6 +201,113 @@ exports[`IndicesList should change per page 1`] = `
`;
+exports[`IndicesList should highlight fully when an exact match 1`] = `
+
+
+
+
+
+
+
+ logs
+
+ tash
+
+
+
+
+
+
+
+ some_logs
+
+
+
+
+
+
+
+
+
+
+
+
+ }
+ closePopover={[Function]}
+ display="inline-block"
+ hasArrow={true}
+ id="customizablePagination"
+ isOpen={false}
+ ownFocus={true}
+ panelPaddingSize="none"
+ >
+
+ 5
+ ,
+
+ 10
+ ,
+
+ 20
+ ,
+
+ 50
+ ,
+ ]
+ }
+ />
+
+
+
+
+`;
+
exports[`IndicesList should highlight the query in the matches 1`] = `
- es
+
+ es
+
diff --git a/src/plugins/data_view_editor/public/components/preview_panel/indices_list/indices_list.test.tsx b/src/plugins/data_view_editor/public/components/preview_panel/indices_list/indices_list.test.tsx
index 074865006a385..54b996416c223 100644
--- a/src/plugins/data_view_editor/public/components/preview_panel/indices_list/indices_list.test.tsx
+++ b/src/plugins/data_view_editor/public/components/preview_panel/indices_list/indices_list.test.tsx
@@ -7,24 +7,39 @@
*/
import React from 'react';
-import { IndicesList } from '.';
+import { IndicesList, IndicesListProps, PER_PAGE_STORAGE_KEY } from './indices_list';
import { shallow } from 'enzyme';
import { MatchedItem } from '@kbn/data-views-plugin/public';
+import { Storage } from '@kbn/kibana-utils-plugin/public';
const indices = [
{ name: 'kibana', tags: [] },
{ name: 'es', tags: [] },
] as unknown as MatchedItem[];
+const similarIndices = [
+ { name: 'logstash', tags: [] },
+ { name: 'some_logs', tags: [] },
+] as unknown as MatchedItem[];
+
describe('IndicesList', () => {
+ const commonProps: Omit = {
+ indices,
+ isExactMatch: jest.fn(() => false),
+ };
+
+ afterEach(() => {
+ new Storage(localStorage).remove(PER_PAGE_STORAGE_KEY);
+ });
+
it('should render normally', () => {
- const component = shallow();
+ const component = shallow();
expect(component).toMatchSnapshot();
});
it('should change pages', () => {
- const component = shallow();
+ const component = shallow();
const instance = component.instance() as IndicesList;
@@ -36,7 +51,7 @@ describe('IndicesList', () => {
});
it('should change per page', () => {
- const component = shallow();
+ const component = shallow();
const instance = component.instance() as IndicesList;
instance.onChangePerPage(1);
@@ -46,14 +61,33 @@ describe('IndicesList', () => {
});
it('should highlight the query in the matches', () => {
- const component = shallow();
+ const component = shallow(
+ indexName === 'es'}
+ />
+ );
+
+ expect(component).toMatchSnapshot();
+ });
+
+ it('should highlight fully when an exact match', () => {
+ const component = shallow(
+ indexName === 'some_logs'}
+ />
+ );
expect(component).toMatchSnapshot();
});
describe('updating props', () => {
it('should render all new indices', () => {
- const component = shallow();
+ const component = shallow();
const moreIndices = [
...indices,
diff --git a/src/plugins/data_view_editor/public/components/preview_panel/indices_list/indices_list.tsx b/src/plugins/data_view_editor/public/components/preview_panel/indices_list/indices_list.tsx
index f307bbfc43889..d7542a9e70184 100644
--- a/src/plugins/data_view_editor/public/components/preview_panel/indices_list/indices_list.tsx
+++ b/src/plugins/data_view_editor/public/components/preview_panel/indices_list/indices_list.tsx
@@ -25,13 +25,14 @@ import {
} from '@elastic/eui';
import { Pager } from '@elastic/eui';
-
+import { Storage } from '@kbn/kibana-utils-plugin/public';
import { FormattedMessage } from '@kbn/i18n-react';
import { MatchedItem, Tag } from '@kbn/data-views-plugin/public';
-interface IndicesListProps {
+export interface IndicesListProps {
indices: MatchedItem[];
query: string;
+ isExactMatch: (indexName: string) => boolean;
}
interface IndicesListState {
@@ -41,15 +42,20 @@ interface IndicesListState {
}
const PER_PAGE_INCREMENTS = [5, 10, 20, 50];
+export const PER_PAGE_STORAGE_KEY = 'dataViews.previewPanel.indicesPerPage';
export class IndicesList extends React.Component {
pager: Pager;
+ storage: Storage;
+
constructor(props: IndicesListProps) {
super(props);
+ this.storage = new Storage(localStorage);
+
this.state = {
page: 0,
- perPage: PER_PAGE_INCREMENTS[1],
+ perPage: this.storage.get(PER_PAGE_STORAGE_KEY) || PER_PAGE_INCREMENTS[1],
isPerPageControlOpen: false,
};
@@ -75,6 +81,7 @@ export class IndicesList extends React.Component {
@@ -144,11 +151,20 @@ export class IndicesList extends React.Component q.trim());
+ if (isExactMatch(indexName)) {
+ return {indexName};
+ }
+
+ const queryAsArray = query
+ .split(',')
+ .map((q) => q.trim())
+ .filter(Boolean);
let queryIdx = -1;
let queryWithoutWildcard = '';
for (let i = 0; i < queryAsArray.length; i++) {
@@ -162,6 +178,7 @@ export class IndicesList extends React.Component {
diff --git a/src/plugins/data_view_editor/public/components/preview_panel/preview_panel.test.tsx b/src/plugins/data_view_editor/public/components/preview_panel/preview_panel.test.tsx
new file mode 100644
index 0000000000000..66ba61b466a62
--- /dev/null
+++ b/src/plugins/data_view_editor/public/components/preview_panel/preview_panel.test.tsx
@@ -0,0 +1,124 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import React from 'react';
+import { findTestSubject } from '@elastic/eui/lib/test';
+import { EuiTable, EuiButtonGroup } from '@elastic/eui';
+import { mountWithIntl } from '@kbn/test-jest-helpers';
+import { INDEX_PATTERN_TYPE, MatchedItem } from '@kbn/data-views-plugin/public';
+import { Props as PreviewPanelProps, PreviewPanel } from './preview_panel';
+import { from } from 'rxjs';
+
+const indices = [
+ { name: 'kibana_1', tags: [] },
+ { name: 'kibana_2', tags: [] },
+ { name: 'es', tags: [] },
+] as unknown as MatchedItem[];
+
+describe('DataViewEditor PreviewPanel', () => {
+ const commonProps: Omit = {
+ type: INDEX_PATTERN_TYPE.DEFAULT,
+ allowHidden: false,
+ };
+
+ it('should render normally by default', async () => {
+ const matchedIndices$: PreviewPanelProps['matchedIndices$'] = from([
+ {
+ allIndices: indices,
+ exactMatchedIndices: [],
+ partialMatchedIndices: [],
+ visibleIndices: indices,
+ },
+ ]);
+ const component = await mountWithIntl(
+
+ );
+
+ expect(component.find(EuiTable).exists()).toBeTruthy();
+ expect(component.find(EuiButtonGroup).exists()).toBeFalsy();
+ });
+
+ it('should render matching indices and can switch to all indices', async () => {
+ const matchedIndices$: PreviewPanelProps['matchedIndices$'] = from([
+ {
+ allIndices: indices,
+ exactMatchedIndices: [indices[0], indices[1]],
+ partialMatchedIndices: [],
+ visibleIndices: [indices[0], indices[1]],
+ },
+ ]);
+ const component = await mountWithIntl(
+
+ );
+
+ expect(component.find(EuiTable).exists()).toBeTruthy();
+ expect(component.find(EuiButtonGroup).exists()).toBeTruthy();
+
+ expect(component.find('.euiButtonGroupButton-isSelected').first().text()).toBe(
+ 'Matching sources'
+ );
+
+ findTestSubject(component, 'allIndices').simulate('change', {
+ target: {
+ value: true,
+ },
+ });
+
+ await component.update();
+
+ expect(component.find('.euiButtonGroupButton-isSelected').first().text()).toBe('All sources');
+ });
+
+ it('should render matching indices with warnings', async () => {
+ const matchedIndices$: PreviewPanelProps['matchedIndices$'] = from([
+ {
+ allIndices: indices,
+ exactMatchedIndices: [],
+ partialMatchedIndices: [indices[0], indices[1]],
+ visibleIndices: [indices[0], indices[1]],
+ },
+ ]);
+ const component = await mountWithIntl(
+
+ );
+
+ expect(component.find(EuiTable).exists()).toBeTruthy();
+ expect(component.find(EuiButtonGroup).exists()).toBeTruthy();
+ });
+
+ it('should render all indices tab when ends with a comma and can switch to matching sources', async () => {
+ const matchedIndices$: PreviewPanelProps['matchedIndices$'] = from([
+ {
+ allIndices: indices,
+ exactMatchedIndices: [indices[0]],
+ partialMatchedIndices: [],
+ visibleIndices: [indices[0]],
+ },
+ ]);
+ const component = await mountWithIntl(
+
+ );
+
+ expect(component.find(EuiTable).exists()).toBeTruthy();
+ expect(component.find(EuiButtonGroup).exists()).toBeTruthy();
+
+ expect(component.find('.euiButtonGroupButton-isSelected').first().text()).toBe('All sources');
+
+ findTestSubject(component, 'onlyMatchingIndices').simulate('change', {
+ target: {
+ value: true,
+ },
+ });
+
+ await component.update();
+
+ expect(component.find('.euiButtonGroupButton-isSelected').first().text()).toBe(
+ 'Matching sources'
+ );
+ });
+});
diff --git a/src/plugins/data_view_editor/public/components/preview_panel/preview_panel.tsx b/src/plugins/data_view_editor/public/components/preview_panel/preview_panel.tsx
index 07b1fd91b85b6..629d2ae8bde46 100644
--- a/src/plugins/data_view_editor/public/components/preview_panel/preview_panel.tsx
+++ b/src/plugins/data_view_editor/public/components/preview_panel/preview_panel.tsx
@@ -6,8 +6,9 @@
* Side Public License, v 1.
*/
-import React from 'react';
-import { EuiSpacer } from '@elastic/eui';
+import React, { useState } from 'react';
+import { EuiButtonGroup, EuiSpacer } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
import useObservable from 'react-use/lib/useObservable';
import { Observable } from 'rxjs';
import { INDEX_PATTERN_TYPE } from '@kbn/data-views-plugin/public';
@@ -17,7 +18,27 @@ import { matchedIndiciesDefault } from '../../data_view_editor_service';
import { MatchedIndicesSet } from '../../types';
-interface Props {
+enum ViewMode {
+ allIndices = 'allIndices',
+ onlyMatchingIndices = 'onlyMatchingIndices',
+}
+
+const viewModeButtons = [
+ {
+ id: ViewMode.allIndices,
+ label: i18n.translate('indexPatternEditor.previewPanel.viewModeGroup.allSourcesButton', {
+ defaultMessage: 'All sources',
+ }),
+ },
+ {
+ id: ViewMode.onlyMatchingIndices,
+ label: i18n.translate('indexPatternEditor.previewPanel.viewModeGroup.matchingSourcesButton', {
+ defaultMessage: 'Matching sources',
+ }),
+ },
+];
+
+export interface Props {
type: INDEX_PATTERN_TYPE;
allowHidden: boolean;
title: string;
@@ -25,20 +46,35 @@ interface Props {
}
export const PreviewPanel = ({ type, allowHidden, title = '', matchedIndices$ }: Props) => {
+ const [viewMode, setViewMode] = useState();
const matched = useObservable(matchedIndices$, matchedIndiciesDefault);
- const indicesListContent =
- matched.visibleIndices.length || matched.allIndices.length ? (
- <>
-
-
- >
- ) : (
- <>>
- );
+
+ let currentlyVisibleIndices;
+ let currentViewMode;
+
+ if (
+ (title.length && !isAboutToIncludeMoreIndices(title) && viewMode !== ViewMode.allIndices) ||
+ viewMode === ViewMode.onlyMatchingIndices
+ ) {
+ currentlyVisibleIndices = matched.visibleIndices;
+ currentViewMode = ViewMode.onlyMatchingIndices;
+ } else {
+ currentlyVisibleIndices = matched.allIndices;
+ currentViewMode = ViewMode.allIndices;
+ }
+
+ const indicesListContent = currentlyVisibleIndices.length ? (
+
+ title.length > 0 && matched.exactMatchedIndices.some((index) => index.name === indexName)
+ }
+ />
+ ) : (
+ <>>
+ );
return (
<>
@@ -48,7 +84,23 @@ export const PreviewPanel = ({ type, allowHidden, title = '', matchedIndices$ }:
isIncludingSystemIndices={allowHidden}
query={title}
/>
+
+ {Boolean(title) && currentlyVisibleIndices.length > 0 && (
+ setViewMode(id as ViewMode)}
+ />
+ )}
{indicesListContent}
>
);
};
+
+function isAboutToIncludeMoreIndices(query: string) {
+ return query.trimEnd().endsWith(',');
+}
diff --git a/src/plugins/data_view_editor/tsconfig.json b/src/plugins/data_view_editor/tsconfig.json
index 3f1744281db90..99e066ee3fe66 100644
--- a/src/plugins/data_view_editor/tsconfig.json
+++ b/src/plugins/data_view_editor/tsconfig.json
@@ -17,6 +17,7 @@
"@kbn/i18n",
"@kbn/test-jest-helpers",
"@kbn/ui-theme",
+ "@kbn/kibana-utils-plugin",
],
"exclude": [
"target/**/*",
diff --git a/src/plugins/data_views/common/index.ts b/src/plugins/data_views/common/index.ts
index 5f7b3a544db9d..a26895f6f9d78 100644
--- a/src/plugins/data_views/common/index.ts
+++ b/src/plugins/data_views/common/index.ts
@@ -79,3 +79,4 @@ export type {
IndexPatternLoadExpressionFunctionDefinition,
} from './expressions';
export { getIndexPatternLoadMeta } from './expressions';
+export { DataViewMissingIndices } from './lib/errors';
diff --git a/src/plugins/discover/public/application/main/components/sidebar/discover_field.test.tsx b/src/plugins/discover/public/application/main/components/sidebar/discover_field.test.tsx
index eb433bb6ee7c3..31919d1e2de20 100644
--- a/src/plugins/discover/public/application/main/components/sidebar/discover_field.test.tsx
+++ b/src/plugins/discover/public/application/main/components/sidebar/discover_field.test.tsx
@@ -136,8 +136,7 @@ async function getComponent({
return { comp, props };
}
-// FLAKY: https://github.com/elastic/kibana/issues/148349
-describe.skip('discover sidebar field', function () {
+describe('discover sidebar field', function () {
beforeEach(() => {
(DetailsUtil.getDetails as jest.Mock).mockClear();
});
@@ -244,6 +243,10 @@ describe.skip('discover sidebar field', function () {
await comp.update();
expect(comp.find(EuiPopover).prop('isOpen')).toBe(true);
+
+ await new Promise((resolve) => setTimeout(resolve, 0));
+ await comp.update();
+
expect(findTestSubject(comp, 'dscFieldStats-title').text()).toBe('Top values');
expect(findTestSubject(comp, 'dscFieldStats-topValues-bucket')).toHaveLength(2);
expect(
diff --git a/src/plugins/unified_search/kibana.jsonc b/src/plugins/unified_search/kibana.jsonc
index 008b9d9fe03d2..5f146723b7c2e 100644
--- a/src/plugins/unified_search/kibana.jsonc
+++ b/src/plugins/unified_search/kibana.jsonc
@@ -17,7 +17,8 @@
"dataViews",
"data",
"uiActions",
- "screenshotMode"
+ "screenshotMode",
+ "savedObjectsManagement"
],
"optionalPlugins": [
"usageCollection"
diff --git a/src/plugins/unified_search/public/types.ts b/src/plugins/unified_search/public/types.ts
index 557c31865a417..66abc195b5b19 100755
--- a/src/plugins/unified_search/public/types.ts
+++ b/src/plugins/unified_search/public/types.ts
@@ -16,6 +16,7 @@ import { UsageCollectionSetup, UsageCollectionStart } from '@kbn/usage-collectio
import { Query, AggregateQuery } from '@kbn/es-query';
import { CoreStart, DocLinksStart } from '@kbn/core/public';
import { IStorageWrapper } from '@kbn/kibana-utils-plugin/public';
+import { SavedObjectsManagementPluginStart } from '@kbn/saved-objects-management-plugin/public';
import { AutocompleteSetup, AutocompleteStart } from './autocomplete';
import type { IndexPatternSelectProps, StatefulSearchBarProps } from '.';
import type { FiltersBuilderProps } from './filters_builder/filters_builder';
@@ -92,4 +93,5 @@ export interface IUnifiedSearchPluginServices extends Partial {
dataViews: DataViewsPublicPluginStart;
dataViewEditor: DataViewEditorStart;
usageCollection?: UsageCollectionStart;
+ savedObjectsManagement: SavedObjectsManagementPluginStart;
}
diff --git a/src/plugins/unified_search/tsconfig.json b/src/plugins/unified_search/tsconfig.json
index 0a4ab525d04b7..d11390cf0d18a 100644
--- a/src/plugins/unified_search/tsconfig.json
+++ b/src/plugins/unified_search/tsconfig.json
@@ -38,6 +38,7 @@
"@kbn/utility-types-jest",
"@kbn/react-field",
"@kbn/ui-theme",
+ "@kbn/saved-objects-management-plugin",
],
"exclude": [
"target/**/*",
diff --git a/test/functional/apps/console/_comments.ts b/test/functional/apps/console/_comments.ts
index 3250035275976..de8bcda60786b 100644
--- a/test/functional/apps/console/_comments.ts
+++ b/test/functional/apps/console/_comments.ts
@@ -15,7 +15,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
const retry = getService('retry');
const PageObjects = getPageObjects(['common', 'console', 'header']);
- describe('console app', function testComments() {
+ // Failing: See https://github.com/elastic/kibana/issues/138160
+ describe.skip('console app', function testComments() {
this.tags('includeFirefox');
before(async () => {
log.debug('navigateTo console');
diff --git a/test/functional/apps/context/_discover_navigation.ts b/test/functional/apps/context/_discover_navigation.ts
index 9f8e86ad7352e..24da7da392190 100644
--- a/test/functional/apps/context/_discover_navigation.ts
+++ b/test/functional/apps/context/_discover_navigation.ts
@@ -48,6 +48,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
for (const [columnName, value] of TEST_FILTER_COLUMN_NAMES) {
await filterBar.addFilter({ field: columnName, operation: 'is', value });
+ await PageObjects.header.waitUntilLoadingHasFinished();
}
});
@@ -82,7 +83,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await retry.waitFor('next anchor timestamp matches previous anchor timestamp', async () => {
// get the timestamp of the first row
const firstContextTimestamp = await getTimestamp(false);
- await dataGrid.clickRowToggle({ isAnchorRow: true });
+ await dataGrid.clickRowToggle({ rowIndex: 0 });
const rowActions = await dataGrid.getRowActions({ rowIndex: 0 });
await rowActions[1].click();
diff --git a/test/functional/apps/discover/group1/_discover_histogram.ts b/test/functional/apps/discover/group1/_discover_histogram.ts
index e451eee881335..03608566969a5 100644
--- a/test/functional/apps/discover/group1/_discover_histogram.ts
+++ b/test/functional/apps/discover/group1/_discover_histogram.ts
@@ -270,5 +270,27 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
expect(canvasExists).to.be(false);
});
});
+
+ it('should recover from broken query search when clearing the query bar', async () => {
+ await PageObjects.common.navigateToApp('discover');
+ await PageObjects.discover.waitUntilSearchingHasFinished();
+ await PageObjects.timePicker.setDefaultAbsoluteRange();
+ // Make sure the chart is visible
+ await testSubjects.click('unifiedHistogramChartOptionsToggle');
+ await testSubjects.click('unifiedHistogramChartToggle');
+ await PageObjects.discover.waitUntilSearchingHasFinished();
+ // type an invalid search query, hit refresh
+ await queryBar.setQuery('this is > not valid');
+ await queryBar.submitQuery();
+ // check the error state
+ expect(await testSubjects.exists('embeddable-lens-failure')).to.be(true);
+
+ // now remove the query
+ await queryBar.clearQuery();
+ await queryBar.submitQuery();
+ await PageObjects.discover.waitUntilSearchingHasFinished();
+ // check no error state
+ expect(await PageObjects.discover.isChartVisible()).to.be(true);
+ });
});
}
diff --git a/test/functional/services/filter_bar.ts b/test/functional/services/filter_bar.ts
index 7e28176e764e7..656efa1b002f9 100644
--- a/test/functional/services/filter_bar.ts
+++ b/test/functional/services/filter_bar.ts
@@ -304,6 +304,7 @@ export class FilterBarService extends FtrService {
await this.testSubjects.clickWhenNotDisabled('saveFilter');
});
+ await this.testSubjects.waitForDeleted('saveFilter');
await this.header.awaitGlobalLoadingIndicatorHidden();
}
diff --git a/tsconfig.base.json b/tsconfig.base.json
index 1e8bd1ef0ade0..6247306af2a2e 100644
--- a/tsconfig.base.json
+++ b/tsconfig.base.json
@@ -1060,6 +1060,8 @@
"@kbn/securitysolution-es-utils/*": ["packages/kbn-securitysolution-es-utils/*"],
"@kbn/securitysolution-exception-list-components": ["packages/kbn-securitysolution-exception-list-components"],
"@kbn/securitysolution-exception-list-components/*": ["packages/kbn-securitysolution-exception-list-components/*"],
+ "@kbn/securitysolution-grouping": ["packages/kbn-securitysolution-grouping"],
+ "@kbn/securitysolution-grouping/*": ["packages/kbn-securitysolution-grouping/*"],
"@kbn/securitysolution-hook-utils": ["packages/kbn-securitysolution-hook-utils"],
"@kbn/securitysolution-hook-utils/*": ["packages/kbn-securitysolution-hook-utils/*"],
"@kbn/securitysolution-io-ts-alerting-types": ["packages/kbn-securitysolution-io-ts-alerting-types"],
diff --git a/x-pack/plugins/alerting/server/task_runner/rule_action_helper.test.ts b/x-pack/plugins/alerting/server/task_runner/rule_action_helper.test.ts
index c69d394d258ec..87af43fe58d2e 100644
--- a/x-pack/plugins/alerting/server/task_runner/rule_action_helper.test.ts
+++ b/x-pack/plugins/alerting/server/task_runner/rule_action_helper.test.ts
@@ -38,8 +38,7 @@ const mockAction: RuleAction = {
const mockSummaryAction: RuleAction = {
id: '1',
- // @ts-ignore
- group: null,
+ group: 'default',
actionTypeId: 'slack',
params: {},
frequency: {
diff --git a/x-pack/plugins/alerting/server/task_runner/rule_action_helper.ts b/x-pack/plugins/alerting/server/task_runner/rule_action_helper.ts
index 2252e38b7c43b..5fa8da9b25d21 100644
--- a/x-pack/plugins/alerting/server/task_runner/rule_action_helper.ts
+++ b/x-pack/plugins/alerting/server/task_runner/rule_action_helper.ts
@@ -69,7 +69,7 @@ export const isSummaryActionThrottled = ({
};
export const generateActionHash = (action: RuleAction) => {
- return `${action.actionTypeId}:${action.group || 'summary'}:${
+ return `${action.actionTypeId}:${action.frequency?.summary ? 'summary' : action.group}:${
action.frequency?.throttle || 'no-throttling'
}`;
};
@@ -82,7 +82,9 @@ export const getSummaryActionsFromTaskState = ({
summaryActions?: ThrottledActions;
}) => {
return Object.entries(summaryActions).reduce((newObj, [key, val]) => {
- const actionExists = actions.some((action) => generateActionHash(action) === key);
+ const actionExists = actions.some(
+ (action) => action.frequency?.summary && generateActionHash(action) === key
+ );
if (actionExists) {
return { ...newObj, [key]: val };
} else {
diff --git a/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts b/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts
index 779b467160b2d..7a000815e3959 100644
--- a/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts
+++ b/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts
@@ -1486,7 +1486,7 @@ describe('Task Runner', () => {
generateEnqueueFunctionInput({ isBulk, id: '1', foo: true })
);
expect(result.state.summaryActions).toEqual({
- 'slack:default:1h': { date: new Date(DATE_1970) },
+ 'slack:summary:1h': { date: new Date(DATE_1970) },
});
}
);
diff --git a/x-pack/plugins/apm/server/lib/anomaly_detection/create_anomaly_detection_jobs.ts b/x-pack/plugins/apm/server/lib/anomaly_detection/create_anomaly_detection_jobs.ts
index 85c9550fd230d..636c7b2fde323 100644
--- a/x-pack/plugins/apm/server/lib/anomaly_detection/create_anomaly_detection_jobs.ts
+++ b/x-pack/plugins/apm/server/lib/anomaly_detection/create_anomaly_detection_jobs.ts
@@ -136,12 +136,12 @@ async function createAnomalyDetectionJob({
],
});
- waitForIndexStatus({
+ await waitForIndexStatus({
client: esClient,
index: '.ml-*',
timeout: DEFAULT_TIMEOUT,
status: 'yellow',
- });
+ })();
return anomalyDetectionJob;
});
diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/field_type_icon/__snapshots__/field_type_icon.test.tsx.snap b/x-pack/plugins/data_visualizer/public/application/common/components/field_type_icon/__snapshots__/field_type_icon.test.tsx.snap
deleted file mode 100644
index af4464cbc6b4e..0000000000000
--- a/x-pack/plugins/data_visualizer/public/application/common/components/field_type_icon/__snapshots__/field_type_icon.test.tsx.snap
+++ /dev/null
@@ -1,15 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`FieldTypeIcon render component when type matches a field type 1`] = `
-
-
-
-`;
diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/field_type_icon/field_type_icon.test.tsx b/x-pack/plugins/data_visualizer/public/application/common/components/field_type_icon/field_type_icon.test.tsx
index e01890d8b0a0f..04e8059163960 100644
--- a/x-pack/plugins/data_visualizer/public/application/common/components/field_type_icon/field_type_icon.test.tsx
+++ b/x-pack/plugins/data_visualizer/public/application/common/components/field_type_icon/field_type_icon.test.tsx
@@ -6,47 +6,60 @@
*/
import React from 'react';
-import { mount, shallow } from 'enzyme';
-
+import { render, fireEvent, waitFor } from '@testing-library/react';
import { FieldTypeIcon } from './field_type_icon';
import { SUPPORTED_FIELD_TYPES } from '../../../../../common/constants';
describe('FieldTypeIcon', () => {
- test(`render component when type matches a field type`, () => {
- const typeIconComponent = shallow(
-
+ it('renders label and icon but not tooltip content on mouseover if tooltipEnabled=false', async () => {
+ const { getByText, container } = render(
+
);
- expect(typeIconComponent).toMatchSnapshot();
- });
- // TODO: Broken with Jest 27
- test.skip(`render with tooltip and test hovering`, () => {
- // Use fake timers so we don't have to wait for the EuiToolTip timeout
- jest.useFakeTimers({ legacyFakeTimers: true });
+ expect(container.querySelector('[data-test-subj="dvFieldTypeIcon-keyword"]')).toBeDefined();
- const typeIconComponent = mount(
-
- );
-
- expect(typeIconComponent.find('EuiToolTip').children()).toHaveLength(1);
+ fireEvent.mouseOver(getByText('Keyword'));
- typeIconComponent.simulate('mouseover');
+ await waitFor(
+ () => {
+ const tooltip = document.querySelector('[data-test-subj="dvFieldTypeTooltip"]');
+ expect(tooltip).toBeNull();
+ },
+ { timeout: 1500 } // Account for long delay on tooltips
+ );
+ });
- // Run the timers so the EuiTooltip will be visible
- jest.runAllTimers();
+ it(`renders component when type matches a field type`, () => {
+ const { container } = render(
+
+ );
- typeIconComponent.update();
- expect(typeIconComponent.find('EuiToolTip').children()).toHaveLength(2);
+ expect(container.querySelector('[data-test-subj="dvFieldTypeIcon-keyword"]')).toBeDefined();
+ expect(container).toHaveTextContent('keyword');
+ });
- typeIconComponent.simulate('mouseout');
+ it('shows tooltip content on mouseover', async () => {
+ const { getByText, container } = render(
+
+ );
+ expect(container.querySelector('[data-test-subj="dvFieldTypeIcon-keyword"]')).toBeDefined();
+ expect(container).toHaveTextContent('keyword');
- // Run the timers so the EuiTooltip will be hidden again
- jest.runAllTimers();
+ fireEvent.mouseOver(getByText('keyword'));
- typeIconComponent.update();
- expect(typeIconComponent.find('EuiToolTip').children()).toHaveLength(2);
+ await waitFor(
+ () => {
+ const tooltip = document.querySelector('[data-test-subj="dvFieldTypeTooltip"]');
+ expect(tooltip).toBeVisible();
+ expect(tooltip?.textContent).toEqual('Keyword');
+ },
+ { timeout: 1500 } // Account for long delay on tooltips
+ );
+ fireEvent.mouseOut(getByText('keyword'));
- // Clearing all mocks will also reset fake timers.
- jest.clearAllMocks();
+ await waitFor(() => {
+ const tooltip = document.querySelector('[data-test-subj="dvFieldTypeTooltip"]');
+ expect(tooltip).toBeNull();
+ });
});
});
diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/field_type_icon/field_type_icon.tsx b/x-pack/plugins/data_visualizer/public/application/common/components/field_type_icon/field_type_icon.tsx
index f7b7ed8142aac..1f77f0284803f 100644
--- a/x-pack/plugins/data_visualizer/public/application/common/components/field_type_icon/field_type_icon.tsx
+++ b/x-pack/plugins/data_visualizer/public/application/common/components/field_type_icon/field_type_icon.tsx
@@ -27,8 +27,13 @@ export const FieldTypeIcon: FC = ({ tooltipEnabled = false,
});
if (tooltipEnabled === true) {
return (
-
-
+
+
);
}
diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/index/create_custom_pipeline_api_logic.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/index/create_custom_pipeline_api_logic.ts
index 7d9a7ede6ad98..ba3b551b23602 100644
--- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/index/create_custom_pipeline_api_logic.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/index/create_custom_pipeline_api_logic.ts
@@ -5,6 +5,7 @@
* 2.0.
*/
+import { IngestPipeline } from '@elastic/elasticsearch/lib/api/types';
import { i18n } from '@kbn/i18n';
import { createApiLogic } from '../../../shared/api_logic/create_api_logic';
@@ -14,9 +15,7 @@ export interface CreateCustomPipelineApiLogicArgs {
indexName: string;
}
-export interface CreateCustomPipelineApiLogicResponse {
- created: string[];
-}
+export type CreateCustomPipelineApiLogicResponse = Record;
export const createCustomPipeline = async ({
indexName,
diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/pipelines/revert_connector_pipeline_api_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/pipelines/revert_connector_pipeline_api_logic.test.ts
new file mode 100644
index 0000000000000..2753fadb77aea
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/pipelines/revert_connector_pipeline_api_logic.test.ts
@@ -0,0 +1,20 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { mockHttpValues } from '../../../__mocks__/kea_logic';
+
+import { revertConnectorPipeline } from './revert_connector_pipeline_api_logic';
+
+describe('RevertConnectorPipelineApiLogic', () => {
+ it('should call delete pipeline endpoint', () => {
+ const { http } = mockHttpValues;
+ revertConnectorPipeline({ indexName: 'indexName' });
+ expect(http.delete).toHaveBeenCalledWith(
+ '/internal/enterprise_search/indices/indexName/pipelines'
+ );
+ });
+});
diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/pipelines/revert_connector_pipeline_api_logic.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/pipelines/revert_connector_pipeline_api_logic.ts
new file mode 100644
index 0000000000000..a97cafdcd1881
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/pipelines/revert_connector_pipeline_api_logic.ts
@@ -0,0 +1,26 @@
+/*
+ * 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 { Actions, createApiLogic } from '../../../shared/api_logic/create_api_logic';
+import { HttpLogic } from '../../../shared/http';
+
+export interface RevertConnectorPipelineArgs {
+ indexName: string;
+}
+
+export const revertConnectorPipeline = async ({ indexName }: RevertConnectorPipelineArgs) => {
+ const route = `/internal/enterprise_search/indices/${indexName}/pipelines`;
+
+ return await HttpLogic.values.http.delete(route);
+};
+
+export const RevertConnectorPipelineApilogic = createApiLogic(
+ ['revert_connector_pipeline_api'],
+ revertConnectorPipeline
+);
+
+export type RevertConnectorPipelineActions = Actions;
diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ingest_pipelines/customize_pipeline_item.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ingest_pipelines/customize_pipeline_item.tsx
index 96d213ced0f2d..e95fb524f29ab 100644
--- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ingest_pipelines/customize_pipeline_item.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ingest_pipelines/customize_pipeline_item.tsx
@@ -9,20 +9,32 @@ import React from 'react';
import { useActions, useValues } from 'kea';
-import { EuiButtonEmpty, EuiFlexGroup, EuiSpacer, EuiText } from '@elastic/eui';
+import { EuiButtonEmpty, EuiConfirmModal, EuiFlexGroup, EuiSpacer, EuiText } from '@elastic/eui';
+
import { i18n } from '@kbn/i18n';
+import { Status } from '../../../../../../../common/types/api';
+
+import { CANCEL_BUTTON_LABEL } from '../../../../../shared/constants';
+
import { KibanaLogic } from '../../../../../shared/kibana';
import { LicensingLogic } from '../../../../../shared/licensing';
import { CreateCustomPipelineApiLogic } from '../../../../api/index/create_custom_pipeline_api_logic';
+import { RevertConnectorPipelineApilogic } from '../../../../api/pipelines/revert_connector_pipeline_api_logic';
import { IndexViewLogic } from '../../index_view_logic';
+import { PipelinesLogic } from '../pipelines_logic';
export const CustomizeIngestPipelineItem: React.FC = () => {
const { indexName, ingestionMethod } = useValues(IndexViewLogic);
const { isCloud } = useValues(KibanaLogic);
const { hasPlatinumLicense } = useValues(LicensingLogic);
const { makeRequest: createCustomPipeline } = useActions(CreateCustomPipelineApiLogic);
+ const { status: createStatus } = useValues(CreateCustomPipelineApiLogic);
+ const { isDeleteModalOpen, hasIndexIngestionPipeline } = useValues(PipelinesLogic);
+ const { closeDeleteModal, openDeleteModal } = useActions(PipelinesLogic);
+ const { makeRequest: revertPipeline } = useActions(RevertConnectorPipelineApilogic);
+ const { status: revertStatus } = useValues(RevertConnectorPipelineApilogic);
const isGated = !isCloud && !hasPlatinumLicense;
return (
@@ -49,17 +61,61 @@ export const CustomizeIngestPipelineItem: React.FC = () => {
)}
- createCustomPipeline({ indexName })}
- >
- {i18n.translate(
- 'xpack.enterpriseSearch.content.index.pipelines.ingestFlyout.copyButtonLabel',
- { defaultMessage: 'Copy and customize' }
- )}
-
+ {isDeleteModalOpen && (
+ revertPipeline({ indexName })}
+ cancelButtonText={CANCEL_BUTTON_LABEL}
+ confirmButtonText={i18n.translate(
+ 'xpack.enterpriseSearch.content.index.pipelines.deleteModal.confirmButton',
+ {
+ defaultMessage: 'Delete pipeline',
+ }
+ )}
+ buttonColor="danger"
+ >
+
+ {i18n.translate(
+ 'xpack.enterpriseSearch.content.index.pipelines.deleteModal.description',
+ {
+ defaultMessage:
+ 'This will delete any custom pipelines associated with this index, including machine learning inference pipelines. The index will revert to using the default ingest pipeline.',
+ }
+ )}
+
+
+ )}
+ {hasIndexIngestionPipeline ? (
+ openDeleteModal()}
+ >
+ {i18n.translate(
+ 'xpack.enterpriseSearch.content.index.pipelines.ingestFlyout.revertPipelineLabel',
+ { defaultMessage: 'Delete custom pipeline' }
+ )}
+
+ ) : (
+ createCustomPipeline({ indexName })}
+ >
+ {i18n.translate(
+ 'xpack.enterpriseSearch.content.index.pipelines.ingestFlyout.copyButtonLabel',
+ { defaultMessage: 'Copy and customize' }
+ )}
+
+ )}
>
diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ingest_pipelines/ingest_pipelines_card.test.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ingest_pipelines/ingest_pipelines_card.test.tsx
index 0ca347da0712e..a6c68b47c7b5a 100644
--- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ingest_pipelines/ingest_pipelines_card.test.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ingest_pipelines/ingest_pipelines_card.test.tsx
@@ -12,8 +12,6 @@ import React from 'react';
import { shallow } from 'enzyme';
-import { IngestPipeline } from '@elastic/elasticsearch/lib/api/types';
-
import { DEFAULT_PIPELINE_NAME } from '../../../../../../../common/constants';
import { CustomPipelineItem } from './custom_pipeline_item';
@@ -52,28 +50,4 @@ describe('IngestPipelinesCard', () => {
expect(wrapper.find(CustomizeIngestPipelineItem)).toHaveLength(1);
expect(wrapper.find(CustomPipelineItem)).toHaveLength(0);
});
- it('does not render customize cta with index ingest pipeline', () => {
- const pipelineName = crawlerIndex.name;
- const pipelines: Record = {
- [pipelineName]: {},
- [`${pipelineName}@custom`]: {
- processors: [],
- },
- };
- setMockValues({
- ...DEFAULT_VALUES,
- data: pipelines,
- hasIndexIngestionPipeline: true,
- pipelineName,
- pipelineState: {
- ...DEFAULT_VALUES.pipelineState,
- name: pipelineName,
- },
- });
-
- const wrapper = shallow();
- expect(wrapper.find(CustomizeIngestPipelineItem)).toHaveLength(0);
- expect(wrapper.find(DefaultPipelineItem)).toHaveLength(1);
- expect(wrapper.find(CustomPipelineItem)).toHaveLength(1);
- });
});
diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ingest_pipelines/ingest_pipelines_card.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ingest_pipelines/ingest_pipelines_card.tsx
index d57b4142d2905..64f195e217891 100644
--- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ingest_pipelines/ingest_pipelines_card.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ingest_pipelines/ingest_pipelines_card.tsx
@@ -24,14 +24,8 @@ import { IngestPipelineFlyout } from './ingest_pipeline_flyout';
export const IngestPipelinesCard: React.FC = () => {
const { indexName, ingestionMethod } = useValues(IndexViewLogic);
- const {
- canSetPipeline,
- hasIndexIngestionPipeline,
- index,
- pipelineName,
- pipelineState,
- showPipelineSettings,
- } = useValues(PipelinesLogic);
+ const { canSetPipeline, index, pipelineName, pipelineState, showPipelineSettings } =
+ useValues(PipelinesLogic);
const { closePipelineSettings, openPipelineSettings, setPipelineState, savePipeline } =
useActions(PipelinesLogic);
const { makeRequest: fetchCustomPipeline } = useActions(FetchCustomPipelineApiLogic);
@@ -45,7 +39,7 @@ export const IngestPipelinesCard: React.FC = () => {
return (
<>
- {!hasIndexIngestionPipeline && }
+
{showPipelineSettings && (
{
- const { showAddMlInferencePipelineModal, hasIndexIngestionPipeline, index, pipelineName } =
- useValues(PipelinesLogic);
+ const {
+ showMissingPipelineCallout,
+ showAddMlInferencePipelineModal,
+ hasIndexIngestionPipeline,
+ index,
+ pipelineName,
+ } = useValues(PipelinesLogic);
const { closeAddMlInferencePipelineModal, openAddMlInferencePipelineModal } =
useActions(PipelinesLogic);
+ const { indexName } = useValues(IndexNameLogic);
+ const { makeRequest: revertPipeline } = useActions(RevertConnectorPipelineApilogic);
const apiIndex = isApiIndex(index);
const pipelinesTabs: EuiTabbedContentTab[] = [
@@ -66,6 +78,36 @@ export const SearchIndexPipelines: React.FC = () => {
return (
<>
+ {showMissingPipelineCallout && (
+
+
+ {i18n.translate(
+ 'xpack.enterpriseSearch.content.indices.pipelines.missingPipeline.description',
+ {
+ defaultMessage:
+ 'The custom pipeline for this index has been deleted. This may affect connector data ingestion. Its configuration will need to be reverted to the default pipeline settings.',
+ }
+ )}
+
+ revertPipeline({ indexName })}>
+ {i18n.translate(
+ 'xpack.enterpriseSearch.content.indices.pipelines.missingPipeline.buttonLabel',
+ {
+ defaultMessage: 'Revert pipeline to default',
+ }
+ )}
+
+
+ )}
diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/pipelines_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/pipelines_logic.test.ts
index a16bd36ee54ba..69e01d45750fe 100644
--- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/pipelines_logic.test.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/pipelines_logic.test.ts
@@ -35,10 +35,12 @@ const DEFAULT_VALUES = {
hasIndexIngestionPipeline: false,
index: undefined,
indexName: '',
+ isDeleteModalOpen: false,
mlInferencePipelineProcessors: undefined,
pipelineName: DEFAULT_PIPELINE_VALUES.name,
pipelineState: DEFAULT_PIPELINE_VALUES,
showAddMlInferencePipelineModal: false,
+ showMissingPipelineCallout: false,
showPipelineSettings: false,
};
@@ -145,16 +147,16 @@ describe('PipelinesLogic', () => {
});
});
describe('createCustomPipelineSuccess', () => {
- it('should call flashSuccessToast', () => {
+ it('should call flashSuccessToast and update pipelines', () => {
PipelinesLogic.actions.setPipelineState = jest.fn();
PipelinesLogic.actions.savePipeline = jest.fn();
PipelinesLogic.actions.fetchCustomPipeline = jest.fn();
PipelinesLogic.actions.fetchIndexApiSuccess(connectorIndex);
- PipelinesLogic.actions.createCustomPipelineSuccess({ created: ['a', 'b'] });
+ PipelinesLogic.actions.createCustomPipelineSuccess({ [connectorIndex.name]: {} });
expect(flashSuccessToast).toHaveBeenCalledWith('Custom pipeline created');
expect(PipelinesLogic.actions.setPipelineState).toHaveBeenCalledWith({
...PipelinesLogic.values.pipelineState,
- name: 'a',
+ name: connectorIndex.name,
});
expect(PipelinesLogic.actions.savePipeline).toHaveBeenCalled();
expect(PipelinesLogic.actions.fetchCustomPipeline).toHaveBeenCalled();
diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/pipelines_logic.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/pipelines_logic.ts
index dbe0239028ad5..bf71dc215243f 100644
--- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/pipelines_logic.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/pipelines_logic.ts
@@ -66,6 +66,10 @@ import {
} from '../../../api/pipelines/detach_ml_inference_pipeline';
import { FetchMlInferencePipelineProcessorsApiLogic } from '../../../api/pipelines/fetch_ml_inference_pipeline_processors';
+import {
+ RevertConnectorPipelineActions,
+ RevertConnectorPipelineApilogic,
+} from '../../../api/pipelines/revert_connector_pipeline_api_logic';
import { isApiIndex, isConnectorIndex, isCrawlerIndex } from '../../../utils/indices';
type PipelinesActions = Pick<
@@ -77,6 +81,7 @@ type PipelinesActions = Pick<
AttachMlInferencePipelineResponse
>['apiSuccess'];
closeAddMlInferencePipelineModal: () => void;
+ closeDeleteModal: () => void;
closePipelineSettings: () => void;
createCustomPipeline: Actions<
CreateCustomPipelineApiLogicArgs,
@@ -132,7 +137,9 @@ type PipelinesActions = Pick<
fetchMlInferenceProcessors: typeof FetchMlInferencePipelineProcessorsApiLogic.actions.makeRequest;
fetchMlInferenceProcessorsApiError: (error: HttpError) => HttpError;
openAddMlInferencePipelineModal: () => void;
+ openDeleteModal: () => void;
openPipelineSettings: () => void;
+ revertPipelineSuccess: RevertConnectorPipelineActions['apiSuccess'];
savePipeline: () => void;
setPipelineState(pipeline: IngestPipelineParams): {
pipeline: IngestPipelineParams;
@@ -148,18 +155,22 @@ interface PipelinesValues {
hasIndexIngestionPipeline: boolean;
index: CachedFetchIndexApiLogicValues['fetchIndexApiData'];
indexName: string;
+ isDeleteModalOpen: boolean;
mlInferencePipelineProcessors: InferencePipeline[];
pipelineName: string;
pipelineState: IngestPipelineParams;
showAddMlInferencePipelineModal: boolean;
+ showMissingPipelineCallout: boolean;
showPipelineSettings: boolean;
}
export const PipelinesLogic = kea>({
actions: {
closeAddMlInferencePipelineModal: true,
+ closeDeleteModal: true,
closePipelineSettings: true,
openAddMlInferencePipelineModal: true,
+ openDeleteModal: true,
openPipelineSettings: true,
savePipeline: true,
setPipelineState: (pipeline: IngestPipelineParams) => ({ pipeline }),
@@ -201,6 +212,8 @@ export const PipelinesLogic = kea {
- actions.setPipelineState({ ...values.pipelineState, name: created[0] });
+ createCustomPipelineSuccess: (created) => {
+ actions.fetchCustomPipelineSuccess(created);
+ actions.setPipelineState({ ...values.pipelineState, name: values.indexName });
actions.savePipeline();
actions.fetchCustomPipeline({ indexName: values.index.name });
},
@@ -313,6 +327,19 @@ export const PipelinesLogic = kea {
+ if (isConnectorIndex(values.index) || isCrawlerIndex(values.index)) {
+ if (values.index.connector) {
+ // had to split up these if checks rather than nest them or typescript wouldn't recognize connector as defined
+ actions.fetchIndexApiSuccess({
+ ...values.index,
+ connector: { ...values.index.connector, pipeline: values.defaultPipelineValues },
+ });
+ actions.fetchCustomPipelineSuccess({});
+ }
+ }
+ actions.fetchCustomPipeline({ indexName: values.indexName });
+ },
savePipeline: () => {
if (isConnectorIndex(values.index) || isCrawlerIndex(values.index)) {
if (values.index.connector) {
@@ -326,6 +353,14 @@ export const PipelinesLogic = kea ({
+ isDeleteModalOpen: [
+ false,
+ {
+ closeDeleteModal: () => false,
+ openDeleteModal: () => true,
+ revertPipelineSuccess: () => false,
+ },
+ ],
pipelineState: [
DEFAULT_PIPELINE_VALUES,
{
@@ -381,5 +416,25 @@ export const PipelinesLogic = kea
customPipelineData && customPipelineData[indexName] ? indexName : pipelineState.name,
],
+ showMissingPipelineCallout: [
+ () => [
+ selectors.hasIndexIngestionPipeline,
+ selectors.pipelineName,
+ selectors.customPipelineData,
+ selectors.index,
+ ],
+ (
+ hasCustomPipeline: boolean,
+ pipelineName: string,
+ customPipelineData: Record | undefined,
+ index: ElasticsearchIndexWithIngestion
+ ) =>
+ Boolean(
+ hasCustomPipeline &&
+ customPipelineData &&
+ !customPipelineData[pipelineName] &&
+ isConnectorIndex(index)
+ ),
+ ],
}),
});
diff --git a/x-pack/plugins/enterprise_search/public/assets/source_icons/azure_cloud.svg b/x-pack/plugins/enterprise_search/public/assets/source_icons/azure_blob.svg
similarity index 100%
rename from x-pack/plugins/enterprise_search/public/assets/source_icons/azure_cloud.svg
rename to x-pack/plugins/enterprise_search/public/assets/source_icons/azure_blob.svg
diff --git a/x-pack/plugins/enterprise_search/server/integrations.ts b/x-pack/plugins/enterprise_search/server/integrations.ts
index b7d1516764187..94b12b35d4120 100644
--- a/x-pack/plugins/enterprise_search/server/integrations.ts
+++ b/x-pack/plugins/enterprise_search/server/integrations.ts
@@ -617,14 +617,14 @@ export const registerEnterpriseSearchIntegrations = (
});
customIntegrations.registerCustomIntegration({
- id: 'azure_cloud_storage',
- title: i18n.translate('xpack.enterpriseSearch.workplaceSearch.integrations.azureCloud', {
- defaultMessage: 'Azure Cloud Storage',
+ id: 'azure_blob_storage',
+ title: i18n.translate('xpack.enterpriseSearch.workplaceSearch.integrations.azureBlob', {
+ defaultMessage: 'Azure Blob Storage',
}),
description: i18n.translate(
- 'xpack.enterpriseSearch.workplaceSearch.integrations.azureCloudDescription',
+ 'xpack.enterpriseSearch.workplaceSearch.integrations.azureBlobDescription',
{
- defaultMessage: 'Search over your content on Azure Cloud Storage with Enterprise Search.',
+ defaultMessage: 'Search over your content on Azure Blob Storage with Enterprise Search.',
}
),
categories: ['enterprise_search', 'elastic_stack', 'custom'],
@@ -632,7 +632,7 @@ export const registerEnterpriseSearchIntegrations = (
icons: [
{
type: 'svg',
- src: http.basePath.prepend('/plugins/enterpriseSearch/assets/source_icons/azure_cloud.svg'),
+ src: http.basePath.prepend('/plugins/enterpriseSearch/assets/source_icons/azure_blob.svg'),
},
],
shipper: 'enterprise_search',
diff --git a/x-pack/plugins/enterprise_search/server/lib/pipelines/create_pipeline_definitions.test.ts b/x-pack/plugins/enterprise_search/server/lib/pipelines/create_pipeline_definitions.test.ts
index 23232fda4199d..91859d66f6e55 100644
--- a/x-pack/plugins/enterprise_search/server/lib/pipelines/create_pipeline_definitions.test.ts
+++ b/x-pack/plugins/enterprise_search/server/lib/pipelines/create_pipeline_definitions.test.ts
@@ -8,8 +8,6 @@ import { merge } from 'lodash';
import { ElasticsearchClient } from '@kbn/core/server';
-import { getInferencePipelineNameFromIndexName } from '../../utils/ml_inference_pipeline_utils';
-
import { createIndexPipelineDefinitions } from './create_pipeline_definitions';
import { formatMlPipelineBody } from './create_pipeline_definitions';
@@ -22,19 +20,13 @@ describe('createIndexPipelineDefinitions util function', () => {
},
};
- const expectedResult = {
- created: [indexName, `${indexName}@custom`, getInferencePipelineNameFromIndexName(indexName)],
- };
-
beforeEach(() => {
jest.clearAllMocks();
});
it('should create the pipelines', async () => {
mockClient.ingest.putPipeline.mockImplementation(() => Promise.resolve({ acknowledged: true }));
- await expect(
- createIndexPipelineDefinitions(indexName, mockClient as unknown as ElasticsearchClient)
- ).resolves.toEqual(expectedResult);
+ await createIndexPipelineDefinitions(indexName, mockClient as unknown as ElasticsearchClient);
expect(mockClient.ingest.putPipeline).toHaveBeenCalledTimes(3);
});
});
diff --git a/x-pack/plugins/enterprise_search/server/lib/pipelines/create_pipeline_definitions.ts b/x-pack/plugins/enterprise_search/server/lib/pipelines/create_pipeline_definitions.ts
index 8b511ab22c3e7..209d9d4787ea3 100644
--- a/x-pack/plugins/enterprise_search/server/lib/pipelines/create_pipeline_definitions.ts
+++ b/x-pack/plugins/enterprise_search/server/lib/pipelines/create_pipeline_definitions.ts
@@ -5,6 +5,7 @@
* 2.0.
*/
+import { IngestPipeline } from '@elastic/elasticsearch/lib/api/types';
import { ElasticsearchClient } from '@kbn/core/server';
import { generateMlInferencePipelineBody } from '../../../common/ml_inference_pipeline';
@@ -14,10 +15,6 @@ import {
} from '../../../common/types/pipelines';
import { getInferencePipelineNameFromIndexName } from '../../utils/ml_inference_pipeline_utils';
-export interface CreatedPipelines {
- created: string[];
-}
-
/**
* Used to create index-specific Ingest Pipelines to be used in conjunction with Enterprise Search
* ingestion mechanisms. Three pipelines are created:
@@ -33,189 +30,202 @@ export interface CreatedPipelines {
export const createIndexPipelineDefinitions = async (
indexName: string,
esClient: ElasticsearchClient
-): Promise => {
+): Promise> => {
// TODO: add back descriptions (see: https://github.com/elastic/elasticsearch-specification/issues/1827)
- await esClient.ingest.putPipeline({
- description: `Enterprise Search Machine Learning Inference pipeline for the '${indexName}' index`,
- id: getInferencePipelineNameFromIndexName(indexName),
- processors: [],
- version: 1,
- });
- await esClient.ingest.putPipeline({
- description: `Enterprise Search customizable ingest pipeline for the '${indexName}' index`,
- id: `${indexName}@custom`,
- processors: [],
- version: 1,
- });
- await esClient.ingest.putPipeline({
- _meta: {
- managed: true,
- managed_by: 'Enterprise Search',
- },
- description: `Enterprise Search ingest pipeline for the '${indexName}' index`,
- id: `${indexName}`,
- processors: [
- {
- attachment: {
- field: '_attachment',
- if: 'ctx?._extract_binary_content == true',
- ignore_missing: true,
- indexed_chars_field: '_attachment_indexed_chars',
- on_failure: [
- {
- append: {
- field: '_ingestion_errors',
- value: [
- [
- "Processor 'attachment' in pipeline '{{ _ingest.on_failure_pipeline }}' failed with message '{{ _ingest.on_failure_message }}'",
+ let result: Record = {};
+ try {
+ const mlPipeline = {
+ description: `Enterprise Search Machine Learning Inference pipeline for the '${indexName}' index`,
+ id: getInferencePipelineNameFromIndexName(indexName),
+ processors: [],
+ version: 1,
+ };
+ await esClient.ingest.putPipeline(mlPipeline);
+ result = { ...result, [mlPipeline.id]: mlPipeline };
+ const customPipeline = {
+ description: `Enterprise Search customizable ingest pipeline for the '${indexName}' index`,
+ id: `${indexName}@custom`,
+ processors: [],
+ version: 1,
+ };
+ await esClient.ingest.putPipeline(customPipeline);
+ result = { ...result, [customPipeline.id]: customPipeline };
+ const ingestPipeline = {
+ _meta: {
+ managed: true,
+ managed_by: 'Enterprise Search',
+ },
+ description: `Enterprise Search ingest pipeline for the '${indexName}' index`,
+ id: `${indexName}`,
+ processors: [
+ {
+ attachment: {
+ field: '_attachment',
+ if: 'ctx?._extract_binary_content == true',
+ ignore_missing: true,
+ indexed_chars_field: '_attachment_indexed_chars',
+ on_failure: [
+ {
+ append: {
+ field: '_ingestion_errors',
+ value: [
+ [
+ "Processor 'attachment' in pipeline '{{ _ingest.on_failure_pipeline }}' failed with message '{{ _ingest.on_failure_message }}'",
+ ],
],
- ],
+ },
},
- },
- ],
- target_field: '_extracted_attachment',
+ ],
+ target_field: '_extracted_attachment',
+ },
},
- },
- {
- set: {
- field: 'body',
- if: 'ctx?._extract_binary_content == true',
- on_failure: [
- {
- append: {
- field: '_ingestion_errors',
- value: [
- [
- "Processor 'set' with tag 'set_body' in pipeline '{{ _ingest.on_failure_pipeline }}' failed with message '{{ _ingest.on_failure_message }}'",
+ {
+ set: {
+ field: 'body',
+ if: 'ctx?._extract_binary_content == true',
+ on_failure: [
+ {
+ append: {
+ field: '_ingestion_errors',
+ value: [
+ [
+ "Processor 'set' with tag 'set_body' in pipeline '{{ _ingest.on_failure_pipeline }}' failed with message '{{ _ingest.on_failure_message }}'",
+ ],
],
- ],
+ },
},
- },
- ],
- tag: 'set_body',
- value: '{{{_extracted_attachment.content}}}',
+ ],
+ tag: 'set_body',
+ value: '{{{_extracted_attachment.content}}}',
+ },
},
- },
- {
- pipeline: {
- if: 'ctx?._run_ml_inference == true',
- name: getInferencePipelineNameFromIndexName(indexName),
- on_failure: [
- {
- append: {
- field: '_ingestion_errors',
- value: [
- "Processor 'pipeline' with tag 'index_ml_inference_pipeline' in pipeline '{{ _ingest.on_failure_pipeline }}' failed with message '{{ _ingest.on_failure_message }}'",
- ],
+ {
+ pipeline: {
+ if: 'ctx?._run_ml_inference == true',
+ name: getInferencePipelineNameFromIndexName(indexName),
+ on_failure: [
+ {
+ append: {
+ field: '_ingestion_errors',
+ value: [
+ "Processor 'pipeline' with tag 'index_ml_inference_pipeline' in pipeline '{{ _ingest.on_failure_pipeline }}' failed with message '{{ _ingest.on_failure_message }}'",
+ ],
+ },
},
- },
- ],
- tag: 'index_ml_inference_pipeline',
+ ],
+ tag: 'index_ml_inference_pipeline',
+ },
},
- },
- {
- pipeline: {
- name: `${indexName}@custom`,
- on_failure: [
- {
- append: {
- field: '_ingestion_errors',
- value: [
- "Processor 'pipeline' with tag 'index_custom_pipeline' in pipeline '{{ _ingest.on_failure_pipeline }}' failed with message '{{ _ingest.on_failure_message }}'",
- ],
+ {
+ pipeline: {
+ name: `${indexName}@custom`,
+ on_failure: [
+ {
+ append: {
+ field: '_ingestion_errors',
+ value: [
+ "Processor 'pipeline' with tag 'index_custom_pipeline' in pipeline '{{ _ingest.on_failure_pipeline }}' failed with message '{{ _ingest.on_failure_message }}'",
+ ],
+ },
},
- },
- ],
- tag: 'index_custom_pipeline',
+ ],
+ tag: 'index_custom_pipeline',
+ },
},
- },
- {
- gsub: {
- field: 'body',
- if: 'ctx?._extract_binary_content == true',
- ignore_missing: true,
- on_failure: [
- {
- append: {
- field: '_ingestion_errors',
- value: [
- "Processor 'gsub' with tag 'remove_replacement_chars' in pipeline '{{ _ingest.on_failure_pipeline }}' failed with message '{{ _ingest.on_failure_message }}'",
- ],
+ {
+ gsub: {
+ field: 'body',
+ if: 'ctx?._extract_binary_content == true',
+ ignore_missing: true,
+ on_failure: [
+ {
+ append: {
+ field: '_ingestion_errors',
+ value: [
+ "Processor 'gsub' with tag 'remove_replacement_chars' in pipeline '{{ _ingest.on_failure_pipeline }}' failed with message '{{ _ingest.on_failure_message }}'",
+ ],
+ },
},
- },
- ],
- pattern: '�',
- replacement: '',
- tag: 'remove_replacement_chars',
+ ],
+ pattern: '�',
+ replacement: '',
+ tag: 'remove_replacement_chars',
+ },
},
- },
- {
- gsub: {
- field: 'body',
- if: 'ctx?._reduce_whitespace == true',
- ignore_missing: true,
- on_failure: [
- {
- append: {
- field: '_ingestion_errors',
- value: [
- "Processor 'gsub' with tag 'remove_extra_whitespace' in pipeline '{{ _ingest.on_failure_pipeline }}' failed with message '{{ _ingest.on_failure_message }}'",
- ],
+ {
+ gsub: {
+ field: 'body',
+ if: 'ctx?._reduce_whitespace == true',
+ ignore_missing: true,
+ on_failure: [
+ {
+ append: {
+ field: '_ingestion_errors',
+ value: [
+ "Processor 'gsub' with tag 'remove_extra_whitespace' in pipeline '{{ _ingest.on_failure_pipeline }}' failed with message '{{ _ingest.on_failure_message }}'",
+ ],
+ },
},
- },
- ],
- pattern: '\\s+',
- replacement: ' ',
- tag: 'remove_extra_whitespace',
+ ],
+ pattern: '\\s+',
+ replacement: ' ',
+ tag: 'remove_extra_whitespace',
+ },
},
- },
- {
- trim: {
- field: 'body',
- if: 'ctx?._reduce_whitespace == true',
- ignore_missing: true,
- on_failure: [
- {
- append: {
- field: '_ingestion_errors',
- value: [
- "Processor 'trim' in pipeline '{{ _ingest.on_failure_pipeline }}' failed with message '{{ _ingest.on_failure_message }}'",
- ],
+ {
+ trim: {
+ field: 'body',
+ if: 'ctx?._reduce_whitespace == true',
+ ignore_missing: true,
+ on_failure: [
+ {
+ append: {
+ field: '_ingestion_errors',
+ value: [
+ "Processor 'trim' in pipeline '{{ _ingest.on_failure_pipeline }}' failed with message '{{ _ingest.on_failure_message }}'",
+ ],
+ },
},
- },
- ],
+ ],
+ },
},
- },
- {
- remove: {
- field: [
- '_attachment',
- '_attachment_indexed_chars',
- '_extracted_attachment',
- '_extract_binary_content',
- '_reduce_whitespace',
- '_run_ml_inference',
- ],
- ignore_missing: true,
- on_failure: [
- {
- append: {
- field: '_ingestion_errors',
- value: [
- "Processor 'remove' with tag 'remove_meta_fields' in pipeline '{{ _ingest.on_failure_pipeline }}' failed with message '{{ _ingest.on_failure_message }}'",
- ],
+ {
+ remove: {
+ field: [
+ '_attachment',
+ '_attachment_indexed_chars',
+ '_extracted_attachment',
+ '_extract_binary_content',
+ '_reduce_whitespace',
+ '_run_ml_inference',
+ ],
+ ignore_missing: true,
+ on_failure: [
+ {
+ append: {
+ field: '_ingestion_errors',
+ value: [
+ "Processor 'remove' with tag 'remove_meta_fields' in pipeline '{{ _ingest.on_failure_pipeline }}' failed with message '{{ _ingest.on_failure_message }}'",
+ ],
+ },
},
- },
- ],
- tag: 'remove_meta_fields',
+ ],
+ tag: 'remove_meta_fields',
+ },
},
- },
- ],
- version: 1,
- });
- return {
- created: [indexName, `${indexName}@custom`, getInferencePipelineNameFromIndexName(indexName)],
- };
+ ],
+ version: 1,
+ };
+ await esClient.ingest.putPipeline(ingestPipeline);
+ result = { ...result, [ingestPipeline.id]: ingestPipeline };
+ return result;
+ } catch (error) {
+ // clean up pipelines if one failed to create
+ for (const id of Object.keys(result)) {
+ await esClient.ingest.deletePipeline({ id });
+ }
+ throw error;
+ }
};
/**
diff --git a/x-pack/plugins/enterprise_search/server/lib/pipelines/delete_pipelines.ts b/x-pack/plugins/enterprise_search/server/lib/pipelines/delete_pipelines.ts
new file mode 100644
index 0000000000000..3038f48004ea9
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/server/lib/pipelines/delete_pipelines.ts
@@ -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 { IScopedClusterClient } from '@kbn/core/server';
+
+import { getInferencePipelineNameFromIndexName } from '../../utils/ml_inference_pipeline_utils';
+
+export const deleteIndexPipelines = async (
+ client: IScopedClusterClient,
+ indexName: string
+): Promise<{ deleted: string[] }> => {
+ const deleted: string[] = [];
+ const promises = [
+ client.asCurrentUser.ingest
+ .deletePipeline({ id: indexName })
+ .then(() => deleted.push(indexName)),
+ client.asCurrentUser.ingest
+ .deletePipeline({
+ id: getInferencePipelineNameFromIndexName(indexName),
+ })
+ .then(() => deleted.push(getInferencePipelineNameFromIndexName(indexName))),
+ client.asCurrentUser.ingest
+ .deletePipeline({ id: `${indexName}@custom` })
+ .then(() => deleted.push(`${indexName}@custom`)),
+ ];
+ await Promise.allSettled(promises);
+ return {
+ deleted,
+ };
+};
diff --git a/x-pack/plugins/enterprise_search/server/lib/pipelines/revert_custom_pipeline.ts b/x-pack/plugins/enterprise_search/server/lib/pipelines/revert_custom_pipeline.ts
new file mode 100644
index 0000000000000..60a9e5cfcf97d
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/server/lib/pipelines/revert_custom_pipeline.ts
@@ -0,0 +1,29 @@
+/*
+ * 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 { IScopedClusterClient } from '@kbn/core/server';
+
+import { CONNECTORS_INDEX } from '../..';
+
+import { fetchConnectorByIndexName } from '../connectors/fetch_connectors';
+
+import { deleteIndexPipelines } from './delete_pipelines';
+
+import { getDefaultPipeline } from './get_default_pipeline';
+
+export const revertCustomPipeline = async (client: IScopedClusterClient, indexName: string) => {
+ const connector = await fetchConnectorByIndexName(client, indexName);
+ if (connector) {
+ const pipeline = await getDefaultPipeline(client);
+ await client.asCurrentUser.update({
+ doc: { pipeline },
+ id: connector?.id,
+ index: CONNECTORS_INDEX,
+ });
+ }
+ return await deleteIndexPipelines(client, indexName);
+};
diff --git a/x-pack/plugins/enterprise_search/server/routes/enterprise_search/indices.ts b/x-pack/plugins/enterprise_search/server/routes/enterprise_search/indices.ts
index 238a882142b7c..5bf70def81b76 100644
--- a/x-pack/plugins/enterprise_search/server/routes/enterprise_search/indices.ts
+++ b/x-pack/plugins/enterprise_search/server/routes/enterprise_search/indices.ts
@@ -41,9 +41,11 @@ import { deleteMlInferencePipeline } from '../../lib/indices/pipelines/ml_infere
import { detachMlInferencePipeline } from '../../lib/indices/pipelines/ml_inference/pipeline_processors/detach_ml_inference_pipeline';
import { fetchMlInferencePipelineProcessors } from '../../lib/indices/pipelines/ml_inference/pipeline_processors/get_ml_inference_pipeline_processors';
import { createIndexPipelineDefinitions } from '../../lib/pipelines/create_pipeline_definitions';
+import { deleteIndexPipelines } from '../../lib/pipelines/delete_pipelines';
import { getCustomPipelines } from '../../lib/pipelines/get_custom_pipelines';
import { getPipeline } from '../../lib/pipelines/get_pipeline';
import { getMlInferencePipelines } from '../../lib/pipelines/ml_inference/get_ml_inference_pipelines';
+import { revertCustomPipeline } from '../../lib/pipelines/revert_custom_pipeline';
import { RouteDependencies } from '../../plugin';
import { createError } from '../../utils/create_error';
import { elasticsearchErrorHandler } from '../../utils/elasticsearch_error_handler';
@@ -196,6 +198,8 @@ export function registerIndexRoutes({
await deleteConnectorById(client, connector.id);
}
+ await deleteIndexPipelines(client, indexName);
+
await client.asCurrentUser.indices.delete({ index: indexName });
return response.ok({
@@ -299,6 +303,27 @@ export function registerIndexRoutes({
})
);
+ router.delete(
+ {
+ path: '/internal/enterprise_search/indices/{indexName}/pipelines',
+ validate: {
+ params: schema.object({
+ indexName: schema.string(),
+ }),
+ },
+ },
+ elasticsearchErrorHandler(log, async (context, request, response) => {
+ const indexName = decodeURIComponent(request.params.indexName);
+ const { client } = (await context.core).elasticsearch;
+ const body = await revertCustomPipeline(client, indexName);
+
+ return response.ok({
+ body,
+ headers: { 'content-type': 'application/json' },
+ });
+ })
+ );
+
router.get(
{
path: '/internal/enterprise_search/indices/{indexName}/pipelines',
diff --git a/x-pack/plugins/fleet/common/experimental_features.ts b/x-pack/plugins/fleet/common/experimental_features.ts
index 8fae99c53ded7..b8861bfbb1caa 100644
--- a/x-pack/plugins/fleet/common/experimental_features.ts
+++ b/x-pack/plugins/fleet/common/experimental_features.ts
@@ -19,7 +19,7 @@ export const allowedExperimentalValues = Object.freeze({
experimentalDataStreamSettings: false,
displayAgentMetrics: true,
showIntegrationsSubcategories: false,
- agentFqdnMode: true,
+ agentFqdnMode: false,
showExperimentalShipperOptions: false,
fleetServerStandalone: false,
});
diff --git a/x-pack/plugins/graph/kibana.jsonc b/x-pack/plugins/graph/kibana.jsonc
index c47a12d71cc92..5c85742b492a2 100644
--- a/x-pack/plugins/graph/kibana.jsonc
+++ b/x-pack/plugins/graph/kibana.jsonc
@@ -16,7 +16,9 @@
"navigation",
"savedObjects",
"unifiedSearch",
- "inspector"
+ "inspector",
+ "savedObjectsManagement",
+ "savedObjectsFinder",
],
"optionalPlugins": [
"home",
diff --git a/x-pack/plugins/graph/public/application.tsx b/x-pack/plugins/graph/public/application.tsx
index bbb14a96ac5eb..90cbc2b88b19f 100644
--- a/x-pack/plugins/graph/public/application.tsx
+++ b/x-pack/plugins/graph/public/application.tsx
@@ -35,6 +35,7 @@ import('./font_awesome');
import { SavedObjectsStart } from '@kbn/saved-objects-plugin/public';
import { SpacesApi } from '@kbn/spaces-plugin/public';
import { KibanaThemeProvider, toMountPoint } from '@kbn/kibana-react-plugin/public';
+import { SavedObjectsManagementPluginStart } from '@kbn/saved-objects-management-plugin/public';
import { GraphSavePolicy } from './types';
import { graphRouter } from './router';
import { checkLicense } from '../common/check_license';
@@ -72,6 +73,7 @@ export interface GraphDependencies {
history: ScopedHistory;
spaces?: SpacesApi;
inspect: InspectorPublicPluginStart;
+ savedObjectsManagement: SavedObjectsManagementPluginStart;
}
export type GraphServices = Omit;
diff --git a/x-pack/plugins/graph/public/apps/workspace_route.tsx b/x-pack/plugins/graph/public/apps/workspace_route.tsx
index 75aafeb1a7868..8fcff177d054b 100644
--- a/x-pack/plugins/graph/public/apps/workspace_route.tsx
+++ b/x-pack/plugins/graph/public/apps/workspace_route.tsx
@@ -43,6 +43,7 @@ export const WorkspaceRoute = ({
spaces,
indexPatterns: getIndexPatternProvider,
inspect,
+ savedObjectsManagement,
},
}: WorkspaceRouteProps) => {
/**
@@ -70,9 +71,10 @@ export const WorkspaceRoute = ({
storage,
data,
unifiedSearch,
+ savedObjectsManagement,
...coreStart,
}),
- [coreStart, data, storage, unifiedSearch]
+ [coreStart, data, storage, unifiedSearch, savedObjectsManagement]
);
const { loading, requestAdapter, callNodeProxy, callSearchNodeProxy, handleSearchQueryError } =
diff --git a/x-pack/plugins/graph/public/components/guidance_panel/guidance_panel.tsx b/x-pack/plugins/graph/public/components/guidance_panel/guidance_panel.tsx
index 81be2e191516a..6b6c06dbc02ba 100644
--- a/x-pack/plugins/graph/public/components/guidance_panel/guidance_panel.tsx
+++ b/x-pack/plugins/graph/public/components/guidance_panel/guidance_panel.tsx
@@ -77,7 +77,7 @@ function GuidancePanelComponent(props: GuidancePanelProps) {
const kibana = useKibana();
const { services, overlays } = kibana;
- const { http, uiSettings, application, data } = services;
+ const { http, uiSettings, application, data, savedObjectsManagement } = services;
const [hasDataViews, setHasDataViews] = useState(true);
useEffect(() => {
@@ -90,7 +90,7 @@ function GuidancePanelComponent(props: GuidancePanelProps) {
if (!overlays || !application) return null;
const onOpenDatasourcePicker = () => {
- openSourceModal({ overlays, http, uiSettings }, onIndexPatternSelected);
+ openSourceModal({ overlays, http, uiSettings, savedObjectsManagement }, onIndexPatternSelected);
};
let content = (
diff --git a/x-pack/plugins/graph/public/components/search_bar.test.tsx b/x-pack/plugins/graph/public/components/search_bar.test.tsx
index ca21e16c0fb36..e1ee8cb9d6331 100644
--- a/x-pack/plugins/graph/public/components/search_bar.test.tsx
+++ b/x-pack/plugins/graph/public/components/search_bar.test.tsx
@@ -29,6 +29,7 @@ import { GraphStore, setDatasource, submitSearchSaga } from '../state_management
import { ReactWrapper } from 'enzyme';
import { createMockGraphStore } from '../state_management/mocks';
import { Provider } from 'react-redux';
+import { SavedObjectsManagementPluginStart } from '@kbn/saved-objects-management-plugin/public';
jest.mock('../services/source_modal', () => ({ openSourceModal: jest.fn() }));
@@ -42,6 +43,7 @@ function getServiceMocks() {
},
} as IUiSettingsClient,
savedObjects: {} as SavedObjectsStart,
+ savedObjectsManagement: {} as SavedObjectsManagementPluginStart,
notifications: {} as NotificationsStart,
docLinks: {
links: {
diff --git a/x-pack/plugins/graph/public/components/search_bar.tsx b/x-pack/plugins/graph/public/components/search_bar.tsx
index fcd5d576116d9..5bf23c1705dec 100644
--- a/x-pack/plugins/graph/public/components/search_bar.tsx
+++ b/x-pack/plugins/graph/public/components/search_bar.tsx
@@ -107,9 +107,9 @@ export function SearchBarComponent(props: SearchBarStateProps & SearchBarProps)
notifications,
http,
docLinks,
+ savedObjectsManagement,
} = services;
if (!overlays) return null;
-
return (