diff --git a/.buildkite/scripts/steps/storybooks/build_and_upload.ts b/.buildkite/scripts/steps/storybooks/build_and_upload.ts
index dcceca7848910..945f85a820971 100644
--- a/.buildkite/scripts/steps/storybooks/build_and_upload.ts
+++ b/.buildkite/scripts/steps/storybooks/build_and_upload.ts
@@ -15,7 +15,7 @@ const STORYBOOKS = [
'apm',
'canvas',
'ci_composite',
- 'cloud',
+ 'cloud_chat',
'coloring',
'chart_icons',
'controls',
diff --git a/docs/api/saved-objects/find.asciidoc b/docs/api/saved-objects/find.asciidoc
index 43c7f4cde8fa8..275bd1c21f9ed 100644
--- a/docs/api/saved-objects/find.asciidoc
+++ b/docs/api/saved-objects/find.asciidoc
@@ -52,6 +52,15 @@ experimental[] Retrieve a paginated set of {kib} saved objects by various condit
`has_reference`::
(Optional, object) Filters to objects that have a relationship with the type and ID combination.
+`has_reference_operator`::
+ (Optional, string) The operator to use for the `has_reference` parameter. Either `OR` or `AND`. Defaults to `OR`.
+
+`has_no_reference`::
+ (Optional, object) Filters to objects that do not have a relationship with the type and ID combination.
+
+`has_no_reference_operator`::
+ (Optional, string) The operator to use for the `has_no_reference` parameter. Either `OR` or `AND`. Defaults to `OR`.
+
`filter`::
(Optional, string) The filter is a KQL string with the caveat that if you filter with an attribute from your saved object type,
it should look like that: `savedObjectType.attributes.title: "myTitle"`. However, If you use a root attribute of a saved
diff --git a/docs/developer/plugin-list.asciidoc b/docs/developer/plugin-list.asciidoc
index f4fc9c67508ef..407261c6f1d7e 100644
--- a/docs/developer/plugin-list.asciidoc
+++ b/docs/developer/plugin-list.asciidoc
@@ -424,10 +424,22 @@ The plugin exposes the static DefaultEditorController class to consume.
|The cloud plugin adds Cloud-specific features to Kibana.
+|{kib-repo}blob/{branch}/x-pack/plugins/cloud_integrations/cloud_chat/README.md[cloudChat]
+|Integrates with DriftChat in order to provide live support to our Elastic Cloud users. This plugin should only run on Elastic Cloud.
+
+
|{kib-repo}blob/{branch}/x-pack/plugins/cloud_integrations/cloud_experiments/README.mdx[cloudExperiments]
|The Cloud Experiments Service provides the necessary APIs to implement A/B testing scenarios, fetching the variations in configuration and reporting back metrics to track conversion rates of the experiments.
+|{kib-repo}blob/{branch}/x-pack/plugins/cloud_integrations/cloud_full_story/README.md[cloudFullStory]
+|Integrates with FullStory in order to provide better product analytics, so we can understand how our users make use of Kibana. This plugin should only run on Elastic Cloud.
+
+
+|{kib-repo}blob/{branch}/x-pack/plugins/cloud_integrations/cloud_links/README.md[cloudLinks]
+|Adds all the links to the Elastic Cloud console.
+
+
|{kib-repo}blob/{branch}/x-pack/plugins/cloud_security_posture/README.md[cloudSecurityPosture]
|Cloud Posture automates the identification and remediation of risks across cloud infrastructures
diff --git a/packages/core/apps/core-apps-browser-internal/src/status/lib/load_status.test.ts b/packages/core/apps/core-apps-browser-internal/src/status/lib/load_status.test.ts
index ac40eedfccb7d..dd750a56fbf2d 100644
--- a/packages/core/apps/core-apps-browser-internal/src/status/lib/load_status.test.ts
+++ b/packages/core/apps/core-apps-browser-internal/src/status/lib/load_status.test.ts
@@ -61,6 +61,19 @@ const mockedResponse: StatusResponse = {
'15m': 0.1,
},
},
+ elasticsearch_client: {
+ protocol: 'https',
+ connectedNodes: 3,
+ nodesWithActiveSockets: 3,
+ nodesWithIdleSockets: 1,
+ totalActiveSockets: 25,
+ totalIdleSockets: 2,
+ totalQueuedRequests: 0,
+ mostActiveNodeSockets: 15,
+ averageActiveSocketsPerNode: 8,
+ mostIdleNodeSockets: 2,
+ averageIdleSocketsPerNode: 0.5,
+ },
process: {
pid: 1,
memory: {
diff --git a/packages/core/elasticsearch/core-elasticsearch-client-server-internal/index.ts b/packages/core/elasticsearch/core-elasticsearch-client-server-internal/index.ts
index aa1364c179e18..6f1f276c7d089 100644
--- a/packages/core/elasticsearch/core-elasticsearch-client-server-internal/index.ts
+++ b/packages/core/elasticsearch/core-elasticsearch-client-server-internal/index.ts
@@ -9,7 +9,7 @@
export { ScopedClusterClient } from './src/scoped_cluster_client';
export { ClusterClient } from './src/cluster_client';
export { configureClient } from './src/configure_client';
-export { AgentManager } from './src/agent_manager';
+export { type AgentStore, AgentManager } from './src/agent_manager';
export { getRequestDebugMeta, getErrorMessage } from './src/log_query_and_deprecation';
export {
PRODUCT_RESPONSE_HEADER,
diff --git a/packages/core/elasticsearch/core-elasticsearch-client-server-internal/src/agent_manager.test.ts b/packages/core/elasticsearch/core-elasticsearch-client-server-internal/src/agent_manager.test.ts
index 811d9d95831ef..dfa8a077d2e53 100644
--- a/packages/core/elasticsearch/core-elasticsearch-client-server-internal/src/agent_manager.test.ts
+++ b/packages/core/elasticsearch/core-elasticsearch-client-server-internal/src/agent_manager.test.ts
@@ -104,10 +104,10 @@ describe('AgentManager', () => {
const agentFactory = agentManager.getAgentFactory();
const agent = agentFactory({ url: new URL('http://elastic-node-1:9200') });
// eslint-disable-next-line dot-notation
- expect(agentManager['httpStore'].has(agent)).toEqual(true);
+ expect(agentManager['agents'].has(agent)).toEqual(true);
agent.destroy();
// eslint-disable-next-line dot-notation
- expect(agentManager['httpStore'].has(agent)).toEqual(false);
+ expect(agentManager['agents'].has(agent)).toEqual(false);
});
});
@@ -122,4 +122,21 @@ describe('AgentManager', () => {
});
});
});
+
+ describe('#getAgents()', () => {
+ it('returns the created HTTP and HTTPs Agent instances', () => {
+ const agentManager = new AgentManager();
+ const agentFactory1 = agentManager.getAgentFactory();
+ const agentFactory2 = agentManager.getAgentFactory();
+ const agent1 = agentFactory1({ url: new URL('http://elastic-node-1:9200') });
+ const agent2 = agentFactory2({ url: new URL('http://elastic-node-1:9200') });
+ const agent3 = agentFactory1({ url: new URL('https://elastic-node-1:9200') });
+ const agent4 = agentFactory2({ url: new URL('https://elastic-node-1:9200') });
+
+ const agents = agentManager.getAgents();
+
+ expect(agents.size).toEqual(4);
+ expect([...agents]).toEqual(expect.arrayContaining([agent1, agent2, agent3, agent4]));
+ });
+ });
});
diff --git a/packages/core/elasticsearch/core-elasticsearch-client-server-internal/src/agent_manager.ts b/packages/core/elasticsearch/core-elasticsearch-client-server-internal/src/agent_manager.ts
index eb68014561d77..9a57cc44e04ad 100644
--- a/packages/core/elasticsearch/core-elasticsearch-client-server-internal/src/agent_manager.ts
+++ b/packages/core/elasticsearch/core-elasticsearch-client-server-internal/src/agent_manager.ts
@@ -8,7 +8,7 @@
import { Agent as HttpAgent } from 'http';
import { Agent as HttpsAgent } from 'https';
-import { ConnectionOptions, HttpAgentOptions } from '@elastic/elasticsearch';
+import type { ConnectionOptions, HttpAgentOptions } from '@elastic/elasticsearch';
const HTTPS = 'https:';
const DEFAULT_CONFIG: HttpAgentOptions = {
@@ -22,6 +22,14 @@ const DEFAULT_CONFIG: HttpAgentOptions = {
export type NetworkAgent = HttpAgent | HttpsAgent;
export type AgentFactory = (connectionOpts: ConnectionOptions) => NetworkAgent;
+export interface AgentFactoryProvider {
+ getAgentFactory(agentOptions?: HttpAgentOptions): AgentFactory;
+}
+
+export interface AgentStore {
+ getAgents(): Set;
+}
+
/**
* Allows obtaining Agent factories, which can then be fed into elasticsearch-js's Client class.
* Ideally, we should obtain one Agent factory for each ES Client class.
@@ -33,15 +41,11 @@ export type AgentFactory = (connectionOpts: ConnectionOptions) => NetworkAgent;
* exposes methods that can modify the underlying pools, effectively impacting the connections of other Clients.
* @internal
**/
-export class AgentManager {
- // Stores Https Agent instances
- private httpsStore: Set;
- // Stores Http Agent instances
- private httpStore: Set;
+export class AgentManager implements AgentFactoryProvider, AgentStore {
+ private agents: Set;
constructor(private agentOptions: HttpAgentOptions = DEFAULT_CONFIG) {
- this.httpsStore = new Set();
- this.httpStore = new Set();
+ this.agents = new Set();
}
public getAgentFactory(agentOptions?: HttpAgentOptions): AgentFactory {
@@ -61,8 +65,8 @@ export class AgentManager {
connectionOpts.tls
);
httpsAgent = new HttpsAgent(config);
- this.httpsStore.add(httpsAgent);
- dereferenceOnDestroy(this.httpsStore, httpsAgent);
+ this.agents.add(httpsAgent);
+ dereferenceOnDestroy(this.agents, httpsAgent);
}
return httpsAgent;
@@ -71,19 +75,23 @@ export class AgentManager {
if (!httpAgent) {
const config = Object.assign({}, DEFAULT_CONFIG, this.agentOptions, agentOptions);
httpAgent = new HttpAgent(config);
- this.httpStore.add(httpAgent);
- dereferenceOnDestroy(this.httpStore, httpAgent);
+ this.agents.add(httpAgent);
+ dereferenceOnDestroy(this.agents, httpAgent);
}
return httpAgent;
};
}
+
+ public getAgents(): Set {
+ return this.agents;
+ }
}
-const dereferenceOnDestroy = (protocolStore: Set, agent: NetworkAgent) => {
+const dereferenceOnDestroy = (store: Set, agent: NetworkAgent) => {
const doDestroy = agent.destroy.bind(agent);
agent.destroy = () => {
- protocolStore.delete(agent);
+ store.delete(agent);
doDestroy();
};
};
diff --git a/packages/core/elasticsearch/core-elasticsearch-client-server-internal/src/cluster_client.test.ts b/packages/core/elasticsearch/core-elasticsearch-client-server-internal/src/cluster_client.test.ts
index e5be9fc0ab718..f371e3425b0c7 100644
--- a/packages/core/elasticsearch/core-elasticsearch-client-server-internal/src/cluster_client.test.ts
+++ b/packages/core/elasticsearch/core-elasticsearch-client-server-internal/src/cluster_client.test.ts
@@ -46,7 +46,7 @@ describe('ClusterClient', () => {
let authHeaders: ReturnType;
let internalClient: jest.Mocked;
let scopedClient: jest.Mocked;
- let agentManager: AgentManager;
+ let agentFactoryProvider: AgentManager;
const mockTransport = { mockTransport: true };
@@ -54,7 +54,7 @@ describe('ClusterClient', () => {
logger = loggingSystemMock.createLogger();
internalClient = createClient();
scopedClient = createClient();
- agentManager = new AgentManager();
+ agentFactoryProvider = new AgentManager();
authHeaders = httpServiceMock.createAuthHeaderStorage();
authHeaders.get.mockImplementation(() => ({
@@ -84,21 +84,21 @@ describe('ClusterClient', () => {
authHeaders,
type: 'custom-type',
getExecutionContext: getExecutionContextMock,
- agentManager,
+ agentFactoryProvider,
kibanaVersion,
});
expect(configureClientMock).toHaveBeenCalledTimes(2);
expect(configureClientMock).toHaveBeenCalledWith(config, {
logger,
- agentManager,
+ agentFactoryProvider,
kibanaVersion,
type: 'custom-type',
getExecutionContext: getExecutionContextMock,
});
expect(configureClientMock).toHaveBeenCalledWith(config, {
logger,
- agentManager,
+ agentFactoryProvider,
kibanaVersion,
type: 'custom-type',
getExecutionContext: getExecutionContextMock,
@@ -113,7 +113,7 @@ describe('ClusterClient', () => {
logger,
type: 'custom-type',
authHeaders,
- agentManager,
+ agentFactoryProvider,
kibanaVersion,
});
@@ -128,7 +128,7 @@ describe('ClusterClient', () => {
logger,
type: 'custom-type',
authHeaders,
- agentManager,
+ agentFactoryProvider,
kibanaVersion,
});
const request = httpServerMock.createKibanaRequest();
@@ -155,7 +155,7 @@ describe('ClusterClient', () => {
authHeaders,
getExecutionContext,
getUnauthorizedErrorHandler,
- agentManager,
+ agentFactoryProvider,
kibanaVersion,
});
const request = httpServerMock.createKibanaRequest();
@@ -179,7 +179,7 @@ describe('ClusterClient', () => {
authHeaders,
getExecutionContext,
getUnauthorizedErrorHandler,
- agentManager,
+ agentFactoryProvider,
kibanaVersion,
});
const request = httpServerMock.createKibanaRequest();
@@ -212,7 +212,7 @@ describe('ClusterClient', () => {
logger,
type: 'custom-type',
authHeaders,
- agentManager,
+ agentFactoryProvider,
kibanaVersion,
});
const request = httpServerMock.createKibanaRequest();
@@ -237,7 +237,7 @@ describe('ClusterClient', () => {
logger,
type: 'custom-type',
authHeaders,
- agentManager,
+ agentFactoryProvider,
kibanaVersion,
});
const request = httpServerMock.createKibanaRequest({
@@ -271,7 +271,7 @@ describe('ClusterClient', () => {
logger,
type: 'custom-type',
authHeaders,
- agentManager,
+ agentFactoryProvider,
kibanaVersion,
});
const request = httpServerMock.createKibanaRequest({});
@@ -305,7 +305,7 @@ describe('ClusterClient', () => {
logger,
type: 'custom-type',
authHeaders,
- agentManager,
+ agentFactoryProvider,
kibanaVersion,
});
const request = httpServerMock.createKibanaRequest({
@@ -344,7 +344,7 @@ describe('ClusterClient', () => {
logger,
type: 'custom-type',
authHeaders,
- agentManager,
+ agentFactoryProvider,
kibanaVersion,
});
const request = httpServerMock.createKibanaRequest({});
@@ -373,7 +373,7 @@ describe('ClusterClient', () => {
logger,
type: 'custom-type',
authHeaders,
- agentManager,
+ agentFactoryProvider,
kibanaVersion,
});
const request = httpServerMock.createKibanaRequest({
@@ -410,7 +410,7 @@ describe('ClusterClient', () => {
logger,
type: 'custom-type',
authHeaders,
- agentManager,
+ agentFactoryProvider,
kibanaVersion,
});
const request = httpServerMock.createKibanaRequest({});
@@ -445,7 +445,7 @@ describe('ClusterClient', () => {
logger,
type: 'custom-type',
authHeaders,
- agentManager,
+ agentFactoryProvider,
kibanaVersion,
});
const request = httpServerMock.createKibanaRequest({
@@ -482,7 +482,7 @@ describe('ClusterClient', () => {
logger,
type: 'custom-type',
authHeaders,
- agentManager,
+ agentFactoryProvider,
kibanaVersion,
});
const request = httpServerMock.createKibanaRequest();
@@ -513,7 +513,7 @@ describe('ClusterClient', () => {
logger,
type: 'custom-type',
authHeaders,
- agentManager,
+ agentFactoryProvider,
kibanaVersion,
});
const request = httpServerMock.createKibanaRequest({
@@ -547,7 +547,7 @@ describe('ClusterClient', () => {
logger,
type: 'custom-type',
authHeaders,
- agentManager,
+ agentFactoryProvider,
kibanaVersion,
});
const request = httpServerMock.createKibanaRequest({
@@ -579,7 +579,7 @@ describe('ClusterClient', () => {
logger,
type: 'custom-type',
authHeaders,
- agentManager,
+ agentFactoryProvider,
kibanaVersion,
});
const request = {
@@ -612,7 +612,7 @@ describe('ClusterClient', () => {
logger,
type: 'custom-type',
authHeaders,
- agentManager,
+ agentFactoryProvider,
kibanaVersion,
});
const request = {
@@ -640,7 +640,7 @@ describe('ClusterClient', () => {
logger,
type: 'custom-type',
authHeaders,
- agentManager,
+ agentFactoryProvider,
kibanaVersion,
});
@@ -658,7 +658,7 @@ describe('ClusterClient', () => {
logger,
type: 'custom-type',
authHeaders,
- agentManager,
+ agentFactoryProvider,
kibanaVersion,
});
@@ -703,7 +703,7 @@ describe('ClusterClient', () => {
logger,
type: 'custom-type',
authHeaders,
- agentManager,
+ agentFactoryProvider,
kibanaVersion,
});
@@ -720,7 +720,7 @@ describe('ClusterClient', () => {
logger,
type: 'custom-type',
authHeaders,
- agentManager,
+ agentFactoryProvider,
kibanaVersion,
});
diff --git a/packages/core/elasticsearch/core-elasticsearch-client-server-internal/src/cluster_client.ts b/packages/core/elasticsearch/core-elasticsearch-client-server-internal/src/cluster_client.ts
index f243c98ecf798..2a2f6ef1334a2 100644
--- a/packages/core/elasticsearch/core-elasticsearch-client-server-internal/src/cluster_client.ts
+++ b/packages/core/elasticsearch/core-elasticsearch-client-server-internal/src/cluster_client.ts
@@ -24,9 +24,12 @@ import type { ElasticsearchClientConfig } from '@kbn/core-elasticsearch-server';
import { configureClient } from './configure_client';
import { ScopedClusterClient } from './scoped_cluster_client';
import { getDefaultHeaders } from './headers';
-import { createInternalErrorHandler, InternalUnauthorizedErrorHandler } from './retry_unauthorized';
+import {
+ createInternalErrorHandler,
+ type InternalUnauthorizedErrorHandler,
+} from './retry_unauthorized';
import { createTransport } from './create_transport';
-import { AgentManager } from './agent_manager';
+import type { AgentFactoryProvider } from './agent_manager';
const noop = () => undefined;
@@ -49,7 +52,7 @@ export class ClusterClient implements ICustomClusterClient {
authHeaders,
getExecutionContext = noop,
getUnauthorizedErrorHandler = noop,
- agentManager,
+ agentFactoryProvider,
kibanaVersion,
}: {
config: ElasticsearchClientConfig;
@@ -58,7 +61,7 @@ export class ClusterClient implements ICustomClusterClient {
authHeaders?: IAuthHeadersStorage;
getExecutionContext?: () => string | undefined;
getUnauthorizedErrorHandler?: () => UnauthorizedErrorHandler | undefined;
- agentManager: AgentManager;
+ agentFactoryProvider: AgentFactoryProvider;
kibanaVersion: string;
}) {
this.config = config;
@@ -71,7 +74,7 @@ export class ClusterClient implements ICustomClusterClient {
logger,
type,
getExecutionContext,
- agentManager,
+ agentFactoryProvider,
kibanaVersion,
});
this.rootScopedClient = configureClient(config, {
@@ -79,7 +82,7 @@ export class ClusterClient implements ICustomClusterClient {
type,
getExecutionContext,
scoped: true,
- agentManager,
+ agentFactoryProvider,
kibanaVersion,
});
}
diff --git a/packages/core/elasticsearch/core-elasticsearch-client-server-internal/src/configure_client.test.ts b/packages/core/elasticsearch/core-elasticsearch-client-server-internal/src/configure_client.test.ts
index 40824d306ac48..fe511f46278d9 100644
--- a/packages/core/elasticsearch/core-elasticsearch-client-server-internal/src/configure_client.test.ts
+++ b/packages/core/elasticsearch/core-elasticsearch-client-server-internal/src/configure_client.test.ts
@@ -10,7 +10,6 @@ jest.mock('./log_query_and_deprecation', () => ({
__esModule: true,
instrumentEsQueryAndDeprecationLogger: jest.fn(),
}));
-jest.mock('./agent_manager');
import { Agent } from 'http';
import {
@@ -24,9 +23,8 @@ import { ClusterConnectionPool } from '@elastic/elasticsearch';
import type { ElasticsearchClientConfig } from '@kbn/core-elasticsearch-server';
import { configureClient } from './configure_client';
import { instrumentEsQueryAndDeprecationLogger } from './log_query_and_deprecation';
-import { AgentManager } from './agent_manager';
+import { type AgentFactoryProvider, AgentManager } from './agent_manager';
-const AgentManagerMock = AgentManager as jest.Mock;
const kibanaVersion = '1.0.0';
const createFakeConfig = (
@@ -46,31 +44,17 @@ const createFakeClient = () => {
return client;
};
-const createFakeAgentFactory = (logger: MockedLogger) => {
- const agentFactory = () => new Agent();
-
- AgentManagerMock.mockImplementationOnce(() => {
- const agentManager = new AgentManager();
- agentManager.getAgentFactory = () => agentFactory;
- return agentManager;
- });
-
- const agentManager = new AgentManager();
-
- return { agentManager, agentFactory };
-};
-
describe('configureClient', () => {
let logger: MockedLogger;
let config: ElasticsearchClientConfig;
- let agentManager: AgentManager;
+ let agentFactoryProvider: AgentFactoryProvider;
beforeEach(() => {
logger = loggingSystemMock.createLogger();
config = createFakeConfig();
parseClientOptionsMock.mockReturnValue({});
ClientMock.mockImplementation(() => createFakeClient());
- agentManager = new AgentManager();
+ agentFactoryProvider = new AgentManager();
});
afterEach(() => {
@@ -80,14 +64,26 @@ describe('configureClient', () => {
});
it('calls `parseClientOptions` with the correct parameters', () => {
- configureClient(config, { logger, type: 'test', scoped: false, agentManager, kibanaVersion });
+ configureClient(config, {
+ logger,
+ type: 'test',
+ scoped: false,
+ agentFactoryProvider,
+ kibanaVersion,
+ });
expect(parseClientOptionsMock).toHaveBeenCalledTimes(1);
expect(parseClientOptionsMock).toHaveBeenCalledWith(config, false, kibanaVersion);
parseClientOptionsMock.mockClear();
- configureClient(config, { logger, type: 'test', scoped: true, agentManager, kibanaVersion });
+ configureClient(config, {
+ logger,
+ type: 'test',
+ scoped: true,
+ agentFactoryProvider,
+ kibanaVersion,
+ });
expect(parseClientOptionsMock).toHaveBeenCalledTimes(1);
expect(parseClientOptionsMock).toHaveBeenCalledWith(config, true, kibanaVersion);
@@ -103,7 +99,7 @@ describe('configureClient', () => {
logger,
type: 'test',
scoped: false,
- agentManager,
+ agentFactoryProvider,
kibanaVersion,
});
@@ -112,13 +108,17 @@ describe('configureClient', () => {
expect(client).toBe(ClientMock.mock.results[0].value);
});
- it('constructs a client using the provided `agentManager`', () => {
- const { agentManager: customAgentManager, agentFactory } = createFakeAgentFactory(logger);
+ it('constructs a client using the provided `agentFactoryProvider`', () => {
+ const agentFactory = () => new Agent();
+ const customAgentFactoryProvider = {
+ getAgentFactory: () => agentFactory,
+ };
+
const client = configureClient(config, {
logger,
type: 'test',
scoped: false,
- agentManager: customAgentManager,
+ agentFactoryProvider: customAgentFactoryProvider,
kibanaVersion,
});
@@ -134,7 +134,7 @@ describe('configureClient', () => {
type: 'test',
scoped: false,
getExecutionContext,
- agentManager,
+ agentFactoryProvider,
kibanaVersion,
});
@@ -148,7 +148,7 @@ describe('configureClient', () => {
type: 'test',
scoped: true,
getExecutionContext,
- agentManager,
+ agentFactoryProvider,
kibanaVersion,
});
@@ -164,7 +164,7 @@ describe('configureClient', () => {
logger,
type: 'test',
scoped: false,
- agentManager,
+ agentFactoryProvider,
kibanaVersion,
});
@@ -185,7 +185,7 @@ describe('configureClient', () => {
logger,
type: 'test',
scoped: false,
- agentManager,
+ agentFactoryProvider,
kibanaVersion,
});
@@ -203,7 +203,7 @@ describe('configureClient', () => {
logger,
type: 'test',
scoped: false,
- agentManager,
+ agentFactoryProvider,
kibanaVersion,
});
diff --git a/packages/core/elasticsearch/core-elasticsearch-client-server-internal/src/configure_client.ts b/packages/core/elasticsearch/core-elasticsearch-client-server-internal/src/configure_client.ts
index e1c8048c6a89e..2fd7a4d4a74bb 100644
--- a/packages/core/elasticsearch/core-elasticsearch-client-server-internal/src/configure_client.ts
+++ b/packages/core/elasticsearch/core-elasticsearch-client-server-internal/src/configure_client.ts
@@ -12,7 +12,7 @@ import type { ElasticsearchClientConfig } from '@kbn/core-elasticsearch-server';
import { parseClientOptions } from './client_config';
import { instrumentEsQueryAndDeprecationLogger } from './log_query_and_deprecation';
import { createTransport } from './create_transport';
-import { AgentManager } from './agent_manager';
+import type { AgentFactoryProvider } from './agent_manager';
const noop = () => undefined;
@@ -23,14 +23,14 @@ export const configureClient = (
type,
scoped = false,
getExecutionContext = noop,
- agentManager,
+ agentFactoryProvider,
kibanaVersion,
}: {
logger: Logger;
type: string;
scoped?: boolean;
getExecutionContext?: () => string | undefined;
- agentManager: AgentManager;
+ agentFactoryProvider: AgentFactoryProvider;
kibanaVersion: string;
}
): Client => {
@@ -38,7 +38,7 @@ export const configureClient = (
const KibanaTransport = createTransport({ getExecutionContext });
const client = new Client({
...clientOptions,
- agent: agentManager.getAgentFactory(clientOptions.agent),
+ agent: agentFactoryProvider.getAgentFactory(clientOptions.agent),
Transport: KibanaTransport,
Connection: HttpConnection,
// using ClusterConnectionPool until https://github.com/elastic/elasticsearch-js/issues/1714 is addressed
diff --git a/packages/core/elasticsearch/core-elasticsearch-client-server-mocks/index.ts b/packages/core/elasticsearch/core-elasticsearch-client-server-mocks/index.ts
index c46381d57a7b6..0b66d449df013 100644
--- a/packages/core/elasticsearch/core-elasticsearch-client-server-mocks/index.ts
+++ b/packages/core/elasticsearch/core-elasticsearch-client-server-mocks/index.ts
@@ -15,3 +15,4 @@ export type {
DeeplyMockedApi,
ElasticsearchClientMock,
} from './src/mocks';
+export { createAgentStoreMock } from './src/agent_manager.mocks';
diff --git a/packages/core/elasticsearch/core-elasticsearch-client-server-mocks/src/agent_manager.mocks.ts b/packages/core/elasticsearch/core-elasticsearch-client-server-mocks/src/agent_manager.mocks.ts
new file mode 100644
index 0000000000000..2fd8812b3aae0
--- /dev/null
+++ b/packages/core/elasticsearch/core-elasticsearch-client-server-mocks/src/agent_manager.mocks.ts
@@ -0,0 +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 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 { AgentStore, NetworkAgent } from '@kbn/core-elasticsearch-client-server-internal';
+
+export const createAgentStoreMock = (agents: Set = new Set()): AgentStore => ({
+ getAgents: jest.fn(() => agents),
+});
diff --git a/packages/core/elasticsearch/core-elasticsearch-server-internal/src/elasticsearch_service.test.mocks.ts b/packages/core/elasticsearch/core-elasticsearch-server-internal/src/elasticsearch_service.test.mocks.ts
index cd6d36f0cb111..68a56ff28bc8d 100644
--- a/packages/core/elasticsearch/core-elasticsearch-server-internal/src/elasticsearch_service.test.mocks.ts
+++ b/packages/core/elasticsearch/core-elasticsearch-server-internal/src/elasticsearch_service.test.mocks.ts
@@ -6,8 +6,14 @@
* Side Public License, v 1.
*/
+import type { AgentManager } from '@kbn/core-elasticsearch-client-server-internal';
+
export const MockClusterClient = jest.fn();
-export const MockAgentManager = jest.fn();
+export const MockAgentManager: jest.MockedClass = jest.fn().mockReturnValue({
+ getAgents: jest.fn(),
+ getAgentFactory: jest.fn(),
+});
+
jest.mock('@kbn/core-elasticsearch-client-server-internal', () => ({
ClusterClient: MockClusterClient,
AgentManager: MockAgentManager,
diff --git a/packages/core/elasticsearch/core-elasticsearch-server-internal/src/elasticsearch_service.test.ts b/packages/core/elasticsearch/core-elasticsearch-server-internal/src/elasticsearch_service.test.ts
index 5b54a2c35683e..ecd364b4283cf 100644
--- a/packages/core/elasticsearch/core-elasticsearch-server-internal/src/elasticsearch_service.test.ts
+++ b/packages/core/elasticsearch/core-elasticsearch-server-internal/src/elasticsearch_service.test.ts
@@ -135,7 +135,7 @@ describe('#preboot', () => {
);
});
- it('creates a ClusterClient using the internal AgentManager', async () => {
+ it('creates a ClusterClient using the internal AgentManager as AgentFactoryProvider ', async () => {
const prebootContract = await elasticsearchService.preboot();
const customConfig = { keepAlive: true };
const clusterClient = prebootContract.createClient('custom-type', customConfig);
@@ -145,7 +145,7 @@ describe('#preboot', () => {
expect(MockClusterClient).toHaveBeenCalledTimes(1);
expect(MockClusterClient.mock.calls[0][0]).toEqual(
// eslint-disable-next-line dot-notation
- expect.objectContaining({ agentManager: elasticsearchService['agentManager'] })
+ expect.objectContaining({ agentFactoryProvider: elasticsearchService['agentManager'] })
);
});
@@ -201,6 +201,11 @@ describe('#setup', () => {
);
});
+ it('returns an AgentStore as part of the contract', async () => {
+ const setupContract = await elasticsearchService.setup(setupDeps);
+ expect(typeof setupContract.agentStore.getAgents).toEqual('function');
+ });
+
it('esNodeVersionCompatibility$ only starts polling when subscribed to', async () => {
const mockedClient = mockClusterClientInstance.asInternalUser;
mockedClient.nodes.info.mockImplementation(() =>
diff --git a/packages/core/elasticsearch/core-elasticsearch-server-internal/src/elasticsearch_service.ts b/packages/core/elasticsearch/core-elasticsearch-server-internal/src/elasticsearch_service.ts
index f345732c7a7c4..fddff84293140 100644
--- a/packages/core/elasticsearch/core-elasticsearch-server-internal/src/elasticsearch_service.ts
+++ b/packages/core/elasticsearch/core-elasticsearch-server-internal/src/elasticsearch_service.ts
@@ -120,6 +120,7 @@ export class ElasticsearchService
}
this.unauthorizedErrorHandler = handler;
},
+ agentStore: this.agentManager,
};
}
@@ -182,7 +183,7 @@ export class ElasticsearchService
authHeaders: this.authHeaders,
getExecutionContext: () => this.executionContextClient?.getAsHeader(),
getUnauthorizedErrorHandler: () => this.unauthorizedErrorHandler,
- agentManager: this.agentManager,
+ agentFactoryProvider: this.agentManager,
kibanaVersion: this.kibanaVersion,
});
}
diff --git a/packages/core/elasticsearch/core-elasticsearch-server-internal/src/types.ts b/packages/core/elasticsearch/core-elasticsearch-server-internal/src/types.ts
index 8d05ad0c4cd0a..b03b86c7bdd1c 100644
--- a/packages/core/elasticsearch/core-elasticsearch-server-internal/src/types.ts
+++ b/packages/core/elasticsearch/core-elasticsearch-server-internal/src/types.ts
@@ -12,6 +12,7 @@ import type {
ElasticsearchServiceStart,
ElasticsearchServiceSetup,
} from '@kbn/core-elasticsearch-server';
+import type { AgentStore } from '@kbn/core-elasticsearch-client-server-internal';
import type { ServiceStatus } from '@kbn/core-status-common';
import type { NodesVersionCompatibility, NodeInfo } from './version_check/ensure_es_version';
import type { ClusterInfo } from './get_cluster_info';
@@ -21,6 +22,7 @@ export type InternalElasticsearchServicePreboot = ElasticsearchServicePreboot;
/** @internal */
export interface InternalElasticsearchServiceSetup extends ElasticsearchServiceSetup {
+ agentStore: AgentStore;
clusterInfo$: Observable;
esNodesCompatibility$: Observable;
status$: Observable>;
diff --git a/packages/core/elasticsearch/core-elasticsearch-server-mocks/src/elasticsearch_service.mock.ts b/packages/core/elasticsearch/core-elasticsearch-server-mocks/src/elasticsearch_service.mock.ts
index a1323be0ea71b..26d81da24318c 100644
--- a/packages/core/elasticsearch/core-elasticsearch-server-mocks/src/elasticsearch_service.mock.ts
+++ b/packages/core/elasticsearch/core-elasticsearch-server-mocks/src/elasticsearch_service.mock.ts
@@ -13,6 +13,7 @@ import {
elasticsearchClientMock,
type ClusterClientMock,
type CustomClusterClientMock,
+ createAgentStoreMock,
} from '@kbn/core-elasticsearch-client-server-mocks';
import type {
ElasticsearchClientConfig,
@@ -94,6 +95,7 @@ const createInternalSetupContractMock = () => {
level: ServiceStatusLevels.available,
summary: 'Elasticsearch is available',
}),
+ agentStore: createAgentStoreMock(),
};
return internalSetupContract;
};
diff --git a/packages/core/elasticsearch/core-elasticsearch-server/src/client/cluster_client.ts b/packages/core/elasticsearch/core-elasticsearch-server/src/client/cluster_client.ts
index 57eadf70ef68a..a8e065d357ee1 100644
--- a/packages/core/elasticsearch/core-elasticsearch-server/src/client/cluster_client.ts
+++ b/packages/core/elasticsearch/core-elasticsearch-server/src/client/cluster_client.ts
@@ -7,8 +7,8 @@
*/
import type { ElasticsearchClient } from './client';
-import { ScopeableRequest } from './scopeable_request';
-import { IScopedClusterClient } from './scoped_cluster_client';
+import type { ScopeableRequest } from './scopeable_request';
+import type { IScopedClusterClient } from './scoped_cluster_client';
/**
* Represents an Elasticsearch cluster API client created by the platform.
diff --git a/packages/core/metrics/core-metrics-collectors-server-internal/BUILD.bazel b/packages/core/metrics/core-metrics-collectors-server-internal/BUILD.bazel
index 2b789e97cbe69..9761bcbf1cefb 100644
--- a/packages/core/metrics/core-metrics-collectors-server-internal/BUILD.bazel
+++ b/packages/core/metrics/core-metrics-collectors-server-internal/BUILD.bazel
@@ -39,6 +39,8 @@ RUNTIME_DEPS = [
"//packages/kbn-logging",
"@npm//moment",
"@npm//getos",
+ ### test dependencies
+ "//packages/core/elasticsearch/core-elasticsearch-client-server-mocks",
]
TYPES_DEPS = [
@@ -50,6 +52,7 @@ TYPES_DEPS = [
"@npm//@types/hapi__hapi",
"//packages/kbn-logging:npm_module_types",
"//packages/core/metrics/core-metrics-server:npm_module_types",
+ "//packages/core/elasticsearch/core-elasticsearch-client-server-internal:npm_module_types",
]
jsts_transpiler(
diff --git a/packages/core/metrics/core-metrics-collectors-server-internal/index.ts b/packages/core/metrics/core-metrics-collectors-server-internal/index.ts
index a4639202353e1..351129cdc8ba3 100644
--- a/packages/core/metrics/core-metrics-collectors-server-internal/index.ts
+++ b/packages/core/metrics/core-metrics-collectors-server-internal/index.ts
@@ -11,3 +11,4 @@ export type { OpsMetricsCollectorOptions } from './src/os';
export { ProcessMetricsCollector } from './src/process';
export { ServerMetricsCollector } from './src/server';
export { EventLoopDelaysMonitor } from './src/event_loop_delays_monitor';
+export { ElasticsearchClientsMetricsCollector } from './src/elasticsearch_client';
diff --git a/packages/core/metrics/core-metrics-collectors-server-internal/src/elasticsearch_client.test.ts b/packages/core/metrics/core-metrics-collectors-server-internal/src/elasticsearch_client.test.ts
new file mode 100644
index 0000000000000..363fca6430dbe
--- /dev/null
+++ b/packages/core/metrics/core-metrics-collectors-server-internal/src/elasticsearch_client.test.ts
@@ -0,0 +1,33 @@
+/*
+ * 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 { Agent as HttpAgent } from 'http';
+import { Agent as HttpsAgent } from 'https';
+import { sampleEsClientMetrics } from '@kbn/core-metrics-server-mocks';
+import { createAgentStoreMock } from '@kbn/core-elasticsearch-client-server-mocks';
+import { getAgentsSocketsStatsMock } from './get_agents_sockets_stats.test.mocks';
+import { ElasticsearchClientsMetricsCollector } from './elasticsearch_client';
+import { getAgentsSocketsStats } from './get_agents_sockets_stats';
+
+jest.mock('@kbn/core-elasticsearch-client-server-internal');
+
+describe('ElasticsearchClientsMetricsCollector', () => {
+ test('#collect calls getAgentsSocketsStats with the Agents managed by the provided AgentManager', async () => {
+ const agents = new Set([new HttpAgent(), new HttpsAgent()]);
+ const agentStore = createAgentStoreMock(agents);
+ getAgentsSocketsStatsMock.mockReturnValueOnce(sampleEsClientMetrics);
+
+ const esClientsMetricsCollector = new ElasticsearchClientsMetricsCollector(agentStore);
+ const metrics = await esClientsMetricsCollector.collect();
+
+ expect(agentStore.getAgents).toHaveBeenCalledTimes(1);
+ expect(getAgentsSocketsStats).toHaveBeenCalledTimes(1);
+ expect(getAgentsSocketsStats).toHaveBeenNthCalledWith(1, agents);
+ expect(metrics).toEqual(sampleEsClientMetrics);
+ });
+});
diff --git a/packages/core/metrics/core-metrics-collectors-server-internal/src/elasticsearch_client.ts b/packages/core/metrics/core-metrics-collectors-server-internal/src/elasticsearch_client.ts
new file mode 100644
index 0000000000000..278fd0218f8c0
--- /dev/null
+++ b/packages/core/metrics/core-metrics-collectors-server-internal/src/elasticsearch_client.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 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 { ElasticsearchClientsMetrics, MetricsCollector } from '@kbn/core-metrics-server';
+import type { AgentStore } from '@kbn/core-elasticsearch-client-server-internal';
+import { getAgentsSocketsStats } from './get_agents_sockets_stats';
+
+export class ElasticsearchClientsMetricsCollector
+ implements MetricsCollector
+{
+ constructor(private readonly agentStore: AgentStore) {}
+
+ public async collect(): Promise {
+ return await getAgentsSocketsStats(this.agentStore.getAgents());
+ }
+
+ public reset() {
+ // we do not have a state in this Collector, aka metrics are not accumulated over time.
+ // Thus, we don't need to perform any cleanup to reset the collected metrics
+ }
+}
diff --git a/packages/core/metrics/core-metrics-collectors-server-internal/src/get_agents_sockets_stats.test.mocks.ts b/packages/core/metrics/core-metrics-collectors-server-internal/src/get_agents_sockets_stats.test.mocks.ts
new file mode 100644
index 0000000000000..4e9688ccc91b9
--- /dev/null
+++ b/packages/core/metrics/core-metrics-collectors-server-internal/src/get_agents_sockets_stats.test.mocks.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 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 { Agent as HttpAgent } from 'http';
+import { Agent as HttpsAgent } from 'https';
+
+import { getAgentsSocketsStats } from './get_agents_sockets_stats';
+
+export const getHttpAgentMock = (overrides: Partial) => {
+ return Object.assign(new HttpAgent(), overrides);
+};
+
+export const getHttpsAgentMock = (overrides: Partial) => {
+ return Object.assign(new HttpsAgent(), overrides);
+};
+
+export const getAgentsSocketsStatsMock: jest.MockedFunction =
+ jest.fn();
+
+jest.doMock('./get_agents_sockets_stats', () => {
+ return {
+ getAgentsSocketsStats: getAgentsSocketsStatsMock,
+ };
+});
diff --git a/packages/core/metrics/core-metrics-collectors-server-internal/src/get_agents_sockets_stats.test.ts b/packages/core/metrics/core-metrics-collectors-server-internal/src/get_agents_sockets_stats.test.ts
new file mode 100644
index 0000000000000..513bf2caa8545
--- /dev/null
+++ b/packages/core/metrics/core-metrics-collectors-server-internal/src/get_agents_sockets_stats.test.ts
@@ -0,0 +1,147 @@
+/*
+ * 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 { Socket } from 'net';
+import { Agent, IncomingMessage } from 'http';
+import { getAgentsSocketsStats } from './get_agents_sockets_stats';
+import { getHttpAgentMock, getHttpsAgentMock } from './get_agents_sockets_stats.test.mocks';
+
+jest.mock('net');
+
+const mockSocket = new Socket();
+const mockIncomingMessage = new IncomingMessage(mockSocket);
+
+describe('getAgentsSocketsStats()', () => {
+ it('extracts aggregated stats from the specified agents', () => {
+ const agent1 = getHttpAgentMock({
+ sockets: {
+ node1: [mockSocket, mockSocket, mockSocket],
+ node2: [mockSocket],
+ },
+ freeSockets: {
+ node1: [mockSocket],
+ node3: [mockSocket, mockSocket, mockSocket, mockSocket],
+ },
+ requests: {
+ node1: [mockIncomingMessage, mockIncomingMessage],
+ },
+ });
+
+ const agent2 = getHttpAgentMock({
+ sockets: {
+ node1: [mockSocket, mockSocket, mockSocket],
+ node4: [mockSocket],
+ },
+ freeSockets: {
+ node3: [mockSocket, mockSocket, mockSocket, mockSocket],
+ },
+ requests: {
+ node4: [mockIncomingMessage, mockIncomingMessage, mockIncomingMessage, mockIncomingMessage],
+ },
+ });
+
+ const stats = getAgentsSocketsStats(new Set([agent1, agent2]));
+ expect(stats).toEqual({
+ averageActiveSocketsPerNode: 2.6666666666666665,
+ averageIdleSocketsPerNode: 4.5,
+ connectedNodes: 4,
+ mostActiveNodeSockets: 6,
+ mostIdleNodeSockets: 8,
+ nodesWithActiveSockets: 3,
+ nodesWithIdleSockets: 2,
+ protocol: 'http',
+ totalActiveSockets: 8,
+ totalIdleSockets: 9,
+ totalQueuedRequests: 6,
+ });
+ });
+
+ it('takes into account Agent types to determine the `protocol`', () => {
+ const httpAgent = getHttpAgentMock({
+ sockets: { node1: [mockSocket] },
+ freeSockets: {},
+ requests: {},
+ });
+
+ const httpsAgent = getHttpsAgentMock({
+ sockets: { node1: [mockSocket] },
+ freeSockets: {},
+ requests: {},
+ });
+
+ const noAgents = new Set();
+ const httpAgents = new Set([httpAgent, httpAgent]);
+ const httpsAgents = new Set([httpsAgent, httpsAgent]);
+ const mixedAgents = new Set([httpAgent, httpsAgent]);
+
+ expect(getAgentsSocketsStats(noAgents).protocol).toEqual('none');
+ expect(getAgentsSocketsStats(httpAgents).protocol).toEqual('http');
+ expect(getAgentsSocketsStats(httpsAgents).protocol).toEqual('https');
+ expect(getAgentsSocketsStats(mixedAgents).protocol).toEqual('mixed');
+ });
+
+ it('does not take into account those Agents that have not had any connection to any node', () => {
+ const pristineAgentProps = {
+ sockets: {},
+ freeSockets: {},
+ requests: {},
+ };
+ const agent1 = getHttpAgentMock(pristineAgentProps);
+ const agent2 = getHttpAgentMock(pristineAgentProps);
+ const agent3 = getHttpAgentMock(pristineAgentProps);
+
+ const stats = getAgentsSocketsStats(new Set([agent1, agent2, agent3]));
+
+ expect(stats).toEqual({
+ averageActiveSocketsPerNode: 0,
+ averageIdleSocketsPerNode: 0,
+ connectedNodes: 0,
+ mostActiveNodeSockets: 0,
+ mostIdleNodeSockets: 0,
+ nodesWithActiveSockets: 0,
+ nodesWithIdleSockets: 0,
+ protocol: 'none',
+ totalActiveSockets: 0,
+ totalIdleSockets: 0,
+ totalQueuedRequests: 0,
+ });
+ });
+
+ it('takes into account those Agents that have hold mappings to one or more nodes, but that do not currently have any pending requests, active connections or idle connections', () => {
+ const emptyAgentProps = {
+ sockets: {
+ node1: [],
+ },
+ freeSockets: {
+ node2: [],
+ },
+ requests: {
+ node3: [],
+ },
+ };
+
+ const agent1 = getHttpAgentMock(emptyAgentProps);
+ const agent2 = getHttpAgentMock(emptyAgentProps);
+
+ const stats = getAgentsSocketsStats(new Set([agent1, agent2]));
+
+ expect(stats).toEqual({
+ averageActiveSocketsPerNode: 0,
+ averageIdleSocketsPerNode: 0,
+ connectedNodes: 3,
+ mostActiveNodeSockets: 0,
+ mostIdleNodeSockets: 0,
+ nodesWithActiveSockets: 0,
+ nodesWithIdleSockets: 0,
+ protocol: 'http',
+ totalActiveSockets: 0,
+ totalIdleSockets: 0,
+ totalQueuedRequests: 0,
+ });
+ });
+});
diff --git a/packages/core/metrics/core-metrics-collectors-server-internal/src/get_agents_sockets_stats.ts b/packages/core/metrics/core-metrics-collectors-server-internal/src/get_agents_sockets_stats.ts
new file mode 100644
index 0000000000000..e28c92a56a8a4
--- /dev/null
+++ b/packages/core/metrics/core-metrics-collectors-server-internal/src/get_agents_sockets_stats.ts
@@ -0,0 +1,81 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 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 { NetworkAgent } from '@kbn/core-elasticsearch-client-server-internal';
+import { Agent as HttpsAgent } from 'https';
+import { mean } from 'lodash';
+import type {
+ ElasticsearchClientProtocol,
+ ElasticsearchClientsMetrics,
+} from '@kbn/core-metrics-server';
+
+export const getAgentsSocketsStats = (agents: Set): ElasticsearchClientsMetrics => {
+ const nodes = new Set();
+ let totalActiveSockets = 0;
+ let totalIdleSockets = 0;
+ let totalQueuedRequests = 0;
+ let http: boolean = false;
+ let https: boolean = false;
+
+ const nodesWithActiveSockets: Record = {};
+ const nodesWithIdleSockets: Record = {};
+
+ agents.forEach((agent) => {
+ const agentRequests = Object.entries(agent.requests) ?? [];
+ const agentSockets = Object.entries(agent.sockets) ?? [];
+ const agentFreeSockets = Object.entries(agent.freeSockets) ?? [];
+
+ if (agentRequests.length || agentSockets.length || agentFreeSockets.length) {
+ if (agent instanceof HttpsAgent) https = true;
+ else http = true;
+
+ agentRequests.forEach(([node, queue]) => {
+ nodes.add(node);
+ totalQueuedRequests += queue?.length ?? 0;
+ });
+
+ agentSockets.forEach(([node, sockets]) => {
+ nodes.add(node);
+ const activeSockets = sockets?.length ?? 0;
+ totalActiveSockets += activeSockets;
+ nodesWithActiveSockets[node] = (nodesWithActiveSockets[node] ?? 0) + activeSockets;
+ });
+
+ agentFreeSockets.forEach(([node, freeSockets]) => {
+ nodes.add(node);
+ const idleSockets = freeSockets?.length ?? 0;
+ totalIdleSockets += idleSockets;
+ nodesWithIdleSockets[node] = (nodesWithIdleSockets[node] ?? 0) + idleSockets;
+ });
+ }
+ });
+
+ const activeSocketCounters = Object.values(nodesWithActiveSockets);
+ const idleSocketCounters = Object.values(nodesWithIdleSockets);
+ const protocol: ElasticsearchClientProtocol = http
+ ? https
+ ? 'mixed'
+ : 'http'
+ : https
+ ? 'https'
+ : 'none';
+
+ return {
+ protocol,
+ connectedNodes: nodes.size,
+ nodesWithActiveSockets: activeSocketCounters.filter(Boolean).length,
+ nodesWithIdleSockets: idleSocketCounters.filter(Boolean).length,
+ totalActiveSockets,
+ totalIdleSockets,
+ totalQueuedRequests,
+ mostActiveNodeSockets: activeSocketCounters.length ? Math.max(...activeSocketCounters) : 0,
+ averageActiveSocketsPerNode: activeSocketCounters.length ? mean(activeSocketCounters) : 0,
+ mostIdleNodeSockets: idleSocketCounters.length ? Math.max(...idleSocketCounters) : 0,
+ averageIdleSocketsPerNode: idleSocketCounters.length ? mean(idleSocketCounters) : 0,
+ };
+};
diff --git a/packages/core/metrics/core-metrics-server-internal/BUILD.bazel b/packages/core/metrics/core-metrics-server-internal/BUILD.bazel
index da7883016afd2..0a7f393ec0b31 100644
--- a/packages/core/metrics/core-metrics-server-internal/BUILD.bazel
+++ b/packages/core/metrics/core-metrics-server-internal/BUILD.bazel
@@ -37,11 +37,15 @@ NPM_MODULE_EXTRA_FILES = [
RUNTIME_DEPS = [
"@npm//rxjs",
"@npm//moment",
- "//packages/kbn-logging-mocks",
"//packages/kbn-config-schema",
- "//packages/core/http/core-http-server-mocks",
"//packages/core/metrics/core-metrics-collectors-server-internal",
+ "//packages/core/elasticsearch/core-elasticsearch-server-internal",
+ ### test dependencies
+ "//packages/kbn-logging-mocks",
+ "//packages/core/http/core-http-server-mocks",
+ "//packages/core/metrics/core-metrics-server-mocks",
"//packages/core/metrics/core-metrics-collectors-server-mocks",
+ "//packages/core/elasticsearch/core-elasticsearch-server-mocks",
]
@@ -57,6 +61,7 @@ TYPES_DEPS = [
"//packages/core/http/core-http-server-internal:npm_module_types",
"//packages/core/metrics/core-metrics-server:npm_module_types",
"//packages/core/metrics/core-metrics-collectors-server-internal:npm_module_types",
+ "//packages/core/elasticsearch/core-elasticsearch-server-internal:npm_module_types",
]
diff --git a/packages/core/metrics/core-metrics-server-internal/src/logging/get_ops_metrics_log.test.ts b/packages/core/metrics/core-metrics-server-internal/src/logging/get_ops_metrics_log.test.ts
index 8de7a5fa5dadf..d997433667e27 100644
--- a/packages/core/metrics/core-metrics-server-internal/src/logging/get_ops_metrics_log.test.ts
+++ b/packages/core/metrics/core-metrics-server-internal/src/logging/get_ops_metrics_log.test.ts
@@ -8,6 +8,7 @@
import type { OpsMetrics } from '@kbn/core-metrics-server';
import { getEcsOpsMetricsLog } from './get_ops_metrics_log';
+import { sampleEsClientMetrics } from '@kbn/core-metrics-server-mocks';
import { collectorMock } from '@kbn/core-metrics-collectors-server-mocks';
function createBaseOpsMetrics(): OpsMetrics {
@@ -24,6 +25,7 @@ function createBaseOpsMetrics(): OpsMetrics {
memory: { total_in_bytes: 1, free_in_bytes: 1, used_in_bytes: 1 },
uptime_in_millis: 1,
},
+ elasticsearch_client: sampleEsClientMetrics,
response_times: { avg_in_millis: 1, max_in_millis: 1 },
requests: { disconnects: 1, total: 1, statusCodes: { '200': 1 } },
concurrent_connections: 1,
diff --git a/packages/core/metrics/core-metrics-server-internal/src/metrics_service.test.ts b/packages/core/metrics/core-metrics-server-internal/src/metrics_service.test.ts
index de78b534b2dc7..351e2aca43f56 100644
--- a/packages/core/metrics/core-metrics-server-internal/src/metrics_service.test.ts
+++ b/packages/core/metrics/core-metrics-server-internal/src/metrics_service.test.ts
@@ -8,13 +8,15 @@
import moment from 'moment';
+import { take } from 'rxjs/operators';
import { configServiceMock } from '@kbn/config-mocks';
import { mockCoreContext } from '@kbn/core-base-server-mocks';
import { loggingSystemMock } from '@kbn/core-logging-server-mocks';
import { httpServiceMock } from '@kbn/core-http-server-mocks';
+import { elasticsearchServiceMock } from '@kbn/core-elasticsearch-server-mocks';
import { mockOpsCollector } from './metrics_service.test.mocks';
import { MetricsService } from './metrics_service';
-import { take } from 'rxjs/operators';
+import { OpsMetricsCollector } from './ops_metrics_collector';
const testInterval = 100;
@@ -24,6 +26,7 @@ const logger = loggingSystemMock.create();
describe('MetricsService', () => {
const httpMock = httpServiceMock.createInternalSetupContract();
+ const esServiceMock = elasticsearchServiceMock.createInternalSetup();
let metricsService: MetricsService;
beforeEach(() => {
@@ -43,9 +46,16 @@ describe('MetricsService', () => {
describe('#start', () => {
it('invokes setInterval with the configured interval', async () => {
- await metricsService.setup({ http: httpMock });
+ await metricsService.setup({ http: httpMock, elasticsearchService: esServiceMock });
await metricsService.start();
+ expect(OpsMetricsCollector).toHaveBeenCalledTimes(1);
+ expect(OpsMetricsCollector).toHaveBeenCalledWith(
+ httpMock.server,
+ esServiceMock.agentStore,
+ expect.objectContaining({ logger: logger.get('metrics') })
+ );
+
expect(setInterval).toHaveBeenCalledTimes(1);
expect(setInterval).toHaveBeenCalledWith(expect.any(Function), testInterval);
});
@@ -53,7 +63,7 @@ describe('MetricsService', () => {
it('collects the metrics at every interval', async () => {
mockOpsCollector.collect.mockResolvedValue(dummyMetrics);
- await metricsService.setup({ http: httpMock });
+ await metricsService.setup({ http: httpMock, elasticsearchService: esServiceMock });
await metricsService.start();
expect(mockOpsCollector.collect).toHaveBeenCalledTimes(1);
@@ -68,7 +78,7 @@ describe('MetricsService', () => {
it('resets the collector after each collection', async () => {
mockOpsCollector.collect.mockResolvedValue(dummyMetrics);
- await metricsService.setup({ http: httpMock });
+ await metricsService.setup({ http: httpMock, elasticsearchService: esServiceMock });
const { getOpsMetrics$ } = await metricsService.start();
// `advanceTimersByTime` only ensure the interval handler is executed
@@ -108,7 +118,7 @@ describe('MetricsService', () => {
.mockResolvedValueOnce(firstMetrics)
.mockResolvedValueOnce(secondMetrics);
- await metricsService.setup({ http: httpMock });
+ await metricsService.setup({ http: httpMock, elasticsearchService: esServiceMock });
const { getOpsMetrics$ } = await metricsService.start();
const nextEmission = async () => {
@@ -157,7 +167,7 @@ describe('MetricsService', () => {
mockOpsCollector.collect
.mockResolvedValueOnce(firstMetrics)
.mockResolvedValueOnce(secondMetrics);
- await metricsService.setup({ http: httpMock });
+ await metricsService.setup({ http: httpMock, elasticsearchService: esServiceMock });
const { getOpsMetrics$ } = await metricsService.start();
const nextEmission = async () => {
@@ -176,7 +186,7 @@ describe('MetricsService', () => {
it('omits metrics from log message if they are missing or malformed', async () => {
const opsLogger = logger.get('metrics', 'ops');
mockOpsCollector.collect.mockResolvedValueOnce({ secondMetrics: 'metrics' });
- await metricsService.setup({ http: httpMock });
+ await metricsService.setup({ http: httpMock, elasticsearchService: esServiceMock });
await metricsService.start();
expect(loggingSystemMock.collect(opsLogger).debug[0]).toMatchInlineSnapshot(`
Array [
@@ -219,7 +229,7 @@ describe('MetricsService', () => {
describe('#stop', () => {
it('stops the metrics interval', async () => {
- await metricsService.setup({ http: httpMock });
+ await metricsService.setup({ http: httpMock, elasticsearchService: esServiceMock });
const { getOpsMetrics$ } = await metricsService.start();
expect(mockOpsCollector.collect).toHaveBeenCalledTimes(1);
@@ -235,7 +245,7 @@ describe('MetricsService', () => {
});
it('completes the metrics observable', async () => {
- await metricsService.setup({ http: httpMock });
+ await metricsService.setup({ http: httpMock, elasticsearchService: esServiceMock });
const { getOpsMetrics$ } = await metricsService.start();
let completed = false;
diff --git a/packages/core/metrics/core-metrics-server-internal/src/metrics_service.ts b/packages/core/metrics/core-metrics-server-internal/src/metrics_service.ts
index 8a05b4b57843c..95a9dc09bba57 100644
--- a/packages/core/metrics/core-metrics-server-internal/src/metrics_service.ts
+++ b/packages/core/metrics/core-metrics-server-internal/src/metrics_service.ts
@@ -10,6 +10,7 @@ import { firstValueFrom, ReplaySubject } from 'rxjs';
import type { CoreContext, CoreService } from '@kbn/core-base-server-internal';
import type { Logger } from '@kbn/logging';
import type { InternalHttpServiceSetup } from '@kbn/core-http-server-internal';
+import type { InternalElasticsearchServiceSetup } from '@kbn/core-elasticsearch-server-internal';
import type {
OpsMetrics,
MetricsServiceSetup,
@@ -21,6 +22,7 @@ import { getEcsOpsMetricsLog } from './logging';
export interface MetricsServiceSetupDeps {
http: InternalHttpServiceSetup;
+ elasticsearchService: InternalElasticsearchServiceSetup;
}
/** @internal */
@@ -45,12 +47,15 @@ export class MetricsService
this.opsMetricsLogger = coreContext.logger.get('metrics', 'ops');
}
- public async setup({ http }: MetricsServiceSetupDeps): Promise {
+ public async setup({
+ http,
+ elasticsearchService,
+ }: MetricsServiceSetupDeps): Promise {
const config = await firstValueFrom(
this.coreContext.configService.atPath(OPS_CONFIG_PATH)
);
- this.metricsCollector = new OpsMetricsCollector(http.server, {
+ this.metricsCollector = new OpsMetricsCollector(http.server, elasticsearchService.agentStore, {
logger: this.logger,
...config.cGroupOverrides,
});
diff --git a/packages/core/metrics/core-metrics-server-internal/src/ops_metrics_collector.test.mocks.ts b/packages/core/metrics/core-metrics-server-internal/src/ops_metrics_collector.test.mocks.ts
index b96449fdc2f64..d70753b9f4644 100644
--- a/packages/core/metrics/core-metrics-server-internal/src/ops_metrics_collector.test.mocks.ts
+++ b/packages/core/metrics/core-metrics-server-internal/src/ops_metrics_collector.test.mocks.ts
@@ -11,11 +11,13 @@ import { collectorMock } from '@kbn/core-metrics-collectors-server-mocks';
export const mockOsCollector = collectorMock.create();
export const mockProcessCollector = collectorMock.create();
export const mockServerCollector = collectorMock.create();
+export const mockEsClientCollector = collectorMock.create();
jest.doMock('@kbn/core-metrics-collectors-server-internal', () => {
return {
OsMetricsCollector: jest.fn().mockImplementation(() => mockOsCollector),
ProcessMetricsCollector: jest.fn().mockImplementation(() => mockProcessCollector),
ServerMetricsCollector: jest.fn().mockImplementation(() => mockServerCollector),
+ ElasticsearchClientsMetricsCollector: jest.fn().mockImplementation(() => mockEsClientCollector),
};
});
diff --git a/packages/core/metrics/core-metrics-server-internal/src/ops_metrics_collector.test.ts b/packages/core/metrics/core-metrics-server-internal/src/ops_metrics_collector.test.ts
index cd80c35b37f86..87011a663404f 100644
--- a/packages/core/metrics/core-metrics-server-internal/src/ops_metrics_collector.test.ts
+++ b/packages/core/metrics/core-metrics-server-internal/src/ops_metrics_collector.test.ts
@@ -8,7 +8,10 @@
import { loggerMock } from '@kbn/logging-mocks';
import { httpServiceMock } from '@kbn/core-http-server-mocks';
+import { sampleEsClientMetrics } from '@kbn/core-metrics-server-mocks';
+import { AgentManager } from '@kbn/core-elasticsearch-client-server-internal';
import {
+ mockEsClientCollector,
mockOsCollector,
mockProcessCollector,
mockServerCollector,
@@ -20,7 +23,8 @@ describe('OpsMetricsCollector', () => {
beforeEach(() => {
const hapiServer = httpServiceMock.createInternalSetupContract().server;
- collector = new OpsMetricsCollector(hapiServer, { logger: loggerMock.create() });
+ const agentManager = new AgentManager();
+ collector = new OpsMetricsCollector(hapiServer, agentManager, { logger: loggerMock.create() });
mockOsCollector.collect.mockResolvedValue('osMetrics');
});
@@ -33,12 +37,14 @@ describe('OpsMetricsCollector', () => {
requests: 'serverRequestsMetrics',
response_times: 'serverTimingMetrics',
});
+ mockEsClientCollector.collect.mockResolvedValue(sampleEsClientMetrics);
const metrics = await collector.collect();
expect(mockOsCollector.collect).toHaveBeenCalledTimes(1);
expect(mockProcessCollector.collect).toHaveBeenCalledTimes(1);
expect(mockServerCollector.collect).toHaveBeenCalledTimes(1);
+ expect(mockEsClientCollector.collect).toHaveBeenCalledTimes(1);
expect(metrics).toEqual({
collected_at: expect.any(Date),
@@ -47,6 +53,7 @@ describe('OpsMetricsCollector', () => {
os: 'osMetrics',
requests: 'serverRequestsMetrics',
response_times: 'serverTimingMetrics',
+ elasticsearch_client: sampleEsClientMetrics,
});
});
});
diff --git a/packages/core/metrics/core-metrics-server-internal/src/ops_metrics_collector.ts b/packages/core/metrics/core-metrics-server-internal/src/ops_metrics_collector.ts
index 10958d93c2562..8a10f4071b11b 100644
--- a/packages/core/metrics/core-metrics-server-internal/src/ops_metrics_collector.ts
+++ b/packages/core/metrics/core-metrics-server-internal/src/ops_metrics_collector.ts
@@ -8,28 +8,33 @@
import { Server as HapiServer } from '@hapi/hapi';
import type { OpsMetrics, MetricsCollector } from '@kbn/core-metrics-server';
+import type { AgentStore } from '@kbn/core-elasticsearch-client-server-internal';
import {
ProcessMetricsCollector,
OsMetricsCollector,
type OpsMetricsCollectorOptions,
ServerMetricsCollector,
+ ElasticsearchClientsMetricsCollector,
} from '@kbn/core-metrics-collectors-server-internal';
export class OpsMetricsCollector implements MetricsCollector {
private readonly processCollector: ProcessMetricsCollector;
private readonly osCollector: OsMetricsCollector;
private readonly serverCollector: ServerMetricsCollector;
+ private readonly esClientCollector: ElasticsearchClientsMetricsCollector;
- constructor(server: HapiServer, opsOptions: OpsMetricsCollectorOptions) {
+ constructor(server: HapiServer, agentStore: AgentStore, opsOptions: OpsMetricsCollectorOptions) {
this.processCollector = new ProcessMetricsCollector();
this.osCollector = new OsMetricsCollector(opsOptions);
this.serverCollector = new ServerMetricsCollector(server);
+ this.esClientCollector = new ElasticsearchClientsMetricsCollector(agentStore);
}
public async collect(): Promise {
- const [processes, os, server] = await Promise.all([
+ const [processes, os, esClient, server] = await Promise.all([
this.processCollector.collect(),
this.osCollector.collect(),
+ this.esClientCollector.collect(),
this.serverCollector.collect(),
]);
@@ -43,6 +48,7 @@ export class OpsMetricsCollector implements MetricsCollector {
process: processes[0],
processes,
os,
+ elasticsearch_client: esClient,
...server,
};
}
diff --git a/packages/core/metrics/core-metrics-server-mocks/index.ts b/packages/core/metrics/core-metrics-server-mocks/index.ts
index d252b2253243e..02d13b8ed5ad8 100644
--- a/packages/core/metrics/core-metrics-server-mocks/index.ts
+++ b/packages/core/metrics/core-metrics-server-mocks/index.ts
@@ -6,4 +6,4 @@
* Side Public License, v 1.
*/
-export { metricsServiceMock } from './src/metrics_service.mock';
+export { metricsServiceMock, sampleEsClientMetrics } from './src/metrics_service.mock';
diff --git a/packages/core/metrics/core-metrics-server-mocks/src/metrics_service.mock.ts b/packages/core/metrics/core-metrics-server-mocks/src/metrics_service.mock.ts
index 6bbe176ce37e8..44601caeaa85c 100644
--- a/packages/core/metrics/core-metrics-server-mocks/src/metrics_service.mock.ts
+++ b/packages/core/metrics/core-metrics-server-mocks/src/metrics_service.mock.ts
@@ -17,7 +17,25 @@ import {
mocked as eventLoopDelaysMonitorMock,
collectorMock,
} from '@kbn/core-metrics-collectors-server-mocks';
-import type { MetricsServiceSetup, MetricsServiceStart } from '@kbn/core-metrics-server';
+import type {
+ ElasticsearchClientsMetrics,
+ MetricsServiceSetup,
+ MetricsServiceStart,
+} from '@kbn/core-metrics-server';
+
+export const sampleEsClientMetrics: ElasticsearchClientsMetrics = {
+ protocol: 'https',
+ connectedNodes: 3,
+ nodesWithActiveSockets: 3,
+ nodesWithIdleSockets: 1,
+ totalActiveSockets: 25,
+ totalIdleSockets: 2,
+ totalQueuedRequests: 0,
+ mostActiveNodeSockets: 15,
+ averageActiveSocketsPerNode: 8,
+ mostIdleNodeSockets: 2,
+ averageIdleSocketsPerNode: 0.5,
+};
const createInternalSetupContractMock = () => {
const setupContract: jest.Mocked = {
@@ -39,6 +57,7 @@ const createInternalSetupContractMock = () => {
memory: { total_in_bytes: 1, free_in_bytes: 1, used_in_bytes: 1 },
uptime_in_millis: 1,
},
+ elasticsearch_client: sampleEsClientMetrics,
response_times: { avg_in_millis: 1, max_in_millis: 1 },
requests: { disconnects: 1, total: 1, statusCodes: { '200': 1 } },
concurrent_connections: 1,
diff --git a/packages/core/metrics/core-metrics-server/index.ts b/packages/core/metrics/core-metrics-server/index.ts
index 51e0b7fe3d95d..49bd2a4251623 100644
--- a/packages/core/metrics/core-metrics-server/index.ts
+++ b/packages/core/metrics/core-metrics-server/index.ts
@@ -14,4 +14,6 @@ export type {
OpsProcessMetrics,
OpsOsMetrics,
OpsServerMetrics,
+ ElasticsearchClientProtocol,
+ ElasticsearchClientsMetrics,
} from './src/metrics';
diff --git a/packages/core/metrics/core-metrics-server/src/metrics.ts b/packages/core/metrics/core-metrics-server/src/metrics.ts
index dbfa643c8eccc..958f6b75f55e4 100644
--- a/packages/core/metrics/core-metrics-server/src/metrics.ts
+++ b/packages/core/metrics/core-metrics-server/src/metrics.ts
@@ -40,6 +40,44 @@ export interface IntervalHistogram {
};
}
+/**
+ * Protocol(s) used by the Elasticsearch Client
+ * @public
+ */
+
+export type ElasticsearchClientProtocol = 'none' | 'http' | 'https' | 'mixed';
+
+/**
+ * Metrics related to the elasticsearch clients
+ * @public
+ */
+export interface ElasticsearchClientsMetrics {
+ /** The protocol (or protocols) that these Agents are using */
+ protocol: ElasticsearchClientProtocol;
+ /** Number of ES nodes that ES-js client is connecting to */
+ connectedNodes: number;
+ /** Number of nodes with active connections */
+ nodesWithActiveSockets: number;
+ /** Number of nodes with available connections (alive but idle).
+ * Note that a node can have both active and idle connections at the same time
+ */
+ nodesWithIdleSockets: number;
+ /** Total number of active sockets (all nodes, all connections) */
+ totalActiveSockets: number;
+ /** Total number of available sockets (alive but idle, all nodes, all connections) */
+ totalIdleSockets: number;
+ /** Total number of queued requests (all nodes, all connections) */
+ totalQueuedRequests: number;
+ /** Number of active connections of the node with most active connections */
+ mostActiveNodeSockets: number;
+ /** Average of active sockets per node (all connections) */
+ averageActiveSocketsPerNode: number;
+ /** Number of idle connections of the node with most idle connections */
+ mostIdleNodeSockets: number;
+ /** Average of available (idle) sockets per node (all connections) */
+ averageIdleSocketsPerNode: number;
+}
+
/**
* Process related metrics
* @public
@@ -165,6 +203,10 @@ export interface OpsServerMetrics {
export interface OpsMetrics {
/** Time metrics were recorded at. */
collected_at: Date;
+ /**
+ * Metrics related to the elasticsearch client
+ */
+ elasticsearch_client: ElasticsearchClientsMetrics;
/**
* Process related metrics.
* @deprecated use the processes field instead.
diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/repository.test.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/repository.test.ts
index 0739c9acab8f5..d9a65f984c222 100644
--- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/repository.test.ts
+++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/repository.test.ts
@@ -4182,6 +4182,10 @@ describe('SavedObjectsRepository', () => {
type: 'foo',
id: '1',
},
+ hasNoReference: {
+ type: 'bar',
+ id: '1',
+ },
};
it(`passes mappings, registry, and search options to getSearchDsl`, async () => {
diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/repository.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/repository.ts
index 5569141c7fa0e..f48e031bd23c4 100644
--- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/repository.ts
+++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/repository.ts
@@ -1129,6 +1129,8 @@ export class SavedObjectsRepository implements ISavedObjectsRepository {
rootSearchFields,
hasReference,
hasReferenceOperator,
+ hasNoReference,
+ hasNoReferenceOperator,
page = FIND_DEFAULT_PAGE,
perPage = FIND_DEFAULT_PER_PAGE,
pit,
@@ -1235,6 +1237,8 @@ export class SavedObjectsRepository implements ISavedObjectsRepository {
typeToNamespacesMap,
hasReference,
hasReferenceOperator,
+ hasNoReference,
+ hasNoReferenceOperator,
kueryNode,
}),
},
diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/search_dsl/query_params.test.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/search_dsl/query_params.test.ts
index c502665468e6c..20ce3a2f46b2e 100644
--- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/search_dsl/query_params.test.ts
+++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/search_dsl/query_params.test.ts
@@ -195,17 +195,18 @@ describe('#getQueryParams', () => {
});
});
- describe('reference filter clause', () => {
- describe('`hasReference` parameter', () => {
- it('does not call `getReferencesFilter` when `hasReference` is not specified', () => {
- getQueryParams({
- registry,
- hasReference: undefined,
- });
-
- expect(getReferencesFilterMock).not.toHaveBeenCalled();
+ describe('reference/noreference filter clause', () => {
+ it('does not call `getReferencesFilter` when neither `hasReference` nor `hasNoReference` are specified', () => {
+ getQueryParams({
+ registry,
+ hasReference: undefined,
+ hasNoReference: undefined,
});
+ expect(getReferencesFilterMock).not.toHaveBeenCalled();
+ });
+
+ describe('`hasReference` parameter', () => {
it('calls `getReferencesFilter` with the correct parameters', () => {
const hasReference = { id: 'foo', type: 'bar' };
getQueryParams({
@@ -235,6 +236,38 @@ describe('#getQueryParams', () => {
expect(filters.some((filter) => filter.references_filter === true)).toBeDefined();
});
});
+
+ describe('`hasNoReference` parameter', () => {
+ it('calls `getReferencesFilter` with the correct parameters', () => {
+ const hasNoReference = { id: 'noFoo', type: 'bar' };
+ getQueryParams({
+ registry,
+ hasNoReference,
+ hasNoReferenceOperator: 'AND',
+ });
+
+ expect(getReferencesFilterMock).toHaveBeenCalledTimes(1);
+ expect(getReferencesFilterMock).toHaveBeenCalledWith({
+ must: false,
+ references: [hasNoReference],
+ operator: 'AND',
+ });
+ });
+
+ it('includes the return of `getReferencesFilter` in the `filter` clause', () => {
+ getReferencesFilterMock.mockReturnValue({ references_filter: true });
+
+ const hasNoReference = { id: 'noFoo', type: 'bar' };
+ const result = getQueryParams({
+ registry,
+ hasNoReference,
+ hasReferenceOperator: 'AND',
+ });
+
+ const filters: any[] = result.query.bool.filter;
+ expect(filters.some((filter) => filter.references_filter === true)).toBeDefined();
+ });
+ });
});
describe('type filter clauses', () => {
diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/search_dsl/query_params.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/search_dsl/query_params.ts
index 669f2a273569b..896b934c90b80 100644
--- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/search_dsl/query_params.ts
+++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/search_dsl/query_params.ts
@@ -7,6 +7,7 @@
*/
import * as esKuery from '@kbn/es-query';
+import type { SavedObjectTypeIdTuple } from '@kbn/core-saved-objects-common';
type KueryNode = any;
@@ -123,11 +124,6 @@ function getClauseForType(
};
}
-export interface HasReferenceQueryParams {
- type: string;
- id: string;
-}
-
export type SearchOperator = 'AND' | 'OR';
interface QueryParams {
@@ -139,8 +135,10 @@ interface QueryParams {
defaultSearchOperator?: SearchOperator;
searchFields?: string[];
rootSearchFields?: string[];
- hasReference?: HasReferenceQueryParams | HasReferenceQueryParams[];
+ hasReference?: SavedObjectTypeIdTuple | SavedObjectTypeIdTuple[];
hasReferenceOperator?: SearchOperator;
+ hasNoReference?: SavedObjectTypeIdTuple | SavedObjectTypeIdTuple[];
+ hasNoReferenceOperator?: SearchOperator;
kueryNode?: KueryNode;
}
@@ -148,6 +146,13 @@ interface QueryParams {
const uniqNamespaces = (namespacesToNormalize?: string[]) =>
namespacesToNormalize ? Array.from(new Set(namespacesToNormalize)) : undefined;
+const toArray = (val: unknown) => {
+ if (typeof val === 'undefined') {
+ return val;
+ }
+ return !Array.isArray(val) ? [val] : val;
+};
+
/**
* Get the "query" related keys for the search body
*/
@@ -162,6 +167,8 @@ export function getQueryParams({
defaultSearchOperator,
hasReference,
hasReferenceOperator,
+ hasNoReference,
+ hasNoReferenceOperator,
kueryNode,
}: QueryParams) {
const types = getTypes(
@@ -169,9 +176,8 @@ export function getQueryParams({
typeToNamespacesMap ? Array.from(typeToNamespacesMap.keys()) : type
);
- if (hasReference && !Array.isArray(hasReference)) {
- hasReference = [hasReference];
- }
+ hasReference = toArray(hasReference);
+ hasNoReference = toArray(hasNoReference);
const bool: any = {
filter: [
@@ -184,6 +190,15 @@ export function getQueryParams({
}),
]
: []),
+ ...(hasNoReference?.length
+ ? [
+ getReferencesFilter({
+ references: hasNoReference,
+ operator: hasNoReferenceOperator,
+ must: false,
+ }),
+ ]
+ : []),
{
bool: {
should: types.map((shouldType) => {
diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/search_dsl/references_filter.test.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/search_dsl/references_filter.test.ts
index 9a042579c8e8f..127f3a94edd21 100644
--- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/search_dsl/references_filter.test.ts
+++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/search_dsl/references_filter.test.ts
@@ -20,29 +20,141 @@ describe('getReferencesFilter', () => {
},
});
- describe('when using the `OR` operator', () => {
- it('generates one `should` clause per type of reference', () => {
+ describe('for "must" match clauses', () => {
+ describe('when using the `OR` operator', () => {
+ it('generates one `should` clause per type of reference', () => {
+ const references = [
+ { type: 'foo', id: 'foo-1' },
+ { type: 'foo', id: 'foo-2' },
+ { type: 'foo', id: 'foo-3' },
+ { type: 'bar', id: 'bar-1' },
+ { type: 'bar', id: 'bar-2' },
+ ];
+ const clause = getReferencesFilter({
+ references,
+ operator: 'OR',
+ });
+
+ expect(clause).toEqual({
+ bool: {
+ should: [
+ nestedRefMustClauses([
+ { terms: { 'references.id': ['foo-1', 'foo-2', 'foo-3'] } },
+ { term: { 'references.type': 'foo' } },
+ ]),
+ nestedRefMustClauses([
+ { terms: { 'references.id': ['bar-1', 'bar-2'] } },
+ { term: { 'references.type': 'bar' } },
+ ]),
+ ],
+ minimum_should_match: 1,
+ },
+ });
+ });
+
+ it('does not include more than `maxTermsPerClause` per `terms` clauses', () => {
+ const references = [
+ { type: 'foo', id: 'foo-1' },
+ { type: 'foo', id: 'foo-2' },
+ { type: 'foo', id: 'foo-3' },
+ { type: 'foo', id: 'foo-4' },
+ { type: 'foo', id: 'foo-5' },
+ { type: 'bar', id: 'bar-1' },
+ { type: 'bar', id: 'bar-2' },
+ { type: 'bar', id: 'bar-3' },
+ { type: 'dolly', id: 'dolly-1' },
+ ];
+ const clause = getReferencesFilter({
+ references,
+ operator: 'OR',
+ maxTermsPerClause: 2,
+ });
+
+ expect(clause).toEqual({
+ bool: {
+ should: [
+ nestedRefMustClauses([
+ { terms: { 'references.id': ['foo-1', 'foo-2'] } },
+ { term: { 'references.type': 'foo' } },
+ ]),
+ nestedRefMustClauses([
+ { terms: { 'references.id': ['foo-3', 'foo-4'] } },
+ { term: { 'references.type': 'foo' } },
+ ]),
+ nestedRefMustClauses([
+ { terms: { 'references.id': ['foo-5'] } },
+ { term: { 'references.type': 'foo' } },
+ ]),
+ nestedRefMustClauses([
+ { terms: { 'references.id': ['bar-1', 'bar-2'] } },
+ { term: { 'references.type': 'bar' } },
+ ]),
+ nestedRefMustClauses([
+ { terms: { 'references.id': ['bar-3'] } },
+ { term: { 'references.type': 'bar' } },
+ ]),
+ nestedRefMustClauses([
+ { terms: { 'references.id': ['dolly-1'] } },
+ { term: { 'references.type': 'dolly' } },
+ ]),
+ ],
+ minimum_should_match: 1,
+ },
+ });
+ });
+ });
+
+ describe('when using the `AND` operator', () => {
+ it('generates one `must` clause per reference', () => {
+ const references = [
+ { type: 'foo', id: 'foo-1' },
+ { type: 'foo', id: 'foo-2' },
+ { type: 'bar', id: 'bar-1' },
+ ];
+
+ const clause = getReferencesFilter({
+ references,
+ operator: 'AND',
+ });
+
+ expect(clause).toEqual({
+ bool: {
+ must: references.map((ref) => ({
+ nested: {
+ path: 'references',
+ query: {
+ bool: {
+ must: [
+ { term: { 'references.id': ref.id } },
+ { term: { 'references.type': ref.type } },
+ ],
+ },
+ },
+ },
+ })),
+ },
+ });
+ });
+ });
+
+ it('defaults to using the `OR` operator', () => {
const references = [
{ type: 'foo', id: 'foo-1' },
- { type: 'foo', id: 'foo-2' },
- { type: 'foo', id: 'foo-3' },
{ type: 'bar', id: 'bar-1' },
- { type: 'bar', id: 'bar-2' },
];
const clause = getReferencesFilter({
references,
- operator: 'OR',
});
expect(clause).toEqual({
bool: {
should: [
nestedRefMustClauses([
- { terms: { 'references.id': ['foo-1', 'foo-2', 'foo-3'] } },
+ { terms: { 'references.id': ['foo-1'] } },
{ term: { 'references.type': 'foo' } },
]),
nestedRefMustClauses([
- { terms: { 'references.id': ['bar-1', 'bar-2'] } },
+ { terms: { 'references.id': ['bar-1'] } },
{ term: { 'references.type': 'bar' } },
]),
],
@@ -50,115 +162,156 @@ describe('getReferencesFilter', () => {
},
});
});
+ });
+
+ describe('for "must_not" match clauses', () => {
+ describe('when using the `OR` operator', () => {
+ it('generates one `must_not` clause per type of reference', () => {
+ const references = [
+ { type: 'foo', id: 'foo-1' },
+ { type: 'foo', id: 'foo-2' },
+ { type: 'foo', id: 'foo-3' },
+ { type: 'bar', id: 'bar-1' },
+ { type: 'bar', id: 'bar-2' },
+ ];
+ const clause = getReferencesFilter({
+ references,
+ operator: 'OR',
+ must: false,
+ });
+
+ expect(clause).toEqual({
+ bool: {
+ must_not: [
+ nestedRefMustClauses([
+ { terms: { 'references.id': ['foo-1', 'foo-2', 'foo-3'] } },
+ { term: { 'references.type': 'foo' } },
+ ]),
+ nestedRefMustClauses([
+ { terms: { 'references.id': ['bar-1', 'bar-2'] } },
+ { term: { 'references.type': 'bar' } },
+ ]),
+ ],
+ },
+ });
+ });
+
+ it('does not include more than `maxTermsPerClause` per `terms` clauses', () => {
+ const references = [
+ { type: 'foo', id: 'foo-1' },
+ { type: 'foo', id: 'foo-2' },
+ { type: 'foo', id: 'foo-3' },
+ { type: 'foo', id: 'foo-4' },
+ { type: 'foo', id: 'foo-5' },
+ { type: 'bar', id: 'bar-1' },
+ { type: 'bar', id: 'bar-2' },
+ { type: 'bar', id: 'bar-3' },
+ { type: 'dolly', id: 'dolly-1' },
+ ];
+ const clause = getReferencesFilter({
+ references,
+ operator: 'OR',
+ maxTermsPerClause: 2,
+ must: false,
+ });
+
+ expect(clause).toEqual({
+ bool: {
+ must_not: [
+ nestedRefMustClauses([
+ { terms: { 'references.id': ['foo-1', 'foo-2'] } },
+ { term: { 'references.type': 'foo' } },
+ ]),
+ nestedRefMustClauses([
+ { terms: { 'references.id': ['foo-3', 'foo-4'] } },
+ { term: { 'references.type': 'foo' } },
+ ]),
+ nestedRefMustClauses([
+ { terms: { 'references.id': ['foo-5'] } },
+ { term: { 'references.type': 'foo' } },
+ ]),
+ nestedRefMustClauses([
+ { terms: { 'references.id': ['bar-1', 'bar-2'] } },
+ { term: { 'references.type': 'bar' } },
+ ]),
+ nestedRefMustClauses([
+ { terms: { 'references.id': ['bar-3'] } },
+ { term: { 'references.type': 'bar' } },
+ ]),
+ nestedRefMustClauses([
+ { terms: { 'references.id': ['dolly-1'] } },
+ { term: { 'references.type': 'dolly' } },
+ ]),
+ ],
+ },
+ });
+ });
+ });
- it('does not include mode than `maxTermsPerClause` per `terms` clauses', () => {
+ describe('when using the `AND` operator', () => {
+ it('generates one `must` clause per reference', () => {
+ const references = [
+ { type: 'foo', id: 'foo-1' },
+ { type: 'foo', id: 'foo-2' },
+ { type: 'bar', id: 'bar-1' },
+ ];
+
+ const clause = getReferencesFilter({
+ references,
+ operator: 'AND',
+ must: false,
+ });
+
+ expect(clause).toEqual({
+ bool: {
+ must_not: [
+ {
+ bool: {
+ must: references.map((ref) => ({
+ nested: {
+ path: 'references',
+ query: {
+ bool: {
+ must: [
+ { term: { 'references.id': ref.id } },
+ { term: { 'references.type': ref.type } },
+ ],
+ },
+ },
+ },
+ })),
+ },
+ },
+ ],
+ },
+ });
+ });
+ });
+
+ it('defaults to using the `OR` operator', () => {
const references = [
{ type: 'foo', id: 'foo-1' },
- { type: 'foo', id: 'foo-2' },
- { type: 'foo', id: 'foo-3' },
- { type: 'foo', id: 'foo-4' },
- { type: 'foo', id: 'foo-5' },
{ type: 'bar', id: 'bar-1' },
- { type: 'bar', id: 'bar-2' },
- { type: 'bar', id: 'bar-3' },
- { type: 'dolly', id: 'dolly-1' },
];
const clause = getReferencesFilter({
references,
- operator: 'OR',
- maxTermsPerClause: 2,
+ must: false,
});
expect(clause).toEqual({
bool: {
- should: [
- nestedRefMustClauses([
- { terms: { 'references.id': ['foo-1', 'foo-2'] } },
- { term: { 'references.type': 'foo' } },
- ]),
+ must_not: [
nestedRefMustClauses([
- { terms: { 'references.id': ['foo-3', 'foo-4'] } },
+ { terms: { 'references.id': ['foo-1'] } },
{ term: { 'references.type': 'foo' } },
]),
nestedRefMustClauses([
- { terms: { 'references.id': ['foo-5'] } },
- { term: { 'references.type': 'foo' } },
- ]),
- nestedRefMustClauses([
- { terms: { 'references.id': ['bar-1', 'bar-2'] } },
+ { terms: { 'references.id': ['bar-1'] } },
{ term: { 'references.type': 'bar' } },
]),
- nestedRefMustClauses([
- { terms: { 'references.id': ['bar-3'] } },
- { term: { 'references.type': 'bar' } },
- ]),
- nestedRefMustClauses([
- { terms: { 'references.id': ['dolly-1'] } },
- { term: { 'references.type': 'dolly' } },
- ]),
],
- minimum_should_match: 1,
- },
- });
- });
- });
-
- describe('when using the `AND` operator', () => {
- it('generates one `must` clause per reference', () => {
- const references = [
- { type: 'foo', id: 'foo-1' },
- { type: 'foo', id: 'foo-2' },
- { type: 'bar', id: 'bar-1' },
- ];
-
- const clause = getReferencesFilter({
- references,
- operator: 'AND',
- });
-
- expect(clause).toEqual({
- bool: {
- must: references.map((ref) => ({
- nested: {
- path: 'references',
- query: {
- bool: {
- must: [
- { term: { 'references.id': ref.id } },
- { term: { 'references.type': ref.type } },
- ],
- },
- },
- },
- })),
},
});
});
});
-
- it('defaults to using the `OR` operator', () => {
- const references = [
- { type: 'foo', id: 'foo-1' },
- { type: 'bar', id: 'bar-1' },
- ];
- const clause = getReferencesFilter({
- references,
- });
-
- expect(clause).toEqual({
- bool: {
- should: [
- nestedRefMustClauses([
- { terms: { 'references.id': ['foo-1'] } },
- { term: { 'references.type': 'foo' } },
- ]),
- nestedRefMustClauses([
- { terms: { 'references.id': ['bar-1'] } },
- { term: { 'references.type': 'bar' } },
- ]),
- ],
- minimum_should_match: 1,
- },
- });
- });
});
diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/search_dsl/references_filter.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/search_dsl/references_filter.ts
index b0849560d2e43..4dd6bc640f174 100644
--- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/search_dsl/references_filter.ts
+++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/search_dsl/references_filter.ts
@@ -6,35 +6,61 @@
* Side Public License, v 1.
*/
-import type { HasReferenceQueryParams, SearchOperator } from './query_params';
+import type { SavedObjectTypeIdTuple } from '@kbn/core-saved-objects-common';
+
+import type { SearchOperator } from './query_params';
export function getReferencesFilter({
references,
operator = 'OR',
maxTermsPerClause = 1000,
+ must = true,
}: {
- references: HasReferenceQueryParams[];
+ references: SavedObjectTypeIdTuple[];
operator?: SearchOperator;
maxTermsPerClause?: number;
+ must?: boolean;
}) {
if (operator === 'AND') {
+ if (must) {
+ return {
+ bool: {
+ must: references.map(getNestedTermClauseForReference),
+ },
+ };
+ }
+
return {
bool: {
- must: references.map(getNestedTermClauseForReference),
+ must_not: [
+ {
+ bool: {
+ must: references.map(getNestedTermClauseForReference),
+ },
+ },
+ ],
},
};
} else {
+ if (must) {
+ return {
+ bool: {
+ should: getAggregatedTermsClauses(references, maxTermsPerClause),
+ minimum_should_match: 1,
+ },
+ };
+ }
+
return {
bool: {
- should: getAggregatedTermsClauses(references, maxTermsPerClause),
- minimum_should_match: 1,
+ must_not: getAggregatedTermsClauses(references, maxTermsPerClause),
},
};
}
}
const getAggregatedTermsClauses = (
- references: HasReferenceQueryParams[],
+ references: SavedObjectTypeIdTuple[],
maxTermsPerClause: number
) => {
const refTypeToIds = references.reduce((map, { type, id }) => {
@@ -58,7 +84,7 @@ const createChunks = (array: T[], chunkSize: number): T[][] => {
return chunks;
};
-export const getNestedTermClauseForReference = (reference: HasReferenceQueryParams) => {
+export const getNestedTermClauseForReference = (reference: SavedObjectTypeIdTuple) => {
return {
nested: {
path: 'references',
diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/search_dsl/search_dsl.test.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/search_dsl/search_dsl.test.ts
index d1ed7251b2414..84ef7c232d775 100644
--- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/search_dsl/search_dsl.test.ts
+++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/search_dsl/search_dsl.test.ts
@@ -49,7 +49,7 @@ describe('getSearchDsl', () => {
});
describe('passes control', () => {
- it('passes (mappings, schema, namespaces, type, typeToNamespacesMap, search, searchFields, rootSearchFields, hasReference, hasReferenceOperator) to getQueryParams', () => {
+ it('passes (mappings, schema, namespaces, type, typeToNamespacesMap, search, searchFields, rootSearchFields, hasReference, hasReferenceOperator, hasNoReference, hasNoReferenceOperator) to getQueryParams', () => {
const opts = {
namespaces: ['foo-namespace'],
type: 'foo',
@@ -63,6 +63,11 @@ describe('getSearchDsl', () => {
id: '1',
},
hasReferenceOperator: 'AND' as queryParamsNS.SearchOperator,
+ hasNoReference: {
+ type: 'noBar',
+ id: '1',
+ },
+ hasNoReferenceOperator: 'AND' as queryParamsNS.SearchOperator,
};
getSearchDsl(mappings, registry, opts);
@@ -78,6 +83,8 @@ describe('getSearchDsl', () => {
defaultSearchOperator: opts.defaultSearchOperator,
hasReference: opts.hasReference,
hasReferenceOperator: opts.hasReferenceOperator,
+ hasNoReference: opts.hasNoReference,
+ hasNoReferenceOperator: opts.hasNoReferenceOperator,
});
});
diff --git a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/search_dsl/search_dsl.ts b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/search_dsl/search_dsl.ts
index 980bf800755b9..381f20069d25a 100644
--- a/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/search_dsl/search_dsl.ts
+++ b/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/search_dsl/search_dsl.ts
@@ -12,7 +12,8 @@ import * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import type { SavedObjectsPitParams } from '@kbn/core-saved-objects-api-server';
import type { ISavedObjectTypeRegistry } from '@kbn/core-saved-objects-server';
import type { IndexMapping } from '@kbn/core-saved-objects-base-server-internal';
-import { getQueryParams, HasReferenceQueryParams, SearchOperator } from './query_params';
+import type { SavedObjectTypeIdTuple } from '@kbn/core-saved-objects-common';
+import { getQueryParams, SearchOperator } from './query_params';
import { getPitParams } from './pit_params';
import { getSortingParams } from './sorting_params';
@@ -30,8 +31,10 @@ interface GetSearchDslOptions {
namespaces?: string[];
pit?: SavedObjectsPitParams;
typeToNamespacesMap?: Map;
- hasReference?: HasReferenceQueryParams | HasReferenceQueryParams[];
+ hasReference?: SavedObjectTypeIdTuple | SavedObjectTypeIdTuple[];
hasReferenceOperator?: SearchOperator;
+ hasNoReference?: SavedObjectTypeIdTuple | SavedObjectTypeIdTuple[];
+ hasNoReferenceOperator?: SearchOperator;
kueryNode?: KueryNode;
}
@@ -54,6 +57,8 @@ export function getSearchDsl(
typeToNamespacesMap,
hasReference,
hasReferenceOperator,
+ hasNoReference,
+ hasNoReferenceOperator,
kueryNode,
} = options;
@@ -77,6 +82,8 @@ export function getSearchDsl(
defaultSearchOperator,
hasReference,
hasReferenceOperator,
+ hasNoReference,
+ hasNoReferenceOperator,
kueryNode,
}),
...getSortingParams(mappings, type, sortField, sortOrder),
diff --git a/packages/core/saved-objects/core-saved-objects-api-server/src/apis/find.ts b/packages/core/saved-objects/core-saved-objects-api-server/src/apis/find.ts
index 49042029f334b..a50506c96c8e5 100644
--- a/packages/core/saved-objects/core-saved-objects-api-server/src/apis/find.ts
+++ b/packages/core/saved-objects/core-saved-objects-api-server/src/apis/find.ts
@@ -66,11 +66,23 @@ export interface SavedObjectsFindOptions {
* Use `hasReferenceOperator` to specify the operator to use when searching for multiple references.
*/
hasReference?: SavedObjectsFindOptionsReference | SavedObjectsFindOptionsReference[];
+
/**
* The operator to use when searching by multiple references using the `hasReference` option. Defaults to `OR`
*/
hasReferenceOperator?: 'AND' | 'OR';
+ /**
+ * Search for documents *not* having a reference to the specified objects.
+ * Use `hasNoReferenceOperator` to specify the operator to use when searching for multiple references.
+ */
+ hasNoReference?: SavedObjectsFindOptionsReference | SavedObjectsFindOptionsReference[];
+
+ /**
+ * The operator to use when searching by multiple references using the `hasNoReference` option. Defaults to `OR`
+ */
+ hasNoReferenceOperator?: 'AND' | 'OR';
+
/**
* The search operator to use with the provided filter. Defaults to `OR`
*/
diff --git a/packages/core/saved-objects/core-saved-objects-browser-internal/src/saved_objects_client.test.ts b/packages/core/saved-objects/core-saved-objects-browser-internal/src/saved_objects_client.test.ts
index 6c2966ee9775f..7825b09cf29bd 100644
--- a/packages/core/saved-objects/core-saved-objects-browser-internal/src/saved_objects_client.test.ts
+++ b/packages/core/saved-objects/core-saved-objects-browser-internal/src/saved_objects_client.test.ts
@@ -612,6 +612,7 @@ describe('SavedObjectsClient', () => {
defaultSearchOperator: 'OR' as const,
fields: ['title'],
hasReference: { id: '1', type: 'reference' },
+ hasNoReference: { id: '1', type: 'reference' },
page: 10,
perPage: 100,
search: 'what is the meaning of life?|life',
@@ -633,6 +634,7 @@ describe('SavedObjectsClient', () => {
"fields": Array [
"title",
],
+ "has_no_reference": "{\\"id\\":\\"1\\",\\"type\\":\\"reference\\"}",
"has_reference": "{\\"id\\":\\"1\\",\\"type\\":\\"reference\\"}",
"page": 10,
"per_page": 100,
diff --git a/packages/core/saved-objects/core-saved-objects-browser-internal/src/saved_objects_client.ts b/packages/core/saved-objects/core-saved-objects-browser-internal/src/saved_objects_client.ts
index dd2feed58123f..1fd111186f551 100644
--- a/packages/core/saved-objects/core-saved-objects-browser-internal/src/saved_objects_client.ts
+++ b/packages/core/saved-objects/core-saved-objects-browser-internal/src/saved_objects_client.ts
@@ -292,6 +292,8 @@ export class SavedObjectsClient implements SavedObjectsClientContract {
fields: 'fields',
hasReference: 'has_reference',
hasReferenceOperator: 'has_reference_operator',
+ hasNoReference: 'has_no_reference',
+ hasNoReferenceOperator: 'has_no_reference_operator',
page: 'page',
perPage: 'per_page',
search: 'search',
@@ -315,6 +317,9 @@ export class SavedObjectsClient implements SavedObjectsClientContract {
if (query.has_reference) {
query.has_reference = JSON.stringify(query.has_reference);
}
+ if (query.has_no_reference) {
+ query.has_no_reference = JSON.stringify(query.has_no_reference);
+ }
// `aggs` is a structured object. we need to stringify it before sending it, as `fetch`
// is not doing it implicitly.
diff --git a/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/find.ts b/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/find.ts
index 4587cb1ebeb09..983b31caf7a2b 100644
--- a/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/find.ts
+++ b/packages/core/saved-objects/core-saved-objects-server-internal/src/routes/find.ts
@@ -45,6 +45,10 @@ export const registerFindRoute = (
schema.oneOf([referenceSchema, schema.arrayOf(referenceSchema)])
),
has_reference_operator: searchOperatorSchema,
+ has_no_reference: schema.maybe(
+ schema.oneOf([referenceSchema, schema.arrayOf(referenceSchema)])
+ ),
+ has_no_reference_operator: searchOperatorSchema,
fields: schema.maybe(schema.oneOf([schema.string(), schema.arrayOf(schema.string())])),
filter: schema.maybe(schema.string()),
aggs: schema.maybe(schema.string()),
@@ -88,6 +92,8 @@ export const registerFindRoute = (
sortField: query.sort_field,
hasReference: query.has_reference,
hasReferenceOperator: query.has_reference_operator,
+ hasNoReference: query.has_no_reference,
+ hasNoReferenceOperator: query.has_no_reference_operator,
fields: typeof query.fields === 'string' ? [query.fields] : query.fields,
filter: query.filter,
aggs,
diff --git a/packages/core/status/core-status-server-internal/src/routes/status.ts b/packages/core/status/core-status-server-internal/src/routes/status.ts
index 34a5a9b4dcd20..199f55159a7c6 100644
--- a/packages/core/status/core-status-server-internal/src/routes/status.ts
+++ b/packages/core/status/core-status-server-internal/src/routes/status.ts
@@ -135,6 +135,7 @@ export const registerStatusRoute = ({
...lastMetrics.requests,
status_codes: lastMetrics.requests.statusCodes,
},
+ elasticsearch_client: lastMetrics.elasticsearch_client,
},
};
diff --git a/packages/kbn-doc-links/src/get_doc_links.ts b/packages/kbn-doc-links/src/get_doc_links.ts
index 8ef5a68a3f98c..445bf9458d457 100644
--- a/packages/kbn-doc-links/src/get_doc_links.ts
+++ b/packages/kbn-doc-links/src/get_doc_links.ts
@@ -130,6 +130,7 @@ export const getDocLinks = ({ kibanaBranch }: GetDocLinkOptions): DocLinks => {
crawlerGettingStarted: `${ENTERPRISE_SEARCH_DOCS}crawler-getting-started.html`,
crawlerManaging: `${ENTERPRISE_SEARCH_DOCS}crawler-managing.html`,
crawlerOverview: `${ENTERPRISE_SEARCH_DOCS}crawler.html`,
+ deployTrainedModels: `${MACHINE_LEARNING_DOCS}ml-nlp-deploy-models.html`,
documentLevelSecurity: `${ELASTICSEARCH_DOCS}document-level-security.html`,
ingestPipelines: `${ENTERPRISE_SEARCH_DOCS}ingest-pipelines.html`,
languageAnalyzers: `${ELASTICSEARCH_DOCS}analysis-lang-analyzer.html`,
diff --git a/packages/kbn-doc-links/src/types.ts b/packages/kbn-doc-links/src/types.ts
index aed1b552bdb30..d9902a7b11de3 100644
--- a/packages/kbn-doc-links/src/types.ts
+++ b/packages/kbn-doc-links/src/types.ts
@@ -115,6 +115,7 @@ export interface DocLinks {
readonly crawlerGettingStarted: string;
readonly crawlerManaging: string;
readonly crawlerOverview: string;
+ readonly deployTrainedModels: string;
readonly documentLevelSecurity: string;
readonly ingestPipelines: string;
readonly languageAnalyzers: string;
diff --git a/packages/kbn-optimizer/limits.yml b/packages/kbn-optimizer/limits.yml
index 5cd1458028626..67064af8cddc5 100644
--- a/packages/kbn-optimizer/limits.yml
+++ b/packages/kbn-optimizer/limits.yml
@@ -10,7 +10,10 @@ pageLoadAssetSize:
cases: 144442
charts: 55000
cloud: 21076
+ cloudChat: 19894
cloudExperiments: 59358
+ cloudFullStory: 18493
+ cloudLinks: 17629
cloudSecurityPosture: 19109
console: 46091
controls: 40000
diff --git a/src/cli_setup/utils.ts b/src/cli_setup/utils.ts
index 5c66fa84c0f30..47b8199f16ea0 100644
--- a/src/cli_setup/utils.ts
+++ b/src/cli_setup/utils.ts
@@ -48,7 +48,7 @@ export const elasticsearch = new ElasticsearchService(logger, kibanaPackageJson.
logger,
type,
// we use an independent AgentManager for cli_setup, no need to track performance of this one
- agentManager: new AgentManager(),
+ agentFactoryProvider: new AgentManager(),
kibanaVersion: kibanaPackageJson.version,
});
},
diff --git a/src/core/server/integration_tests/saved_objects/routes/find.test.ts b/src/core/server/integration_tests/saved_objects/routes/find.test.ts
index ab3ca6c459dae..2c7b1c9838b50 100644
--- a/src/core/server/integration_tests/saved_objects/routes/find.test.ts
+++ b/src/core/server/integration_tests/saved_objects/routes/find.test.ts
@@ -123,6 +123,7 @@ describe('GET /api/saved_objects/_find', () => {
type: ['foo', 'bar'],
defaultSearchOperator: 'OR',
hasReferenceOperator: 'OR',
+ hasNoReferenceOperator: 'OR',
});
});
@@ -213,6 +214,73 @@ describe('GET /api/saved_objects/_find', () => {
);
});
+ it('accepts the query parameter has_no_reference as an object', async () => {
+ const references = querystring.escape(
+ JSON.stringify({
+ id: '1',
+ type: 'reference',
+ })
+ );
+ await supertest(httpSetup.server.listener)
+ .get(`/api/saved_objects/_find?type=foo&has_no_reference=${references}`)
+ .expect(200);
+
+ expect(savedObjectsClient.find).toHaveBeenCalledTimes(1);
+
+ const options = savedObjectsClient.find.mock.calls[0][0];
+ expect(options.hasNoReference).toEqual({
+ id: '1',
+ type: 'reference',
+ });
+ });
+
+ it('accepts the query parameter has_no_reference as an array', async () => {
+ const references = querystring.escape(
+ JSON.stringify([
+ {
+ id: '1',
+ type: 'reference',
+ },
+ {
+ id: '2',
+ type: 'reference',
+ },
+ ])
+ );
+ await supertest(httpSetup.server.listener)
+ .get(`/api/saved_objects/_find?type=foo&has_no_reference=${references}`)
+ .expect(200);
+
+ expect(savedObjectsClient.find).toHaveBeenCalledTimes(1);
+
+ const options = savedObjectsClient.find.mock.calls[0][0];
+ expect(options.hasNoReference).toEqual([
+ {
+ id: '1',
+ type: 'reference',
+ },
+ {
+ id: '2',
+ type: 'reference',
+ },
+ ]);
+ });
+
+ it('accepts the query parameter has_no_reference_operator', async () => {
+ await supertest(httpSetup.server.listener)
+ .get('/api/saved_objects/_find?type=foo&has_no_reference_operator=AND')
+ .expect(200);
+
+ expect(savedObjectsClient.find).toHaveBeenCalledTimes(1);
+
+ const options = savedObjectsClient.find.mock.calls[0][0];
+ expect(options).toEqual(
+ expect.objectContaining({
+ hasNoReferenceOperator: 'AND',
+ })
+ );
+ });
+
it('accepts the query parameter search_fields', async () => {
await supertest(httpSetup.server.listener)
.get('/api/saved_objects/_find?type=foo&search_fields=title')
diff --git a/src/core/server/server.ts b/src/core/server/server.ts
index b7f41dd31dd04..e0a2c2c44f254 100644
--- a/src/core/server/server.ts
+++ b/src/core/server/server.ts
@@ -280,7 +280,10 @@ export class Server {
executionContext: executionContextSetup,
});
- const metricsSetup = await this.metrics.setup({ http: httpSetup });
+ const metricsSetup = await this.metrics.setup({
+ http: httpSetup,
+ elasticsearchService: elasticsearchServiceSetup,
+ });
const coreUsageDataSetup = this.coreUsageData.setup({
http: httpSetup,
diff --git a/src/dev/storybook/aliases.ts b/src/dev/storybook/aliases.ts
index b4224e154def5..6f82ec078f7ab 100644
--- a/src/dev/storybook/aliases.ts
+++ b/src/dev/storybook/aliases.ts
@@ -11,7 +11,7 @@ export const storybookAliases = {
apm: 'x-pack/plugins/apm/.storybook',
canvas: 'x-pack/plugins/canvas/storybook',
ci_composite: '.ci/.storybook',
- cloud: 'x-pack/plugins/cloud/.storybook',
+ cloud_chat: 'x-pack/plugins/cloud_integrations/cloud_chat/.storybook',
coloring: 'packages/kbn-coloring/.storybook',
chart_icons: 'packages/kbn-chart-icons/.storybook',
content_management: 'packages/content-management/.storybook',
diff --git a/src/plugins/bfetch/server/plugin.ts b/src/plugins/bfetch/server/plugin.ts
index 0f51f5da62353..85720480cf9a0 100644
--- a/src/plugins/bfetch/server/plugin.ts
+++ b/src/plugins/bfetch/server/plugin.ts
@@ -20,6 +20,7 @@ import {
} from '@kbn/core/server';
import { schema } from '@kbn/config-schema';
import { map$ } from '@kbn/std';
+import { RouteConfigOptions } from '@kbn/core-http-server';
import {
StreamingResponseHandler,
BatchRequestData,
@@ -54,7 +55,8 @@ export interface BfetchServerSetup {
context: RequestHandlerContext
) => StreamingResponseHandler,
method?: 'GET' | 'POST' | 'PUT' | 'DELETE',
- pluginRouter?: ReturnType
+ pluginRouter?: ReturnType,
+ options?: RouteConfigOptions<'get' | 'post' | 'put' | 'delete'>
) => void;
}
@@ -117,14 +119,16 @@ export class BfetchServerPlugin
router: ReturnType;
logger: Logger;
}): BfetchServerSetup['addStreamingResponseRoute'] =>
- (path, handler, method = 'POST', pluginRouter) => {
+ (path, handler, method = 'POST', pluginRouter, options) => {
const httpRouter = pluginRouter || router;
+
const routeDefinition = {
path: `/${removeLeadingSlash(path)}`,
validate: {
body: schema.any(),
query: schema.object({ compress: schema.boolean({ defaultValue: false }) }),
},
+ options,
};
const routeHandler: RequestHandler = async (
context: RequestHandlerContext,
diff --git a/src/plugins/console/public/application/components/settings_modal.tsx b/src/plugins/console/public/application/components/settings_modal.tsx
index 095dde1c29507..67b0e2c0d957a 100644
--- a/src/plugins/console/public/application/components/settings_modal.tsx
+++ b/src/plugins/console/public/application/components/settings_modal.tsx
@@ -77,9 +77,9 @@ export const DevToolsSettingsModal = (props: DevToolsSettingsModalProps) => {
const [polling, setPolling] = useState(props.settings.polling);
const [pollInterval, setPollInterval] = useState(props.settings.pollInterval);
const [tripleQuotes, setTripleQuotes] = useState(props.settings.tripleQuotes);
- const [isHistoryDisabled, setIsHistoryDisabled] = useState(props.settings.isHistoryDisabled);
- const [isKeyboardShortcutsDisabled, setIsKeyboardShortcutsDisabled] = useState(
- props.settings.isKeyboardShortcutsDisabled
+ const [isHistoryEnabled, setIsHistoryEnabled] = useState(props.settings.isHistoryEnabled);
+ const [isKeyboardShortcutsEnabled, setIsKeyboardShortcutsEnabled] = useState(
+ props.settings.isKeyboardShortcutsEnabled
);
const autoCompleteCheckboxes = [
@@ -140,8 +140,8 @@ export const DevToolsSettingsModal = (props: DevToolsSettingsModalProps) => {
polling,
pollInterval,
tripleQuotes,
- isHistoryDisabled,
- isKeyboardShortcutsDisabled,
+ isHistoryEnabled,
+ isKeyboardShortcutsEnabled,
});
}
@@ -153,17 +153,17 @@ export const DevToolsSettingsModal = (props: DevToolsSettingsModalProps) => {
}, []);
const toggleKeyboardShortcuts = useCallback(
- (isDisabled: boolean) => {
+ (isEnabled: boolean) => {
if (props.editorInstance) {
unregisterCommands(props.editorInstance);
- setIsKeyboardShortcutsDisabled(isDisabled);
+ setIsKeyboardShortcutsEnabled(isEnabled);
}
},
[props.editorInstance]
);
const toggleSavingToHistory = useCallback(
- (isDisabled: boolean) => setIsHistoryDisabled(isDisabled),
+ (isEnabled: boolean) => setIsHistoryEnabled(isEnabled),
[]
);
@@ -289,11 +289,11 @@ export const DevToolsSettingsModal = (props: DevToolsSettingsModalProps) => {
}
>
}
onChange={(e) => toggleSavingToHistory(e.target.checked)}
@@ -309,11 +309,11 @@ export const DevToolsSettingsModal = (props: DevToolsSettingsModalProps) => {
}
>
}
onChange={(e) => toggleKeyboardShortcuts(e.target.checked)}
diff --git a/src/plugins/console/public/application/containers/editor/legacy/console_editor/editor.tsx b/src/plugins/console/public/application/containers/editor/legacy/console_editor/editor.tsx
index 74a052646e198..ed8c87b5df147 100644
--- a/src/plugins/console/public/application/containers/editor/legacy/console_editor/editor.tsx
+++ b/src/plugins/console/public/application/containers/editor/legacy/console_editor/editor.tsx
@@ -259,8 +259,8 @@ function EditorUI({ initialTextValue, setEditorInstance }: EditorProps) {
}, [settings]);
useEffect(() => {
- const { isKeyboardShortcutsDisabled } = settings;
- if (!isKeyboardShortcutsDisabled) {
+ const { isKeyboardShortcutsEnabled } = settings;
+ if (isKeyboardShortcutsEnabled) {
registerCommands({
senseEditor: editorInstanceRef.current!,
sendCurrentRequest,
diff --git a/src/plugins/console/public/application/hooks/use_send_current_request/use_send_current_request.test.tsx b/src/plugins/console/public/application/hooks/use_send_current_request/use_send_current_request.test.tsx
index 0c7e4c46d95a6..e895ddc135db8 100644
--- a/src/plugins/console/public/application/hooks/use_send_current_request/use_send_current_request.test.tsx
+++ b/src/plugins/console/public/application/hooks/use_send_current_request/use_send_current_request.test.tsx
@@ -106,7 +106,9 @@ describe('useSendCurrentRequest', () => {
(sendRequest as jest.Mock).mockReturnValue(
[{ request: {} }, { request: {} }] /* two responses to save history */
);
- (mockContextValue.services.settings.toJSON as jest.Mock).mockReturnValue({});
+ (mockContextValue.services.settings.toJSON as jest.Mock).mockReturnValue({
+ isHistoryEnabled: true,
+ });
(mockContextValue.services.history.addToHistory as jest.Mock).mockImplementation(() => {
// Mock throwing
throw new Error('cannot save!');
diff --git a/src/plugins/console/public/application/hooks/use_send_current_request/use_send_current_request.ts b/src/plugins/console/public/application/hooks/use_send_current_request/use_send_current_request.ts
index 87f72571a63e6..28d875c246ca3 100644
--- a/src/plugins/console/public/application/hooks/use_send_current_request/use_send_current_request.ts
+++ b/src/plugins/console/public/application/hooks/use_send_current_request/use_send_current_request.ts
@@ -52,9 +52,9 @@ export const useSendCurrentRequest = () => {
const results = await sendRequest({ http, requests });
let saveToHistoryError: undefined | Error;
- const { isHistoryDisabled } = settings.toJSON();
+ const { isHistoryEnabled } = settings.toJSON();
- if (!isHistoryDisabled) {
+ if (isHistoryEnabled) {
results.forEach(({ request: { path, method, data } }) => {
try {
history.addToHistory(path, method, data);
@@ -84,7 +84,7 @@ export const useSendCurrentRequest = () => {
notifications.toasts.remove(toast);
},
onDisableSavingToHistory: () => {
- settings.setIsHistoryDisabled(true);
+ settings.setIsHistoryEnabled(false);
notifications.toasts.remove(toast);
},
}),
diff --git a/src/plugins/console/public/services/settings.ts b/src/plugins/console/public/services/settings.ts
index aa2280f06064f..e4731dd3f3a31 100644
--- a/src/plugins/console/public/services/settings.ts
+++ b/src/plugins/console/public/services/settings.ts
@@ -15,8 +15,8 @@ export const DEFAULT_SETTINGS = Object.freeze({
tripleQuotes: true,
wrapMode: true,
autocomplete: Object.freeze({ fields: true, indices: true, templates: true, dataStreams: true }),
- isHistoryDisabled: false,
- isKeyboardShortcutsDisabled: false,
+ isHistoryEnabled: true,
+ isKeyboardShortcutsEnabled: true,
});
export interface DevToolsSettings {
@@ -31,8 +31,8 @@ export interface DevToolsSettings {
polling: boolean;
pollInterval: number;
tripleQuotes: boolean;
- isHistoryDisabled: boolean;
- isKeyboardShortcutsDisabled: boolean;
+ isHistoryEnabled: boolean;
+ isKeyboardShortcutsEnabled: boolean;
}
enum SettingKeys {
@@ -42,12 +42,32 @@ enum SettingKeys {
AUTOCOMPLETE_SETTINGS = 'autocomplete_settings',
CONSOLE_POLLING = 'console_polling',
POLL_INTERVAL = 'poll_interval',
- IS_HISTORY_DISABLED = 'is_history_disabled',
- IS_KEYBOARD_SHORTCUTS_DISABLED = 'is_keyboard_shortcuts_disabled',
+ IS_HISTORY_ENABLED = 'is_history_enabled',
+ IS_KEYBOARD_SHORTCUTS_ENABLED = 'is_keyboard_shortcuts_enabled',
}
export class Settings {
- constructor(private readonly storage: Storage) {}
+ constructor(private readonly storage: Storage) {
+ // Migration from old settings to new ones
+ this.addMigrationRule('is_history_disabled', SettingKeys.IS_HISTORY_ENABLED, (value: any) => {
+ return !value;
+ });
+ this.addMigrationRule(
+ 'is_keyboard_shortcuts_disabled',
+ SettingKeys.IS_KEYBOARD_SHORTCUTS_ENABLED,
+ (value: any) => {
+ return !value;
+ }
+ );
+ }
+
+ private addMigrationRule(previousKey: string, newKey: string, migration: (value: any) => any) {
+ const value = this.storage.get(previousKey);
+ if (value !== undefined) {
+ this.storage.set(newKey, migration(value));
+ this.storage.delete(previousKey);
+ }
+ }
getFontSize() {
return this.storage.get(SettingKeys.FONT_SIZE, DEFAULT_SETTINGS.fontSize);
@@ -94,13 +114,13 @@ export class Settings {
return true;
}
- setIsHistoryDisabled(isDisabled: boolean) {
- this.storage.set(SettingKeys.IS_HISTORY_DISABLED, isDisabled);
+ setIsHistoryEnabled(isEnabled: boolean) {
+ this.storage.set(SettingKeys.IS_HISTORY_ENABLED, isEnabled);
return true;
}
- getIsHistoryDisabled() {
- return this.storage.get(SettingKeys.IS_HISTORY_DISABLED, DEFAULT_SETTINGS.isHistoryDisabled);
+ getIsHistoryEnabled() {
+ return this.storage.get(SettingKeys.IS_HISTORY_ENABLED, DEFAULT_SETTINGS.isHistoryEnabled);
}
setPollInterval(interval: number) {
@@ -111,15 +131,15 @@ export class Settings {
return this.storage.get(SettingKeys.POLL_INTERVAL, DEFAULT_SETTINGS.pollInterval);
}
- setIsKeyboardShortcutsDisabled(disable: boolean) {
- this.storage.set(SettingKeys.IS_KEYBOARD_SHORTCUTS_DISABLED, disable);
+ setIsKeyboardShortcutsEnabled(isEnabled: boolean) {
+ this.storage.set(SettingKeys.IS_KEYBOARD_SHORTCUTS_ENABLED, isEnabled);
return true;
}
getIsKeyboardShortcutsDisabled() {
return this.storage.get(
- SettingKeys.IS_KEYBOARD_SHORTCUTS_DISABLED,
- DEFAULT_SETTINGS.isKeyboardShortcutsDisabled
+ SettingKeys.IS_KEYBOARD_SHORTCUTS_ENABLED,
+ DEFAULT_SETTINGS.isKeyboardShortcutsEnabled
);
}
@@ -131,8 +151,8 @@ export class Settings {
fontSize: parseFloat(this.getFontSize()),
polling: Boolean(this.getPolling()),
pollInterval: this.getPollInterval(),
- isHistoryDisabled: Boolean(this.getIsHistoryDisabled()),
- isKeyboardShortcutsDisabled: Boolean(this.getIsKeyboardShortcutsDisabled()),
+ isHistoryEnabled: Boolean(this.getIsHistoryEnabled()),
+ isKeyboardShortcutsEnabled: Boolean(this.getIsKeyboardShortcutsDisabled()),
};
}
@@ -143,8 +163,8 @@ export class Settings {
autocomplete,
polling,
pollInterval,
- isHistoryDisabled,
- isKeyboardShortcutsDisabled,
+ isHistoryEnabled,
+ isKeyboardShortcutsEnabled,
}: DevToolsSettings) {
this.setFontSize(fontSize);
this.setWrapMode(wrapMode);
@@ -152,8 +172,8 @@ export class Settings {
this.setAutocomplete(autocomplete);
this.setPolling(polling);
this.setPollInterval(pollInterval);
- this.setIsHistoryDisabled(isHistoryDisabled);
- this.setIsKeyboardShortcutsDisabled(isKeyboardShortcutsDisabled);
+ this.setIsHistoryEnabled(isHistoryEnabled);
+ this.setIsKeyboardShortcutsEnabled(isKeyboardShortcutsEnabled);
}
}
diff --git a/src/plugins/home/kibana.json b/src/plugins/home/kibana.json
index 02b33e814e2a1..72b4d6cb8fd0b 100644
--- a/src/plugins/home/kibana.json
+++ b/src/plugins/home/kibana.json
@@ -8,6 +8,6 @@
"server": true,
"ui": true,
"requiredPlugins": ["dataViews", "share", "urlForwarding"],
- "optionalPlugins": ["usageCollection", "customIntegrations"],
+ "optionalPlugins": ["usageCollection", "customIntegrations", "cloud"],
"requiredBundles": ["kibanaReact"]
}
diff --git a/src/plugins/home/public/plugin.test.ts b/src/plugins/home/public/plugin.test.ts
index 12243944ef0f0..a6c6012a28ed6 100644
--- a/src/plugins/home/public/plugin.test.ts
+++ b/src/plugins/home/public/plugin.test.ts
@@ -11,6 +11,7 @@ import { HomePublicPlugin } from './plugin';
import { coreMock } from '@kbn/core/public/mocks';
import { urlForwardingPluginMock } from '@kbn/url-forwarding-plugin/public/mocks';
import { SharePluginSetup } from '@kbn/share-plugin/public';
+import { cloudMock } from '@kbn/cloud-plugin/public/mocks';
const mockInitializerContext = coreMock.createPluginInitializerContext();
const mockShare = {} as SharePluginSetup;
@@ -24,14 +25,11 @@ describe('HomePublicPlugin', () => {
});
describe('setup', () => {
- test('registers tutorial directory to feature catalogue', async () => {
- const setup = await new HomePublicPlugin(mockInitializerContext).setup(
- coreMock.createSetup() as any,
- {
- share: mockShare,
- urlForwarding: urlForwardingPluginMock.createSetupContract(),
- }
- );
+ test('registers tutorial directory to feature catalogue', () => {
+ const setup = new HomePublicPlugin(mockInitializerContext).setup(coreMock.createSetup(), {
+ share: mockShare,
+ urlForwarding: urlForwardingPluginMock.createSetupContract(),
+ });
expect(setup).toHaveProperty('featureCatalogue');
expect(setup.featureCatalogue.register).toHaveBeenCalledTimes(1);
expect(setup.featureCatalogue.register).toHaveBeenCalledWith(
@@ -44,53 +42,73 @@ describe('HomePublicPlugin', () => {
);
});
- test('wires up and returns registry', async () => {
- const setup = await new HomePublicPlugin(mockInitializerContext).setup(
- coreMock.createSetup() as any,
- {
- share: mockShare,
- urlForwarding: urlForwardingPluginMock.createSetupContract(),
- }
- );
+ test('wires up and returns registry', () => {
+ const setup = new HomePublicPlugin(mockInitializerContext).setup(coreMock.createSetup(), {
+ share: mockShare,
+ urlForwarding: urlForwardingPluginMock.createSetupContract(),
+ });
expect(setup).toHaveProperty('featureCatalogue');
expect(setup.featureCatalogue).toHaveProperty('register');
});
- test('wires up and returns environment service', async () => {
- const setup = await new HomePublicPlugin(mockInitializerContext).setup(
- coreMock.createSetup() as any,
- {
- share: {} as SharePluginSetup,
- urlForwarding: urlForwardingPluginMock.createSetupContract(),
- }
- );
+ test('wires up and returns environment service', () => {
+ const setup = new HomePublicPlugin(mockInitializerContext).setup(coreMock.createSetup(), {
+ share: {} as SharePluginSetup,
+ urlForwarding: urlForwardingPluginMock.createSetupContract(),
+ });
expect(setup).toHaveProperty('environment');
expect(setup.environment).toHaveProperty('update');
});
- test('wires up and returns tutorial service', async () => {
- const setup = await new HomePublicPlugin(mockInitializerContext).setup(
- coreMock.createSetup() as any,
- {
- share: mockShare,
- urlForwarding: urlForwardingPluginMock.createSetupContract(),
- }
- );
+ test('wires up and returns tutorial service', () => {
+ const setup = new HomePublicPlugin(mockInitializerContext).setup(coreMock.createSetup(), {
+ share: mockShare,
+ urlForwarding: urlForwardingPluginMock.createSetupContract(),
+ });
expect(setup).toHaveProperty('tutorials');
expect(setup.tutorials).toHaveProperty('setVariable');
});
- test('wires up and returns welcome service', async () => {
- const setup = await new HomePublicPlugin(mockInitializerContext).setup(
- coreMock.createSetup() as any,
- {
- share: mockShare,
- urlForwarding: urlForwardingPluginMock.createSetupContract(),
- }
- );
+ test('wires up and returns welcome service', () => {
+ const setup = new HomePublicPlugin(mockInitializerContext).setup(coreMock.createSetup(), {
+ share: mockShare,
+ urlForwarding: urlForwardingPluginMock.createSetupContract(),
+ });
expect(setup).toHaveProperty('welcomeScreen');
expect(setup.welcomeScreen).toHaveProperty('registerOnRendered');
expect(setup.welcomeScreen).toHaveProperty('registerTelemetryNoticeRenderer');
});
+
+ test('sets the cloud environment variable when the cloud plugin is present but isCloudEnabled: false', () => {
+ const cloud = { ...cloudMock.createSetup(), isCloudEnabled: false };
+ const plugin = new HomePublicPlugin(mockInitializerContext);
+ const setup = plugin.setup(coreMock.createSetup(), {
+ cloud,
+ share: mockShare,
+ urlForwarding: urlForwardingPluginMock.createSetupContract(),
+ });
+ expect(setup.environment.update).toHaveBeenCalledTimes(1);
+ expect(setup.environment.update).toHaveBeenCalledWith({ cloud: false });
+ expect(setup.tutorials.setVariable).toHaveBeenCalledTimes(0);
+ });
+
+ test('when cloud is enabled, it sets the cloud environment and the tutorials variable "cloud"', () => {
+ const cloud = { ...cloudMock.createSetup(), isCloudEnabled: true };
+ const plugin = new HomePublicPlugin(mockInitializerContext);
+ const setup = plugin.setup(coreMock.createSetup(), {
+ cloud,
+ share: mockShare,
+ urlForwarding: urlForwardingPluginMock.createSetupContract(),
+ });
+ expect(setup.environment.update).toHaveBeenCalledTimes(1);
+ expect(setup.environment.update).toHaveBeenCalledWith({ cloud: true });
+ expect(setup.tutorials.setVariable).toHaveBeenCalledTimes(1);
+ expect(setup.tutorials.setVariable).toHaveBeenCalledWith('cloud', {
+ id: 'mock-cloud-id',
+ baseUrl: 'base-url',
+ deploymentUrl: 'deployment-url',
+ profileUrl: 'profile-url',
+ });
+ });
});
});
diff --git a/src/plugins/home/public/plugin.ts b/src/plugins/home/public/plugin.ts
index 642a8d575e078..e27ddf107a5ee 100644
--- a/src/plugins/home/public/plugin.ts
+++ b/src/plugins/home/public/plugin.ts
@@ -20,6 +20,7 @@ import { UsageCollectionSetup } from '@kbn/usage-collection-plugin/public';
import { UrlForwardingSetup, UrlForwardingStart } from '@kbn/url-forwarding-plugin/public';
import { AppNavLinkStatus } from '@kbn/core/public';
import { SharePluginSetup } from '@kbn/share-plugin/public';
+import type { CloudSetup } from '@kbn/cloud-plugin/public';
import { PLUGIN_ID, HOME_APP_BASE_PATH } from '../common/constants';
import { setServices } from './application/kibana_services';
import { ConfigSchema } from '../config';
@@ -42,6 +43,7 @@ export interface HomePluginStartDependencies {
}
export interface HomePluginSetupDependencies {
+ cloud?: CloudSetup;
share: SharePluginSetup;
usageCollection?: UsageCollectionSetup;
urlForwarding: UrlForwardingSetup;
@@ -66,7 +68,7 @@ export class HomePublicPlugin
public setup(
core: CoreSetup,
- { share, urlForwarding, usageCollection }: HomePluginSetupDependencies
+ { cloud, share, urlForwarding, usageCollection }: HomePluginSetupDependencies
): HomePublicPluginSetup {
core.application.register({
id: PLUGIN_ID,
@@ -127,10 +129,25 @@ export class HomePublicPlugin
order: 500,
});
+ const environment = { ...this.environmentService.setup() };
+ const tutorials = { ...this.tutorialService.setup() };
+ if (cloud) {
+ environment.update({ cloud: cloud.isCloudEnabled });
+ if (cloud.isCloudEnabled) {
+ tutorials.setVariable('cloud', {
+ id: cloud.cloudId,
+ baseUrl: cloud.baseUrl,
+ // Cloud's API already provides the full URLs
+ profileUrl: cloud.profileUrl?.replace(cloud.baseUrl ?? '', ''),
+ deploymentUrl: cloud.deploymentUrl?.replace(cloud.baseUrl ?? '', ''),
+ });
+ }
+ }
+
return {
featureCatalogue,
- environment: { ...this.environmentService.setup() },
- tutorials: { ...this.tutorialService.setup() },
+ environment,
+ tutorials,
addData: { ...this.addDataService.setup() },
welcomeScreen: { ...this.welcomeService.setup() },
};
diff --git a/src/plugins/home/public/services/environment/environment.mock.ts b/src/plugins/home/public/services/environment/environment.mock.ts
index 713a59ceac7bf..f2d4747d44d6a 100644
--- a/src/plugins/home/public/services/environment/environment.mock.ts
+++ b/src/plugins/home/public/services/environment/environment.mock.ts
@@ -18,14 +18,13 @@ const createSetupMock = (): jest.Mocked => {
const createMock = (): jest.Mocked> => {
const service = {
- setup: jest.fn(),
+ setup: jest.fn(createSetupMock),
getEnvironment: jest.fn(() => ({
cloud: false,
apmUi: false,
ml: false,
})),
};
- service.setup.mockImplementation(createSetupMock);
return service;
};
diff --git a/src/plugins/home/tsconfig.json b/src/plugins/home/tsconfig.json
index 8e617896e3f96..af121720eee0e 100644
--- a/src/plugins/home/tsconfig.json
+++ b/src/plugins/home/tsconfig.json
@@ -15,6 +15,7 @@
{ "path": "../kibana_react/tsconfig.json" },
{ "path": "../share/tsconfig.json" },
{ "path": "../url_forwarding/tsconfig.json" },
- { "path": "../usage_collection/tsconfig.json" }
+ { "path": "../usage_collection/tsconfig.json" },
+ { "path": "../../../x-pack/plugins/cloud/tsconfig.json" }
]
}
diff --git a/src/plugins/kibana_usage_collection/server/collectors/ops_stats/__snapshots__/ops_stats_collector.test.ts.snap b/src/plugins/kibana_usage_collection/server/collectors/ops_stats/__snapshots__/ops_stats_collector.test.ts.snap
index f962eca858199..d77d43293480b 100644
--- a/src/plugins/kibana_usage_collection/server/collectors/ops_stats/__snapshots__/ops_stats_collector.test.ts.snap
+++ b/src/plugins/kibana_usage_collection/server/collectors/ops_stats/__snapshots__/ops_stats_collector.test.ts.snap
@@ -3,6 +3,19 @@
exports[`telemetry_ops_stats should return something when there is a metric 1`] = `
Object {
"concurrent_connections": 1,
+ "elasticsearch_client": Object {
+ "averageActiveSocketsPerNode": 8,
+ "averageIdleSocketsPerNode": 0.5,
+ "connectedNodes": 3,
+ "mostActiveNodeSockets": 15,
+ "mostIdleNodeSockets": 2,
+ "nodesWithActiveSockets": 3,
+ "nodesWithIdleSockets": 1,
+ "protocol": "https",
+ "totalActiveSockets": 25,
+ "totalIdleSockets": 2,
+ "totalQueuedRequests": 0,
+ },
"os": Object {
"load": Object {
"15m": 1,
diff --git a/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/configurations/xy/layers.test.ts b/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/configurations/xy/layers.test.ts
index 6c94971397d3e..46e9d9e1fae2a 100644
--- a/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/configurations/xy/layers.test.ts
+++ b/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/configurations/xy/layers.test.ts
@@ -24,6 +24,33 @@ jest.mock('uuid', () => ({
v4: () => 'test-id',
}));
+const mockedIndices = [
+ {
+ id: 'test',
+ title: 'test',
+ timeFieldName: 'test_field',
+ getFieldByName: (name: string) => ({ aggregatable: name !== 'host' }),
+ },
+] as unknown as DataView[];
+
+const indexPatternsService = {
+ getDefault: jest.fn(() =>
+ Promise.resolve({
+ id: 'default',
+ title: 'index',
+ getFieldByName: (name: string) => ({ aggregatable: name !== 'host' }),
+ })
+ ),
+ get: jest.fn((id) => Promise.resolve({ ...mockedIndices[0], id })),
+ find: jest.fn((search: string, size: number) => {
+ if (size !== 1) {
+ // shouldn't request more than one data view since there is a significant performance penalty
+ throw new Error('trying to fetch too many data views');
+ }
+ return Promise.resolve(mockedIndices || []);
+ }),
+} as unknown as DataViewsPublicPluginStart;
+
describe('getLayers', () => {
const dataSourceLayers: Record = [
{
@@ -331,10 +358,16 @@ describe('getLayers', () => {
series: [createSeries({ metrics: staticValueMetric })],
});
- test.each<[string, [Record, Panel], Array>]>([
+ test.each<
+ [
+ string,
+ [Record, Panel, DataViewsPublicPluginStart, boolean],
+ Array>
+ ]
+ >([
[
'data layer if columns do not include static column',
- [dataSourceLayers, panel],
+ [dataSourceLayers, panel, indexPatternsService, false],
[
{
layerType: 'data',
@@ -353,9 +386,30 @@ describe('getLayers', () => {
},
],
],
+ [
+ 'data layer with "left" axisMode if isSingleAxis is provided',
+ [dataSourceLayers, panel, indexPatternsService, true],
+ [
+ {
+ layerType: 'data',
+ accessors: ['column-id-1'],
+ xAccessor: 'column-id-2',
+ splitAccessor: 'column-id-3',
+ seriesType: 'area',
+ layerId: 'test-layer-1',
+ yConfig: [
+ {
+ forAccessor: 'column-id-1',
+ axisMode: 'left',
+ color: '#68BC00',
+ },
+ ],
+ },
+ ],
+ ],
[
'reference line layer if columns include static column',
- [dataSourceLayersWithStatic, panelWithStaticValue],
+ [dataSourceLayersWithStatic, panelWithStaticValue, indexPatternsService, false],
[
{
layerType: 'referenceLine',
@@ -364,9 +418,10 @@ describe('getLayers', () => {
yConfig: [
{
forAccessor: 'column-id-1',
- axisMode: 'right',
+ axisMode: 'left',
color: '#68BC00',
fill: 'below',
+ lineWidth: 1,
},
],
},
@@ -374,7 +429,7 @@ describe('getLayers', () => {
],
[
'correct colors if columns include percentile columns',
- [dataSourceLayersWithPercentile, panelWithPercentileMetric],
+ [dataSourceLayersWithPercentile, panelWithPercentileMetric, indexPatternsService, false],
[
{
yConfig: [
@@ -394,7 +449,12 @@ describe('getLayers', () => {
],
[
'correct colors if columns include percentile rank columns',
- [dataSourceLayersWithPercentileRank, panelWithPercentileRankMetric],
+ [
+ dataSourceLayersWithPercentileRank,
+ panelWithPercentileRankMetric,
+ indexPatternsService,
+ false,
+ ],
[
{
yConfig: [
@@ -414,7 +474,7 @@ describe('getLayers', () => {
],
[
'annotation layer gets correct params and converts color, extraFields and icons',
- [dataSourceLayersWithStatic, panelWithSingleAnnotation],
+ [dataSourceLayersWithStatic, panelWithSingleAnnotation, indexPatternsService, false],
[
{
layerType: 'referenceLine',
@@ -423,9 +483,10 @@ describe('getLayers', () => {
yConfig: [
{
forAccessor: 'column-id-1',
- axisMode: 'right',
+ axisMode: 'left',
color: '#68BC00',
fill: 'below',
+ lineWidth: 1,
},
],
},
@@ -459,7 +520,12 @@ describe('getLayers', () => {
],
[
'annotation layer should gets correct default params',
- [dataSourceLayersWithStatic, panelWithSingleAnnotationWithoutQueryStringAndTimefield],
+ [
+ dataSourceLayersWithStatic,
+ panelWithSingleAnnotationWithoutQueryStringAndTimefield,
+ indexPatternsService,
+ false,
+ ],
[
{
layerType: 'referenceLine',
@@ -468,9 +534,10 @@ describe('getLayers', () => {
yConfig: [
{
forAccessor: 'column-id-1',
- axisMode: 'right',
+ axisMode: 'left',
color: '#68BC00',
fill: 'below',
+ lineWidth: 1,
},
],
},
@@ -504,7 +571,7 @@ describe('getLayers', () => {
],
[
'multiple annotations with different data views create separate layers',
- [dataSourceLayersWithStatic, panelWithMultiAnnotations],
+ [dataSourceLayersWithStatic, panelWithMultiAnnotations, indexPatternsService, false],
[
{
layerType: 'referenceLine',
@@ -513,9 +580,10 @@ describe('getLayers', () => {
yConfig: [
{
forAccessor: 'column-id-1',
- axisMode: 'right',
+ axisMode: 'left',
color: '#68BC00',
fill: 'below',
+ lineWidth: 1,
},
],
},
@@ -598,7 +666,12 @@ describe('getLayers', () => {
],
[
'annotation layer gets correct dataView when none is defined',
- [dataSourceLayersWithStatic, panelWithSingleAnnotationDefaultDataView],
+ [
+ dataSourceLayersWithStatic,
+ panelWithSingleAnnotationDefaultDataView,
+ indexPatternsService,
+ false,
+ ],
[
{
layerType: 'referenceLine',
@@ -607,9 +680,10 @@ describe('getLayers', () => {
yConfig: [
{
forAccessor: 'column-id-1',
- axisMode: 'right',
+ axisMode: 'left',
color: '#68BC00',
fill: 'below',
+ lineWidth: 1,
},
],
},
@@ -642,34 +716,7 @@ describe('getLayers', () => {
],
],
])('should return %s', async (_, input, expected) => {
- const layers = await getLayers(...input, indexPatternsService as DataViewsPublicPluginStart);
+ const layers = await getLayers(...input);
expect(layers).toEqual(expected.map(expect.objectContaining));
});
});
-
-const mockedIndices = [
- {
- id: 'test',
- title: 'test',
- timeFieldName: 'test_field',
- getFieldByName: (name: string) => ({ aggregatable: name !== 'host' }),
- },
-] as unknown as DataView[];
-
-const indexPatternsService = {
- getDefault: jest.fn(() =>
- Promise.resolve({
- id: 'default',
- title: 'index',
- getFieldByName: (name: string) => ({ aggregatable: name !== 'host' }),
- })
- ),
- get: jest.fn((id) => Promise.resolve({ ...mockedIndices[0], id })),
- find: jest.fn((search: string, size: number) => {
- if (size !== 1) {
- // shouldn't request more than one data view since there is a significant performance penalty
- throw new Error('trying to fetch too many data views');
- }
- return Promise.resolve(mockedIndices || []);
- }),
-} as unknown as DataViewsPublicPluginStart;
diff --git a/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/configurations/xy/layers.ts b/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/configurations/xy/layers.ts
index ec0e24e2db873..8784c2952807d 100644
--- a/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/configurations/xy/layers.ts
+++ b/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/configurations/xy/layers.ts
@@ -24,7 +24,7 @@ import { getDefaultQueryLanguage } from '../../../../application/components/lib/
import { fetchIndexPattern } from '../../../../../common/index_patterns_utils';
import { ICON_TYPES_MAP } from '../../../../application/visualizations/constants';
import { SUPPORTED_METRICS } from '../../metrics';
-import type { Annotation, Metric, Panel } from '../../../../../common/types';
+import type { Annotation, Metric, Panel, Series } from '../../../../../common/types';
import { getSeriesAgg } from '../../series';
import {
isPercentileRanksColumnWithMeta,
@@ -44,6 +44,10 @@ function getPalette(palette: PaletteOutput): PaletteOutput {
: palette;
}
+function getAxisMode(series: Series, model: Panel): YAxisMode {
+ return (series.separate_axis ? series.axis_position : model.axis_position) as YAxisMode;
+}
+
function getColor(
metricColumn: Column,
metric: Metric,
@@ -69,7 +73,8 @@ function nonNullable(value: T): value is NonNullable {
export const getLayers = async (
dataSourceLayers: Record,
model: Panel,
- dataViews: DataViewsPublicPluginStart
+ dataViews: DataViewsPublicPluginStart,
+ isSingleAxis: boolean = false
): Promise => {
const nonAnnotationsLayers: XYLayerConfig[] = Object.keys(dataSourceLayers).map((key) => {
const series = model.series[parseInt(key, 10)];
@@ -84,13 +89,13 @@ export const getLayers = async (
const metricColumns = dataSourceLayer.columns.filter(
(l) => !l.isBucketed && l.columnId !== referenceColumnId
);
- const isReferenceLine = metrics.length === 1 && metrics[0].type === 'static';
+ const isReferenceLine =
+ metricColumns.length === 1 && metricColumns[0].operationType === 'static_value';
const splitAccessor = dataSourceLayer.columns.find(
(column) => column.isBucketed && column.isSplit
)?.columnId;
const chartType = getChartType(series, model.type);
const commonProps = {
- seriesType: chartType,
layerId: dataSourceLayer.layerId,
accessors: metricColumns.map((metricColumn) => {
return metricColumn.columnId;
@@ -102,19 +107,19 @@ export const getLayers = async (
return {
forAccessor: metricColumn.columnId,
color: getColor(metricColumn, metric!, series.color, splitAccessor),
- axisMode: (series.separate_axis
- ? series.axis_position
- : model.axis_position) as YAxisMode,
+ axisMode: isReferenceLine // reference line should be assigned to axis with real data
+ ? model.series.some((s) => s.id !== series.id && getAxisMode(s, model) === 'right')
+ ? 'right'
+ : 'left'
+ : isSingleAxis
+ ? 'left'
+ : getAxisMode(series, model),
...(isReferenceLine && {
- fill: chartType === 'area' ? FillTypes.BELOW : FillTypes.NONE,
+ fill: chartType.includes('area') ? FillTypes.BELOW : FillTypes.NONE,
+ lineWidth: series.line_width,
}),
};
}),
- xAccessor: dataSourceLayer.columns.find((column) => column.isBucketed && !column.isSplit)
- ?.columnId,
- splitAccessor,
- collapseFn: seriesAgg,
- palette: getPalette(series.palette as PaletteOutput),
};
if (isReferenceLine) {
return {
@@ -123,8 +128,14 @@ export const getLayers = async (
};
} else {
return {
+ seriesType: chartType,
layerType: 'data',
...commonProps,
+ xAccessor: dataSourceLayer.columns.find((column) => column.isBucketed && !column.isSplit)
+ ?.columnId,
+ splitAccessor,
+ collapseFn: seriesAgg,
+ palette: getPalette(series.palette as PaletteOutput),
};
}
});
diff --git a/src/plugins/vis_types/timeseries/public/convert_to_lens/timeseries/index.test.ts b/src/plugins/vis_types/timeseries/public/convert_to_lens/timeseries/index.test.ts
index 50aa1a6c6f7f4..c81db38e05384 100644
--- a/src/plugins/vis_types/timeseries/public/convert_to_lens/timeseries/index.test.ts
+++ b/src/plugins/vis_types/timeseries/public/convert_to_lens/timeseries/index.test.ts
@@ -112,6 +112,19 @@ describe('convertToLens', () => {
expect(mockGetBucketsColumns).toBeCalledTimes(1);
});
+ test('should return null for static value with buckets', async () => {
+ mockGetBucketsColumns.mockReturnValue([{}]);
+ mockGetMetricsColumns.mockReturnValue([
+ {
+ operationType: 'static_value',
+ },
+ ]);
+ const result = await convertToLens(model);
+ expect(result).toBeNull();
+ expect(mockGetMetricsColumns).toBeCalledTimes(1);
+ expect(mockGetBucketsColumns).toBeCalledTimes(1);
+ });
+
test('should return state for valid model', async () => {
const result = await convertToLens(model);
expect(result).toBeDefined();
diff --git a/src/plugins/vis_types/timeseries/public/convert_to_lens/timeseries/index.ts b/src/plugins/vis_types/timeseries/public/convert_to_lens/timeseries/index.ts
index 8cbbbf0f9e739..ef678fcc2dab4 100644
--- a/src/plugins/vis_types/timeseries/public/convert_to_lens/timeseries/index.ts
+++ b/src/plugins/vis_types/timeseries/public/convert_to_lens/timeseries/index.ts
@@ -98,11 +98,21 @@ export const convertToLens: ConvertTsvbToLensVisualization = async (model: Panel
return null;
}
+ const isReferenceLine =
+ metricsColumns.length === 1 && metricsColumns[0].operationType === 'static_value';
+
+ // only static value without split is supported
+ if (isReferenceLine && bucketsColumns.length) {
+ return null;
+ }
+
const layerId = uuid();
extendedLayers[layerIdx] = {
indexPatternId,
layerId,
- columns: [...metricsColumns, dateHistogramColumn, ...bucketsColumns],
+ columns: isReferenceLine
+ ? [...metricsColumns]
+ : [...metricsColumns, dateHistogramColumn, ...bucketsColumns],
columnOrder: [],
};
}
diff --git a/src/plugins/vis_types/timeseries/public/convert_to_lens/top_n/index.ts b/src/plugins/vis_types/timeseries/public/convert_to_lens/top_n/index.ts
index 020aaec28f573..130646f72f127 100644
--- a/src/plugins/vis_types/timeseries/public/convert_to_lens/top_n/index.ts
+++ b/src/plugins/vis_types/timeseries/public/convert_to_lens/top_n/index.ts
@@ -86,7 +86,7 @@ export const convertToLens: ConvertTsvbToLensVisualization = async (model, timeR
};
}
- const configLayers = await getLayers(extendedLayers, model, dataViews);
+ const configLayers = await getLayers(extendedLayers, model, dataViews, true);
if (configLayers === null) {
return null;
}
diff --git a/test/analytics/tests/instrumented_events/from_the_browser/loaded_dashboard.ts b/test/analytics/tests/instrumented_events/from_the_browser/loaded_dashboard.ts
index bc04d60c3fb54..7b21a5637d167 100644
--- a/test/analytics/tests/instrumented_events/from_the_browser/loaded_dashboard.ts
+++ b/test/analytics/tests/instrumented_events/from_the_browser/loaded_dashboard.ts
@@ -26,7 +26,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
const dashboardAddPanel = getService('dashboardAddPanel');
const queryBar = getService('queryBar');
- describe('Loaded Dashboard', () => {
+ // Failing: See https://github.com/elastic/kibana/issues/142548
+ describe.skip('Loaded Dashboard', () => {
let fromTimestamp: string | undefined;
const getEvents = async (count: number, options?: GetEventsOptions) =>
diff --git a/test/api_integration/apis/saved_objects/find.ts b/test/api_integration/apis/saved_objects/find.ts
index 4afcc4f162a62..5c11b6f74d7ae 100644
--- a/test/api_integration/apis/saved_objects/find.ts
+++ b/test/api_integration/apis/saved_objects/find.ts
@@ -338,6 +338,131 @@ export default function ({ getService }: FtrProviderContext) {
});
});
+ describe('`has_no_reference` and `has_no_reference_operator` parameters', () => {
+ before(async () => {
+ await kibanaServer.importExport.load(
+ 'test/api_integration/fixtures/kbn_archiver/saved_objects/references.json',
+ { space: SPACE_ID }
+ );
+ });
+ after(async () => {
+ await kibanaServer.importExport.unload(
+ 'test/api_integration/fixtures/kbn_archiver/saved_objects/references.json',
+ { space: SPACE_ID }
+ );
+ });
+
+ it('search for objects not containing a reference', async () => {
+ await supertest
+ .get(`/s/${SPACE_ID}/api/saved_objects/_find`)
+ .query({
+ type: 'visualization',
+ has_no_reference: JSON.stringify({ type: 'ref-type', id: 'ref-1' }),
+ })
+ .expect(200)
+ .then((resp) => {
+ const objects = resp.body.saved_objects;
+ const ids = objects.map((obj: SavedObject) => obj.id);
+ expect(ids).to.contain('only-ref-2');
+ expect(ids).to.contain('only-ref-3');
+ expect(ids).not.to.contain('only-ref-1');
+ expect(ids).not.to.contain('ref-1-and-ref-2');
+ });
+ });
+
+ it('search for multiple references with OR operator', async () => {
+ await supertest
+ .get(`/s/${SPACE_ID}/api/saved_objects/_find`)
+ .query({
+ type: 'visualization',
+ has_no_reference: JSON.stringify([
+ { type: 'ref-type', id: 'ref-1' },
+ { type: 'ref-type', id: 'ref-2' },
+ ]),
+ has_no_reference_operator: 'OR',
+ })
+ .expect(200)
+ .then((resp) => {
+ const objects = resp.body.saved_objects;
+ const ids = objects.map((obj: SavedObject) => obj.id);
+
+ expect(ids).to.contain('only-ref-3');
+ expect(ids).not.to.contain('only-ref-1');
+ expect(ids).not.to.contain('only-ref-2');
+ expect(ids).not.to.contain('ref-1-and-ref-2');
+ });
+ });
+
+ it('search for multiple references with AND operator', async () => {
+ await supertest
+ .get(`/s/${SPACE_ID}/api/saved_objects/_find`)
+ .query({
+ type: 'visualization',
+ has_no_reference: JSON.stringify([
+ { type: 'ref-type', id: 'ref-1' },
+ { type: 'ref-type', id: 'ref-2' },
+ ]),
+ has_no_reference_operator: 'AND',
+ })
+ .expect(200)
+ .then((resp) => {
+ const objects = resp.body.saved_objects;
+ const ids = objects.map((obj: SavedObject) => obj.id);
+ expect(ids).to.contain('only-ref-1');
+ expect(ids).to.contain('only-ref-2');
+ expect(ids).to.contain('only-ref-3');
+ expect(ids).not.to.contain('ref-1-and-ref-2');
+ });
+ });
+ });
+
+ describe('with both `has_reference` and `has_no_reference` parameters', () => {
+ before(async () => {
+ await kibanaServer.importExport.load(
+ 'test/api_integration/fixtures/kbn_archiver/saved_objects/references.json',
+ { space: SPACE_ID }
+ );
+ });
+ after(async () => {
+ await kibanaServer.importExport.unload(
+ 'test/api_integration/fixtures/kbn_archiver/saved_objects/references.json',
+ { space: SPACE_ID }
+ );
+ });
+
+ it('search for objects containing a reference and excluding another reference', async () => {
+ await supertest
+ .get(`/s/${SPACE_ID}/api/saved_objects/_find`)
+ .query({
+ type: 'visualization',
+ has_reference: JSON.stringify({ type: 'ref-type', id: 'ref-1' }),
+ has_no_reference: JSON.stringify({ type: 'ref-type', id: 'ref-2' }),
+ })
+ .expect(200)
+ .then((resp) => {
+ const objects = resp.body.saved_objects;
+ const ids = objects.map((obj: SavedObject) => obj.id);
+ expect(ids).to.eql(['only-ref-1']);
+ });
+ });
+
+ it('search for objects with same reference passed to `has_reference` and `has_no_reference`', async () => {
+ await supertest
+ .get(`/s/${SPACE_ID}/api/saved_objects/_find`)
+ .query({
+ type: 'visualization',
+ has_reference: JSON.stringify({ type: 'ref-type', id: 'ref-1' }),
+ has_no_reference: JSON.stringify({ type: 'ref-type', id: 'ref-1' }),
+ })
+ .expect(200)
+ .then((resp) => {
+ const objects = resp.body.saved_objects;
+ const ids = objects.map((obj: SavedObject) => obj.id);
+ expect(ids).to.eql([]);
+ });
+ });
+ });
+
describe('searching for special characters', () => {
before(async () => {
await kibanaServer.importExport.load(
diff --git a/test/plugin_functional/config.ts b/test/plugin_functional/config.ts
index b2dbc762ab657..750da63e27d1c 100644
--- a/test/plugin_functional/config.ts
+++ b/test/plugin_functional/config.ts
@@ -60,6 +60,14 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) {
'--corePluginDeprecations.noLongerUsed=still_using',
// for testing set buffer duration to 0 to immediately flush counters into saved objects.
'--usageCollection.usageCounters.bufferDuration=0',
+ // explicitly enable the cloud integration plugins to validate the rendered config keys
+ '--xpack.cloud_integrations.chat.enabled=true',
+ '--xpack.cloud_integrations.chat.chatURL=a_string',
+ '--xpack.cloud_integrations.experiments.enabled=true',
+ '--xpack.cloud_integrations.experiments.launch_darkly.sdk_key=a_string',
+ '--xpack.cloud_integrations.experiments.launch_darkly.client_id=a_string',
+ '--xpack.cloud_integrations.full_story.enabled=true',
+ '--xpack.cloud_integrations.full_story.org_id=a_string',
...plugins.map(
(pluginDir) => `--plugin-path=${path.resolve(__dirname, 'plugins', pluginDir)}`
),
diff --git a/test/plugin_functional/test_suites/core_plugins/rendering.ts b/test/plugin_functional/test_suites/core_plugins/rendering.ts
index cbc98ec7bb07b..4633a374ee9d5 100644
--- a/test/plugin_functional/test_suites/core_plugins/rendering.ts
+++ b/test/plugin_functional/test_suites/core_plugins/rendering.ts
@@ -171,14 +171,17 @@ export default function ({ getService }: PluginFunctionalProviderContext) {
'xpack.cases.markdownPlugins.lens (boolean)',
'xpack.ccr.ui.enabled (boolean)',
'xpack.cloud.base_url (string)',
- 'xpack.cloud.chat.chatURL (string)',
- 'xpack.cloud.chat.enabled (boolean)',
'xpack.cloud.cname (string)',
'xpack.cloud.deployment_url (string)',
- 'xpack.cloud.full_story.enabled (boolean)',
- 'xpack.cloud.full_story.org_id (any)',
+ 'xpack.cloud_integrations.chat.chatURL (string)',
+ // No PII. This is an escape patch to override LaunchDarkly's flag resolution mechanism for testing or quick fix.
+ 'xpack.cloud_integrations.experiments.flag_overrides (record)',
+ // Commented because it's inside a schema conditional, and the test is not able to resolve it. But it's shared.
+ // Added here for documentation purposes.
+ // 'xpack.cloud_integrations.experiments.launch_darkly.client_id (string)',
+ 'xpack.cloud_integrations.full_story.org_id (any)',
// No PII. Just the list of event types we want to forward to FullStory.
- 'xpack.cloud.full_story.eventTypesAllowlist (array)',
+ 'xpack.cloud_integrations.full_story.eventTypesAllowlist (array)',
'xpack.cloud.id (string)',
'xpack.cloud.organization_url (string)',
'xpack.cloud.profile_url (string)',
diff --git a/tsconfig.base.json b/tsconfig.base.json
index b62beb6650448..3054a36f2bb86 100644
--- a/tsconfig.base.json
+++ b/tsconfig.base.json
@@ -313,8 +313,14 @@
"@kbn/canvas-plugin/*": ["x-pack/plugins/canvas/*"],
"@kbn/cases-plugin": ["x-pack/plugins/cases"],
"@kbn/cases-plugin/*": ["x-pack/plugins/cases/*"],
+ "@kbn/cloud-chat-plugin": ["x-pack/plugins/cloud_integrations/cloud_chat"],
+ "@kbn/cloud-chat-plugin/*": ["x-pack/plugins/cloud_integrations/cloud_chat/*"],
"@kbn/cloud-experiments-plugin": ["x-pack/plugins/cloud_integrations/cloud_experiments"],
"@kbn/cloud-experiments-plugin/*": ["x-pack/plugins/cloud_integrations/cloud_experiments/*"],
+ "@kbn/cloud-full-story-plugin": ["x-pack/plugins/cloud_integrations/cloud_full_story"],
+ "@kbn/cloud-full-story-plugin/*": ["x-pack/plugins/cloud_integrations/cloud_full_story/*"],
+ "@kbn/cloud-links-plugin": ["x-pack/plugins/cloud_integrations/cloud_links"],
+ "@kbn/cloud-links-plugin/*": ["x-pack/plugins/cloud_integrations/cloud_links/*"],
"@kbn/cloud-security-posture-plugin": ["x-pack/plugins/cloud_security_posture"],
"@kbn/cloud-security-posture-plugin/*": ["x-pack/plugins/cloud_security_posture/*"],
"@kbn/cloud-plugin": ["x-pack/plugins/cloud"],
diff --git a/x-pack/.i18nrc.json b/x-pack/.i18nrc.json
index 83466ba749605..4f89798c71faf 100644
--- a/x-pack/.i18nrc.json
+++ b/x-pack/.i18nrc.json
@@ -10,6 +10,8 @@
"xpack.canvas": "plugins/canvas",
"xpack.cases": "plugins/cases",
"xpack.cloud": "plugins/cloud",
+ "xpack.cloudChat": "plugins/cloud_integrations/cloud_chat",
+ "xpack.cloudLinks": "plugins/cloud_integrations/cloud_links",
"xpack.csp": "plugins/cloud_security_posture",
"xpack.dashboard": "plugins/dashboard_enhanced",
"xpack.discover": "plugins/discover_enhanced",
diff --git a/x-pack/plugins/actions/server/actions_config.test.ts b/x-pack/plugins/actions/server/actions_config.test.ts
index a6b68d907cb44..b1af4a843b496 100644
--- a/x-pack/plugins/actions/server/actions_config.test.ts
+++ b/x-pack/plugins/actions/server/actions_config.test.ts
@@ -43,6 +43,15 @@ const defaultActionsConfig: ActionsConfig = {
};
describe('ensureUriAllowed', () => {
+ test('throws an error when the Uri is an empty string', () => {
+ const config: ActionsConfig = defaultActionsConfig;
+ expect(() =>
+ getActionsConfigurationUtilities(config).ensureUriAllowed('')
+ ).toThrowErrorMatchingInlineSnapshot(
+ `"target url \\"\\" is not added to the Kibana config xpack.actions.allowedHosts"`
+ );
+ });
+
test('returns true when "any" hostnames are allowed', () => {
const config: ActionsConfig = {
...defaultActionsConfig,
diff --git a/x-pack/plugins/actions/server/index.ts b/x-pack/plugins/actions/server/index.ts
index 7f9b45c368e90..1c7a66978ffb3 100644
--- a/x-pack/plugins/actions/server/index.ts
+++ b/x-pack/plugins/actions/server/index.ts
@@ -139,3 +139,5 @@ export const config: PluginConfigDescriptor = {
},
],
};
+
+export { urlAllowListValidator } from './sub_action_framework/helpers';
diff --git a/x-pack/plugins/actions/server/sub_action_framework/README.md b/x-pack/plugins/actions/server/sub_action_framework/README.md
index 90951692f5457..7c2ab0755a0ad 100644
--- a/x-pack/plugins/actions/server/sub_action_framework/README.md
+++ b/x-pack/plugins/actions/server/sub_action_framework/README.md
@@ -6,6 +6,7 @@ The Kibana actions plugin provides a framework to create executable actions that
- Register a sub action and map it to a function of your choice.
- Define a schema for the parameters of your sub action.
+- Define custom validators (or use the provided helpers) for the parameters of your sub action.
- Define a response schema for responses from external services.
- Create connectors that are supported by the Cases management system.
@@ -353,4 +354,19 @@ plugins.actions.registerSubActionConnectorType({
});
```
-You can see a full example in [x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/sub_action_connector.ts](../../../../test/alerting_api_integration/common/fixtures/plugins/alerts/server/sub_action_connector.ts)
\ No newline at end of file
+You can see a full example in [x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/sub_action_connector.ts](../../../../test/alerting_api_integration/common/fixtures/plugins/alerts/server/sub_action_connector.ts)
+
+### Example: Register sub action connector with custom validators
+
+The sub actions framework allows custom validators during registration of the connector type. Below is an example of including the URL validation for the `TestSubActionConnector` `url` configuration field.
+
+```typescript
+plugins.actions.registerSubActionConnectorType({
+ id: '.test-sub-action-connector',
+ name: 'Test: Sub action connector',
+ minimumLicenseRequired: 'platinum' as const,
+ schema: { config: TestConfigSchema, secrets: TestSecretsSchema },
+ validators: [{type: ValidatorType.CONFIG, validate: urlAllowListValidator('url')}]
+ Service: TestSubActionConnector,
+});
+```
diff --git a/x-pack/plugins/actions/server/sub_action_framework/helpers/index.ts b/x-pack/plugins/actions/server/sub_action_framework/helpers/index.ts
new file mode 100644
index 0000000000000..c69caff6b0c71
--- /dev/null
+++ b/x-pack/plugins/actions/server/sub_action_framework/helpers/index.ts
@@ -0,0 +1,8 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+export { urlAllowListValidator } from './validators';
diff --git a/x-pack/plugins/actions/server/sub_action_framework/helpers/validators.ts b/x-pack/plugins/actions/server/sub_action_framework/helpers/validators.ts
new file mode 100644
index 0000000000000..7618fef0f3ea4
--- /dev/null
+++ b/x-pack/plugins/actions/server/sub_action_framework/helpers/validators.ts
@@ -0,0 +1,30 @@
+/*
+ * 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 { i18n } from '@kbn/i18n';
+import { get } from 'lodash';
+import { ValidatorServices } from '../../types';
+
+export const urlAllowListValidator = (urlKey: string) => {
+ return (obj: T, validatorServices: ValidatorServices) => {
+ const { configurationUtilities } = validatorServices;
+ try {
+ const url = get(obj, urlKey, '');
+
+ configurationUtilities.ensureUriAllowed(url);
+ } catch (allowListError) {
+ throw new Error(
+ i18n.translate('xpack.actions.subActionsFramework.urlValidationError', {
+ defaultMessage: 'error validating url: {message}',
+ values: {
+ message: allowListError.message,
+ },
+ })
+ );
+ }
+ };
+};
diff --git a/x-pack/plugins/actions/server/sub_action_framework/types.ts b/x-pack/plugins/actions/server/sub_action_framework/types.ts
index cdc05524cf842..f584d73d24443 100644
--- a/x-pack/plugins/actions/server/sub_action_framework/types.ts
+++ b/x-pack/plugins/actions/server/sub_action_framework/types.ts
@@ -10,7 +10,7 @@ import { Logger } from '@kbn/logging';
import type { LicenseType } from '@kbn/licensing-plugin/common/types';
import { ActionsConfigurationUtilities } from '../actions_config';
-import { ActionTypeParams, Services } from '../types';
+import { ActionTypeParams, Services, ValidatorType as ValidationSchema } from '../types';
import { SubActionConnector } from './sub_action_connector';
export interface ServiceParams {
@@ -34,6 +34,29 @@ export type IServiceAbstract = abstract new (
params: ServiceParams
) => SubActionConnector;
+export enum ValidatorType {
+ CONFIG,
+ SECRETS,
+}
+
+interface Validate {
+ validator: ValidateFn;
+}
+
+export type ValidateFn = NonNullable['customValidator']>;
+
+interface ConfigValidator extends Validate {
+ type: ValidatorType.CONFIG;
+}
+
+interface SecretsValidator extends Validate {
+ type: ValidatorType.SECRETS;
+}
+
+export type Validators = Array<
+ ConfigValidator | SecretsValidator
+>;
+
export interface SubActionConnectorType {
id: string;
name: string;
@@ -43,6 +66,7 @@ export interface SubActionConnectorType {
config: Type;
secrets: Type;
};
+ validators?: Array | SecretsValidator>;
Service: IService;
}
diff --git a/x-pack/plugins/actions/server/sub_action_framework/validators.test.ts b/x-pack/plugins/actions/server/sub_action_framework/validators.test.ts
index 6cae35141b498..b28adc0b545bf 100644
--- a/x-pack/plugins/actions/server/sub_action_framework/validators.test.ts
+++ b/x-pack/plugins/actions/server/sub_action_framework/validators.test.ts
@@ -14,7 +14,7 @@ import {
TestSecrets,
TestSubActionConnector,
} from './mocks';
-import { IService } from './types';
+import { IService, SubActionConnectorType, ValidatorType } from './types';
import { buildValidators } from './validators';
describe('Validators', () => {
@@ -36,6 +36,39 @@ describe('Validators', () => {
return buildValidators({ configurationUtilities: mockedActionsConfig, connector });
};
+ const createValidatorWithCustomValidation = (Service: IService) => {
+ const configValidator = jest.fn();
+ const secretsValidator = jest.fn();
+
+ const connector: SubActionConnectorType = {
+ id: '.test',
+ name: 'Test',
+ minimumLicenseRequired: 'basic' as const,
+ supportedFeatureIds: ['alerting'],
+ schema: {
+ config: TestConfigSchema,
+ secrets: TestSecretsSchema,
+ },
+ validators: [
+ {
+ type: ValidatorType.CONFIG,
+ validator: configValidator,
+ },
+ {
+ type: ValidatorType.SECRETS,
+ validator: secretsValidator,
+ },
+ ],
+ Service,
+ };
+
+ return {
+ validators: buildValidators({ configurationUtilities: mockedActionsConfig, connector }),
+ configValidator,
+ secretsValidator,
+ };
+ };
+
beforeEach(() => {
jest.resetAllMocks();
jest.clearAllMocks();
@@ -96,4 +129,28 @@ describe('Validators', () => {
const { params } = validator;
expect(() => params.schema.validate({ subAction, subActionParams: {} })).toThrow();
});
+
+ it('calls the config and secrets custom validator functions', () => {
+ const validator = createValidatorWithCustomValidation(TestSubActionConnector);
+
+ validator.validators.config.customValidator?.(
+ { url: 'http://www.example.com' },
+ { configurationUtilities: mockedActionsConfig }
+ );
+
+ validator.validators.secrets.customValidator?.(
+ { password: '123', username: 'sam' },
+ { configurationUtilities: mockedActionsConfig }
+ );
+
+ expect(validator.configValidator).toHaveBeenCalledWith(
+ { url: 'http://www.example.com' },
+ expect.anything()
+ );
+
+ expect(validator.secretsValidator).toHaveBeenCalledWith(
+ { password: '123', username: 'sam' },
+ expect.anything()
+ );
+ });
});
diff --git a/x-pack/plugins/actions/server/sub_action_framework/validators.ts b/x-pack/plugins/actions/server/sub_action_framework/validators.ts
index be6dafed28163..e9cbbb3ae8f80 100644
--- a/x-pack/plugins/actions/server/sub_action_framework/validators.ts
+++ b/x-pack/plugins/actions/server/sub_action_framework/validators.ts
@@ -7,8 +7,8 @@
import { schema } from '@kbn/config-schema';
import { ActionsConfigurationUtilities } from '../actions_config';
-import { ActionTypeConfig, ActionTypeSecrets } from '../types';
-import { SubActionConnectorType } from './types';
+import { ActionTypeConfig, ActionTypeSecrets, ValidatorServices } from '../types';
+import { SubActionConnectorType, ValidateFn, Validators, ValidatorType } from './types';
export const buildValidators = <
Config extends ActionTypeConfig,
@@ -20,12 +20,16 @@ export const buildValidators = <
configurationUtilities: ActionsConfigurationUtilities;
connector: SubActionConnectorType;
}) => {
+ const { config, secrets } = buildCustomValidators(connector.validators);
+
return {
config: {
schema: connector.schema.config,
+ customValidator: config,
},
secrets: {
schema: connector.schema.secrets,
+ customValidator: secrets,
},
params: {
schema: schema.object({
@@ -42,3 +46,35 @@ export const buildValidators = <
},
};
};
+
+const buildCustomValidators = (validators?: Validators) => {
+ const partitionedValidators: {
+ config: Array>;
+ secrets: Array>;
+ } = { config: [], secrets: [] };
+
+ for (const validatorInfo of validators ?? []) {
+ if (validatorInfo.type === ValidatorType.CONFIG) {
+ partitionedValidators.config.push(validatorInfo.validator);
+ } else {
+ partitionedValidators.secrets.push(validatorInfo.validator);
+ }
+ }
+
+ return {
+ config: createCustomValidatorFunction(partitionedValidators.config),
+ secrets: createCustomValidatorFunction(partitionedValidators.secrets),
+ };
+};
+
+const createCustomValidatorFunction = (validators: Array>) => {
+ if (validators.length <= 0) {
+ return;
+ }
+
+ return (value: T, validatorServices: ValidatorServices) => {
+ for (const validate of validators) {
+ validate(value, validatorServices);
+ }
+ };
+};
diff --git a/x-pack/plugins/actions/server/types.ts b/x-pack/plugins/actions/server/types.ts
index ae344d4f62dbc..c92761ad0a288 100644
--- a/x-pack/plugins/actions/server/types.ts
+++ b/x-pack/plugins/actions/server/types.ts
@@ -91,7 +91,7 @@ export type ExecutorType = (
options: ActionTypeExecutorOptions
) => Promise>;
-interface ValidatorType {
+export interface ValidatorType {
schema: {
validate(value: unknown): Type;
};
diff --git a/x-pack/plugins/aiops/common/api/explain_log_rate_spikes/actions.ts b/x-pack/plugins/aiops/common/api/explain_log_rate_spikes/actions.ts
index e050946a489be..7c4e3a47f8b79 100644
--- a/x-pack/plugins/aiops/common/api/explain_log_rate_spikes/actions.ts
+++ b/x-pack/plugins/aiops/common/api/explain_log_rate_spikes/actions.ts
@@ -18,6 +18,7 @@ export const API_ACTION_NAME = {
ADD_CHANGE_POINTS_GROUP: 'add_change_point_group',
ADD_CHANGE_POINTS_GROUP_HISTOGRAM: 'add_change_point_group_histogram',
ADD_ERROR: 'add_error',
+ PING: 'ping',
RESET: 'reset',
UPDATE_LOADING_STATE: 'update_loading_state',
} as const;
@@ -89,6 +90,14 @@ export function addErrorAction(payload: ApiActionAddError['payload']): ApiAction
};
}
+interface ApiActionPing {
+ type: typeof API_ACTION_NAME.PING;
+}
+
+export function pingAction(): ApiActionPing {
+ return { type: API_ACTION_NAME.PING };
+}
+
interface ApiActionReset {
type: typeof API_ACTION_NAME.RESET;
}
@@ -121,5 +130,6 @@ export type AiopsExplainLogRateSpikesApiAction =
| ApiActionAddChangePointsHistogram
| ApiActionAddChangePointsGroupHistogram
| ApiActionAddError
+ | ApiActionPing
| ApiActionReset
| ApiActionUpdateLoadingState;
diff --git a/x-pack/plugins/aiops/common/api/explain_log_rate_spikes/index.ts b/x-pack/plugins/aiops/common/api/explain_log_rate_spikes/index.ts
index 5628b509980ad..c092b34c8b2b6 100644
--- a/x-pack/plugins/aiops/common/api/explain_log_rate_spikes/index.ts
+++ b/x-pack/plugins/aiops/common/api/explain_log_rate_spikes/index.ts
@@ -11,6 +11,7 @@ export {
addChangePointsGroupHistogramAction,
addChangePointsHistogramAction,
addErrorAction,
+ pingAction,
resetAction,
updateLoadingStateAction,
API_ACTION_NAME,
diff --git a/x-pack/plugins/aiops/public/components/explain_log_rate_spikes/explain_log_rate_spikes_analysis.tsx b/x-pack/plugins/aiops/public/components/explain_log_rate_spikes/explain_log_rate_spikes_analysis.tsx
index 2425161615915..9949ec537b77a 100644
--- a/x-pack/plugins/aiops/public/components/explain_log_rate_spikes/explain_log_rate_spikes_analysis.tsx
+++ b/x-pack/plugins/aiops/public/components/explain_log_rate_spikes/explain_log_rate_spikes_analysis.tsx
@@ -172,6 +172,33 @@ export const ExplainLogRateSpikesAnalysis: FC
onCancel={cancel}
shouldRerunAnalysis={shouldRerunAnalysis}
/>
+ {errors.length > 0 ? (
+ <>
+
+
+ {errors.length === 1 ? (
+ {errors[0]}
+ ) : (
+
+ {errors.map((e, i) => (
+ - {e}
+ ))}
+
+ )}
+
+
+
+ >
+ ) : null}
{showSpikeAnalysisTable && foundGroups && (
}
/>
)}
- {errors.length > 0 && (
- <>
-
-
- {errors.length === 1 ? (
- {errors[0]}
- ) : (
-
- {errors.map((e, i) => (
- - {e}
- ))}
-
- )}
-
-
-
- >
- )}
{showSpikeAnalysisTable && groupResults && foundGroups ? (
{
+ logInfoMessage('aborted$ subscription trigger.');
shouldStop = true;
controller.abort();
});
request.events.completed$.subscribe(() => {
+ logInfoMessage('completed$ subscription trigger.');
shouldStop = true;
controller.abort();
});
- const { end, push, responseWithHeaders } = streamFactory(
- request.headers,
- logger,
- true
- );
+ const {
+ end: streamEnd,
+ push,
+ responseWithHeaders,
+ } = streamFactory(request.headers, logger, true);
+
+ function pushPing() {
+ push(pingAction());
+ }
+
+ const pingInterval = setInterval(pushPing, 1000);
+
+ function end() {
+ logInfoMessage('Ending analysis.');
+ clearInterval(pingInterval);
+ streamEnd();
+ }
function endWithUpdatedLoadingState() {
push(
@@ -114,9 +138,16 @@ export const defineExplainLogRateSpikesRoute = (
end();
}
+ function pushError(m: string) {
+ logInfoMessage('Push error.');
+ push(addErrorAction(m));
+ }
+
// Async IIFE to run the analysis while not blocking returning `responseWithHeaders`.
(async () => {
+ logInfoMessage('Reset.');
push(resetAction());
+ logInfoMessage('Load field candidates.');
push(
updateLoadingStateAction({
ccsWarning: false,
@@ -134,7 +165,8 @@ export const defineExplainLogRateSpikesRoute = (
try {
fieldCandidates = await fetchFieldCandidates(client, request.body);
} catch (e) {
- push(addErrorAction(e.toString()));
+ logger.error(`Failed to fetch field candidates, got: \n${e.toString()}`);
+ pushError(`Failed to fetch field candidates.`);
end();
return;
}
@@ -168,17 +200,33 @@ export const defineExplainLogRateSpikesRoute = (
const changePoints: ChangePoint[] = [];
const fieldsToSample = new Set();
const chunkSize = 10;
+ let chunkCount = 0;
const fieldCandidatesChunks = chunk(fieldCandidates, chunkSize);
+ logInfoMessage('Fetch p-values.');
+
for (const fieldCandidatesChunk of fieldCandidatesChunks) {
+ chunkCount++;
+ logInfoMessage(`Fetch p-values. Chunk ${chunkCount} of ${fieldCandidatesChunks.length}`);
let pValues: Awaited>;
try {
- pValues = await fetchChangePointPValues(client, request.body, fieldCandidatesChunk);
+ pValues = await fetchChangePointPValues(
+ client,
+ request.body,
+ fieldCandidatesChunk,
+ logger,
+ pushError
+ );
} catch (e) {
- push(addErrorAction(e.toString()));
- end();
- return;
+ logger.error(
+ `Failed to fetch p-values for ${JSON.stringify(
+ fieldCandidatesChunk
+ )}, got: \n${e.toString()}`
+ );
+ pushError(`Failed to fetch p-values for ${JSON.stringify(fieldCandidatesChunk)}.`);
+ // Still continue the analysis even if chunks of p-value queries fail.
+ continue;
}
if (pValues.length > 0) {
@@ -210,12 +258,15 @@ export const defineExplainLogRateSpikesRoute = (
);
if (shouldStop) {
+ logInfoMessage('shouldStop fetching p-values.');
+
end();
return;
}
}
if (changePoints?.length === 0) {
+ logInfoMessage('Stopping analysis, did not find change points.');
endWithUpdatedLoadingState();
return;
}
@@ -224,16 +275,27 @@ export const defineExplainLogRateSpikesRoute = (
{ fieldName: request.body.timeFieldName, type: KBN_FIELD_TYPES.DATE },
];
- const [overallTimeSeries] = (await fetchHistogramsForFields(
- client,
- request.body.index,
- { match_all: {} },
- // fields
- histogramFields,
- // samplerShardSize
- -1,
- undefined
- )) as [NumericChartData];
+ logInfoMessage('Fetch overall histogram.');
+
+ let overallTimeSeries: NumericChartData | undefined;
+ try {
+ overallTimeSeries = (
+ (await fetchHistogramsForFields(
+ client,
+ request.body.index,
+ { match_all: {} },
+ // fields
+ histogramFields,
+ // samplerShardSize
+ -1,
+ undefined
+ )) as [NumericChartData]
+ )[0];
+ } catch (e) {
+ logger.error(`Failed to fetch the overall histogram data, got: \n${e.toString()}`);
+ pushError(`Failed to fetch overall histogram data.`);
+ // Still continue the analysis even if loading the overall histogram fails.
+ }
function pushHistogramDataLoadingState() {
push(
@@ -251,6 +313,8 @@ export const defineExplainLogRateSpikesRoute = (
}
if (groupingEnabled) {
+ logInfoMessage('Group results.');
+
push(
updateLoadingStateAction({
ccsWarning: false,
@@ -283,208 +347,242 @@ export const defineExplainLogRateSpikesRoute = (
(g) => g.group.length > 1
);
- const { fields, df } = await fetchFrequentItems(
- client,
- request.body.index,
- JSON.parse(request.body.searchQuery) as estypes.QueryDslQueryContainer,
- deduplicatedChangePoints,
- request.body.timeFieldName,
- request.body.deviationMin,
- request.body.deviationMax
- );
-
- // The way the `frequent_items` aggregations works could return item sets that include
- // field/value pairs that are not part of the original list of significant change points.
- // This cleans up groups and removes those unrelated field/value pairs.
- const filteredDf = df
- .map((fi) => {
- fi.set = Object.entries(fi.set).reduce(
- (set, [field, value]) => {
- if (
- changePoints.some((cp) => cp.fieldName === field && cp.fieldValue === value)
- ) {
- set[field] = value;
+ try {
+ const { fields, df } = await fetchFrequentItems(
+ client,
+ request.body.index,
+ JSON.parse(request.body.searchQuery) as estypes.QueryDslQueryContainer,
+ deduplicatedChangePoints,
+ request.body.timeFieldName,
+ request.body.deviationMin,
+ request.body.deviationMax,
+ logger,
+ pushError
+ );
+
+ if (fields.length > 0 && df.length > 0) {
+ // The way the `frequent_items` aggregations works could return item sets that include
+ // field/value pairs that are not part of the original list of significant change points.
+ // This cleans up groups and removes those unrelated field/value pairs.
+ const filteredDf = df
+ .map((fi) => {
+ fi.set = Object.entries(fi.set).reduce(
+ (set, [field, value]) => {
+ if (
+ changePoints.some((cp) => cp.fieldName === field && cp.fieldValue === value)
+ ) {
+ set[field] = value;
+ }
+ return set;
+ },
+ {}
+ );
+ fi.size = Object.keys(fi.set).length;
+ return fi;
+ })
+ .filter((fi) => fi.size > 1);
+
+ // `frequent_items` returns lot of different small groups of field/value pairs that co-occur.
+ // The following steps analyse these small groups, identify overlap between these groups,
+ // and then summarize them in larger groups where possible.
+
+ // Get a tree structure based on `frequent_items`.
+ const { root } = getSimpleHierarchicalTree(filteredDf, true, false, fields);
+
+ // Each leave of the tree will be a summarized group of co-occuring field/value pairs.
+ const treeLeaves = getSimpleHierarchicalTreeLeaves(root, []);
+
+ // To be able to display a more cleaned up results table in the UI, we identify field/value pairs
+ // that occur in multiple groups. This will allow us to highlight field/value pairs that are
+ // unique to a group in a better way. This step will also re-add duplicates we identified in the
+ // beginning and didn't pass on to the `frequent_items` agg.
+ const fieldValuePairCounts = getFieldValuePairCounts(treeLeaves);
+ const changePointGroups = markDuplicates(treeLeaves, fieldValuePairCounts).map(
+ (g) => {
+ const group = [...g.group];
+
+ for (const groupItem of g.group) {
+ const { duplicate } = groupItem;
+ const duplicates = groupedChangePoints.find((d) =>
+ d.group.some(
+ (dg) =>
+ dg.fieldName === groupItem.fieldName &&
+ dg.fieldValue === groupItem.fieldValue
+ )
+ );
+
+ if (duplicates !== undefined) {
+ group.push(
+ ...duplicates.group.map((d) => {
+ return {
+ fieldName: d.fieldName,
+ fieldValue: d.fieldValue,
+ duplicate,
+ };
+ })
+ );
+ }
}
- return set;
- },
- {}
- );
- fi.size = Object.keys(fi.set).length;
- return fi;
- })
- .filter((fi) => fi.size > 1);
-
- // `frequent_items` returns lot of different small groups of field/value pairs that co-occur.
- // The following steps analyse these small groups, identify overlap between these groups,
- // and then summarize them in larger groups where possible.
-
- // Get a tree structure based on `frequent_items`.
- const { root } = getSimpleHierarchicalTree(filteredDf, true, false, fields);
-
- // Each leave of the tree will be a summarized group of co-occuring field/value pairs.
- const treeLeaves = getSimpleHierarchicalTreeLeaves(root, []);
-
- // To be able to display a more cleaned up results table in the UI, we identify field/value pairs
- // that occur in multiple groups. This will allow us to highlight field/value pairs that are
- // unique to a group in a better way. This step will also re-add duplicates we identified in the
- // beginning and didn't pass on to the `frequent_items` agg.
- const fieldValuePairCounts = getFieldValuePairCounts(treeLeaves);
- const changePointGroups = markDuplicates(treeLeaves, fieldValuePairCounts).map((g) => {
- const group = [...g.group];
-
- for (const groupItem of g.group) {
- const { duplicate } = groupItem;
- const duplicates = groupedChangePoints.find((d) =>
- d.group.some(
- (dg) =>
- dg.fieldName === groupItem.fieldName && dg.fieldValue === groupItem.fieldValue
- )
- );
-
- if (duplicates !== undefined) {
- group.push(
- ...duplicates.group.map((d) => {
- return {
- fieldName: d.fieldName,
- fieldValue: d.fieldValue,
- duplicate,
- };
- })
- );
- }
- }
- return {
- ...g,
- group,
- };
- });
-
- // Some field/value pairs might not be part of the `frequent_items` result set, for example
- // because they don't co-occur with other field/value pairs or because of the limits we set on the query.
- // In this next part we identify those missing pairs and add them as individual groups.
- const missingChangePoints = deduplicatedChangePoints.filter((cp) => {
- return !changePointGroups.some((cpg) => {
- return cpg.group.some(
- (d) => d.fieldName === cp.fieldName && d.fieldValue === cp.fieldValue
+ return {
+ ...g,
+ group,
+ };
+ }
);
- });
- });
- changePointGroups.push(
- ...missingChangePoints.map(({ fieldName, fieldValue, doc_count: docCount, pValue }) => {
- const duplicates = groupedChangePoints.find((d) =>
- d.group.some((dg) => dg.fieldName === fieldName && dg.fieldValue === fieldValue)
+ // Some field/value pairs might not be part of the `frequent_items` result set, for example
+ // because they don't co-occur with other field/value pairs or because of the limits we set on the query.
+ // In this next part we identify those missing pairs and add them as individual groups.
+ const missingChangePoints = deduplicatedChangePoints.filter((cp) => {
+ return !changePointGroups.some((cpg) => {
+ return cpg.group.some(
+ (d) => d.fieldName === cp.fieldName && d.fieldValue === cp.fieldValue
+ );
+ });
+ });
+
+ changePointGroups.push(
+ ...missingChangePoints.map(
+ ({ fieldName, fieldValue, doc_count: docCount, pValue }) => {
+ const duplicates = groupedChangePoints.find((d) =>
+ d.group.some(
+ (dg) => dg.fieldName === fieldName && dg.fieldValue === fieldValue
+ )
+ );
+ if (duplicates !== undefined) {
+ return {
+ id: `${stringHash(
+ JSON.stringify(
+ duplicates.group.map((d) => ({
+ fieldName: d.fieldName,
+ fieldValue: d.fieldValue,
+ }))
+ )
+ )}`,
+ group: duplicates.group.map((d) => ({
+ fieldName: d.fieldName,
+ fieldValue: d.fieldValue,
+ duplicate: false,
+ })),
+ docCount,
+ pValue,
+ };
+ } else {
+ return {
+ id: `${stringHash(JSON.stringify({ fieldName, fieldValue }))}`,
+ group: [
+ {
+ fieldName,
+ fieldValue,
+ duplicate: false,
+ },
+ ],
+ docCount,
+ pValue,
+ };
+ }
+ }
+ )
);
- if (duplicates !== undefined) {
- return {
- id: `${stringHash(
- JSON.stringify(
- duplicates.group.map((d) => ({
- fieldName: d.fieldName,
- fieldValue: d.fieldValue,
- }))
- )
- )}`,
- group: duplicates.group.map((d) => ({
- fieldName: d.fieldName,
- fieldValue: d.fieldValue,
- duplicate: false,
- })),
- docCount,
- pValue,
- };
- } else {
- return {
- id: `${stringHash(JSON.stringify({ fieldName, fieldValue }))}`,
- group: [
- {
- fieldName,
- fieldValue,
- duplicate: false,
- },
- ],
- docCount,
- pValue,
- };
- }
- })
- );
-
- // Finally, we'll find out if there's at least one group with at least two items,
- // only then will we return the groups to the clients and make the grouping option available.
- const maxItems = Math.max(...changePointGroups.map((g) => g.group.length));
- if (maxItems > 1) {
- push(addChangePointsGroupAction(changePointGroups));
- }
+ // Finally, we'll find out if there's at least one group with at least two items,
+ // only then will we return the groups to the clients and make the grouping option available.
+ const maxItems = Math.max(...changePointGroups.map((g) => g.group.length));
- loaded += PROGRESS_STEP_GROUPING;
+ if (maxItems > 1) {
+ push(addChangePointsGroupAction(changePointGroups));
+ }
- pushHistogramDataLoadingState();
+ loaded += PROGRESS_STEP_GROUPING;
- if (changePointGroups) {
- await asyncForEach(changePointGroups, async (cpg, index) => {
- const histogramQuery = {
- bool: {
- filter: cpg.group.map((d) => ({
- term: { [d.fieldName]: d.fieldValue },
- })),
- },
- };
+ pushHistogramDataLoadingState();
- const [cpgTimeSeries] = (await fetchHistogramsForFields(
- client,
- request.body.index,
- histogramQuery,
- // fields
- [
- {
- fieldName: request.body.timeFieldName,
- type: KBN_FIELD_TYPES.DATE,
- interval: overallTimeSeries.interval,
- min: overallTimeSeries.stats[0],
- max: overallTimeSeries.stats[1],
- },
- ],
- // samplerShardSize
- -1,
- undefined
- )) as [NumericChartData];
+ logInfoMessage('Fetch group histograms.');
- const histogram =
- overallTimeSeries.data.map((o, i) => {
- const current = cpgTimeSeries.data.find(
- (d1) => d1.key_as_string === o.key_as_string
- ) ?? {
- doc_count: 0,
- };
- return {
- key: o.key,
- key_as_string: o.key_as_string ?? '',
- doc_count_change_point: current.doc_count,
- doc_count_overall: Math.max(0, o.doc_count - current.doc_count),
+ await asyncForEach(changePointGroups, async (cpg) => {
+ if (overallTimeSeries !== undefined) {
+ const histogramQuery = {
+ bool: {
+ filter: cpg.group.map((d) => ({
+ term: { [d.fieldName]: d.fieldValue },
+ })),
+ },
};
- }) ?? [];
- push(
- addChangePointsGroupHistogramAction([
- {
- id: cpg.id,
- histogram,
- },
- ])
- );
- });
+ let cpgTimeSeries: NumericChartData;
+ try {
+ cpgTimeSeries = (
+ (await fetchHistogramsForFields(
+ client,
+ request.body.index,
+ histogramQuery,
+ // fields
+ [
+ {
+ fieldName: request.body.timeFieldName,
+ type: KBN_FIELD_TYPES.DATE,
+ interval: overallTimeSeries.interval,
+ min: overallTimeSeries.stats[0],
+ max: overallTimeSeries.stats[1],
+ },
+ ],
+ // samplerShardSize
+ -1,
+ undefined
+ )) as [NumericChartData]
+ )[0];
+ } catch (e) {
+ logger.error(
+ `Failed to fetch the histogram data for group #${
+ cpg.id
+ }, got: \n${e.toString()}`
+ );
+ pushError(`Failed to fetch the histogram data for group #${cpg.id}.`);
+ return;
+ }
+ const histogram =
+ overallTimeSeries.data.map((o, i) => {
+ const current = cpgTimeSeries.data.find(
+ (d1) => d1.key_as_string === o.key_as_string
+ ) ?? {
+ doc_count: 0,
+ };
+ return {
+ key: o.key,
+ key_as_string: o.key_as_string ?? '',
+ doc_count_change_point: current.doc_count,
+ doc_count_overall: Math.max(0, o.doc_count - current.doc_count),
+ };
+ }) ?? [];
+
+ push(
+ addChangePointsGroupHistogramAction([
+ {
+ id: cpg.id,
+ histogram,
+ },
+ ])
+ );
+ }
+ });
+ }
+ } catch (e) {
+ logger.error(
+ `Failed to transform field/value pairs into groups, got: \n${e.toString()}`
+ );
+ pushError(`Failed to transform field/value pairs into groups.`);
}
}
loaded += PROGRESS_STEP_HISTOGRAMS_GROUPS;
+ logInfoMessage('Fetch field/value histograms.');
+
// time series filtered by fields
- if (changePoints) {
- await asyncForEach(changePoints, async (cp, index) => {
- if (changePoints) {
+ if (changePoints && overallTimeSeries !== undefined) {
+ await asyncForEach(changePoints, async (cp) => {
+ if (overallTimeSeries !== undefined) {
const histogramQuery = {
bool: {
filter: [
@@ -495,24 +593,40 @@ export const defineExplainLogRateSpikesRoute = (
},
};
- const [cpTimeSeries] = (await fetchHistogramsForFields(
- client,
- request.body.index,
- histogramQuery,
- // fields
- [
- {
- fieldName: request.body.timeFieldName,
- type: KBN_FIELD_TYPES.DATE,
- interval: overallTimeSeries.interval,
- min: overallTimeSeries.stats[0],
- max: overallTimeSeries.stats[1],
- },
- ],
- // samplerShardSize
- -1,
- undefined
- )) as [NumericChartData];
+ let cpTimeSeries: NumericChartData;
+
+ try {
+ cpTimeSeries = (
+ (await fetchHistogramsForFields(
+ client,
+ request.body.index,
+ histogramQuery,
+ // fields
+ [
+ {
+ fieldName: request.body.timeFieldName,
+ type: KBN_FIELD_TYPES.DATE,
+ interval: overallTimeSeries.interval,
+ min: overallTimeSeries.stats[0],
+ max: overallTimeSeries.stats[1],
+ },
+ ],
+ // samplerShardSize
+ -1,
+ undefined
+ )) as [NumericChartData]
+ )[0];
+ } catch (e) {
+ logger.error(
+ `Failed to fetch the histogram data for field/value pair "${cp.fieldName}:${
+ cp.fieldValue
+ }", got: \n${e.toString()}`
+ );
+ pushError(
+ `Failed to fetch the histogram data for field/value pair "${cp.fieldName}:${cp.fieldValue}".`
+ );
+ return;
+ }
const histogram =
overallTimeSeries.data.map((o, i) => {
diff --git a/x-pack/plugins/aiops/server/routes/queries/fetch_change_point_p_values.ts b/x-pack/plugins/aiops/server/routes/queries/fetch_change_point_p_values.ts
index 03242a4bc8ae5..0fb7f90c89c12 100644
--- a/x-pack/plugins/aiops/server/routes/queries/fetch_change_point_p_values.ts
+++ b/x-pack/plugins/aiops/server/routes/queries/fetch_change_point_p_values.ts
@@ -8,6 +8,7 @@ import { uniqBy } from 'lodash';
import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import { ElasticsearchClient } from '@kbn/core/server';
+import type { Logger } from '@kbn/logging';
import { ChangePoint } from '@kbn/ml-agg-utils';
import { SPIKE_ANALYSIS_THRESHOLD } from '../../../common/constants';
import type { AiopsExplainLogRateSpikesSchema } from '../../../common/api/explain_log_rate_spikes';
@@ -92,7 +93,9 @@ interface Aggs extends estypes.AggregationsSignificantLongTermsAggregate {
export const fetchChangePointPValues = async (
esClient: ElasticsearchClient,
params: AiopsExplainLogRateSpikesSchema,
- fieldNames: string[]
+ fieldNames: string[],
+ logger: Logger,
+ emitError: (m: string) => void
): Promise => {
const result: ChangePoint[] = [];
@@ -101,7 +104,16 @@ export const fetchChangePointPValues = async (
const resp = await esClient.search(request);
if (resp.aggregations === undefined) {
- throw new Error('fetchChangePoint failed, did not return aggregations.');
+ logger.error(
+ `Failed to fetch p-value aggregation for fieldName "${fieldName}", got: \n${JSON.stringify(
+ resp,
+ null,
+ 2
+ )}`
+ );
+ emitError(`Failed to fetch p-value aggregation for fieldName "${fieldName}".`);
+ // Still continue the analysis even if individual p-value queries fail.
+ continue;
}
const overallResult = resp.aggregations.change_point_p_value;
diff --git a/x-pack/plugins/aiops/server/routes/queries/fetch_frequent_items.ts b/x-pack/plugins/aiops/server/routes/queries/fetch_frequent_items.ts
index 055c22397064f..c9444aaca22af 100644
--- a/x-pack/plugins/aiops/server/routes/queries/fetch_frequent_items.ts
+++ b/x-pack/plugins/aiops/server/routes/queries/fetch_frequent_items.ts
@@ -10,6 +10,7 @@ import { uniq, uniqWith, pick, isEqual } from 'lodash';
import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import type { ElasticsearchClient } from '@kbn/core-elasticsearch-server';
+import type { Logger } from '@kbn/logging';
import type { ChangePoint, FieldValuePair } from '@kbn/ml-agg-utils';
interface FrequentItemsAggregation extends estypes.AggregationsSamplerAggregation {
@@ -53,9 +54,11 @@ export async function fetchFrequentItems(
changePoints: ChangePoint[],
timeFieldName: string,
deviationMin: number,
- deviationMax: number
+ deviationMax: number,
+ logger: Logger,
+ emitError: (m: string) => void
) {
- // get unique fields that are left
+ // get unique fields from change points
const fields = [...new Set(changePoints.map((t) => t.fieldName))];
// TODO add query params
@@ -91,6 +94,8 @@ export async function fetchFrequentItems(
sampleProbability = Math.min(0.5, minDocCount / totalDocCount);
}
+ logger.debug(`frequent_items sample probability: ${sampleProbability}`);
+
// frequent items can be slow, so sample and use 10% min_support
const aggs: Record = {
sample: {
@@ -103,7 +108,7 @@ export async function fetchFrequentItems(
frequent_items: {
minimum_set_size: 2,
size: 200,
- minimum_support: 0.01,
+ minimum_support: 0.1,
fields: aggFields,
},
},
@@ -125,12 +130,18 @@ export async function fetchFrequentItems(
{ maxRetries: 0 }
);
- const totalDocCountFi = (body.hits.total as estypes.SearchTotalHits).value;
-
if (body.aggregations === undefined) {
- throw new Error('fetchFrequentItems failed, did not return aggregations.');
+ logger.error(`Failed to fetch frequent_items, got: \n${JSON.stringify(body, null, 2)}`);
+ emitError(`Failed to fetch frequent_items.`);
+ return {
+ fields: [],
+ df: [],
+ totalDocCount: 0,
+ };
}
+ const totalDocCountFi = (body.hits.total as estypes.SearchTotalHits).value;
+
const shape = body.aggregations.sample.fi.buckets.length;
let maximum = shape;
if (maximum > 50000) {
diff --git a/x-pack/plugins/alerting/server/routes/resolve_rule.ts b/x-pack/plugins/alerting/server/routes/resolve_rule.ts
index cde747f9272fe..b3576c0c5ed44 100644
--- a/x-pack/plugins/alerting/server/routes/resolve_rule.ts
+++ b/x-pack/plugins/alerting/server/routes/resolve_rule.ts
@@ -75,7 +75,7 @@ export const resolveRuleRoute = (
verifyAccessAndContext(licenseState, async function (context, req, res) {
const rulesClient = (await context.alerting).getRulesClient();
const { id } = req.params;
- const rule = await rulesClient.resolve({ id });
+ const rule = await rulesClient.resolve({ id, includeSnoozeData: true });
return res.ok({
body: rewriteBodyRes(rule),
});
diff --git a/x-pack/plugins/alerting/server/rules_client/rules_client.ts b/x-pack/plugins/alerting/server/rules_client/rules_client.ts
index 7374868a11dbb..d08f12f054a50 100644
--- a/x-pack/plugins/alerting/server/rules_client/rules_client.ts
+++ b/x-pack/plugins/alerting/server/rules_client/rules_client.ts
@@ -715,9 +715,11 @@ export class RulesClient {
public async resolve({
id,
includeLegacyId,
+ includeSnoozeData = false,
}: {
id: string;
includeLegacyId?: boolean;
+ includeSnoozeData?: boolean;
}): Promise> {
const { saved_object: result, ...resolveResponse } =
await this.unsecuredSavedObjectsClient.resolve('alert', id);
@@ -750,7 +752,9 @@ export class RulesClient {
result.attributes.alertTypeId,
result.attributes,
result.references,
- includeLegacyId
+ includeLegacyId,
+ false,
+ includeSnoozeData
);
return {
diff --git a/x-pack/plugins/alerting/server/rules_client/tests/resolve.test.ts b/x-pack/plugins/alerting/server/rules_client/tests/resolve.test.ts
index 297c4b6d60fcc..b4a48be3a37fc 100644
--- a/x-pack/plugins/alerting/server/rules_client/tests/resolve.test.ts
+++ b/x-pack/plugins/alerting/server/rules_client/tests/resolve.test.ts
@@ -201,6 +201,58 @@ describe('resolve()', () => {
`);
});
+ test('calls saved objects client with id and includeSnoozeData params', async () => {
+ const rulesClient = new RulesClient(rulesClientParams);
+ unsecuredSavedObjectsClient.resolve.mockResolvedValueOnce({
+ saved_object: {
+ id: '1',
+ type: 'alert',
+ attributes: {
+ legacyId: 'some-legacy-id',
+ alertTypeId: '123',
+ schedule: { interval: '10s' },
+ params: {
+ bar: true,
+ },
+ createdAt: new Date().toISOString(),
+ updatedAt: new Date().toISOString(),
+ snoozeSchedule: [
+ {
+ duration: 10000,
+ rRule: {
+ dtstart: new Date().toISOString(),
+ tzid: 'UTC',
+ count: 1,
+ },
+ },
+ ],
+ muteAll: false,
+ actions: [
+ {
+ group: 'default',
+ actionRef: 'action_0',
+ params: {
+ foo: true,
+ },
+ },
+ ],
+ notifyWhen: 'onActiveAlert',
+ },
+ references: [
+ {
+ name: 'action_0',
+ type: 'action',
+ id: '1',
+ },
+ ],
+ },
+ outcome: 'aliasMatch',
+ alias_target_id: '2',
+ });
+ const result = await rulesClient.resolve({ id: '1', includeSnoozeData: true });
+ expect(result.isSnoozedUntil).toBeTruthy();
+ });
+
test('should call useSavedObjectReferences.injectReferences if defined for rule type', async () => {
const injectReferencesFn = jest.fn().mockReturnValue({
bar: true,
diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/e2e/power_user/feature_flag/comparison.cy.ts b/x-pack/plugins/apm/ftr_e2e/cypress/e2e/power_user/feature_flag/comparison.cy.ts
index d1159efd0fc90..7d40105db192e 100644
--- a/x-pack/plugins/apm/ftr_e2e/cypress/e2e/power_user/feature_flag/comparison.cy.ts
+++ b/x-pack/plugins/apm/ftr_e2e/cypress/e2e/power_user/feature_flag/comparison.cy.ts
@@ -36,19 +36,19 @@ describe('Comparison feature flag', () => {
it('shows the comparison feature enabled in services overview', () => {
cy.visitKibana('/app/apm/services');
cy.get('input[type="checkbox"]#comparison').should('be.checked');
- cy.get('[data-test-subj="comparisonSelect"]').should('not.be.disabled');
+ cy.getByTestSubj('comparisonSelect').should('not.be.disabled');
});
it('shows the comparison feature enabled in dependencies overview', () => {
cy.visitKibana('/app/apm/dependencies');
cy.get('input[type="checkbox"]#comparison').should('be.checked');
- cy.get('[data-test-subj="comparisonSelect"]').should('not.be.disabled');
+ cy.getByTestSubj('comparisonSelect').should('not.be.disabled');
});
it('shows the comparison feature disabled in service map overview page', () => {
cy.visitKibana('/app/apm/service-map');
cy.get('input[type="checkbox"]#comparison').should('be.checked');
- cy.get('[data-test-subj="comparisonSelect"]').should('not.be.disabled');
+ cy.getByTestSubj('comparisonSelect').should('not.be.disabled');
});
});
@@ -71,7 +71,7 @@ describe('Comparison feature flag', () => {
it('shows the comparison feature disabled in services overview', () => {
cy.visitKibana('/app/apm/services');
cy.get('input[type="checkbox"]#comparison').should('not.be.checked');
- cy.get('[data-test-subj="comparisonSelect"]').should('be.disabled');
+ cy.getByTestSubj('comparisonSelect').should('be.disabled');
});
it('shows the comparison feature disabled in dependencies overview page', () => {
@@ -81,13 +81,13 @@ describe('Comparison feature flag', () => {
cy.visitKibana('/app/apm/dependencies');
cy.wait('@topDependenciesRequest');
cy.get('input[type="checkbox"]#comparison').should('not.be.checked');
- cy.get('[data-test-subj="comparisonSelect"]').should('be.disabled');
+ cy.getByTestSubj('comparisonSelect').should('be.disabled');
});
it('shows the comparison feature disabled in service map overview page', () => {
cy.visitKibana('/app/apm/service-map');
cy.get('input[type="checkbox"]#comparison').should('not.be.checked');
- cy.get('[data-test-subj="comparisonSelect"]').should('be.disabled');
+ cy.getByTestSubj('comparisonSelect').should('be.disabled');
});
});
});
diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/e2e/power_user/integration_settings/integration_policy.cy.ts b/x-pack/plugins/apm/ftr_e2e/cypress/e2e/power_user/integration_settings/integration_policy.cy.ts
index c25e6a6800311..5d275770e462d 100644
--- a/x-pack/plugins/apm/ftr_e2e/cypress/e2e/power_user/integration_settings/integration_policy.cy.ts
+++ b/x-pack/plugins/apm/ftr_e2e/cypress/e2e/power_user/integration_settings/integration_policy.cy.ts
@@ -60,21 +60,19 @@ describe('when navigating to integration page', () => {
cy.visitKibana(integrationsPath);
// open integration policy form
- cy.get('[data-test-subj="integration-card:epr:apm:featured').click();
+ cy.getByTestSubj('integration-card:epr:apm:featured').click();
cy.contains('Elastic APM in Fleet').click();
cy.contains('a', 'APM integration').click();
- cy.get('[data-test-subj="addIntegrationPolicyButton"]').click();
+ cy.getByTestSubj('addIntegrationPolicyButton').click();
});
it('checks validators for required fields', () => {
const requiredFields = policyFormFields.filter((field) => field.required);
requiredFields.map((field) => {
- cy.get(`[data-test-subj="${field.selector}"`).clear();
- cy.get('[data-test-subj="createPackagePolicySaveButton"').should(
- 'be.disabled'
- );
- cy.get(`[data-test-subj="${field.selector}"`).type(field.value);
+ cy.getByTestSubj(field.selector).clear();
+ cy.getByTestSubj('createPackagePolicySaveButton').should('be.disabled');
+ cy.getByTestSubj(field.selector).type(field.value);
});
});
diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/e2e/power_user/settings/agent_configurations.cy.ts b/x-pack/plugins/apm/ftr_e2e/cypress/e2e/power_user/settings/agent_configurations.cy.ts
index 5be39b4f082dc..47f8c537b100c 100644
--- a/x-pack/plugins/apm/ftr_e2e/cypress/e2e/power_user/settings/agent_configurations.cy.ts
+++ b/x-pack/plugins/apm/ftr_e2e/cypress/e2e/power_user/settings/agent_configurations.cy.ts
@@ -90,7 +90,7 @@ describe('Agent configuration', () => {
'/api/apm/settings/agent-configuration/environments?*'
).as('serviceEnvironmentApi');
cy.contains('Create configuration').click();
- cy.get('[data-test-subj="serviceNameComboBox"]')
+ cy.getByTestSubj('serviceNameComboBox')
.click()
.type('opbeans-node')
.type('{enter}');
@@ -98,7 +98,7 @@ describe('Agent configuration', () => {
cy.contains('opbeans-node').realClick();
cy.wait('@serviceEnvironmentApi');
- cy.get('[data-test-subj="serviceEnviromentComboBox"]')
+ cy.getByTestSubj('serviceEnviromentComboBox')
.click({ force: true })
.type('prod')
.type('{enter}');
@@ -115,14 +115,11 @@ describe('Agent configuration', () => {
'/api/apm/settings/agent-configuration/environments'
).as('serviceEnvironmentApi');
cy.contains('Create configuration').click();
- cy.get('[data-test-subj="serviceNameComboBox"]')
- .click()
- .type('All')
- .type('{enter}');
+ cy.getByTestSubj('serviceNameComboBox').click().type('All').type('{enter}');
cy.contains('All').realClick();
cy.wait('@serviceEnvironmentApi');
- cy.get('[data-test-subj="serviceEnviromentComboBox"]')
+ cy.getByTestSubj('serviceEnviromentComboBox')
.click({ force: true })
.type('All');
diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/e2e/power_user/settings/custom_links.cy.ts b/x-pack/plugins/apm/ftr_e2e/cypress/e2e/power_user/settings/custom_links.cy.ts
index 615ff2b49a85a..b680f745609bc 100644
--- a/x-pack/plugins/apm/ftr_e2e/cypress/e2e/power_user/settings/custom_links.cy.ts
+++ b/x-pack/plugins/apm/ftr_e2e/cypress/e2e/power_user/settings/custom_links.cy.ts
@@ -52,7 +52,7 @@ describe('Custom links', () => {
it('creates custom link', () => {
cy.visitKibana(basePath);
- const emptyPrompt = cy.get('[data-test-subj="customLinksEmptyPrompt"]');
+ const emptyPrompt = cy.getByTestSubj('customLinksEmptyPrompt');
cy.contains('Create custom link').click();
cy.contains('Create link');
cy.contains('Save').should('be.disabled');
@@ -63,7 +63,7 @@ describe('Custom links', () => {
emptyPrompt.should('not.exist');
cy.contains('foo');
cy.contains('https://foo.com');
- cy.get('[data-test-subj="editCustomLink"]').click();
+ cy.getByTestSubj('editCustomLink').click();
cy.contains('Delete').click();
});
@@ -71,14 +71,14 @@ describe('Custom links', () => {
cy.visitKibana(basePath);
// wait for empty prompt
- cy.get('[data-test-subj="customLinksEmptyPrompt"]').should('be.visible');
+ cy.getByTestSubj('customLinksEmptyPrompt').should('be.visible');
cy.contains('Create custom link').click();
- cy.get('[data-test-subj="filter-0"]').select('service.name');
+ cy.getByTestSubj('filter-0').select('service.name');
cy.get(
'[data-test-subj="service.name.value"] [data-test-subj="comboBoxSearchInput"]'
).type('foo');
- cy.get('[data-test-subj="filter-0"]').select('service.environment');
+ cy.getByTestSubj('filter-0').select('service.environment');
cy.get(
'[data-test-subj="service.environment.value"] [data-test-subj="comboBoxInput"]'
).should('not.contain', 'foo');
diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/e2e/power_user/storage_explorer/storage_explorer.cy.ts b/x-pack/plugins/apm/ftr_e2e/cypress/e2e/power_user/storage_explorer/storage_explorer.cy.ts
index e989ea5cf0faf..20577f8bf5793 100644
--- a/x-pack/plugins/apm/ftr_e2e/cypress/e2e/power_user/storage_explorer/storage_explorer.cy.ts
+++ b/x-pack/plugins/apm/ftr_e2e/cypress/e2e/power_user/storage_explorer/storage_explorer.cy.ts
@@ -85,7 +85,7 @@ describe('Storage Explorer', () => {
});
it('renders the storage timeseries chart', () => {
- cy.get('[data-test-subj="storageExplorerTimeseriesChart"]');
+ cy.getByTestSubj('storageExplorerTimeseriesChart');
});
it('has a list of services and environments', () => {
@@ -115,7 +115,7 @@ describe('Storage Explorer', () => {
it('with the correct environment when changing the environment', () => {
cy.wait(mainAliasNames);
- cy.get('[data-test-subj="environmentFilter"]').type('production');
+ cy.getByTestSubj('environmentFilter').type('production');
cy.contains('button', 'production').click({ force: true });
@@ -148,7 +148,7 @@ describe('Storage Explorer', () => {
it('with the correct lifecycle phase when changing the lifecycle phase', () => {
cy.wait(mainAliasNames);
- cy.get('[data-test-subj="storageExplorerLifecyclePhaseSelect"]').click();
+ cy.getByTestSubj('storageExplorerLifecyclePhaseSelect').click();
cy.contains('button', 'Warm').click();
cy.expectAPIsToHaveBeenCalledWith({
@@ -180,13 +180,13 @@ describe('Storage Explorer', () => {
cy.wait(mainAliasNames);
cy.contains('opbeans-node');
- cy.get('[data-test-subj="storageDetailsButton_opbeans-node"]').click();
- cy.get('[data-test-subj="loadingSpinner"]').should('be.visible');
+ cy.getByTestSubj('storageDetailsButton_opbeans-node').click();
+ cy.getByTestSubj('loadingSpinner').should('be.visible');
cy.wait('@storageDetailsRequest');
cy.contains('Service storage details');
- cy.get('[data-test-subj="storageExplorerTimeseriesChart"]');
- cy.get('[data-test-subj="serviceStorageDetailsTable"]');
+ cy.getByTestSubj('storageExplorerTimeseriesChart');
+ cy.getByTestSubj('serviceStorageDetailsTable');
});
});
});
diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/e2e/read_only_user/deep_links.cy.ts b/x-pack/plugins/apm/ftr_e2e/cypress/e2e/read_only_user/deep_links.cy.ts
index cfcabe85b5b2a..00b842f3265c7 100644
--- a/x-pack/plugins/apm/ftr_e2e/cypress/e2e/read_only_user/deep_links.cy.ts
+++ b/x-pack/plugins/apm/ftr_e2e/cypress/e2e/read_only_user/deep_links.cy.ts
@@ -11,7 +11,7 @@ describe('APM deep links', () => {
});
it('navigates to apm links on search elastic', () => {
cy.visitKibana('/');
- cy.get('[data-test-subj="nav-search-input"]').type('APM');
+ cy.getByTestSubj('nav-search-input').type('APM');
cy.contains('APM');
cy.contains('APM / Services');
cy.contains('APM / Traces');
@@ -23,17 +23,17 @@ describe('APM deep links', () => {
cy.contains('APM').click({ force: true });
cy.url().should('include', '/apm/services');
- cy.get('[data-test-subj="nav-search-input"]').type('APM');
+ cy.getByTestSubj('nav-search-input').type('APM');
// navigates to services page
cy.contains('APM / Services').click({ force: true });
cy.url().should('include', '/apm/services');
- cy.get('[data-test-subj="nav-search-input"]').type('APM');
+ cy.getByTestSubj('nav-search-input').type('APM');
// navigates to traces page
cy.contains('APM / Traces').click({ force: true });
cy.url().should('include', '/apm/traces');
- cy.get('[data-test-subj="nav-search-input"]').type('APM');
+ cy.getByTestSubj('nav-search-input').type('APM');
// navigates to service maps
cy.contains('APM / Service Map').click({ force: true });
cy.url().should('include', '/apm/service-map');
diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/e2e/read_only_user/dependencies.cy.ts b/x-pack/plugins/apm/ftr_e2e/cypress/e2e/read_only_user/dependencies.cy.ts
index 653809a8e04d3..2ef3ae42b1aac 100644
--- a/x-pack/plugins/apm/ftr_e2e/cypress/e2e/read_only_user/dependencies.cy.ts
+++ b/x-pack/plugins/apm/ftr_e2e/cypress/e2e/read_only_user/dependencies.cy.ts
@@ -66,9 +66,9 @@ describe('Dependencies', () => {
})}`
);
- cy.get('[data-test-subj="latencyChart"]');
- cy.get('[data-test-subj="throughputChart"]');
- cy.get('[data-test-subj="errorRateChart"]');
+ cy.getByTestSubj('latencyChart');
+ cy.getByTestSubj('throughputChart');
+ cy.getByTestSubj('errorRateChart');
cy.contains('opbeans-java').click({ force: true });
diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/e2e/read_only_user/errors/error_details.cy.ts b/x-pack/plugins/apm/ftr_e2e/cypress/e2e/read_only_user/errors/error_details.cy.ts
index 19de523c7ab1f..d00d8036df3bb 100644
--- a/x-pack/plugins/apm/ftr_e2e/cypress/e2e/read_only_user/errors/error_details.cy.ts
+++ b/x-pack/plugins/apm/ftr_e2e/cypress/e2e/read_only_user/errors/error_details.cy.ts
@@ -68,13 +68,13 @@ describe('Error details', () => {
it('shows errors distribution chart', () => {
cy.visitKibana(errorDetailsPageHref);
cy.contains('Error group 00000');
- cy.get('[data-test-subj="errorDistribution"]').contains('Occurrences');
+ cy.getByTestSubj('errorDistribution').contains('Occurrences');
});
it('shows top erroneous transactions table', () => {
cy.visitKibana(errorDetailsPageHref);
cy.contains('Top 5 affected transactions');
- cy.get('[data-test-subj="topErroneousTransactionsTable"]')
+ cy.getByTestSubj('topErroneousTransactionsTable')
.contains('a', 'GET /apple 🍎')
.click();
cy.url().should('include', 'opbeans-java/transactions/view');
diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/e2e/read_only_user/errors/errors_page.cy.ts b/x-pack/plugins/apm/ftr_e2e/cypress/e2e/read_only_user/errors/errors_page.cy.ts
index 301b3384ee2eb..8ac95d509d0bd 100644
--- a/x-pack/plugins/apm/ftr_e2e/cypress/e2e/read_only_user/errors/errors_page.cy.ts
+++ b/x-pack/plugins/apm/ftr_e2e/cypress/e2e/read_only_user/errors/errors_page.cy.ts
@@ -81,14 +81,14 @@ describe('Errors page', () => {
it('clicking on type adds a filter in the kuerybar', () => {
cy.visitKibana(javaServiceErrorsPageHref);
- cy.get('[data-test-subj="headerFilterKuerybar"]')
+ cy.getByTestSubj('headerFilterKuerybar')
.invoke('val')
.should('be.empty');
// `force: true` because Cypress says the element is 0x0
cy.contains('exception 0').click({
force: true,
});
- cy.get('[data-test-subj="headerFilterKuerybar"]')
+ cy.getByTestSubj('headerFilterKuerybar')
.its('length')
.should('be.gt', 0);
cy.get('table')
@@ -158,7 +158,7 @@ describe('Check detailed statistics API with multiple errors', () => {
])
);
});
- cy.get('[data-test-subj="pagination-button-1"]').click();
+ cy.getByTestSubj('pagination-button-1').click();
cy.wait('@errorsDetailedStatistics').then((payload) => {
expect(payload.request.body.groupIds).eql(
JSON.stringify([
diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/e2e/read_only_user/home.cy.ts b/x-pack/plugins/apm/ftr_e2e/cypress/e2e/read_only_user/home.cy.ts
index 2ee2f4f019b12..e0c4a3aedd2b3 100644
--- a/x-pack/plugins/apm/ftr_e2e/cypress/e2e/read_only_user/home.cy.ts
+++ b/x-pack/plugins/apm/ftr_e2e/cypress/e2e/read_only_user/home.cy.ts
@@ -69,7 +69,7 @@ describe('Home page', () => {
cy.contains('Services');
cy.contains('opbeans-rum').click({ force: true });
- cy.get('[data-test-subj="headerFilterTransactionType"]').should(
+ cy.getByTestSubj('headerFilterTransactionType').should(
'have.value',
'page-load'
);
diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/e2e/read_only_user/service_inventory/header_filters/header_filters.cy.ts b/x-pack/plugins/apm/ftr_e2e/cypress/e2e/read_only_user/service_inventory/header_filters/header_filters.cy.ts
index c4e87ac15fbe1..4f72e968d81f8 100644
--- a/x-pack/plugins/apm/ftr_e2e/cypress/e2e/read_only_user/service_inventory/header_filters/header_filters.cy.ts
+++ b/x-pack/plugins/apm/ftr_e2e/cypress/e2e/read_only_user/service_inventory/header_filters/header_filters.cy.ts
@@ -44,7 +44,7 @@ describe('Service inventory - header filters', () => {
cy.contains('Services');
cy.contains('opbeans-node');
cy.contains('service 1');
- cy.get('[data-test-subj="headerFilterKuerybar"]')
+ cy.getByTestSubj('headerFilterKuerybar')
.type(`service.name: "${specialServiceName}"`)
.type('{enter}');
cy.contains('service 1');
diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/e2e/read_only_user/service_inventory/service_inventory.cy.ts b/x-pack/plugins/apm/ftr_e2e/cypress/e2e/read_only_user/service_inventory/service_inventory.cy.ts
index 015df91d792e9..2d40c690a8c92 100644
--- a/x-pack/plugins/apm/ftr_e2e/cypress/e2e/read_only_user/service_inventory/service_inventory.cy.ts
+++ b/x-pack/plugins/apm/ftr_e2e/cypress/e2e/read_only_user/service_inventory/service_inventory.cy.ts
@@ -93,7 +93,7 @@ describe('Service inventory', () => {
it('with the correct environment when changing the environment', () => {
cy.wait(mainAliasNames);
- cy.get('[data-test-subj="environmentFilter"]').type('production');
+ cy.getByTestSubj('environmentFilter').type('production');
cy.contains('button', 'production').click();
@@ -175,7 +175,7 @@ describe('Service inventory', () => {
])
);
});
- cy.get('[data-test-subj="pagination-button-1"]').click();
+ cy.getByTestSubj('pagination-button-1').click();
cy.wait('@detailedStatisticsRequest').then((payload) => {
expect(payload.request.body.serviceNames).eql(
JSON.stringify([
diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/e2e/read_only_user/service_overview/errors_table.cy.ts b/x-pack/plugins/apm/ftr_e2e/cypress/e2e/read_only_user/service_overview/errors_table.cy.ts
index b175eb0430ed4..d693148010c7e 100644
--- a/x-pack/plugins/apm/ftr_e2e/cypress/e2e/read_only_user/service_overview/errors_table.cy.ts
+++ b/x-pack/plugins/apm/ftr_e2e/cypress/e2e/read_only_user/service_overview/errors_table.cy.ts
@@ -50,16 +50,12 @@ describe('Errors table', () => {
it('clicking on type adds a filter in the kuerybar and navigates to errors page', () => {
cy.visitKibana(serviceOverviewHref);
- cy.get('[data-test-subj="headerFilterKuerybar"]')
- .invoke('val')
- .should('be.empty');
+ cy.getByTestSubj('headerFilterKuerybar').invoke('val').should('be.empty');
// `force: true` because Cypress says the element is 0x0
cy.contains('Exception').click({
force: true,
});
- cy.get('[data-test-subj="headerFilterKuerybar"]')
- .its('length')
- .should('be.gt', 0);
+ cy.getByTestSubj('headerFilterKuerybar').its('length').should('be.gt', 0);
cy.get('table').find('td:contains("Exception")').should('have.length', 1);
});
diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/e2e/read_only_user/service_overview/header_filters.cy.ts b/x-pack/plugins/apm/ftr_e2e/cypress/e2e/read_only_user/service_overview/header_filters.cy.ts
index 6376d544821aa..8a25024506696 100644
--- a/x-pack/plugins/apm/ftr_e2e/cypress/e2e/read_only_user/service_overview/header_filters.cy.ts
+++ b/x-pack/plugins/apm/ftr_e2e/cypress/e2e/read_only_user/service_overview/header_filters.cy.ts
@@ -77,13 +77,13 @@ describe('Service overview - header filters', () => {
cy.visitKibana(serviceOverviewHref);
cy.contains('opbeans-node');
cy.url().should('not.include', 'transactionType');
- cy.get('[data-test-subj="headerFilterTransactionType"]').should(
+ cy.getByTestSubj('headerFilterTransactionType').should(
'have.value',
'request'
);
- cy.get('[data-test-subj="headerFilterTransactionType"]').select('Worker');
+ cy.getByTestSubj('headerFilterTransactionType').select('Worker');
cy.url().should('include', 'transactionType=Worker');
- cy.get('[data-test-subj="headerFilterTransactionType"]').should(
+ cy.getByTestSubj('headerFilterTransactionType').should(
'have.value',
'Worker'
);
@@ -94,7 +94,7 @@ describe('Service overview - header filters', () => {
cy.intercept('GET', endpoint).as(name);
});
cy.visitKibana(serviceOverviewHref);
- cy.get('[data-test-subj="headerFilterTransactionType"]').should(
+ cy.getByTestSubj('headerFilterTransactionType').should(
'have.value',
'request'
);
@@ -104,9 +104,9 @@ describe('Service overview - header filters', () => {
value: 'transactionType=request',
});
- cy.get('[data-test-subj="headerFilterTransactionType"]').select('Worker');
+ cy.getByTestSubj('headerFilterTransactionType').select('Worker');
cy.url().should('include', 'transactionType=Worker');
- cy.get('[data-test-subj="headerFilterTransactionType"]').should(
+ cy.getByTestSubj('headerFilterTransactionType').should(
'have.value',
'Worker'
);
@@ -129,18 +129,12 @@ describe('Service overview - header filters', () => {
})
);
cy.contains('opbeans-java');
- cy.get('[data-test-subj="headerFilterKuerybar"]').type('transaction.n');
+ cy.getByTestSubj('headerFilterKuerybar').type('transaction.n');
cy.contains('transaction.name');
- cy.get('[data-test-subj="suggestionContainer"]')
- .find('li')
- .first()
- .click();
- cy.get('[data-test-subj="headerFilterKuerybar"]').type(':');
- cy.get('[data-test-subj="suggestionContainer"]')
- .find('li')
- .first()
- .click();
- cy.get('[data-test-subj="headerFilterKuerybar"]').type('{enter}');
+ cy.getByTestSubj('suggestionContainer').find('li').first().click();
+ cy.getByTestSubj('headerFilterKuerybar').type(':');
+ cy.getByTestSubj('suggestionContainer').find('li').first().click();
+ cy.getByTestSubj('headerFilterKuerybar').type('{enter}');
cy.url().should('include', '&kuery=transaction.name');
});
});
diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/e2e/read_only_user/service_overview/instances_table.cy.ts b/x-pack/plugins/apm/ftr_e2e/cypress/e2e/read_only_user/service_overview/instances_table.cy.ts
index 03653df2b0bb6..578b116a10592 100644
--- a/x-pack/plugins/apm/ftr_e2e/cypress/e2e/read_only_user/service_overview/instances_table.cy.ts
+++ b/x-pack/plugins/apm/ftr_e2e/cypress/e2e/read_only_user/service_overview/instances_table.cy.ts
@@ -63,7 +63,7 @@ describe('Instances table', () => {
it('shows empty message', () => {
cy.visitKibana(testServiveHref);
cy.contains('test-service');
- cy.get('[data-test-subj="serviceInstancesTableContainer"]').contains(
+ cy.getByTestSubj('serviceInstancesTableContainer').contains(
'No instances found'
);
});
@@ -77,9 +77,7 @@ describe('Instances table', () => {
it('hides instances table', () => {
cy.visitKibana(serviceRumOverviewHref);
cy.contains('opbeans-rum');
- cy.get('[data-test-subj="serviceInstancesTableContainer"]').should(
- 'not.exist'
- );
+ cy.getByTestSubj('serviceInstancesTableContainer').should('not.exist');
});
});
@@ -109,10 +107,8 @@ describe('Instances table', () => {
cy.contains(serviceNodeName);
cy.wait('@instancesDetailsRequest');
- cy.get(
- `[data-test-subj="instanceDetailsButton_${serviceNodeName}"]`
- ).realClick();
- cy.get('[data-test-subj="loadingSpinner"]').should('be.visible');
+ cy.getByTestSubj(`instanceDetailsButton_${serviceNodeName}`).realClick();
+ cy.getByTestSubj('loadingSpinner').should('be.visible');
cy.wait('@instanceDetailsRequest').then(() => {
cy.contains('Service');
});
@@ -130,9 +126,7 @@ describe('Instances table', () => {
cy.contains(serviceNodeName);
cy.wait('@instancesDetailsRequest');
- cy.get(
- `[data-test-subj="instanceActionsButton_${serviceNodeName}"]`
- ).click();
+ cy.getByTestSubj(`instanceActionsButton_${serviceNodeName}`).click();
cy.contains('Pod logs');
cy.contains('Pod metrics');
// cy.contains('Container logs');
diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/e2e/read_only_user/service_overview/service_overview.cy.ts b/x-pack/plugins/apm/ftr_e2e/cypress/e2e/read_only_user/service_overview/service_overview.cy.ts
index e8319c8efafeb..8173e94557b29 100644
--- a/x-pack/plugins/apm/ftr_e2e/cypress/e2e/read_only_user/service_overview/service_overview.cy.ts
+++ b/x-pack/plugins/apm/ftr_e2e/cypress/e2e/read_only_user/service_overview/service_overview.cy.ts
@@ -109,13 +109,13 @@ describe('Service Overview', () => {
cy.contains('opbeans-node');
// set skipFailures to true to not fail the test when there are accessibility failures
checkA11y({ skipFailures: true });
- cy.get('[data-test-subj="latencyChart"]');
- cy.get('[data-test-subj="throughput"]');
- cy.get('[data-test-subj="transactionsGroupTable"]');
- cy.get('[data-test-subj="serviceOverviewErrorsTable"]');
- cy.get('[data-test-subj="dependenciesTable"]');
- cy.get('[data-test-subj="instancesLatencyDistribution"]');
- cy.get('[data-test-subj="serviceOverviewInstancesTable"]');
+ cy.getByTestSubj('latencyChart');
+ cy.getByTestSubj('throughput');
+ cy.getByTestSubj('transactionsGroupTable');
+ cy.getByTestSubj('serviceOverviewErrorsTable');
+ cy.getByTestSubj('dependenciesTable');
+ cy.getByTestSubj('instancesLatencyDistribution');
+ cy.getByTestSubj('serviceOverviewInstancesTable');
});
});
@@ -134,17 +134,17 @@ describe('Service Overview', () => {
cy.wait('@transactionTypesRequest');
- cy.get('[data-test-subj="headerFilterTransactionType"]').should(
+ cy.getByTestSubj('headerFilterTransactionType').should(
'have.value',
'request'
);
- cy.get('[data-test-subj="headerFilterTransactionType"]').select('Worker');
- cy.get('[data-test-subj="headerFilterTransactionType"]').should(
+ cy.getByTestSubj('headerFilterTransactionType').select('Worker');
+ cy.getByTestSubj('headerFilterTransactionType').should(
'have.value',
'Worker'
);
cy.contains('Transactions').click();
- cy.get('[data-test-subj="headerFilterTransactionType"]').should(
+ cy.getByTestSubj('headerFilterTransactionType').should(
'have.value',
'Worker'
);
@@ -159,18 +159,18 @@ describe('Service Overview', () => {
cy.visitKibana(baseUrl);
cy.wait('@transactionTypesRequest');
- cy.get('[data-test-subj="headerFilterTransactionType"]').should(
+ cy.getByTestSubj('headerFilterTransactionType').should(
'have.value',
'request'
);
- cy.get('[data-test-subj="headerFilterTransactionType"]').select('Worker');
- cy.get('[data-test-subj="headerFilterTransactionType"]').should(
+ cy.getByTestSubj('headerFilterTransactionType').select('Worker');
+ cy.getByTestSubj('headerFilterTransactionType').should(
'have.value',
'Worker'
);
cy.contains('View transactions').click();
- cy.get('[data-test-subj="headerFilterTransactionType"]').should(
+ cy.getByTestSubj('headerFilterTransactionType').should(
'have.value',
'Worker'
);
@@ -226,7 +226,7 @@ describe('Service Overview', () => {
'suggestionsRequest'
);
- cy.get('[data-test-subj="environmentFilter"] input').type('production', {
+ cy.getByTestSubj('environmentFilter').find('input').type('production', {
force: true,
});
@@ -235,9 +235,7 @@ describe('Service Overview', () => {
value: 'fieldValue=production',
});
- cy.get(
- '[data-test-subj="comboBoxOptionsList environmentFilter-optionsList"]'
- )
+ cy.getByTestSubj('comboBoxOptionsList environmentFilter-optionsList')
.contains('production')
.click({ force: true });
@@ -271,11 +269,11 @@ describe('Service Overview', () => {
});
it('when selecting a different comparison window', () => {
- cy.get('[data-test-subj="comparisonSelect"]').should('have.value', '1d');
+ cy.getByTestSubj('comparisonSelect').should('have.value', '1d');
// selects another comparison type
- cy.get('[data-test-subj="comparisonSelect"]').select('1w');
- cy.get('[data-test-subj="comparisonSelect"]').should('have.value', '1w');
+ cy.getByTestSubj('comparisonSelect').select('1w');
+ cy.getByTestSubj('comparisonSelect').should('have.value', '1w');
cy.expectAPIsToHaveBeenCalledWith({
apisIntercepted: aliasNamesWithComparison,
value: 'offset',
diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/e2e/read_only_user/service_overview/time_comparison.cy.ts b/x-pack/plugins/apm/ftr_e2e/cypress/e2e/read_only_user/service_overview/time_comparison.cy.ts
index 718a2a4a06cf7..bce3da42d5a3f 100644
--- a/x-pack/plugins/apm/ftr_e2e/cypress/e2e/read_only_user/service_overview/time_comparison.cy.ts
+++ b/x-pack/plugins/apm/ftr_e2e/cypress/e2e/read_only_user/service_overview/time_comparison.cy.ts
@@ -101,18 +101,18 @@ describe('Service overview: Time Comparison', () => {
cy.visitKibana(serviceOverviewPath);
cy.contains('opbeans-java');
// opens the page with "Day before" selected
- cy.get('[data-test-subj="comparisonSelect"]').should('have.value', '1d');
+ cy.getByTestSubj('comparisonSelect').should('have.value', '1d');
// selects another comparison type
- cy.get('[data-test-subj="comparisonSelect"]').select('1w');
- cy.get('[data-test-subj="comparisonSelect"]').should('have.value', '1w');
+ cy.getByTestSubj('comparisonSelect').select('1w');
+ cy.getByTestSubj('comparisonSelect').should('have.value', '1w');
});
it('changes comparison type when a new time range is selected', () => {
cy.visitKibana(serviceOverviewHref);
cy.contains('opbeans-java');
// Time comparison default value
- cy.get('[data-test-subj="comparisonSelect"]').should('have.value', '1d');
+ cy.getByTestSubj('comparisonSelect').should('have.value', '1d');
cy.contains('Day before');
cy.contains('Week before');
@@ -121,17 +121,14 @@ describe('Service overview: Time Comparison', () => {
'2021-10-20T00:00:00.000Z'
);
- cy.get('[data-test-subj="superDatePickerApplyTimeButton"]').click();
+ cy.getByTestSubj('superDatePickerApplyTimeButton').click();
- cy.get('[data-test-subj="comparisonSelect"]').should(
- 'have.value',
- '864000000ms'
- );
- cy.get('[data-test-subj="comparisonSelect"]').should(
+ cy.getByTestSubj('comparisonSelect').should('have.value', '864000000ms');
+ cy.getByTestSubj('comparisonSelect').should(
'not.contain.text',
'Day before'
);
- cy.get('[data-test-subj="comparisonSelect"]').should(
+ cy.getByTestSubj('comparisonSelect').should(
'not.contain.text',
'Week before'
);
@@ -141,17 +138,14 @@ describe('Service overview: Time Comparison', () => {
cy.contains('Week before');
cy.changeTimeRange('Last 24 hours');
- cy.get('[data-test-subj="comparisonSelect"]').should('have.value', '1d');
+ cy.getByTestSubj('comparisonSelect').should('have.value', '1d');
cy.contains('Day before');
cy.contains('Week before');
cy.changeTimeRange('Last 7 days');
- cy.get('[data-test-subj="comparisonSelect"]').should('have.value', '1w');
- cy.get('[data-test-subj="comparisonSelect"]').should(
- 'contain.text',
- 'Week before'
- );
- cy.get('[data-test-subj="comparisonSelect"]').should(
+ cy.getByTestSubj('comparisonSelect').should('have.value', '1w');
+ cy.getByTestSubj('comparisonSelect').should('contain.text', 'Week before');
+ cy.getByTestSubj('comparisonSelect').should(
'not.contain.text',
'Day before'
);
@@ -170,7 +164,7 @@ describe('Service overview: Time Comparison', () => {
);
cy.contains('opbeans-java');
cy.wait('@throughputChartRequest');
- cy.get('[data-test-subj="throughput"]')
+ cy.getByTestSubj('throughput')
.get('#echHighlighterClipPath__throughput')
.realHover({ position: 'center' });
cy.contains('Week before');
@@ -186,17 +180,17 @@ describe('Service overview: Time Comparison', () => {
cy.contains('opbeans-java');
// Comparison is enabled by default
- cy.get('[data-test-subj="comparisonSelect"]').should('be.enabled');
+ cy.getByTestSubj('comparisonSelect').should('be.enabled');
// toggles off comparison
cy.contains('Comparison').click();
- cy.get('[data-test-subj="comparisonSelect"]').should('be.disabled');
+ cy.getByTestSubj('comparisonSelect').should('be.disabled');
});
it('calls APIs without comparison time range', () => {
cy.visitKibana(serviceOverviewHref);
- cy.get('[data-test-subj="comparisonSelect"]').should('be.enabled');
+ cy.getByTestSubj('comparisonSelect').should('be.enabled');
const offset = `offset=1d`;
// When the page loads it fetches all APIs with comparison time range
@@ -212,7 +206,7 @@ describe('Service overview: Time Comparison', () => {
// toggles off comparison
cy.contains('Comparison').click();
- cy.get('[data-test-subj="comparisonSelect"]').should('be.disabled');
+ cy.getByTestSubj('comparisonSelect').should('be.disabled');
// When comparison is disabled APIs are called withou comparison time range
cy.wait(apisToIntercept.map(({ name }) => `@${name}`)).then(
(interceptions) => {
diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/e2e/read_only_user/transaction_details/span_links.cy.ts b/x-pack/plugins/apm/ftr_e2e/cypress/e2e/read_only_user/transaction_details/span_links.cy.ts
index cddba048e8a18..60b36b10ee4a3 100644
--- a/x-pack/plugins/apm/ftr_e2e/cypress/e2e/read_only_user/transaction_details/span_links.cy.ts
+++ b/x-pack/plugins/apm/ftr_e2e/cypress/e2e/read_only_user/transaction_details/span_links.cy.ts
@@ -50,8 +50,8 @@ describe('Span links', () => {
);
cy.contains('Transaction A').click();
cy.contains('2 Span links');
- cy.get(
- `[data-test-subj="spanLinksBadge_${ids.producerInternalOnlyIds.spanAId}"]`
+ cy.getByTestSubj(
+ `spanLinksBadge_${ids.producerInternalOnlyIds.spanAId}`
).realHover();
cy.contains('2 Span links found');
cy.contains('2 incoming');
@@ -64,8 +64,8 @@ describe('Span links', () => {
);
cy.contains('Transaction B').click();
cy.contains('2 Span links');
- cy.get(
- `[data-test-subj="spanLinksBadge_${ids.producerExternalOnlyIds.spanBId}"]`
+ cy.getByTestSubj(
+ `spanLinksBadge_${ids.producerExternalOnlyIds.spanBId}`
).realHover();
cy.contains('2 Span links found');
cy.contains('1 incoming');
@@ -78,8 +78,8 @@ describe('Span links', () => {
);
cy.contains('Transaction C').click();
cy.contains('2 Span links');
- cy.get(
- `[data-test-subj="spanLinksBadge_${ids.producerConsumerIds.transactionCId}"]`
+ cy.getByTestSubj(
+ `spanLinksBadge_${ids.producerConsumerIds.transactionCId}`
).realHover();
cy.contains('2 Span links found');
cy.contains('1 incoming');
@@ -92,8 +92,8 @@ describe('Span links', () => {
);
cy.contains('Transaction C').click();
cy.contains('1 Span link');
- cy.get(
- `[data-test-subj="spanLinksBadge_${ids.producerConsumerIds.spanCId}"]`
+ cy.getByTestSubj(
+ `spanLinksBadge_${ids.producerConsumerIds.spanCId}`
).realHover();
cy.contains('1 Span link found');
cy.contains('1 incoming');
@@ -106,8 +106,8 @@ describe('Span links', () => {
);
cy.contains('Transaction D').click();
cy.contains('2 Span links');
- cy.get(
- `[data-test-subj="spanLinksBadge_${ids.producerMultipleIds.transactionDId}"]`
+ cy.getByTestSubj(
+ `spanLinksBadge_${ids.producerMultipleIds.transactionDId}`
).realHover();
cy.contains('2 Span links found');
cy.contains('0 incoming');
@@ -120,8 +120,8 @@ describe('Span links', () => {
);
cy.contains('Transaction D').click();
cy.contains('2 Span links');
- cy.get(
- `[data-test-subj="spanLinksBadge_${ids.producerMultipleIds.spanEId}"]`
+ cy.getByTestSubj(
+ `spanLinksBadge_${ids.producerMultipleIds.spanEId}`
).realHover();
cy.contains('2 Span links found');
cy.contains('0 incoming');
@@ -136,7 +136,7 @@ describe('Span links', () => {
);
cy.contains('Transaction A').click();
cy.contains('Span A').click();
- cy.get('[data-test-subj="spanLinksTab"]').click();
+ cy.getByTestSubj('spanLinksTab').click();
cy.contains('producer-consumer')
.should('have.attr', 'href')
.and('include', '/services/producer-consumer/overview');
@@ -155,7 +155,7 @@ describe('Span links', () => {
'include',
`link-to/transaction/${ids.producerMultipleIds.transactionDId}?waterfallItemId=${ids.producerMultipleIds.transactionDId}`
);
- cy.get('[data-test-subj="spanLinkTypeSelect"]').should(
+ cy.getByTestSubj('spanLinkTypeSelect').should(
'contain.text',
'Outgoing links (0)'
);
@@ -167,7 +167,7 @@ describe('Span links', () => {
);
cy.contains('Transaction B').click();
cy.contains('Span B').click();
- cy.get('[data-test-subj="spanLinksTab"]').click();
+ cy.getByTestSubj('spanLinksTab').click();
cy.contains('consumer-multiple')
.should('have.attr', 'href')
@@ -178,9 +178,7 @@ describe('Span links', () => {
'include',
`link-to/transaction/${ids.producerMultipleIds.transactionDId}?waterfallItemId=${ids.producerMultipleIds.spanEId}`
);
- cy.get('[data-test-subj="spanLinkTypeSelect"]').select(
- 'Outgoing links (1)'
- );
+ cy.getByTestSubj('spanLinkTypeSelect').select('Outgoing links (1)');
cy.contains('Unknown');
cy.contains('trace#1-span#1');
});
@@ -193,7 +191,7 @@ describe('Span links', () => {
cy.get(
`[aria-controls="${ids.producerConsumerIds.transactionCId}"]`
).click();
- cy.get('[data-test-subj="spanLinksTab"]').click();
+ cy.getByTestSubj('spanLinksTab').click();
cy.contains('consumer-multiple')
.should('have.attr', 'href')
@@ -205,9 +203,7 @@ describe('Span links', () => {
`link-to/transaction/${ids.producerMultipleIds.transactionDId}?waterfallItemId=${ids.producerMultipleIds.spanEId}`
);
- cy.get('[data-test-subj="spanLinkTypeSelect"]').select(
- 'Outgoing links (1)'
- );
+ cy.getByTestSubj('spanLinkTypeSelect').select('Outgoing links (1)');
cy.contains('producer-internal-only')
.should('have.attr', 'href')
.and('include', '/services/producer-internal-only/overview');
@@ -225,7 +221,7 @@ describe('Span links', () => {
);
cy.contains('Transaction C').click();
cy.contains('Span C').click();
- cy.get('[data-test-subj="spanLinksTab"]').click();
+ cy.getByTestSubj('spanLinksTab').click();
cy.contains('consumer-multiple')
.should('have.attr', 'href')
@@ -237,7 +233,7 @@ describe('Span links', () => {
`link-to/transaction/${ids.producerMultipleIds.transactionDId}?waterfallItemId=${ids.producerMultipleIds.transactionDId}`
);
- cy.get('[data-test-subj="spanLinkTypeSelect"]').should(
+ cy.getByTestSubj('spanLinkTypeSelect').should(
'contain.text',
'Outgoing links (0)'
);
@@ -251,7 +247,7 @@ describe('Span links', () => {
cy.get(
`[aria-controls="${ids.producerMultipleIds.transactionDId}"]`
).click();
- cy.get('[data-test-subj="spanLinksTab"]').click();
+ cy.getByTestSubj('spanLinksTab').click();
cy.contains('producer-consumer')
.should('have.attr', 'href')
@@ -273,7 +269,7 @@ describe('Span links', () => {
`link-to/transaction/${ids.producerInternalOnlyIds.transactionAId}?waterfallItemId=${ids.producerInternalOnlyIds.spanAId}`
);
- cy.get('[data-test-subj="spanLinkTypeSelect"]').should(
+ cy.getByTestSubj('spanLinkTypeSelect').should(
'contain.text',
'Incoming links (0)'
);
@@ -285,7 +281,7 @@ describe('Span links', () => {
);
cy.contains('Transaction D').click();
cy.contains('Span E').click();
- cy.get('[data-test-subj="spanLinksTab"]').click();
+ cy.getByTestSubj('spanLinksTab').click();
cy.contains('producer-external-only')
.should('have.attr', 'href')
@@ -307,7 +303,7 @@ describe('Span links', () => {
`link-to/transaction/${ids.producerConsumerIds.transactionCId}?waterfallItemId=${ids.producerConsumerIds.transactionCId}`
);
- cy.get('[data-test-subj="spanLinkTypeSelect"]').should(
+ cy.getByTestSubj('spanLinkTypeSelect').should(
'contain.text',
'Incoming links (0)'
);
diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/e2e/read_only_user/transaction_details/transaction_details.cy.ts b/x-pack/plugins/apm/ftr_e2e/cypress/e2e/read_only_user/transaction_details/transaction_details.cy.ts
index 5172a5f167fc9..09bd37f5b0b6c 100644
--- a/x-pack/plugins/apm/ftr_e2e/cypress/e2e/read_only_user/transaction_details/transaction_details.cy.ts
+++ b/x-pack/plugins/apm/ftr_e2e/cypress/e2e/read_only_user/transaction_details/transaction_details.cy.ts
@@ -42,15 +42,15 @@ describe('Transaction details', () => {
it('shows transaction name and transaction charts', () => {
cy.contains('h2', 'GET /api/product');
- cy.get('[data-test-subj="latencyChart"]');
- cy.get('[data-test-subj="throughput"]');
- cy.get('[data-test-subj="transactionBreakdownChart"]');
- cy.get('[data-test-subj="errorRate"]');
+ cy.getByTestSubj('latencyChart');
+ cy.getByTestSubj('throughput');
+ cy.getByTestSubj('transactionBreakdownChart');
+ cy.getByTestSubj('errorRate');
});
it('shows top errors table', () => {
cy.contains('Top 5 errors');
- cy.get('[data-test-subj="topErrorsForTransactionTable"]')
+ cy.getByTestSubj('topErrorsForTransactionTable')
.contains('a', '[MockError] Foo')
.click();
cy.url().should('include', 'opbeans-java/errors');
@@ -58,7 +58,7 @@ describe('Transaction details', () => {
describe('when navigating to a trace sample', () => {
it('keeps the same trace sample after reloading the page', () => {
- cy.get('[data-test-subj="pagination-button-last"]').click();
+ cy.getByTestSubj('pagination-button-last').click();
cy.url().then((url) => {
cy.reload();
cy.url().should('eq', url);
diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/e2e/read_only_user/transactions_overview/transactions_overview.cy.ts b/x-pack/plugins/apm/ftr_e2e/cypress/e2e/read_only_user/transactions_overview/transactions_overview.cy.ts
index 83753b7fe2595..2e7e0d336cd5d 100644
--- a/x-pack/plugins/apm/ftr_e2e/cypress/e2e/read_only_user/transactions_overview/transactions_overview.cy.ts
+++ b/x-pack/plugins/apm/ftr_e2e/cypress/e2e/read_only_user/transactions_overview/transactions_overview.cy.ts
@@ -49,17 +49,17 @@ describe('Transactions Overview', () => {
it('persists transaction type selected when navigating to Overview tab', () => {
cy.visitKibana(serviceTransactionsHref);
- cy.get('[data-test-subj="headerFilterTransactionType"]').should(
+ cy.getByTestSubj('headerFilterTransactionType').should(
'have.value',
'request'
);
- cy.get('[data-test-subj="headerFilterTransactionType"]').select('Worker');
- cy.get('[data-test-subj="headerFilterTransactionType"]').should(
+ cy.getByTestSubj('headerFilterTransactionType').select('Worker');
+ cy.getByTestSubj('headerFilterTransactionType').should(
'have.value',
'Worker'
);
cy.get('a[href*="/app/apm/services/opbeans-node/overview"]').click();
- cy.get('[data-test-subj="headerFilterTransactionType"]').should(
+ cy.getByTestSubj('headerFilterTransactionType').should(
'have.value',
'Worker'
);
diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/support/commands.ts b/x-pack/plugins/apm/ftr_e2e/cypress/support/commands.ts
index 7830e791c3655..9e6e0189e636c 100644
--- a/x-pack/plugins/apm/ftr_e2e/cypress/support/commands.ts
+++ b/x-pack/plugins/apm/ftr_e2e/cypress/support/commands.ts
@@ -52,15 +52,19 @@ Cypress.Commands.add(
}
);
+Cypress.Commands.add('getByTestSubj', (selector: string) => {
+ return cy.get(`[data-test-subj="${selector}"]`);
+});
+
Cypress.Commands.add('changeTimeRange', (value: string) => {
- cy.get('[data-test-subj="superDatePickerToggleQuickMenuButton"]').click();
+ cy.getByTestSubj('superDatePickerToggleQuickMenuButton').click();
cy.contains(value).click();
});
Cypress.Commands.add('visitKibana', (url: string) => {
cy.visit(url);
- cy.get('[data-test-subj="kbnLoadingMessage"]').should('exist');
- cy.get('[data-test-subj="kbnLoadingMessage"]').should('not.exist', {
+ cy.getByTestSubj('kbnLoadingMessage').should('exist');
+ cy.getByTestSubj('kbnLoadingMessage').should('not.exist', {
timeout: 50000,
});
});
@@ -70,13 +74,13 @@ Cypress.Commands.add(
(start: string, end: string) => {
const format = 'MMM D, YYYY @ HH:mm:ss.SSS';
- cy.get('[data-test-subj="superDatePickerstartDatePopoverButton"]').click();
- cy.get('[data-test-subj="superDatePickerAbsoluteDateInput"]')
+ cy.getByTestSubj('superDatePickerstartDatePopoverButton').click();
+ cy.getByTestSubj('superDatePickerAbsoluteDateInput')
.eq(0)
.clear({ force: true })
.type(moment(start).format(format), { force: true });
- cy.get('[data-test-subj="superDatePickerendDatePopoverButton"]').click();
- cy.get('[data-test-subj="superDatePickerAbsoluteDateInput"]')
+ cy.getByTestSubj('superDatePickerendDatePopoverButton').click();
+ cy.getByTestSubj('superDatePickerAbsoluteDateInput')
.eq(1)
.clear({ force: true })
.type(moment(end).format(format), { force: true });
diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/support/types.d.ts b/x-pack/plugins/apm/ftr_e2e/cypress/support/types.d.ts
index 2235847e584a4..5d59d4691820a 100644
--- a/x-pack/plugins/apm/ftr_e2e/cypress/support/types.d.ts
+++ b/x-pack/plugins/apm/ftr_e2e/cypress/support/types.d.ts
@@ -22,5 +22,6 @@ declare namespace Cypress {
value: string;
}): void;
updateAdvancedSettings(settings: Record): void;
+ getByTestSubj(selector: string): Chainable>;
}
}
diff --git a/x-pack/plugins/cloud/common/constants.ts b/x-pack/plugins/cloud/common/constants.ts
index 09333e3773fe9..fc37906299d14 100644
--- a/x-pack/plugins/cloud/common/constants.ts
+++ b/x-pack/plugins/cloud/common/constants.ts
@@ -6,7 +6,6 @@
*/
export const ELASTIC_SUPPORT_LINK = 'https://cloud.elastic.co/support';
-export const GET_CHAT_USER_DATA_ROUTE_PATH = '/internal/cloud/chat_user';
/**
* This is the page for managing your snapshots on Cloud.
diff --git a/x-pack/plugins/cloud/kibana.json b/x-pack/plugins/cloud/kibana.json
index 51df5d20d81b9..85434abc87ede 100644
--- a/x-pack/plugins/cloud/kibana.json
+++ b/x-pack/plugins/cloud/kibana.json
@@ -7,7 +7,7 @@
"version": "8.0.0",
"kibanaVersion": "kibana",
"configPath": ["xpack", "cloud"],
- "optionalPlugins": ["cloudExperiments", "usageCollection", "home", "security"],
+ "optionalPlugins": ["usageCollection"],
"server": true,
"ui": true
}
diff --git a/x-pack/plugins/cloud/public/index.ts b/x-pack/plugins/cloud/public/index.ts
index d50798cb15cd2..ee37f85dfb6a7 100644
--- a/x-pack/plugins/cloud/public/index.ts
+++ b/x-pack/plugins/cloud/public/index.ts
@@ -13,5 +13,3 @@ export type { CloudSetup, CloudConfigType, CloudStart } from './plugin';
export function plugin(initializerContext: PluginInitializerContext) {
return new CloudPlugin(initializerContext);
}
-
-export { Chat } from './components';
diff --git a/x-pack/plugins/cloud/public/mocks.tsx b/x-pack/plugins/cloud/public/mocks.tsx
index f31596f3930f5..608e826657b73 100644
--- a/x-pack/plugins/cloud/public/mocks.tsx
+++ b/x-pack/plugins/cloud/public/mocks.tsx
@@ -8,7 +8,6 @@
import React from 'react';
import { CloudStart } from '.';
-import { ServicesProvider } from './services';
function createSetupMock() {
return {
@@ -19,28 +18,22 @@ function createSetupMock() {
deploymentUrl: 'deployment-url',
profileUrl: 'profile-url',
organizationUrl: 'organization-url',
+ registerCloudService: jest.fn(),
};
}
-const config = {
- chat: {
- enabled: true,
- chatURL: 'chat-url',
- user: {
- id: 'user-id',
- email: 'test-user@elastic.co',
- jwt: 'identity-jwt',
- },
- },
-};
-
const getContextProvider: () => React.FC =
() =>
({ children }) =>
- {children};
+ <>{children}>;
const createStartMock = (): jest.Mocked => ({
CloudContextProvider: jest.fn(getContextProvider()),
+ cloudId: 'mock-cloud-id',
+ isCloudEnabled: true,
+ deploymentUrl: 'deployment-url',
+ profileUrl: 'profile-url',
+ organizationUrl: 'organization-url',
});
export const cloudMock = {
diff --git a/x-pack/plugins/cloud/public/plugin.test.ts b/x-pack/plugins/cloud/public/plugin.test.ts
index 599dee5e707b7..efb566761e22a 100644
--- a/x-pack/plugins/cloud/public/plugin.test.ts
+++ b/x-pack/plugins/cloud/public/plugin.test.ts
@@ -5,308 +5,18 @@
* 2.0.
*/
-import { firstValueFrom } from 'rxjs';
-import { Sha256 } from '@kbn/crypto-browser';
-import { nextTick } from '@kbn/test-jest-helpers';
import { coreMock } from '@kbn/core/public/mocks';
-import { homePluginMock } from '@kbn/home-plugin/public/mocks';
-import { securityMock } from '@kbn/security-plugin/public/mocks';
-import { CloudPlugin, type CloudConfigType } from './plugin';
-import { CloudExperimentsPluginSetup } from '@kbn/cloud-experiments-plugin/common';
-import { cloudExperimentsMock } from '@kbn/cloud-experiments-plugin/common/mocks';
+import { CloudPlugin } from './plugin';
const baseConfig = {
base_url: 'https://cloud.elastic.co',
deployment_url: '/abc123',
profile_url: '/user/settings/',
organization_url: '/account/',
- full_story: {
- enabled: false,
- },
- chat: {
- enabled: false,
- },
};
describe('Cloud Plugin', () => {
describe('#setup', () => {
- describe('setupFullStory', () => {
- beforeEach(() => {
- jest.clearAllMocks();
- });
-
- const setupPlugin = async ({ config = {} }: { config?: Partial }) => {
- const initContext = coreMock.createPluginInitializerContext({
- ...baseConfig,
- id: 'cloudId',
- ...config,
- });
-
- const plugin = new CloudPlugin(initContext);
-
- const coreSetup = coreMock.createSetup();
-
- const setup = plugin.setup(coreSetup, {});
-
- // Wait for FullStory dynamic import to resolve
- await new Promise((r) => setImmediate(r));
-
- return { initContext, plugin, setup, coreSetup };
- };
-
- test('register the shipper FullStory with correct args when enabled and org_id are set', async () => {
- const { coreSetup } = await setupPlugin({
- config: { full_story: { enabled: true, org_id: 'foo' } },
- });
-
- expect(coreSetup.analytics.registerShipper).toHaveBeenCalled();
- expect(coreSetup.analytics.registerShipper).toHaveBeenCalledWith(expect.anything(), {
- fullStoryOrgId: 'foo',
- scriptUrl: '/internal/cloud/100/fullstory.js',
- namespace: 'FSKibana',
- });
- });
-
- it('does not call initializeFullStory when enabled=false', async () => {
- const { coreSetup } = await setupPlugin({
- config: { full_story: { enabled: false, org_id: 'foo' } },
- });
- expect(coreSetup.analytics.registerShipper).not.toHaveBeenCalled();
- });
-
- it('does not call initializeFullStory when org_id is undefined', async () => {
- const { coreSetup } = await setupPlugin({ config: { full_story: { enabled: true } } });
- expect(coreSetup.analytics.registerShipper).not.toHaveBeenCalled();
- });
- });
-
- describe('setupTelemetryContext', () => {
- const username = '1234';
- const expectedHashedPlainUsername = new Sha256().update(username, 'utf8').digest('hex');
-
- beforeEach(() => {
- jest.clearAllMocks();
- });
-
- const setupPlugin = async ({
- config = {},
- securityEnabled = true,
- currentUserProps = {},
- }: {
- config?: Partial;
- securityEnabled?: boolean;
- currentUserProps?: Record | Error;
- }) => {
- const initContext = coreMock.createPluginInitializerContext({
- ...baseConfig,
- ...config,
- });
-
- const plugin = new CloudPlugin(initContext);
-
- const coreSetup = coreMock.createSetup();
- const securitySetup = securityMock.createSetup();
- if (currentUserProps instanceof Error) {
- securitySetup.authc.getCurrentUser.mockRejectedValue(currentUserProps);
- } else {
- securitySetup.authc.getCurrentUser.mockResolvedValue(
- securityMock.createMockAuthenticatedUser(currentUserProps)
- );
- }
-
- const setup = plugin.setup(coreSetup, securityEnabled ? { security: securitySetup } : {});
-
- return { initContext, plugin, setup, coreSetup };
- };
-
- test('register the context provider for the cloud user with hashed user ID when security is available', async () => {
- const { coreSetup } = await setupPlugin({
- config: { id: 'cloudId' },
- currentUserProps: { username },
- });
-
- expect(coreSetup.analytics.registerContextProvider).toHaveBeenCalled();
-
- const [{ context$ }] = coreSetup.analytics.registerContextProvider.mock.calls.find(
- ([{ name }]) => name === 'cloud_user_id'
- )!;
-
- await expect(firstValueFrom(context$)).resolves.toEqual({
- userId: '5ef112cfdae3dea57097bc276e275b2816e73ef2a398dc0ffaf5b6b4e3af2041',
- isElasticCloudUser: false,
- });
- });
-
- it('user hash includes cloud id', async () => {
- const { coreSetup: coreSetup1 } = await setupPlugin({
- config: { id: 'esOrg1' },
- currentUserProps: { username },
- });
-
- const [{ context$: context1$ }] =
- coreSetup1.analytics.registerContextProvider.mock.calls.find(
- ([{ name }]) => name === 'cloud_user_id'
- )!;
-
- const { userId: hashId1 } = (await firstValueFrom(context1$)) as { userId: string };
- expect(hashId1).not.toEqual(expectedHashedPlainUsername);
-
- const { coreSetup: coreSetup2 } = await setupPlugin({
- config: { full_story: { enabled: true, org_id: 'foo' }, id: 'esOrg2' },
- currentUserProps: { username },
- });
-
- const [{ context$: context2$ }] =
- coreSetup2.analytics.registerContextProvider.mock.calls.find(
- ([{ name }]) => name === 'cloud_user_id'
- )!;
-
- const { userId: hashId2 } = (await firstValueFrom(context2$)) as { userId: string };
- expect(hashId2).not.toEqual(expectedHashedPlainUsername);
-
- expect(hashId1).not.toEqual(hashId2);
- });
-
- test('user hash does not include cloudId when user is an Elastic Cloud user', async () => {
- const { coreSetup } = await setupPlugin({
- config: { id: 'cloudDeploymentId' },
- currentUserProps: { username, elastic_cloud_user: true },
- });
-
- expect(coreSetup.analytics.registerContextProvider).toHaveBeenCalled();
-
- const [{ context$ }] = coreSetup.analytics.registerContextProvider.mock.calls.find(
- ([{ name }]) => name === 'cloud_user_id'
- )!;
-
- await expect(firstValueFrom(context$)).resolves.toEqual({
- userId: expectedHashedPlainUsername,
- isElasticCloudUser: true,
- });
- });
-
- test('user hash does not include cloudId when not provided', async () => {
- const { coreSetup } = await setupPlugin({
- config: {},
- currentUserProps: { username },
- });
-
- expect(coreSetup.analytics.registerContextProvider).toHaveBeenCalled();
-
- const [{ context$ }] = coreSetup.analytics.registerContextProvider.mock.calls.find(
- ([{ name }]) => name === 'cloud_user_id'
- )!;
-
- await expect(firstValueFrom(context$)).resolves.toEqual({
- userId: expectedHashedPlainUsername,
- isElasticCloudUser: false,
- });
- });
-
- test('user hash is undefined when failed to fetch a user', async () => {
- const { coreSetup } = await setupPlugin({
- currentUserProps: new Error('failed to fetch a user'),
- });
-
- expect(coreSetup.analytics.registerContextProvider).toHaveBeenCalled();
-
- const [{ context$ }] = coreSetup.analytics.registerContextProvider.mock.calls.find(
- ([{ name }]) => name === 'cloud_user_id'
- )!;
-
- await expect(firstValueFrom(context$)).resolves.toEqual({
- userId: undefined,
- isElasticCloudUser: false,
- });
- });
- });
-
- describe('setupChat', () => {
- let consoleMock: jest.SpyInstance;
-
- beforeEach(() => {
- consoleMock = jest.spyOn(console, 'debug').mockImplementation(() => {});
- });
-
- afterEach(() => {
- consoleMock.mockRestore();
- });
-
- const setupPlugin = async ({
- config = {},
- securityEnabled = true,
- currentUserProps = {},
- isCloudEnabled = true,
- failHttp = false,
- }: {
- config?: Partial;
- securityEnabled?: boolean;
- currentUserProps?: Record;
- isCloudEnabled?: boolean;
- failHttp?: boolean;
- }) => {
- const initContext = coreMock.createPluginInitializerContext({
- ...baseConfig,
- id: isCloudEnabled ? 'cloud-id' : null,
- ...config,
- });
-
- const plugin = new CloudPlugin(initContext);
-
- const coreSetup = coreMock.createSetup();
- const coreStart = coreMock.createStart();
-
- if (failHttp) {
- coreSetup.http.get.mockImplementation(() => {
- throw new Error('HTTP request failed');
- });
- }
-
- coreSetup.getStartServices.mockResolvedValue([coreStart, {}, undefined]);
-
- const securitySetup = securityMock.createSetup();
- securitySetup.authc.getCurrentUser.mockResolvedValue(
- securityMock.createMockAuthenticatedUser(currentUserProps)
- );
-
- const setup = plugin.setup(coreSetup, securityEnabled ? { security: securitySetup } : {});
-
- return { initContext, plugin, setup, coreSetup };
- };
-
- it('chatConfig is not retrieved if cloud is not enabled', async () => {
- const { coreSetup } = await setupPlugin({ isCloudEnabled: false });
- expect(coreSetup.http.get).not.toHaveBeenCalled();
- });
-
- it('chatConfig is not retrieved if security is not enabled', async () => {
- const { coreSetup } = await setupPlugin({ securityEnabled: false });
- expect(coreSetup.http.get).not.toHaveBeenCalled();
- });
-
- it('chatConfig is not retrieved if chat is enabled but url is not provided', async () => {
- // @ts-expect-error 2741
- const { coreSetup } = await setupPlugin({ config: { chat: { enabled: true } } });
- expect(coreSetup.http.get).not.toHaveBeenCalled();
- });
-
- it('chatConfig is not retrieved if internal API fails', async () => {
- const { coreSetup } = await setupPlugin({
- config: { chat: { enabled: true, chatURL: 'http://chat.elastic.co' } },
- failHttp: true,
- });
- expect(coreSetup.http.get).toHaveBeenCalled();
- expect(consoleMock).toHaveBeenCalled();
- });
-
- it('chatConfig is retrieved if chat is enabled and url is provided', async () => {
- const { coreSetup } = await setupPlugin({
- config: { chat: { enabled: true, chatURL: 'http://chat.elastic.co' } },
- });
- expect(coreSetup.http.get).toHaveBeenCalled();
- });
- });
-
describe('interface', () => {
const setupPlugin = () => {
const initContext = coreMock.createPluginInitializerContext({
@@ -317,7 +27,7 @@ describe('Cloud Plugin', () => {
const plugin = new CloudPlugin(initContext);
const coreSetup = coreMock.createSetup();
- const setup = plugin.setup(coreSetup, {});
+ const setup = plugin.setup(coreSetup);
return { setup };
};
@@ -361,49 +71,10 @@ describe('Cloud Plugin', () => {
const { setup } = setupPlugin();
expect(setup.cname).toBe('cloud.elastic.co');
});
- });
-
- describe('Set up cloudExperiments', () => {
- describe('when cloud ID is not provided in the config', () => {
- let cloudExperiments: jest.Mocked;
- beforeEach(() => {
- const plugin = new CloudPlugin(coreMock.createPluginInitializerContext(baseConfig));
- cloudExperiments = cloudExperimentsMock.createSetupMock();
- plugin.setup(coreMock.createSetup(), { cloudExperiments });
- });
- test('does not call cloudExperiments.identifyUser', async () => {
- expect(cloudExperiments.identifyUser).not.toHaveBeenCalled();
- });
- });
-
- describe('when cloud ID is provided in the config', () => {
- let cloudExperiments: jest.Mocked;
- beforeEach(() => {
- const plugin = new CloudPlugin(
- coreMock.createPluginInitializerContext({ ...baseConfig, id: 'cloud test' })
- );
- cloudExperiments = cloudExperimentsMock.createSetupMock();
- plugin.setup(coreMock.createSetup(), { cloudExperiments });
- });
-
- test('calls cloudExperiments.identifyUser', async () => {
- expect(cloudExperiments.identifyUser).toHaveBeenCalledTimes(1);
- });
-
- test('the cloud ID is hashed when calling cloudExperiments.identifyUser', async () => {
- expect(cloudExperiments.identifyUser.mock.calls[0][0]).toEqual(
- '1acb4a1cc1c3d672a8d826055d897c2623ceb1d4fb07e46d97986751a36b06cf'
- );
- });
-
- test('specifies the Kibana version when calling cloudExperiments.identifyUser', async () => {
- expect(cloudExperiments.identifyUser.mock.calls[0][1]).toEqual(
- expect.objectContaining({
- kibanaVersion: 'version',
- })
- );
- });
+ it('exposes registerCloudService', () => {
+ const { setup } = setupPlugin();
+ expect(setup.registerCloudService).toBeDefined();
});
});
});
@@ -426,9 +97,8 @@ describe('Cloud Plugin', () => {
})
);
const coreSetup = coreMock.createSetup();
- const homeSetup = homePluginMock.createSetupContract();
- plugin.setup(coreSetup, { home: homeSetup });
+ plugin.setup(coreSetup);
return { coreSetup, plugin };
};
@@ -437,8 +107,7 @@ describe('Cloud Plugin', () => {
const { plugin } = startPlugin();
const coreStart = coreMock.createStart();
- const securityStart = securityMock.createStart();
- plugin.start(coreStart, { security: securityStart });
+ plugin.start(coreStart);
expect(coreStart.chrome.setHelpSupportUrl).toHaveBeenCalledTimes(1);
expect(coreStart.chrome.setHelpSupportUrl.mock.calls[0]).toMatchInlineSnapshot(`
@@ -447,177 +116,5 @@ describe('Cloud Plugin', () => {
]
`);
});
-
- it('does not register custom nav links on anonymous pages', async () => {
- const { plugin } = startPlugin();
-
- const coreStart = coreMock.createStart();
- coreStart.http.anonymousPaths.isAnonymous.mockReturnValue(true);
-
- const securityStart = securityMock.createStart();
- securityStart.authc.getCurrentUser.mockResolvedValue(
- securityMock.createMockAuthenticatedUser({
- elastic_cloud_user: true,
- })
- );
-
- plugin.start(coreStart, { security: securityStart });
-
- await nextTick();
-
- expect(coreStart.chrome.setCustomNavLink).not.toHaveBeenCalled();
- expect(securityStart.authc.getCurrentUser).not.toHaveBeenCalled();
- });
-
- it('registers a custom nav link for cloud users', async () => {
- const { plugin } = startPlugin();
-
- const coreStart = coreMock.createStart();
- const securityStart = securityMock.createStart();
-
- securityStart.authc.getCurrentUser.mockResolvedValue(
- securityMock.createMockAuthenticatedUser({
- elastic_cloud_user: true,
- })
- );
- plugin.start(coreStart, { security: securityStart });
-
- await nextTick();
-
- expect(coreStart.chrome.setCustomNavLink).toHaveBeenCalledTimes(1);
- expect(coreStart.chrome.setCustomNavLink.mock.calls[0]).toMatchInlineSnapshot(`
- Array [
- Object {
- "euiIconType": "logoCloud",
- "href": "https://cloud.elastic.co/abc123",
- "title": "Manage this deployment",
- },
- ]
- `);
- });
-
- it('registers a custom nav link when there is an error retrieving the current user', async () => {
- const { plugin } = startPlugin();
-
- const coreStart = coreMock.createStart();
- const securityStart = securityMock.createStart();
- securityStart.authc.getCurrentUser.mockRejectedValue(new Error('something happened'));
- plugin.start(coreStart, { security: securityStart });
-
- await nextTick();
-
- expect(coreStart.chrome.setCustomNavLink).toHaveBeenCalledTimes(1);
- expect(coreStart.chrome.setCustomNavLink.mock.calls[0]).toMatchInlineSnapshot(`
- Array [
- Object {
- "euiIconType": "logoCloud",
- "href": "https://cloud.elastic.co/abc123",
- "title": "Manage this deployment",
- },
- ]
- `);
- });
-
- it('does not register a custom nav link for non-cloud users', async () => {
- const { plugin } = startPlugin();
-
- const coreStart = coreMock.createStart();
- const securityStart = securityMock.createStart();
- securityStart.authc.getCurrentUser.mockResolvedValue(
- securityMock.createMockAuthenticatedUser({
- elastic_cloud_user: false,
- })
- );
- plugin.start(coreStart, { security: securityStart });
-
- await nextTick();
-
- expect(coreStart.chrome.setCustomNavLink).not.toHaveBeenCalled();
- });
-
- it('registers user profile links for cloud users', async () => {
- const { plugin } = startPlugin();
-
- const coreStart = coreMock.createStart();
- const securityStart = securityMock.createStart();
- securityStart.authc.getCurrentUser.mockResolvedValue(
- securityMock.createMockAuthenticatedUser({
- elastic_cloud_user: true,
- })
- );
- plugin.start(coreStart, { security: securityStart });
-
- await nextTick();
-
- expect(securityStart.navControlService.addUserMenuLinks).toHaveBeenCalledTimes(1);
- expect(securityStart.navControlService.addUserMenuLinks.mock.calls[0]).toMatchInlineSnapshot(`
- Array [
- Array [
- Object {
- "href": "https://cloud.elastic.co/profile/alice",
- "iconType": "user",
- "label": "Edit profile",
- "order": 100,
- "setAsProfile": true,
- },
- Object {
- "href": "https://cloud.elastic.co/org/myOrg",
- "iconType": "gear",
- "label": "Account & Billing",
- "order": 200,
- },
- ],
- ]
- `);
- });
-
- it('registers profile links when there is an error retrieving the current user', async () => {
- const { plugin } = startPlugin();
-
- const coreStart = coreMock.createStart();
- const securityStart = securityMock.createStart();
- securityStart.authc.getCurrentUser.mockRejectedValue(new Error('something happened'));
- plugin.start(coreStart, { security: securityStart });
-
- await nextTick();
-
- expect(securityStart.navControlService.addUserMenuLinks).toHaveBeenCalledTimes(1);
- expect(securityStart.navControlService.addUserMenuLinks.mock.calls[0]).toMatchInlineSnapshot(`
- Array [
- Array [
- Object {
- "href": "https://cloud.elastic.co/profile/alice",
- "iconType": "user",
- "label": "Edit profile",
- "order": 100,
- "setAsProfile": true,
- },
- Object {
- "href": "https://cloud.elastic.co/org/myOrg",
- "iconType": "gear",
- "label": "Account & Billing",
- "order": 200,
- },
- ],
- ]
- `);
- });
-
- it('does not register profile links for non-cloud users', async () => {
- const { plugin } = startPlugin();
-
- const coreStart = coreMock.createStart();
- const securityStart = securityMock.createStart();
- securityStart.authc.getCurrentUser.mockResolvedValue(
- securityMock.createMockAuthenticatedUser({
- elastic_cloud_user: false,
- })
- );
- plugin.start(coreStart, { security: securityStart });
-
- await nextTick();
-
- expect(securityStart.navControlService.addUserMenuLinks).not.toHaveBeenCalled();
- });
});
});
diff --git a/x-pack/plugins/cloud/public/plugin.tsx b/x-pack/plugins/cloud/public/plugin.tsx
index c27668feb09bd..f50f41f3c79cd 100644
--- a/x-pack/plugins/cloud/public/plugin.tsx
+++ b/x-pack/plugins/cloud/public/plugin.tsx
@@ -6,34 +6,12 @@
*/
import React, { FC } from 'react';
-import type {
- CoreSetup,
- CoreStart,
- Plugin,
- PluginInitializerContext,
- HttpStart,
- IBasePath,
- AnalyticsServiceSetup,
-} from '@kbn/core/public';
-import { i18n } from '@kbn/i18n';
-import useObservable from 'react-use/lib/useObservable';
-import { BehaviorSubject, catchError, from, map, of } from 'rxjs';
+import type { CoreSetup, CoreStart, Plugin, PluginInitializerContext } from '@kbn/core/public';
-import type { SecurityPluginSetup, SecurityPluginStart } from '@kbn/security-plugin/public';
-import { HomePublicPluginSetup } from '@kbn/home-plugin/public';
-import { Sha256 } from '@kbn/crypto-browser';
-import type { CloudExperimentsPluginSetup } from '@kbn/cloud-experiments-plugin/common';
import { registerCloudDeploymentIdAnalyticsContext } from '../common/register_cloud_deployment_id_analytics_context';
import { getIsCloudEnabled } from '../common/is_cloud_enabled';
-import {
- ELASTIC_SUPPORT_LINK,
- CLOUD_SNAPSHOTS_PATH,
- GET_CHAT_USER_DATA_ROUTE_PATH,
-} from '../common/constants';
-import type { GetChatUserDataResponseBody } from '../common/types';
-import { createUserMenuLinks } from './user_menu_links';
+import { ELASTIC_SUPPORT_LINK, CLOUD_SNAPSHOTS_PATH } from '../common/constants';
import { getFullCloudUrl } from './utils';
-import { ChatConfig, ServicesProvider } from './services';
export interface CloudConfigType {
id?: string;
@@ -47,23 +25,6 @@ export interface CloudConfigType {
org_id?: string;
eventTypesAllowlist?: string[];
};
- /** Configuration to enable live chat in Cloud-enabled instances of Kibana. */
- chat: {
- /** Determines if chat is enabled. */
- enabled: boolean;
- /** The URL to the remotely-hosted chat application. */
- chatURL: string;
- };
-}
-
-interface CloudSetupDependencies {
- home?: HomePublicPluginSetup;
- security?: Pick;
- cloudExperiments?: CloudExperimentsPluginSetup;
-}
-
-interface CloudStartDependencies {
- security?: SecurityPluginStart;
}
export interface CloudStart {
@@ -71,6 +32,26 @@ export interface CloudStart {
* A React component that provides a pre-wired `React.Context` which connects components to Cloud services.
*/
CloudContextProvider: FC<{}>;
+ /**
+ * `true` when Kibana is running on Elastic Cloud.
+ */
+ isCloudEnabled: boolean;
+ /**
+ * Cloud ID. Undefined if not running on Cloud.
+ */
+ cloudId?: string;
+ /**
+ * The full URL to the deployment management page on Elastic Cloud. Undefined if not running on Cloud.
+ */
+ deploymentUrl?: string;
+ /**
+ * The full URL to the user profile page on Elastic Cloud. Undefined if not running on Cloud.
+ */
+ profileUrl?: string;
+ /**
+ * The full URL to the organization management page on Elastic Cloud. Undefined if not running on Cloud.
+ */
+ organizationUrl?: string;
}
export interface CloudSetup {
@@ -82,268 +63,93 @@ export interface CloudSetup {
organizationUrl?: string;
snapshotsUrl?: string;
isCloudEnabled: boolean;
+ registerCloudService: (contextProvider: FC) => void;
}
-interface SetupFullStoryDeps {
- analytics: AnalyticsServiceSetup;
- basePath: IBasePath;
-}
-
-interface SetupChatDeps extends Pick {
- http: CoreSetup['http'];
+interface CloudUrls {
+ deploymentUrl?: string;
+ profileUrl?: string;
+ organizationUrl?: string;
+ snapshotsUrl?: string;
}
export class CloudPlugin implements Plugin {
private readonly config: CloudConfigType;
private readonly isCloudEnabled: boolean;
- private chatConfig$ = new BehaviorSubject({ enabled: false });
+ private readonly contextProviders: FC[] = [];
constructor(private readonly initializerContext: PluginInitializerContext) {
this.config = this.initializerContext.config.get();
this.isCloudEnabled = getIsCloudEnabled(this.config.id);
}
- public setup(core: CoreSetup, { cloudExperiments, home, security }: CloudSetupDependencies) {
- this.setupTelemetryContext(core.analytics, security, this.config.id);
-
- this.setupFullStory({ analytics: core.analytics, basePath: core.http.basePath }).catch((e) =>
- // eslint-disable-next-line no-console
- console.debug(`Error setting up FullStory: ${e.toString()}`)
- );
+ public setup(core: CoreSetup): CloudSetup {
+ registerCloudDeploymentIdAnalyticsContext(core.analytics, this.config.id);
- const {
- id,
- cname,
- profile_url: profileUrl,
- organization_url: organizationUrl,
- deployment_url: deploymentUrl,
- base_url: baseUrl,
- } = this.config;
-
- if (this.isCloudEnabled && id) {
- // We use the Hashed Cloud Deployment ID as the userId in the Cloud Experiments
- cloudExperiments?.identifyUser(sha256(id), {
- kibanaVersion: this.initializerContext.env.packageInfo.version,
- });
- }
-
- this.setupChat({ http: core.http, security }).catch((e) =>
- // eslint-disable-next-line no-console
- console.debug(`Error setting up Chat: ${e.toString()}`)
- );
-
- if (home) {
- home.environment.update({ cloud: this.isCloudEnabled });
- if (this.isCloudEnabled) {
- home.tutorials.setVariable('cloud', { id, baseUrl, profileUrl, deploymentUrl });
- }
- }
-
- const fullCloudDeploymentUrl = getFullCloudUrl(baseUrl, deploymentUrl);
- const fullCloudProfileUrl = getFullCloudUrl(baseUrl, profileUrl);
- const fullCloudOrganizationUrl = getFullCloudUrl(baseUrl, organizationUrl);
- const fullCloudSnapshotsUrl = `${fullCloudDeploymentUrl}/${CLOUD_SNAPSHOTS_PATH}`;
+ const { id, cname, base_url: baseUrl } = this.config;
return {
cloudId: id,
cname,
baseUrl,
- deploymentUrl: fullCloudDeploymentUrl,
- profileUrl: fullCloudProfileUrl,
- organizationUrl: fullCloudOrganizationUrl,
- snapshotsUrl: fullCloudSnapshotsUrl,
+ ...this.getCloudUrls(),
isCloudEnabled: this.isCloudEnabled,
+ registerCloudService: (contextProvider) => {
+ this.contextProviders.push(contextProvider);
+ },
};
}
- public start(coreStart: CoreStart, { security }: CloudStartDependencies): CloudStart {
- const { deployment_url: deploymentUrl, base_url: baseUrl } = this.config;
+ public start(coreStart: CoreStart): CloudStart {
coreStart.chrome.setHelpSupportUrl(ELASTIC_SUPPORT_LINK);
- const setLinks = (authorized: boolean) => {
- if (!authorized) return;
-
- if (baseUrl && deploymentUrl) {
- coreStart.chrome.setCustomNavLink({
- title: i18n.translate('xpack.cloud.deploymentLinkLabel', {
- defaultMessage: 'Manage this deployment',
- }),
- euiIconType: 'logoCloud',
- href: getFullCloudUrl(baseUrl, deploymentUrl),
- });
- }
-
- if (security && this.isCloudEnabled) {
- const userMenuLinks = createUserMenuLinks(this.config);
- security.navControlService.addUserMenuLinks(userMenuLinks);
- }
- };
-
- this.checkIfAuthorizedForLinks({ http: coreStart.http, security })
- .then(setLinks)
- // In the event of an unexpected error, fail *open*.
- // Cloud admin console will always perform the actual authorization checks.
- .catch(() => setLinks(true));
-
- // There's a risk that the request for chat config will take too much time to complete, and the provider
- // will maintain a stale value. To avoid this, we'll use an Observable.
+ // Nest all the registered context providers under the Cloud Services Provider.
+ // This way, plugins only need to require Cloud's context provider to have all the enriched Cloud services.
const CloudContextProvider: FC = ({ children }) => {
- const chatConfig = useObservable(this.chatConfig$, { enabled: false });
- return {children};
+ return (
+ <>
+ {this.contextProviders.reduce(
+ (acc, ContextProvider) => (
+ {acc}
+ ),
+ children
+ )}
+ >
+ );
};
+ const { deploymentUrl, profileUrl, organizationUrl } = this.getCloudUrls();
+
return {
CloudContextProvider,
+ isCloudEnabled: this.isCloudEnabled,
+ cloudId: this.config.id,
+ deploymentUrl,
+ profileUrl,
+ organizationUrl,
};
}
public stop() {}
- /**
- * Determines if the current user should see links back to Cloud.
- * This isn't a true authorization check, but rather a heuristic to
- * see if the current user is *likely* a cloud deployment administrator.
- *
- * At this point, we do not have enough information to reliably make this determination,
- * but we do know that all cloud deployment admins are superusers by default.
- */
- private async checkIfAuthorizedForLinks({
- http,
- security,
- }: {
- http: HttpStart;
- security?: SecurityPluginStart;
- }) {
- if (http.anonymousPaths.isAnonymous(window.location.pathname)) {
- return false;
- }
- // Security plugin is disabled
- if (!security) return true;
-
- // Otherwise check if user is a cloud user.
- // If user is not defined due to an unexpected error, then fail *open*.
- // Cloud admin console will always perform the actual authorization checks.
- const user = await security.authc.getCurrentUser().catch(() => null);
- return user?.elastic_cloud_user ?? true;
- }
-
- /**
- * If the right config is provided, register the FullStory shipper to the analytics client.
- * @param analytics Core's Analytics service's setup contract.
- * @param basePath Core's http.basePath helper.
- * @private
- */
- private async setupFullStory({ analytics, basePath }: SetupFullStoryDeps) {
- const { enabled, org_id: fullStoryOrgId, eventTypesAllowlist } = this.config.full_story;
- if (!enabled || !fullStoryOrgId) {
- return; // do not load any FullStory code in the browser if not enabled
- }
-
- // Keep this import async so that we do not load any FullStory code into the browser when it is disabled.
- const { FullStoryShipper } = await import('@kbn/analytics-shippers-fullstory');
- analytics.registerShipper(FullStoryShipper, {
- eventTypesAllowlist,
- fullStoryOrgId,
- // Load an Elastic-internally audited script. Ideally, it should be hosted on a CDN.
- scriptUrl: basePath.prepend(
- `/internal/cloud/${this.initializerContext.env.packageInfo.buildNum}/fullstory.js`
- ),
- namespace: 'FSKibana',
- });
- }
-
- /**
- * Set up the Analytics context providers.
- * @param analytics Core's Analytics service. The Setup contract.
- * @param security The security plugin.
- * @param cloudId The Cloud Org ID.
- * @private
- */
- private setupTelemetryContext(
- analytics: AnalyticsServiceSetup,
- security?: Pick,
- cloudId?: string
- ) {
- registerCloudDeploymentIdAnalyticsContext(analytics, cloudId);
-
- if (security) {
- analytics.registerContextProvider({
- name: 'cloud_user_id',
- context$: from(security.authc.getCurrentUser()).pipe(
- map((user) => {
- if (user.elastic_cloud_user) {
- // If the user is managed by ESS, use the plain username as the user ID:
- // The username is expected to be unique for these users,
- // and it matches how users are identified in the Cloud UI, so it allows us to correlate them.
- return { userId: user.username, isElasticCloudUser: true };
- }
-
- return {
- // For the rest of the authentication providers, we want to add the cloud deployment ID to make it unique.
- // Especially in the case of Elasticsearch-backed authentication, where users are commonly repeated
- // across multiple deployments (i.e.: `elastic` superuser).
- userId: cloudId ? `${cloudId}:${user.username}` : user.username,
- isElasticCloudUser: false,
- };
- }),
- // The hashing here is to keep it at clear as possible in our source code that we do not send literal user IDs
- map(({ userId, isElasticCloudUser }) => ({ userId: sha256(userId), isElasticCloudUser })),
- catchError(() => of({ userId: undefined, isElasticCloudUser: false }))
- ),
- schema: {
- userId: {
- type: 'keyword',
- _meta: { description: 'The user id scoped as seen by Cloud (hashed)' },
- },
- isElasticCloudUser: {
- type: 'boolean',
- _meta: {
- description: '`true` if the user is managed by ESS.',
- },
- },
- },
- });
- }
- }
-
- private async setupChat({ http, security }: SetupChatDeps) {
- if (!this.isCloudEnabled) {
- return;
- }
-
- const { enabled, chatURL } = this.config.chat;
-
- if (!security || !enabled || !chatURL) {
- return;
- }
-
- try {
- const {
- email,
- id,
- token: jwt,
- } = await http.get(GET_CHAT_USER_DATA_ROUTE_PATH);
+ private getCloudUrls(): CloudUrls {
+ const {
+ profile_url: profileUrl,
+ organization_url: organizationUrl,
+ deployment_url: deploymentUrl,
+ base_url: baseUrl,
+ } = this.config;
- if (!email || !id || !jwt) {
- return;
- }
+ const fullCloudDeploymentUrl = getFullCloudUrl(baseUrl, deploymentUrl);
+ const fullCloudProfileUrl = getFullCloudUrl(baseUrl, profileUrl);
+ const fullCloudOrganizationUrl = getFullCloudUrl(baseUrl, organizationUrl);
+ const fullCloudSnapshotsUrl = `${fullCloudDeploymentUrl}/${CLOUD_SNAPSHOTS_PATH}`;
- this.chatConfig$.next({
- enabled,
- chatURL,
- user: {
- email,
- id,
- jwt,
- },
- });
- } catch (e) {
- // eslint-disable-next-line no-console
- console.debug(`[cloud.chat] Could not retrieve chat config: ${e.res.status} ${e.message}`, e);
- }
+ return {
+ deploymentUrl: fullCloudDeploymentUrl,
+ profileUrl: fullCloudProfileUrl,
+ organizationUrl: fullCloudOrganizationUrl,
+ snapshotsUrl: fullCloudSnapshotsUrl,
+ };
}
}
-
-function sha256(str: string) {
- return new Sha256().update(str, 'utf8').digest('hex');
-}
diff --git a/x-pack/plugins/cloud/server/config.ts b/x-pack/plugins/cloud/server/config.ts
index aebbc65e50f18..512542c756798 100644
--- a/x-pack/plugins/cloud/server/config.ts
+++ b/x-pack/plugins/cloud/server/config.ts
@@ -18,32 +18,11 @@ const apmConfigSchema = schema.object({
),
});
-const fullStoryConfigSchema = schema.object({
- enabled: schema.boolean({ defaultValue: false }),
- org_id: schema.conditional(
- schema.siblingRef('enabled'),
- true,
- schema.string({ minLength: 1 }),
- schema.maybe(schema.string())
- ),
- eventTypesAllowlist: schema.arrayOf(schema.string(), {
- defaultValue: ['Loaded Kibana'],
- }),
-});
-
-const chatConfigSchema = schema.object({
- enabled: schema.boolean({ defaultValue: false }),
- chatURL: schema.maybe(schema.string()),
-});
-
const configSchema = schema.object({
apm: schema.maybe(apmConfigSchema),
base_url: schema.maybe(schema.string()),
- chat: chatConfigSchema,
- chatIdentitySecret: schema.maybe(schema.string()),
cname: schema.maybe(schema.string()),
deployment_url: schema.maybe(schema.string()),
- full_story: fullStoryConfigSchema,
id: schema.maybe(schema.string()),
organization_url: schema.maybe(schema.string()),
profile_url: schema.maybe(schema.string()),
@@ -54,10 +33,8 @@ export type CloudConfigType = TypeOf;
export const config: PluginConfigDescriptor = {
exposeToBrowser: {
base_url: true,
- chat: true,
cname: true,
deployment_url: true,
- full_story: true,
id: true,
organization_url: true,
profile_url: true,
diff --git a/x-pack/plugins/cloud/server/mocks.ts b/x-pack/plugins/cloud/server/mocks.ts
new file mode 100644
index 0000000000000..557e64edf6cc1
--- /dev/null
+++ b/x-pack/plugins/cloud/server/mocks.ts
@@ -0,0 +1,25 @@
+/*
+ * 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 { CloudSetup } from '.';
+
+function createSetupMock(): jest.Mocked {
+ return {
+ cloudId: 'mock-cloud-id',
+ instanceSizeMb: 1234,
+ deploymentId: 'deployment-id',
+ isCloudEnabled: true,
+ apm: {
+ url: undefined,
+ secretToken: undefined,
+ },
+ };
+}
+
+export const cloudMock = {
+ createSetup: createSetupMock,
+};
diff --git a/x-pack/plugins/cloud/server/plugin.test.ts b/x-pack/plugins/cloud/server/plugin.test.ts
index 05109a4c54816..55be923e98cf8 100644
--- a/x-pack/plugins/cloud/server/plugin.test.ts
+++ b/x-pack/plugins/cloud/server/plugin.test.ts
@@ -7,111 +7,54 @@
import { coreMock } from '@kbn/core/server/mocks';
import { CloudPlugin } from './plugin';
-import { config } from './config';
-import { securityMock } from '@kbn/security-plugin/server/mocks';
-import { usageCollectionPluginMock } from '@kbn/usage-collection-plugin/server/mocks';
-import { cloudExperimentsMock } from '@kbn/cloud-experiments-plugin/common/mocks';
-import { CloudExperimentsPluginSetup } from '@kbn/cloud-experiments-plugin/common';
+
+const baseConfig = {
+ base_url: 'https://cloud.elastic.co',
+ deployment_url: '/abc123',
+ profile_url: '/user/settings/',
+ organization_url: '/account/',
+};
describe('Cloud Plugin', () => {
describe('#setup', () => {
- describe('setupSecurity', () => {
- it('properly handles missing optional Security dependency if Cloud ID is NOT set.', async () => {
- const plugin = new CloudPlugin(
- coreMock.createPluginInitializerContext(config.schema.validate({}))
- );
+ describe('interface', () => {
+ const setupPlugin = () => {
+ const initContext = coreMock.createPluginInitializerContext({
+ ...baseConfig,
+ id: 'cloudId',
+ cname: 'cloud.elastic.co',
+ });
+ const plugin = new CloudPlugin(initContext);
- expect(() =>
- plugin.setup(coreMock.createSetup(), {
- usageCollection: usageCollectionPluginMock.createSetupContract(),
- })
- ).not.toThrow();
- });
+ const coreSetup = coreMock.createSetup();
+ const setup = plugin.setup(coreSetup, {});
- it('properly handles missing optional Security dependency if Cloud ID is set.', async () => {
- const plugin = new CloudPlugin(
- coreMock.createPluginInitializerContext(config.schema.validate({ id: 'my-cloud' }))
- );
+ return { setup };
+ };
- expect(() =>
- plugin.setup(coreMock.createSetup(), {
- usageCollection: usageCollectionPluginMock.createSetupContract(),
- })
- ).not.toThrow();
+ it('exposes isCloudEnabled', () => {
+ const { setup } = setupPlugin();
+ expect(setup.isCloudEnabled).toBe(true);
});
- it('does not notify Security plugin about Cloud environment if Cloud ID is NOT set.', async () => {
- const plugin = new CloudPlugin(
- coreMock.createPluginInitializerContext(config.schema.validate({}))
- );
-
- const securityDependencyMock = securityMock.createSetup();
- plugin.setup(coreMock.createSetup(), {
- security: securityDependencyMock,
- usageCollection: usageCollectionPluginMock.createSetupContract(),
- });
-
- expect(securityDependencyMock.setIsElasticCloudDeployment).not.toHaveBeenCalled();
+ it('exposes cloudId', () => {
+ const { setup } = setupPlugin();
+ expect(setup.cloudId).toBe('cloudId');
});
- it('properly notifies Security plugin about Cloud environment if Cloud ID is set.', async () => {
- const plugin = new CloudPlugin(
- coreMock.createPluginInitializerContext(config.schema.validate({ id: 'my-cloud' }))
- );
-
- const securityDependencyMock = securityMock.createSetup();
- plugin.setup(coreMock.createSetup(), {
- security: securityDependencyMock,
- usageCollection: usageCollectionPluginMock.createSetupContract(),
- });
-
- expect(securityDependencyMock.setIsElasticCloudDeployment).toHaveBeenCalledTimes(1);
+ it('exposes instanceSizeMb', () => {
+ const { setup } = setupPlugin();
+ expect(setup.instanceSizeMb).toBeUndefined();
});
- });
- describe('Set up cloudExperiments', () => {
- describe('when cloud ID is not provided in the config', () => {
- let cloudExperiments: jest.Mocked;
- beforeEach(() => {
- const plugin = new CloudPlugin(
- coreMock.createPluginInitializerContext(config.schema.validate({}))
- );
- cloudExperiments = cloudExperimentsMock.createSetupMock();
- plugin.setup(coreMock.createSetup(), { cloudExperiments });
- });
-
- test('does not call cloudExperiments.identifyUser', async () => {
- expect(cloudExperiments.identifyUser).not.toHaveBeenCalled();
- });
+ it('exposes deploymentId', () => {
+ const { setup } = setupPlugin();
+ expect(setup.deploymentId).toBe('abc123');
});
- describe('when cloud ID is provided in the config', () => {
- let cloudExperiments: jest.Mocked;
- beforeEach(() => {
- const plugin = new CloudPlugin(
- coreMock.createPluginInitializerContext(config.schema.validate({ id: 'cloud test' }))
- );
- cloudExperiments = cloudExperimentsMock.createSetupMock();
- plugin.setup(coreMock.createSetup(), { cloudExperiments });
- });
-
- test('calls cloudExperiments.identifyUser', async () => {
- expect(cloudExperiments.identifyUser).toHaveBeenCalledTimes(1);
- });
-
- test('the cloud ID is hashed when calling cloudExperiments.identifyUser', async () => {
- expect(cloudExperiments.identifyUser.mock.calls[0][0]).toEqual(
- '1acb4a1cc1c3d672a8d826055d897c2623ceb1d4fb07e46d97986751a36b06cf'
- );
- });
-
- test('specifies the Kibana version when calling cloudExperiments.identifyUser', async () => {
- expect(cloudExperiments.identifyUser.mock.calls[0][1]).toEqual(
- expect.objectContaining({
- kibanaVersion: 'version',
- })
- );
- });
+ it('exposes apm', () => {
+ const { setup } = setupPlugin();
+ expect(setup.apm).toStrictEqual({ url: undefined, secretToken: undefined });
});
});
});
diff --git a/x-pack/plugins/cloud/server/plugin.ts b/x-pack/plugins/cloud/server/plugin.ts
index d38a57a4d3bab..9cf1a308800a0 100644
--- a/x-pack/plugins/cloud/server/plugin.ts
+++ b/x-pack/plugins/cloud/server/plugin.ts
@@ -5,24 +5,17 @@
* 2.0.
*/
-import { UsageCollectionSetup } from '@kbn/usage-collection-plugin/server';
-import { CoreSetup, Logger, Plugin, PluginInitializerContext } from '@kbn/core/server';
-import type { SecurityPluginSetup } from '@kbn/security-plugin/server';
-import type { CloudExperimentsPluginSetup } from '@kbn/cloud-experiments-plugin/common';
-import { createSHA256Hash } from '@kbn/crypto';
+import type { CoreSetup, Plugin, PluginInitializerContext } from '@kbn/core/server';
+import type { UsageCollectionSetup } from '@kbn/usage-collection-plugin/server';
import { registerCloudDeploymentIdAnalyticsContext } from '../common/register_cloud_deployment_id_analytics_context';
-import { CloudConfigType } from './config';
+import type { CloudConfigType } from './config';
import { registerCloudUsageCollector } from './collectors';
import { getIsCloudEnabled } from '../common/is_cloud_enabled';
import { parseDeploymentIdFromDeploymentUrl } from './utils';
-import { registerFullstoryRoute } from './routes/fullstory';
-import { registerChatRoute } from './routes/chat';
import { readInstanceSizeMb } from './env';
interface PluginsSetup {
usageCollection?: UsageCollectionSetup;
- security?: SecurityPluginSetup;
- cloudExperiments?: CloudExperimentsPluginSetup;
}
export interface CloudSetup {
@@ -37,52 +30,17 @@ export interface CloudSetup {
}
export class CloudPlugin implements Plugin {
- private readonly logger: Logger;
private readonly config: CloudConfigType;
- private readonly isDev: boolean;
constructor(private readonly context: PluginInitializerContext) {
- this.logger = this.context.logger.get();
this.config = this.context.config.get();
- this.isDev = this.context.env.mode.dev;
}
- public setup(
- core: CoreSetup,
- { cloudExperiments, usageCollection, security }: PluginsSetup
- ): CloudSetup {
- this.logger.debug('Setting up Cloud plugin');
+ public setup(core: CoreSetup, { usageCollection }: PluginsSetup): CloudSetup {
const isCloudEnabled = getIsCloudEnabled(this.config.id);
registerCloudDeploymentIdAnalyticsContext(core.analytics, this.config.id);
registerCloudUsageCollector(usageCollection, { isCloudEnabled });
- if (isCloudEnabled) {
- security?.setIsElasticCloudDeployment();
- }
-
- if (isCloudEnabled && this.config.id) {
- // We use the Cloud ID as the userId in the Cloud Experiments
- cloudExperiments?.identifyUser(createSHA256Hash(this.config.id), {
- kibanaVersion: this.context.env.packageInfo.version,
- });
- }
-
- if (this.config.full_story.enabled) {
- registerFullstoryRoute({
- httpResources: core.http.resources,
- packageInfo: this.context.env.packageInfo,
- });
- }
-
- if (this.config.chat.enabled && this.config.chatIdentitySecret) {
- registerChatRoute({
- router: core.http.createRouter(),
- chatIdentitySecret: this.config.chatIdentitySecret,
- security,
- isDev: this.isDev,
- });
- }
-
return {
cloudId: this.config.id,
instanceSizeMb: readInstanceSizeMb(),
diff --git a/x-pack/plugins/cloud/tsconfig.json b/x-pack/plugins/cloud/tsconfig.json
index d8c8a5c8eca44..ca9ba32ed10b0 100644
--- a/x-pack/plugins/cloud/tsconfig.json
+++ b/x-pack/plugins/cloud/tsconfig.json
@@ -16,8 +16,5 @@
"references": [
{ "path": "../../../src/core/tsconfig.json" },
{ "path": "../../../src/plugins/usage_collection/tsconfig.json" },
- { "path": "../../../src/plugins/home/tsconfig.json" },
- { "path": "../cloud_integrations/cloud_experiments/tsconfig.json" },
- { "path": "../security/tsconfig.json" },
]
}
diff --git a/x-pack/plugins/cloud/.storybook/decorator.tsx b/x-pack/plugins/cloud_integrations/cloud_chat/.storybook/decorator.tsx
similarity index 89%
rename from x-pack/plugins/cloud/.storybook/decorator.tsx
rename to x-pack/plugins/cloud_integrations/cloud_chat/.storybook/decorator.tsx
index 4489b58f75759..3af8d04a598eb 100644
--- a/x-pack/plugins/cloud/.storybook/decorator.tsx
+++ b/x-pack/plugins/cloud_integrations/cloud_chat/.storybook/decorator.tsx
@@ -7,12 +7,11 @@
import React from 'react';
import { DecoratorFn } from '@storybook/react';
-import { ServicesProvider, CloudServices } from '../public/services';
+import { ServicesProvider, CloudChatServices } from '../public/services';
// TODO: move to a storybook implementation of the service using parameters.
-const services: CloudServices = {
+const services: CloudChatServices = {
chat: {
- enabled: true,
chatURL: 'https://elasticcloud-production-chat-us-east-1.s3.amazonaws.com/drift-iframe.html',
user: {
id: 'user-id',
diff --git a/x-pack/plugins/cloud/.storybook/index.ts b/x-pack/plugins/cloud_integrations/cloud_chat/.storybook/index.ts
similarity index 100%
rename from x-pack/plugins/cloud/.storybook/index.ts
rename to x-pack/plugins/cloud_integrations/cloud_chat/.storybook/index.ts
diff --git a/x-pack/plugins/cloud/.storybook/main.ts b/x-pack/plugins/cloud_integrations/cloud_chat/.storybook/main.ts
similarity index 100%
rename from x-pack/plugins/cloud/.storybook/main.ts
rename to x-pack/plugins/cloud_integrations/cloud_chat/.storybook/main.ts
diff --git a/x-pack/plugins/cloud/.storybook/manager.ts b/x-pack/plugins/cloud_integrations/cloud_chat/.storybook/manager.ts
similarity index 100%
rename from x-pack/plugins/cloud/.storybook/manager.ts
rename to x-pack/plugins/cloud_integrations/cloud_chat/.storybook/manager.ts
diff --git a/x-pack/plugins/cloud/.storybook/preview.ts b/x-pack/plugins/cloud_integrations/cloud_chat/.storybook/preview.ts
similarity index 100%
rename from x-pack/plugins/cloud/.storybook/preview.ts
rename to x-pack/plugins/cloud_integrations/cloud_chat/.storybook/preview.ts
diff --git a/x-pack/plugins/cloud_integrations/cloud_chat/README.md b/x-pack/plugins/cloud_integrations/cloud_chat/README.md
new file mode 100755
index 0000000000000..cee3d9f5a6671
--- /dev/null
+++ b/x-pack/plugins/cloud_integrations/cloud_chat/README.md
@@ -0,0 +1,3 @@
+# Cloud Chat
+
+Integrates with DriftChat in order to provide live support to our Elastic Cloud users. This plugin should only run on Elastic Cloud.
diff --git a/x-pack/plugins/cloud_integrations/cloud_chat/common/constants.ts b/x-pack/plugins/cloud_integrations/cloud_chat/common/constants.ts
new file mode 100755
index 0000000000000..d7bd133e5b4f9
--- /dev/null
+++ b/x-pack/plugins/cloud_integrations/cloud_chat/common/constants.ts
@@ -0,0 +1,8 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+export const GET_CHAT_USER_DATA_ROUTE_PATH = '/internal/cloud/chat_user';
diff --git a/x-pack/plugins/cloud/common/types.ts b/x-pack/plugins/cloud_integrations/cloud_chat/common/types.ts
similarity index 100%
rename from x-pack/plugins/cloud/common/types.ts
rename to x-pack/plugins/cloud_integrations/cloud_chat/common/types.ts
diff --git a/x-pack/plugins/cloud_integrations/cloud_chat/jest.config.js b/x-pack/plugins/cloud_integrations/cloud_chat/jest.config.js
new file mode 100644
index 0000000000000..44f6f241d44d0
--- /dev/null
+++ b/x-pack/plugins/cloud_integrations/cloud_chat/jest.config.js
@@ -0,0 +1,18 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+module.exports = {
+ preset: '@kbn/test',
+ rootDir: '../../../../',
+ roots: ['/x-pack/plugins/cloud_integrations/cloud_chat'],
+ coverageDirectory:
+ '/target/kibana-coverage/jest/x-pack/plugins/cloud_integrations/cloud_chat',
+ coverageReporters: ['text', 'html'],
+ collectCoverageFrom: [
+ '/x-pack/plugins/cloud_integrations/cloud_chat/{common,public,server}/**/*.{ts,tsx}',
+ ],
+};
diff --git a/x-pack/plugins/cloud_integrations/cloud_chat/kibana.json b/x-pack/plugins/cloud_integrations/cloud_chat/kibana.json
new file mode 100755
index 0000000000000..76f7e34e71e56
--- /dev/null
+++ b/x-pack/plugins/cloud_integrations/cloud_chat/kibana.json
@@ -0,0 +1,15 @@
+{
+ "id": "cloudChat",
+ "version": "1.0.0",
+ "kibanaVersion": "kibana",
+ "owner": {
+ "name": "Kibana Core",
+ "githubTeam": "kibana-core"
+ },
+ "description": "Chat available on Elastic Cloud deployments for quicker assistance.",
+ "server": true,
+ "ui": true,
+ "configPath": ["xpack", "cloud_integrations", "chat"],
+ "requiredPlugins": ["cloud"],
+ "optionalPlugins": ["security"]
+}
diff --git a/x-pack/plugins/cloud/public/components/chat/chat.stories.tsx b/x-pack/plugins/cloud_integrations/cloud_chat/public/components/chat/chat.stories.tsx
similarity index 99%
rename from x-pack/plugins/cloud/public/components/chat/chat.stories.tsx
rename to x-pack/plugins/cloud_integrations/cloud_chat/public/components/chat/chat.stories.tsx
index 7e673e341cec7..295750ee43039 100644
--- a/x-pack/plugins/cloud/public/components/chat/chat.stories.tsx
+++ b/x-pack/plugins/cloud_integrations/cloud_chat/public/components/chat/chat.stories.tsx
@@ -68,7 +68,6 @@ export const Component = ({ id, email, chatURL, jwt }: Params) => {
return (
{}, onReady, onResize }: Props) => {
}}
size="xs"
>
- {i18n.translate('xpack.cloud.chat.hideChatButtonLabel', {
+ {i18n.translate('xpack.cloudChat.hideChatButtonLabel', {
defaultMessage: 'Hide chat',
})}
@@ -80,7 +80,7 @@ export const Chat = ({ onHide = () => {}, onReady, onResize }: Props) => {
{button}
-
+
{i18n.translate(
'xpack.enterpriseSearch.content.indices.pipelines.addInferencePipelineModal.steps.configure.docsLink',
{
diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/no_models.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/no_models.tsx
index 084fb4244cb7a..66ffbe45c1777 100644
--- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/no_models.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/no_models.tsx
@@ -14,6 +14,8 @@ import { i18n } from '@kbn/i18n';
import noMlModelsGraphicDark from '../../../../../../assets/images/no_ml_models_dark.svg';
import noMlModelsGraphicLight from '../../../../../../assets/images/no_ml_models_light.svg';
+import { docLinks } from '../../../../../shared/doc_links';
+
export const NoModelsPanel: React.FC = () => {
const { colorMode } = useEuiTheme();
@@ -43,8 +45,7 @@ export const NoModelsPanel: React.FC = () => {
>
}
footer={
- // TODO: insert correct docsLink here
-
+
{i18n.translate(
'xpack.enterpriseSearch.appSearch.crawler.crawlRequestsTable.emptyPrompt.docsLink',
{
diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/pipelines.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/pipelines.tsx
index 07be63b54f3b5..9cab24190a2de 100644
--- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/pipelines.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/pipelines.tsx
@@ -87,7 +87,7 @@ export const SearchIndexPipelines: React.FC = () => {
+
{i18n.translate(
'xpack.enterpriseSearch.content.indices.pipelines.mlInferencePipelines.docLink',
{
diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/components/product_selector/product_selector.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/components/product_selector/product_selector.tsx
index 1bb6bb3777d81..7be1264f2a9da 100644
--- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/components/product_selector/product_selector.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_overview/components/product_selector/product_selector.tsx
@@ -19,7 +19,7 @@ import {
EuiSpacer,
EuiTitle,
} from '@elastic/eui';
-import { Chat } from '@kbn/cloud-plugin/public';
+import { Chat } from '@kbn/cloud-chat-plugin/public';
import { i18n } from '@kbn/i18n';
import {
@@ -71,7 +71,7 @@ export const ProductSelector: React.FC = ({
<>
= ({ title, butto
{i18n.translate('xpack.enterpriseSearch.emptyState.description', {
defaultMessage:
- 'An Elasticsearch index is where your content gets stored. Get started by creating an Elasticsearch index and selecting an ingestion method. Options include the Elastic web crawler, third party data integrations, or using Elasticsearch API endpoints.',
+ 'Your content is stored in an Elasticsearch index. Get started by creating an Elasticsearch index and selecting an ingestion method. Options include the Elastic web crawler, third party data integrations, or using Elasticsearch API endpoints.',
})}
diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/constants/labels.ts b/x-pack/plugins/enterprise_search/public/applications/shared/constants/labels.ts
index fa982547f7fc5..edfef1ba1f3fc 100644
--- a/x-pack/plugins/enterprise_search/public/applications/shared/constants/labels.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/shared/constants/labels.ts
@@ -15,10 +15,6 @@ export const PASSWORD_LABEL = i18n.translate('xpack.enterpriseSearch.passwordLab
defaultMessage: 'Password',
});
-export const TOKEN_LABEL = i18n.translate('xpack.enterpriseSearch.tokenLabel', {
- defaultMessage: 'Token',
-});
-
export const TYPE_LABEL = i18n.translate('xpack.enterpriseSearch.typeLabel', {
defaultMessage: 'Type',
});
diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/doc_links/doc_links.ts b/x-pack/plugins/enterprise_search/public/applications/shared/doc_links/doc_links.ts
index 17ee2230b2fb7..975e7981829f2 100644
--- a/x-pack/plugins/enterprise_search/public/applications/shared/doc_links/doc_links.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/shared/doc_links/doc_links.ts
@@ -65,6 +65,7 @@ class DocLinks {
public crawlerGettingStarted: string;
public crawlerManaging: string;
public crawlerOverview: string;
+ public deployTrainedModels: string;
public documentLevelSecurity: string;
public elasticsearchCreateIndex: string;
public elasticsearchGettingStarted: string;
@@ -178,6 +179,7 @@ class DocLinks {
this.crawlerGettingStarted = '';
this.crawlerManaging = '';
this.crawlerOverview = '';
+ this.deployTrainedModels = '';
this.documentLevelSecurity = '';
this.elasticsearchCreateIndex = '';
this.elasticsearchGettingStarted = '';
@@ -293,6 +295,7 @@ class DocLinks {
this.crawlerGettingStarted = docLinks.links.enterpriseSearch.crawlerGettingStarted;
this.crawlerManaging = docLinks.links.enterpriseSearch.crawlerManaging;
this.crawlerOverview = docLinks.links.enterpriseSearch.crawlerOverview;
+ this.deployTrainedModels = docLinks.links.enterpriseSearch.deployTrainedModels;
this.documentLevelSecurity = docLinks.links.enterpriseSearch.documentLevelSecurity;
this.elasticsearchCreateIndex = docLinks.links.elasticsearch.createIndex;
this.elasticsearchGettingStarted = docLinks.links.elasticsearch.gettingStarted;
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 5abd7db73170b..183e27a765c2f 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
@@ -4,6 +4,7 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
+import { merge } from 'lodash';
import { ElasticsearchClient } from '@kbn/core/server';
@@ -41,7 +42,7 @@ describe('createIndexPipelineDefinitions util function', () => {
describe('formatMlPipelineBody util function', () => {
const pipelineName = 'ml-inference-my-ml-proc';
const modelId = 'my-model-id';
- let modelInputField = 'my-model-input-field';
+ const modelInputField = 'my-model-input-field';
const modelType = 'pytorch';
const inferenceConfigKey = 'my-model-type';
const modelTypes = ['pytorch', 'my-model-type'];
@@ -49,6 +50,55 @@ describe('formatMlPipelineBody util function', () => {
const sourceField = 'my-source-field';
const destField = 'my-dest-field';
+ const expectedResult = {
+ description: '',
+ processors: [
+ {
+ remove: {
+ field: `ml.inference.${destField}`,
+ ignore_missing: true,
+ },
+ },
+ {
+ inference: {
+ field_map: {
+ [sourceField]: modelInputField,
+ },
+ model_id: modelId,
+ target_field: `ml.inference.${destField}`,
+ on_failure: [
+ {
+ append: {
+ field: '_source._ingest.inference_errors',
+ value: [
+ {
+ pipeline: pipelineName,
+ message: `Processor 'inference' in pipeline '${pipelineName}' failed with message '{{ _ingest.on_failure_message }}'`,
+ timestamp: '{{{ _ingest.timestamp }}}',
+ },
+ ],
+ },
+ },
+ ],
+ },
+ },
+ {
+ append: {
+ field: '_source._ingest.processors',
+ value: [
+ {
+ model_version: modelVersion,
+ pipeline: pipelineName,
+ processed_timestamp: '{{{ _ingest.timestamp }}}',
+ types: modelTypes,
+ },
+ ],
+ },
+ },
+ ],
+ version: 1,
+ };
+
const mockClient = {
ml: {
getTrainedModels: jest.fn(),
@@ -60,41 +110,6 @@ describe('formatMlPipelineBody util function', () => {
});
it('should return the pipeline body', async () => {
- const expectedResult = {
- description: '',
- processors: [
- {
- remove: {
- field: `ml.inference.${destField}`,
- ignore_missing: true,
- },
- },
- {
- inference: {
- field_map: {
- [sourceField]: modelInputField,
- },
- model_id: modelId,
- target_field: `ml.inference.${destField}`,
- },
- },
- {
- append: {
- field: '_source._ingest.processors',
- value: [
- {
- model_version: modelVersion,
- pipeline: pipelineName,
- processed_timestamp: '{{{ _ingest.timestamp }}}',
- types: modelTypes,
- },
- ],
- },
- },
- ],
- version: 1,
- };
-
const mockResponse = {
count: 1,
trained_model_configs: [
@@ -136,41 +151,19 @@ describe('formatMlPipelineBody util function', () => {
});
it('should insert a placeholder if model has no input fields', async () => {
- modelInputField = 'MODEL_INPUT_FIELD';
- const expectedResult = {
- description: '',
+ const expectedResultWithNoInputField = merge({}, expectedResult, {
processors: [
- {
- remove: {
- field: `ml.inference.${destField}`,
- ignore_missing: true,
- },
- },
+ {}, // append - we'll leave it untouched
{
inference: {
field_map: {
- [sourceField]: modelInputField,
+ [sourceField]: 'MODEL_INPUT_FIELD',
},
- model_id: modelId,
- target_field: `ml.inference.${destField}`,
- },
- },
- {
- append: {
- field: '_source._ingest.processors',
- value: [
- {
- model_version: modelVersion,
- pipeline: pipelineName,
- processed_timestamp: '{{{ _ingest.timestamp }}}',
- types: modelTypes,
- },
- ],
},
},
],
- version: 1,
- };
+ });
+
const mockResponse = {
count: 1,
trained_model_configs: [
@@ -193,7 +186,7 @@ describe('formatMlPipelineBody util function', () => {
destField,
mockClient as unknown as ElasticsearchClient
);
- expect(actualResult).toEqual(expectedResult);
+ expect(actualResult).toEqual(expectedResultWithNoInputField);
expect(mockClient.ml.getTrainedModels).toHaveBeenCalledTimes(1);
});
});
diff --git a/x-pack/plugins/enterprise_search/server/routes/enterprise_search/connectors.ts b/x-pack/plugins/enterprise_search/server/routes/enterprise_search/connectors.ts
index 0aaf30ef126d4..9663b216ec91c 100644
--- a/x-pack/plugins/enterprise_search/server/routes/enterprise_search/connectors.ts
+++ b/x-pack/plugins/enterprise_search/server/routes/enterprise_search/connectors.ts
@@ -254,8 +254,8 @@ export function registerConnectorRoutes({ router, log }: RouteDependencies) {
connectorId: schema.string(),
}),
body: schema.object({
- name: schema.maybe(schema.string()),
- description: schema.maybe(schema.string()),
+ name: schema.string(),
+ description: schema.nullable(schema.string()),
}),
},
},
diff --git a/x-pack/plugins/enterprise_search/tsconfig.json b/x-pack/plugins/enterprise_search/tsconfig.json
index ce288f8b4b97d..10fcc3b8c0d58 100644
--- a/x-pack/plugins/enterprise_search/tsconfig.json
+++ b/x-pack/plugins/enterprise_search/tsconfig.json
@@ -21,6 +21,7 @@
{ "path": "../../../src/plugins/kibana_react/tsconfig.json" },
{ "path": "../../../src/plugins/usage_collection/tsconfig.json" },
{ "path": "../cloud/tsconfig.json" },
+ { "path": "../cloud_integrations/cloud_chat/tsconfig.json" },
{ "path": "../infra/tsconfig.json" },
{ "path": "../features/tsconfig.json" },
{ "path": "../licensing/tsconfig.json" },
diff --git a/x-pack/plugins/fleet/.storybook/context/cloud.ts b/x-pack/plugins/fleet/.storybook/context/cloud.ts
index eccb41d6aa8c0..1bd63d673bb3e 100644
--- a/x-pack/plugins/fleet/.storybook/context/cloud.ts
+++ b/x-pack/plugins/fleet/.storybook/context/cloud.ts
@@ -17,6 +17,7 @@ export const getCloud = ({ isCloudEnabled }: { isCloudEnabled: boolean }) => {
organizationUrl: 'https://organization.url',
profileUrl: 'https://profile.url',
snapshotsUrl: 'https://snapshots.url',
+ registerCloudService: () => {},
};
return cloud;
diff --git a/x-pack/plugins/fleet/common/types/models/agent.ts b/x-pack/plugins/fleet/common/types/models/agent.ts
index fa54f8c943e27..ea2ad78dc10cd 100644
--- a/x-pack/plugins/fleet/common/types/models/agent.ts
+++ b/x-pack/plugins/fleet/common/types/models/agent.ts
@@ -94,7 +94,12 @@ interface AgentBase {
export interface Agent extends AgentBase {
id: string;
access_api_key?: string;
+ // @deprecated
default_api_key_history?: FleetServerAgent['default_api_key_history'];
+ outputs?: Array<{
+ api_key_id: string;
+ to_retire_api_key_ids?: FleetServerAgent['default_api_key_history'];
+ }>;
status?: AgentStatus;
packages: string[];
sort?: Array;
diff --git a/x-pack/plugins/fleet/kibana.json b/x-pack/plugins/fleet/kibana.json
index 6ab87283e0b26..79d8bbd40644e 100644
--- a/x-pack/plugins/fleet/kibana.json
+++ b/x-pack/plugins/fleet/kibana.json
@@ -11,5 +11,5 @@
"requiredPlugins": ["licensing", "data", "encryptedSavedObjects", "navigation", "customIntegrations", "share", "spaces", "security", "unifiedSearch", "savedObjectsTagging", "taskManager"],
"optionalPlugins": ["features", "cloud", "usageCollection", "home", "globalSearch", "telemetry", "discover", "ingestPipelines"],
"extraPublicDirs": ["common"],
- "requiredBundles": ["kibanaReact", "cloud", "esUiShared", "infra", "kibanaUtils", "usageCollection", "unifiedSearch"]
+ "requiredBundles": ["kibanaReact", "cloudChat", "esUiShared", "infra", "kibanaUtils", "usageCollection", "unifiedSearch"]
}
diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/hooks/devtools_request.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/hooks/devtools_request.tsx
new file mode 100644
index 0000000000000..55e91154060b7
--- /dev/null
+++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/hooks/devtools_request.tsx
@@ -0,0 +1,74 @@
+/*
+ * 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 { useMemo } from 'react';
+import { i18n } from '@kbn/i18n';
+
+import { ExperimentalFeaturesService } from '../../../../../services';
+import {
+ generateCreatePackagePolicyDevToolsRequest,
+ generateCreateAgentPolicyDevToolsRequest,
+} from '../../../services';
+import {
+ FLEET_SYSTEM_PACKAGE,
+ HIDDEN_API_REFERENCE_PACKAGES,
+} from '../../../../../../../../common/constants';
+import type { PackageInfo, NewAgentPolicy, NewPackagePolicy } from '../../../../../types';
+import { SelectedPolicyTab } from '../../components';
+
+export function useDevToolsRequest({
+ newAgentPolicy,
+ packagePolicy,
+ packageInfo,
+ selectedPolicyTab,
+ withSysMonitoring,
+}: {
+ withSysMonitoring: boolean;
+ selectedPolicyTab: SelectedPolicyTab;
+ newAgentPolicy: NewAgentPolicy;
+ packagePolicy: NewPackagePolicy;
+ packageInfo?: PackageInfo;
+}) {
+ const { showDevtoolsRequest: isShowDevtoolRequestExperimentEnabled } =
+ ExperimentalFeaturesService.get();
+
+ const showDevtoolsRequest =
+ !HIDDEN_API_REFERENCE_PACKAGES.includes(packageInfo?.name ?? '') &&
+ isShowDevtoolRequestExperimentEnabled;
+
+ const [devtoolRequest, devtoolRequestDescription] = useMemo(() => {
+ if (selectedPolicyTab === SelectedPolicyTab.NEW) {
+ const packagePolicyIsSystem = packagePolicy?.package?.name === FLEET_SYSTEM_PACKAGE;
+ return [
+ `${generateCreateAgentPolicyDevToolsRequest(
+ newAgentPolicy,
+ withSysMonitoring && !packagePolicyIsSystem
+ )}\n\n${generateCreatePackagePolicyDevToolsRequest({
+ ...packagePolicy,
+ })}`,
+ i18n.translate(
+ 'xpack.fleet.createPackagePolicy.devtoolsRequestWithAgentPolicyDescription',
+ {
+ defaultMessage:
+ 'These Kibana requests creates a new agent policy and a new package policy.',
+ }
+ ),
+ ];
+ }
+
+ return [
+ generateCreatePackagePolicyDevToolsRequest({
+ ...packagePolicy,
+ }),
+ i18n.translate('xpack.fleet.createPackagePolicy.devtoolsRequestDescription', {
+ defaultMessage: 'This Kibana request creates a new package policy.',
+ }),
+ ];
+ }, [packagePolicy, newAgentPolicy, withSysMonitoring, selectedPolicyTab]);
+
+ return { showDevtoolsRequest, devtoolRequest, devtoolRequestDescription };
+}
diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/hooks/form.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/hooks/form.tsx
new file mode 100644
index 0000000000000..e0f206ef612a8
--- /dev/null
+++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/hooks/form.tsx
@@ -0,0 +1,318 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { useCallback, useState } from 'react';
+import { i18n } from '@kbn/i18n';
+import { safeLoad } from 'js-yaml';
+
+import type {
+ AgentPolicy,
+ NewPackagePolicy,
+ NewAgentPolicy,
+ CreatePackagePolicyRequest,
+ PackagePolicy,
+ PackageInfo,
+} from '../../../../../types';
+import {
+ useStartServices,
+ sendCreateAgentPolicy,
+ sendCreatePackagePolicy,
+ sendBulkInstallPackages,
+} from '../../../../../hooks';
+import { isVerificationError } from '../../../../../services';
+import { FLEET_ELASTIC_AGENT_PACKAGE, FLEET_SYSTEM_PACKAGE } from '../../../../../../../../common';
+import { useConfirmForceInstall } from '../../../../../../integrations/hooks';
+import { validatePackagePolicy, validationHasErrors } from '../../services';
+import type { PackagePolicyValidationResults } from '../../services';
+import type { PackagePolicyFormState } from '../../types';
+import { SelectedPolicyTab } from '../../components';
+import { useOnSaveNavigate } from '../../hooks';
+
+async function createAgentPolicy({
+ packagePolicy,
+ newAgentPolicy,
+ withSysMonitoring,
+}: {
+ packagePolicy: NewPackagePolicy;
+ newAgentPolicy: NewAgentPolicy;
+ withSysMonitoring: boolean;
+}): Promise {
+ // do not create agent policy with system integration if package policy already is for system package
+ const packagePolicyIsSystem = packagePolicy?.package?.name === FLEET_SYSTEM_PACKAGE;
+ const resp = await sendCreateAgentPolicy(newAgentPolicy, {
+ withSysMonitoring: withSysMonitoring && !packagePolicyIsSystem,
+ });
+ if (resp.error) {
+ throw resp.error;
+ }
+ if (!resp.data) {
+ throw new Error('Invalid agent policy creation no data');
+ }
+ return resp.data.item;
+}
+
+async function savePackagePolicy(pkgPolicy: CreatePackagePolicyRequest['body']) {
+ const result = await sendCreatePackagePolicy(pkgPolicy);
+
+ return result;
+}
+
+export function useOnSubmit({
+ agentCount,
+ selectedPolicyTab,
+ newAgentPolicy,
+ withSysMonitoring,
+ queryParamsPolicyId,
+ packageInfo,
+}: {
+ packageInfo?: PackageInfo;
+ newAgentPolicy: NewAgentPolicy;
+ withSysMonitoring: boolean;
+ selectedPolicyTab: SelectedPolicyTab;
+ agentCount: number;
+ queryParamsPolicyId: string | undefined;
+}) {
+ const { notifications } = useStartServices();
+ const confirmForceInstall = useConfirmForceInstall();
+ // only used to store the resulting package policy once saved
+ const [savedPackagePolicy, setSavedPackagePolicy] = useState();
+ // Form state
+ const [formState, setFormState] = useState('VALID');
+
+ const [agentPolicy, setAgentPolicy] = useState();
+ // New package policy state
+ const [packagePolicy, setPackagePolicy] = useState({
+ name: '',
+ description: '',
+ namespace: 'default',
+ policy_id: '',
+ enabled: true,
+ inputs: [],
+ });
+
+ // Validation state
+ const [validationResults, setValidationResults] = useState();
+ const [hasAgentPolicyError, setHasAgentPolicyError] = useState(false);
+ const hasErrors = validationResults ? validationHasErrors(validationResults) : false;
+
+ // Update agent policy method
+ const updateAgentPolicy = useCallback(
+ (updatedAgentPolicy: AgentPolicy | undefined) => {
+ if (updatedAgentPolicy) {
+ setAgentPolicy(updatedAgentPolicy);
+ if (packageInfo) {
+ setHasAgentPolicyError(false);
+ }
+ } else {
+ setHasAgentPolicyError(true);
+ setAgentPolicy(undefined);
+ }
+
+ // eslint-disable-next-line no-console
+ console.debug('Agent policy updated', updatedAgentPolicy);
+ },
+ [packageInfo, setAgentPolicy]
+ );
+ // Update package policy validation
+ const updatePackagePolicyValidation = useCallback(
+ (newPackagePolicy?: NewPackagePolicy) => {
+ if (packageInfo) {
+ const newValidationResult = validatePackagePolicy(
+ newPackagePolicy || packagePolicy,
+ packageInfo,
+ safeLoad
+ );
+ setValidationResults(newValidationResult);
+ // eslint-disable-next-line no-console
+ console.debug('Package policy validation results', newValidationResult);
+
+ return newValidationResult;
+ }
+ },
+ [packagePolicy, packageInfo]
+ );
+ // Update package policy method
+ const updatePackagePolicy = useCallback(
+ (updatedFields: Partial) => {
+ const newPackagePolicy = {
+ ...packagePolicy,
+ ...updatedFields,
+ };
+ setPackagePolicy(newPackagePolicy);
+
+ // eslint-disable-next-line no-console
+ console.debug('Package policy updated', newPackagePolicy);
+ const newValidationResults = updatePackagePolicyValidation(newPackagePolicy);
+ const hasPackage = newPackagePolicy.package;
+ const hasValidationErrors = newValidationResults
+ ? validationHasErrors(newValidationResults)
+ : false;
+ const hasAgentPolicy = newPackagePolicy.policy_id && newPackagePolicy.policy_id !== '';
+ if (
+ hasPackage &&
+ (hasAgentPolicy || selectedPolicyTab === SelectedPolicyTab.NEW) &&
+ !hasValidationErrors
+ ) {
+ setFormState('VALID');
+ } else {
+ setFormState('INVALID');
+ }
+ },
+ [packagePolicy, setFormState, updatePackagePolicyValidation, selectedPolicyTab]
+ );
+
+ const onSaveNavigate = useOnSaveNavigate({
+ packagePolicy,
+ queryParamsPolicyId,
+ });
+
+ const navigateAddAgent = (policy?: PackagePolicy) =>
+ onSaveNavigate(policy, ['openEnrollmentFlyout']);
+
+ const navigateAddAgentHelp = (policy?: PackagePolicy) =>
+ onSaveNavigate(policy, ['showAddAgentHelp']);
+
+ const onSubmit = useCallback(
+ async ({
+ force,
+ overrideCreatedAgentPolicy,
+ }: { overrideCreatedAgentPolicy?: AgentPolicy; force?: boolean } = {}) => {
+ if (formState === 'VALID' && hasErrors) {
+ setFormState('INVALID');
+ return;
+ }
+ if (agentCount !== 0 && formState !== 'CONFIRM') {
+ setFormState('CONFIRM');
+ return;
+ }
+ let createdPolicy = overrideCreatedAgentPolicy;
+ if (selectedPolicyTab === SelectedPolicyTab.NEW && !overrideCreatedAgentPolicy) {
+ try {
+ setFormState('LOADING');
+ if ((withSysMonitoring || newAgentPolicy.monitoring_enabled?.length) ?? 0 > 0) {
+ const packagesToPreinstall: string[] = [];
+ if (packageInfo) {
+ packagesToPreinstall.push(packageInfo.name);
+ }
+ if (withSysMonitoring) {
+ packagesToPreinstall.push(FLEET_SYSTEM_PACKAGE);
+ }
+ if (newAgentPolicy.monitoring_enabled?.length ?? 0 > 0) {
+ packagesToPreinstall.push(FLEET_ELASTIC_AGENT_PACKAGE);
+ }
+
+ if (packagesToPreinstall.length > 0) {
+ await sendBulkInstallPackages([...new Set(packagesToPreinstall)]);
+ }
+ }
+
+ createdPolicy = await createAgentPolicy({
+ newAgentPolicy,
+ packagePolicy,
+ withSysMonitoring,
+ });
+ setAgentPolicy(createdPolicy);
+ updatePackagePolicy({ policy_id: createdPolicy.id });
+ } catch (e) {
+ setFormState('VALID');
+ notifications.toasts.addError(e, {
+ title: i18n.translate('xpack.fleet.createAgentPolicy.errorNotificationTitle', {
+ defaultMessage: 'Unable to create agent policy',
+ }),
+ });
+ return;
+ }
+ }
+
+ setFormState('LOADING');
+ // passing pkgPolicy with policy_id here as setPackagePolicy doesn't propagate immediately
+ const { error, data } = await savePackagePolicy({
+ ...packagePolicy,
+ policy_id: createdPolicy?.id ?? packagePolicy.policy_id,
+ force,
+ });
+ setFormState(agentCount ? 'SUBMITTED' : 'SUBMITTED_NO_AGENTS');
+ if (!error) {
+ setSavedPackagePolicy(data!.item);
+
+ const hasAgentsAssigned = agentCount && agentPolicy;
+ if (!hasAgentsAssigned) {
+ setFormState('SUBMITTED_NO_AGENTS');
+ return;
+ }
+ onSaveNavigate(data!.item);
+
+ notifications.toasts.addSuccess({
+ title: i18n.translate('xpack.fleet.createPackagePolicy.addedNotificationTitle', {
+ defaultMessage: `'{packagePolicyName}' integration added.`,
+ values: {
+ packagePolicyName: packagePolicy.name,
+ },
+ }),
+ text: hasAgentsAssigned
+ ? i18n.translate('xpack.fleet.createPackagePolicy.addedNotificationMessage', {
+ defaultMessage: `Fleet will deploy updates to all agents that use the '{agentPolicyName}' policy.`,
+ values: {
+ agentPolicyName: agentPolicy!.name,
+ },
+ })
+ : undefined,
+ 'data-test-subj': 'packagePolicyCreateSuccessToast',
+ });
+ } else {
+ if (isVerificationError(error)) {
+ setFormState('VALID'); // don't show the add agent modal
+ const forceInstall = await confirmForceInstall(packagePolicy.package!);
+
+ if (forceInstall) {
+ // skip creating the agent policy because it will have already been successfully created
+ onSubmit({ overrideCreatedAgentPolicy: createdPolicy, force: true });
+ }
+ return;
+ }
+ notifications.toasts.addError(error, {
+ title: 'Error',
+ });
+ setFormState('VALID');
+ }
+ },
+ [
+ formState,
+ hasErrors,
+ agentCount,
+ selectedPolicyTab,
+ packagePolicy,
+ notifications.toasts,
+ agentPolicy,
+ onSaveNavigate,
+ confirmForceInstall,
+ newAgentPolicy,
+ updatePackagePolicy,
+ withSysMonitoring,
+ packageInfo,
+ ]
+ );
+
+ return {
+ agentPolicy,
+ updateAgentPolicy,
+ packagePolicy,
+ updatePackagePolicy,
+ savedPackagePolicy,
+ onSubmit,
+ formState,
+ setFormState,
+ hasErrors,
+ validationResults,
+ setValidationResults,
+ hasAgentPolicyError,
+ setHasAgentPolicyError,
+ // TODO check
+ navigateAddAgent,
+ navigateAddAgentHelp,
+ };
+}
diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/hooks/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/hooks/index.tsx
new file mode 100644
index 0000000000000..33d1cee841590
--- /dev/null
+++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/hooks/index.tsx
@@ -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; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+export { useDevToolsRequest } from './devtools_request';
+export { useOnSubmit } from './form';
diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/index.tsx
index fae3c84f21268..02f36e2cadcfe 100644
--- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/index.tsx
+++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/index.tsx
@@ -21,35 +21,16 @@ import {
EuiErrorBoundary,
} from '@elastic/eui';
import type { EuiStepProps } from '@elastic/eui/src/components/steps/step';
-import { safeLoad } from 'js-yaml';
-import { useCancelAddPackagePolicy, useOnSaveNavigate } from '../hooks';
-import type { CreatePackagePolicyRequest } from '../../../../../../../common/types';
+import { useCancelAddPackagePolicy } from '../hooks';
import { splitPkgKey } from '../../../../../../../common/services';
-import {
- dataTypes,
- FLEET_SYSTEM_PACKAGE,
- HIDDEN_API_REFERENCE_PACKAGES,
-} from '../../../../../../../common/constants';
-import { useConfirmForceInstall } from '../../../../../integrations/hooks';
-import type {
- AgentPolicy,
- NewAgentPolicy,
- NewPackagePolicy,
- PackagePolicy,
-} from '../../../../types';
-import {
- sendCreatePackagePolicy,
- useStartServices,
- useConfig,
- sendGetAgentStatus,
- useGetPackageInfoByKey,
- sendCreateAgentPolicy,
-} from '../../../../hooks';
+import { dataTypes } from '../../../../../../../common/constants';
+import type { NewAgentPolicy } from '../../../../types';
+import { useConfig, sendGetAgentStatus, useGetPackageInfoByKey } from '../../../../hooks';
import {
Loading,
- Error,
+ Error as ErrorComponent,
ExtensionWrapper,
DevtoolsRequestFlyoutButton,
} from '../../../../components';
@@ -57,34 +38,21 @@ import {
import { agentPolicyFormValidation, ConfirmDeployAgentPolicyModal } from '../../components';
import { useUIExtension } from '../../../../hooks';
import type { PackagePolicyEditExtensionComponentProps } from '../../../../types';
-import {
- pkgKeyFromPackageInfo,
- isVerificationError,
- ExperimentalFeaturesService,
-} from '../../../../services';
+import { pkgKeyFromPackageInfo } from '../../../../services';
-import type {
- PackagePolicyFormState,
- AddToPolicyParams,
- CreatePackagePolicyParams,
-} from '../types';
+import type { AddToPolicyParams, CreatePackagePolicyParams } from '../types';
import { IntegrationBreadcrumb } from '../components';
-import type { PackagePolicyValidationResults } from '../services';
-import { validatePackagePolicy, validationHasErrors } from '../services';
import {
StepConfigurePackagePolicy,
StepDefinePackagePolicy,
SelectedPolicyTab,
StepSelectHosts,
} from '../components';
-import {
- generateCreatePackagePolicyDevToolsRequest,
- generateCreateAgentPolicyDevToolsRequest,
-} from '../../services';
import { CreatePackagePolicySinglePageLayout, PostInstallAddAgentModal } from './components';
+import { useDevToolsRequest, useOnSubmit } from './hooks';
const StepsWithLessPadding = styled(EuiSteps)`
.euiStep__content {
@@ -106,12 +74,10 @@ export const CreatePackagePolicySinglePage: CreatePackagePolicyParams = ({
from,
queryParamsPolicyId,
}) => {
- const { notifications } = useStartServices();
const {
agents: { enabled: isFleetEnabled },
} = useConfig();
const { params } = useRouteMatch();
- const [agentPolicy, setAgentPolicy] = useState();
const [newAgentPolicy, setNewAgentPolicy] = useState({
name: 'Agent policy 1',
@@ -123,64 +89,10 @@ export const CreatePackagePolicySinglePage: CreatePackagePolicyParams = ({
const [withSysMonitoring, setWithSysMonitoring] = useState(true);
const validation = agentPolicyFormValidation(newAgentPolicy);
- // only used to store the resulting package policy once saved
- const [savedPackagePolicy, setSavedPackagePolicy] = useState();
-
- // Retrieve agent count
- const agentPolicyId = agentPolicy?.id;
-
- const { cancelClickHandler, cancelUrl } = useCancelAddPackagePolicy({
- from,
- pkgkey: params.pkgkey,
- agentPolicyId,
- });
- useEffect(() => {
- const getAgentCount = async () => {
- const { data } = await sendGetAgentStatus({ policyId: agentPolicyId });
- if (data?.results.total !== undefined) {
- setAgentCount(data.results.total);
- }
- };
-
- if (isFleetEnabled && agentPolicyId) {
- getAgentCount();
- }
- }, [agentPolicyId, isFleetEnabled]);
- const [agentCount, setAgentCount] = useState(0);
-
const [selectedPolicyTab, setSelectedPolicyTab] = useState(
queryParamsPolicyId ? SelectedPolicyTab.EXISTING : SelectedPolicyTab.NEW
);
- // New package policy state
- const [packagePolicy, setPackagePolicy] = useState({
- name: '',
- description: '',
- namespace: 'default',
- policy_id: '',
- enabled: true,
- inputs: [],
- });
-
- const onSaveNavigate = useOnSaveNavigate({
- packagePolicy,
- queryParamsPolicyId,
- });
- const navigateAddAgent = (policy?: PackagePolicy) =>
- onSaveNavigate(policy, ['openEnrollmentFlyout']);
-
- const navigateAddAgentHelp = (policy?: PackagePolicy) =>
- onSaveNavigate(policy, ['showAddAgentHelp']);
-
- const confirmForceInstall = useConfirmForceInstall();
-
- // Validation state
- const [validationResults, setValidationResults] = useState();
- const [hasAgentPolicyError, setHasAgentPolicyError] = useState(false);
-
- // Form state
- const [formState, setFormState] = useState('VALID');
-
const { pkgName, pkgVersion } = splitPkgKey(params.pkgkey);
// Fetch package info
const {
@@ -194,43 +106,50 @@ export const CreatePackagePolicySinglePage: CreatePackagePolicyParams = ({
}
}, [packageInfoData]);
- // Update agent policy method
- const updateAgentPolicy = useCallback(
- (updatedAgentPolicy: AgentPolicy | undefined) => {
- if (updatedAgentPolicy) {
- setAgentPolicy(updatedAgentPolicy);
- if (packageInfo) {
+ const [agentCount, setAgentCount] = useState(0);
+
+ // Save package policy
+ const {
+ onSubmit,
+ updatePackagePolicy,
+ packagePolicy,
+ agentPolicy,
+ updateAgentPolicy,
+ savedPackagePolicy,
+ formState,
+ setFormState,
+ navigateAddAgent,
+ navigateAddAgentHelp,
+ setHasAgentPolicyError,
+ validationResults,
+ hasAgentPolicyError,
+ } = useOnSubmit({
+ agentCount,
+ packageInfo,
+ newAgentPolicy,
+ selectedPolicyTab,
+ withSysMonitoring,
+ queryParamsPolicyId,
+ });
+
+ const setPolicyValidation = useCallback(
+ (selectedTab: SelectedPolicyTab, updatedAgentPolicy: NewAgentPolicy) => {
+ if (selectedTab === SelectedPolicyTab.NEW) {
+ if (
+ !updatedAgentPolicy.name ||
+ updatedAgentPolicy.name.trim() === '' ||
+ !updatedAgentPolicy.namespace ||
+ updatedAgentPolicy.namespace.trim() === ''
+ ) {
+ setHasAgentPolicyError(true);
+ } else {
setHasAgentPolicyError(false);
}
- } else {
- setHasAgentPolicyError(true);
- setAgentPolicy(undefined);
}
-
- // eslint-disable-next-line no-console
- console.debug('Agent policy updated', updatedAgentPolicy);
},
- [packageInfo, setAgentPolicy]
+ [setHasAgentPolicyError]
);
- const setPolicyValidation = (
- selectedTab: SelectedPolicyTab,
- updatedAgentPolicy: NewAgentPolicy
- ) => {
- if (selectedTab === SelectedPolicyTab.NEW) {
- if (
- !updatedAgentPolicy.name ||
- updatedAgentPolicy.name.trim() === '' ||
- !updatedAgentPolicy.namespace ||
- updatedAgentPolicy.namespace.trim() === ''
- ) {
- setHasAgentPolicyError(true);
- } else {
- setHasAgentPolicyError(false);
- }
- }
- };
-
const updateNewAgentPolicy = useCallback(
(updatedFields: Partial) => {
const updatedAgentPolicy = {
@@ -240,7 +159,7 @@ export const CreatePackagePolicySinglePage: CreatePackagePolicyParams = ({
setNewAgentPolicy(updatedAgentPolicy);
setPolicyValidation(selectedPolicyTab, updatedAgentPolicy);
},
- [setNewAgentPolicy, newAgentPolicy, selectedPolicyTab]
+ [setNewAgentPolicy, setPolicyValidation, newAgentPolicy, selectedPolicyTab]
);
const updateSelectedPolicyTab = useCallback(
@@ -248,58 +167,29 @@ export const CreatePackagePolicySinglePage: CreatePackagePolicyParams = ({
setSelectedPolicyTab(selectedTab);
setPolicyValidation(selectedTab, newAgentPolicy);
},
- [setSelectedPolicyTab, newAgentPolicy]
+ [setSelectedPolicyTab, setPolicyValidation, newAgentPolicy]
);
- const hasErrors = validationResults ? validationHasErrors(validationResults) : false;
- // Update package policy validation
- const updatePackagePolicyValidation = useCallback(
- (newPackagePolicy?: NewPackagePolicy) => {
- if (packageInfo) {
- const newValidationResult = validatePackagePolicy(
- newPackagePolicy || packagePolicy,
- packageInfo,
- safeLoad
- );
- setValidationResults(newValidationResult);
- // eslint-disable-next-line no-console
- console.debug('Package policy validation results', newValidationResult);
-
- return newValidationResult;
- }
- },
- [packagePolicy, packageInfo]
- );
+ // Retrieve agent count
+ const agentPolicyId = agentPolicy?.id;
- // Update package policy method
- const updatePackagePolicy = useCallback(
- (updatedFields: Partial) => {
- const newPackagePolicy = {
- ...packagePolicy,
- ...updatedFields,
- };
- setPackagePolicy(newPackagePolicy);
-
- // eslint-disable-next-line no-console
- console.debug('Package policy updated', newPackagePolicy);
- const newValidationResults = updatePackagePolicyValidation(newPackagePolicy);
- const hasPackage = newPackagePolicy.package;
- const hasValidationErrors = newValidationResults
- ? validationHasErrors(newValidationResults)
- : false;
- const hasAgentPolicy = newPackagePolicy.policy_id && newPackagePolicy.policy_id !== '';
- if (
- hasPackage &&
- (hasAgentPolicy || selectedPolicyTab === SelectedPolicyTab.NEW) &&
- !hasValidationErrors
- ) {
- setFormState('VALID');
- } else {
- setFormState('INVALID');
+ const { cancelClickHandler, cancelUrl } = useCancelAddPackagePolicy({
+ from,
+ pkgkey: params.pkgkey,
+ agentPolicyId,
+ });
+ useEffect(() => {
+ const getAgentCount = async () => {
+ const { data } = await sendGetAgentStatus({ policyId: agentPolicyId });
+ if (data?.results.total !== undefined) {
+ setAgentCount(data.results.total);
}
- },
- [packagePolicy, updatePackagePolicyValidation, selectedPolicyTab]
- );
+ };
+
+ if (isFleetEnabled && agentPolicyId) {
+ getAgentCount();
+ }
+ }, [agentPolicyId, isFleetEnabled]);
const handleExtensionViewOnChange = useCallback<
PackagePolicyEditExtensionComponentProps['onChange']
@@ -313,132 +203,16 @@ export const CreatePackagePolicySinglePage: CreatePackagePolicyParams = ({
return prevState;
});
},
- [updatePackagePolicy]
- );
-
- // Save package policy
- const savePackagePolicy = useCallback(
- async (pkgPolicy: CreatePackagePolicyRequest['body']) => {
- setFormState('LOADING');
- const result = await sendCreatePackagePolicy(pkgPolicy);
- setFormState(agentCount ? 'SUBMITTED' : 'SUBMITTED_NO_AGENTS');
- return result;
- },
- [agentCount]
+ [updatePackagePolicy, setFormState]
);
- const createAgentPolicy = useCallback(async (): Promise => {
- let createdAgentPolicy;
- setFormState('LOADING');
- // do not create agent policy with system integration if package policy already is for system package
- const packagePolicyIsSystem = packagePolicy?.package?.name === FLEET_SYSTEM_PACKAGE;
- const resp = await sendCreateAgentPolicy(newAgentPolicy, {
- withSysMonitoring: withSysMonitoring && !packagePolicyIsSystem,
- });
- if (resp.error) {
- setFormState('VALID');
- throw resp.error;
- }
- if (resp.data) {
- createdAgentPolicy = resp.data.item;
- setAgentPolicy(createdAgentPolicy);
- updatePackagePolicy({ policy_id: createdAgentPolicy.id });
- }
- return createdAgentPolicy;
- }, [packagePolicy?.package?.name, newAgentPolicy, withSysMonitoring, updatePackagePolicy]);
-
- const onSubmit = useCallback(
- async ({
- force,
- overrideCreatedAgentPolicy,
- }: { overrideCreatedAgentPolicy?: AgentPolicy; force?: boolean } = {}) => {
- if (formState === 'VALID' && hasErrors) {
- setFormState('INVALID');
- return;
- }
- if (agentCount !== 0 && formState !== 'CONFIRM') {
- setFormState('CONFIRM');
- return;
- }
- let createdPolicy = overrideCreatedAgentPolicy;
- if (selectedPolicyTab === SelectedPolicyTab.NEW && !overrideCreatedAgentPolicy) {
- try {
- createdPolicy = await createAgentPolicy();
- } catch (e) {
- notifications.toasts.addError(e, {
- title: i18n.translate('xpack.fleet.createAgentPolicy.errorNotificationTitle', {
- defaultMessage: 'Unable to create agent policy',
- }),
- });
- return;
- }
- }
-
- setFormState('LOADING');
- // passing pkgPolicy with policy_id here as setPackagePolicy doesn't propagate immediately
- const { error, data } = await savePackagePolicy({
- ...packagePolicy,
- policy_id: createdPolicy?.id ?? packagePolicy.policy_id,
- force,
- });
- if (!error) {
- setSavedPackagePolicy(data!.item);
-
- const hasAgentsAssigned = agentCount && agentPolicy;
- if (!hasAgentsAssigned) {
- setFormState('SUBMITTED_NO_AGENTS');
- return;
- }
- onSaveNavigate(data!.item);
-
- notifications.toasts.addSuccess({
- title: i18n.translate('xpack.fleet.createPackagePolicy.addedNotificationTitle', {
- defaultMessage: `'{packagePolicyName}' integration added.`,
- values: {
- packagePolicyName: packagePolicy.name,
- },
- }),
- text: hasAgentsAssigned
- ? i18n.translate('xpack.fleet.createPackagePolicy.addedNotificationMessage', {
- defaultMessage: `Fleet will deploy updates to all agents that use the '{agentPolicyName}' policy.`,
- values: {
- agentPolicyName: agentPolicy!.name,
- },
- })
- : undefined,
- 'data-test-subj': 'packagePolicyCreateSuccessToast',
- });
- } else {
- if (isVerificationError(error)) {
- setFormState('VALID'); // don't show the add agent modal
- const forceInstall = await confirmForceInstall(packagePolicy.package!);
-
- if (forceInstall) {
- // skip creating the agent policy because it will have already been successfully created
- onSubmit({ overrideCreatedAgentPolicy: createdPolicy, force: true });
- }
- return;
- }
- notifications.toasts.addError(error, {
- title: 'Error',
- });
- setFormState('VALID');
- }
- },
- [
- formState,
- hasErrors,
- agentCount,
- selectedPolicyTab,
- savePackagePolicy,
- packagePolicy,
- createAgentPolicy,
- notifications.toasts,
- agentPolicy,
- onSaveNavigate,
- confirmForceInstall,
- ]
- );
+ const { devtoolRequest, devtoolRequestDescription, showDevtoolsRequest } = useDevToolsRequest({
+ newAgentPolicy,
+ packagePolicy,
+ selectedPolicyTab,
+ withSysMonitoring,
+ packageInfo,
+ });
const integrationInfo = useMemo(
() =>
@@ -488,6 +262,7 @@ export const CreatePackagePolicySinglePage: CreatePackagePolicyParams = ({
withSysMonitoring,
updateSelectedPolicyTab,
queryParamsPolicyId,
+ setHasAgentPolicyError,
]
);
@@ -564,47 +339,10 @@ export const CreatePackagePolicySinglePage: CreatePackagePolicyParams = ({
},
];
- const { showDevtoolsRequest: isShowDevtoolRequestExperimentEnabled } =
- ExperimentalFeaturesService.get();
-
- const showDevtoolsRequest =
- !HIDDEN_API_REFERENCE_PACKAGES.includes(packageInfo?.name ?? '') &&
- isShowDevtoolRequestExperimentEnabled;
-
- const [devtoolRequest, devtoolRequestDescription] = useMemo(() => {
- if (selectedPolicyTab === SelectedPolicyTab.NEW) {
- const packagePolicyIsSystem = packagePolicy?.package?.name === FLEET_SYSTEM_PACKAGE;
- return [
- `${generateCreateAgentPolicyDevToolsRequest(
- newAgentPolicy,
- withSysMonitoring && !packagePolicyIsSystem
- )}\n\n${generateCreatePackagePolicyDevToolsRequest({
- ...packagePolicy,
- })}`,
- i18n.translate(
- 'xpack.fleet.createPackagePolicy.devtoolsRequestWithAgentPolicyDescription',
- {
- defaultMessage:
- 'These Kibana requests creates a new agent policy and a new package policy.',
- }
- ),
- ];
- }
-
- return [
- generateCreatePackagePolicyDevToolsRequest({
- ...packagePolicy,
- }),
- i18n.translate('xpack.fleet.createPackagePolicy.devtoolsRequestDescription', {
- defaultMessage: 'This Kibana request creates a new package policy.',
- }),
- ];
- }, [packagePolicy, newAgentPolicy, withSysMonitoring, selectedPolicyTab]);
-
// Display package error if there is one
if (packageInfoError) {
return (
- {
+ return sendRequest({
+ path: epmRouteService.getBulkInstallPath(),
+ method: 'post',
+ body: {
+ packages,
+ },
+ });
+};
+
export const sendRemovePackage = (pkgName: string, pkgVersion: string, force: boolean = false) => {
return sendRequest({
path: epmRouteService.getRemovePath(pkgName, pkgVersion),
diff --git a/x-pack/plugins/fleet/server/routes/agent/upgrade_handler.test.ts b/x-pack/plugins/fleet/server/routes/agent/upgrade_handler.test.ts
index 5eafcf1a94104..aefcbfc5cd87f 100644
--- a/x-pack/plugins/fleet/server/routes/agent/upgrade_handler.test.ts
+++ b/x-pack/plugins/fleet/server/routes/agent/upgrade_handler.test.ts
@@ -22,5 +22,19 @@ describe('upgrade handler', () => {
it('should not throw if upgrade version is equal to kibana version with snapshot', () => {
expect(() => checkKibanaVersion('8.4.0', '8.4.0-SNAPSHOT')).not.toThrowError();
});
+
+ it('should not throw if force is specified and patch is newer', () => {
+ expect(() => checkKibanaVersion('8.4.1', '8.4.0', true)).not.toThrowError();
+ expect(() => checkKibanaVersion('8.4.1-SNAPSHOT', '8.4.0', true)).not.toThrowError();
+ });
+
+ it('should throw if force is specified and minor is newer', () => {
+ expect(() => checkKibanaVersion('8.5.0', '8.4.0', true)).toThrowError();
+ });
+
+ it('should not throw if force is specified and major and minor is newer', () => {
+ expect(() => checkKibanaVersion('7.5.0', '8.4.0', true)).not.toThrowError();
+ expect(() => checkKibanaVersion('8.4.0', '8.4.0', true)).not.toThrowError();
+ });
});
});
diff --git a/x-pack/plugins/fleet/server/routes/agent/upgrade_handler.ts b/x-pack/plugins/fleet/server/routes/agent/upgrade_handler.ts
index a79edbaa36856..d3fffac7d9050 100644
--- a/x-pack/plugins/fleet/server/routes/agent/upgrade_handler.ts
+++ b/x-pack/plugins/fleet/server/routes/agent/upgrade_handler.ts
@@ -10,6 +10,8 @@ import type { TypeOf } from '@kbn/config-schema';
import semverCoerce from 'semver/functions/coerce';
import semverGt from 'semver/functions/gt';
+import semverMajor from 'semver/functions/major';
+import semverMinor from 'semver/functions/minor';
import type { PostAgentUpgradeResponse, GetCurrentUpgradesResponse } from '../../../common/types';
import type { PostAgentUpgradeRequestSchema, PostBulkAgentUpgradeRequestSchema } from '../../types';
@@ -34,7 +36,7 @@ export const postAgentUpgradeHandler: RequestHandler<
const { version, source_uri: sourceUri, force } = request.body;
const kibanaVersion = appContextService.getKibanaVersion();
try {
- checkKibanaVersion(version, kibanaVersion);
+ checkKibanaVersion(version, kibanaVersion, force);
} catch (err) {
return response.customError({
statusCode: 400,
@@ -114,9 +116,9 @@ export const postBulkAgentsUpgradeHandler: RequestHandler<
} = request.body;
const kibanaVersion = appContextService.getKibanaVersion();
try {
- checkKibanaVersion(version, kibanaVersion);
+ checkKibanaVersion(version, kibanaVersion, force);
const fleetServerAgents = await getAllFleetServerAgents(soClient, esClient);
- checkFleetServerVersion(version, fleetServerAgents);
+ checkFleetServerVersion(version, fleetServerAgents, force);
} catch (err) {
return response.customError({
statusCode: 400,
@@ -158,7 +160,7 @@ export const getCurrentUpgradesHandler: RequestHandler = async (context, request
}
};
-export const checkKibanaVersion = (version: string, kibanaVersion: string) => {
+export const checkKibanaVersion = (version: string, kibanaVersion: string, force = false) => {
// get version number only in case "-SNAPSHOT" is in it
const kibanaVersionNumber = semverCoerce(kibanaVersion)?.version;
if (!kibanaVersionNumber) throw new Error(`kibanaVersion ${kibanaVersionNumber} is not valid`);
@@ -166,14 +168,31 @@ export const checkKibanaVersion = (version: string, kibanaVersion: string) => {
if (!versionToUpgradeNumber)
throw new Error(`version to upgrade ${versionToUpgradeNumber} is not valid`);
- if (semverGt(versionToUpgradeNumber, kibanaVersionNumber))
+ if (!force && semverGt(versionToUpgradeNumber, kibanaVersionNumber)) {
throw new Error(
`cannot upgrade agent to ${versionToUpgradeNumber} because it is higher than the installed kibana version ${kibanaVersionNumber}`
);
+ }
+
+ const kibanaMajorGt = semverMajor(kibanaVersionNumber) > semverMajor(versionToUpgradeNumber);
+ const kibanaMajorEqMinorGte =
+ semverMajor(kibanaVersionNumber) === semverMajor(versionToUpgradeNumber) &&
+ semverMinor(kibanaVersionNumber) >= semverMinor(versionToUpgradeNumber);
+
+ // When force is enabled, only the major and minor versions are checked
+ if (force && !(kibanaMajorGt || kibanaMajorEqMinorGte)) {
+ throw new Error(
+ `cannot force upgrade agent to ${versionToUpgradeNumber} because it does not satisfy the major and minor of the installed kibana version ${kibanaVersionNumber}`
+ );
+ }
};
// Check the installed fleet server version
-const checkFleetServerVersion = (versionToUpgradeNumber: string, fleetServerAgents: Agent[]) => {
+const checkFleetServerVersion = (
+ versionToUpgradeNumber: string,
+ fleetServerAgents: Agent[],
+ force = false
+) => {
const fleetServerVersions = fleetServerAgents.map(
(agent) => agent.local_metadata.elastic.agent.version
) as string[];
@@ -184,9 +203,22 @@ const checkFleetServerVersion = (versionToUpgradeNumber: string, fleetServerAgen
return;
}
- if (semverGt(versionToUpgradeNumber, maxFleetServerVersion)) {
+ if (!force && semverGt(versionToUpgradeNumber, maxFleetServerVersion)) {
throw new Error(
`cannot upgrade agent to ${versionToUpgradeNumber} because it is higher than the latest fleet server version ${maxFleetServerVersion}`
);
}
+
+ const fleetServerMajorGt =
+ semverMajor(maxFleetServerVersion) > semverMajor(versionToUpgradeNumber);
+ const fleetServerMajorEqMinorGte =
+ semverMajor(maxFleetServerVersion) === semverMajor(versionToUpgradeNumber) &&
+ semverMinor(maxFleetServerVersion) >= semverMinor(versionToUpgradeNumber);
+
+ // When force is enabled, only the major and minor versions are checked
+ if (force && !(fleetServerMajorGt || fleetServerMajorEqMinorGte)) {
+ throw new Error(
+ `cannot force upgrade agent to ${versionToUpgradeNumber} because it does not satisfy the major and minor of the latest fleet server version ${maxFleetServerVersion}`
+ );
+ }
};
diff --git a/x-pack/plugins/fleet/server/services/agents/unenroll.test.ts b/x-pack/plugins/fleet/server/services/agents/unenroll.test.ts
index 5beb5c0a9ac00..9169df19fbcfb 100644
--- a/x-pack/plugins/fleet/server/services/agents/unenroll.test.ts
+++ b/x-pack/plugins/fleet/server/services/agents/unenroll.test.ts
@@ -331,6 +331,15 @@ describe('invalidateAPIKeysForAgents', () => {
id: 'defaultApiKeyHistory2',
},
],
+ outputs: [
+ {
+ api_key_id: 'outputApiKey1',
+ to_retire_api_key_ids: [{ id: 'outputApiKeyRetire1' }, { id: 'outputApiKeyRetire2' }],
+ },
+ {
+ api_key_id: 'outputApiKey2',
+ },
+ ],
} as any,
]);
@@ -340,6 +349,10 @@ describe('invalidateAPIKeysForAgents', () => {
'defaultApiKey1',
'defaultApiKeyHistory1',
'defaultApiKeyHistory2',
+ 'outputApiKey1',
+ 'outputApiKeyRetire1',
+ 'outputApiKeyRetire2',
+ 'outputApiKey2',
]);
});
});
diff --git a/x-pack/plugins/fleet/server/services/agents/unenroll_action_runner.ts b/x-pack/plugins/fleet/server/services/agents/unenroll_action_runner.ts
index c735254f18256..fed5d44fe98e8 100644
--- a/x-pack/plugins/fleet/server/services/agents/unenroll_action_runner.ts
+++ b/x-pack/plugins/fleet/server/services/agents/unenroll_action_runner.ts
@@ -215,6 +215,16 @@ export async function invalidateAPIKeysForAgents(agents: Agent[]) {
if (agent.default_api_key_history) {
agent.default_api_key_history.forEach((apiKey) => keys.push(apiKey.id));
}
+ if (agent.outputs) {
+ agent.outputs.forEach((output) => {
+ if (output.api_key_id) {
+ keys.push(output.api_key_id);
+ }
+ if (output.to_retire_api_key_ids) {
+ output.to_retire_api_key_ids.forEach((apiKey) => keys.push(apiKey.id));
+ }
+ });
+ }
return keys;
}, []);
diff --git a/x-pack/plugins/fleet/tsconfig.json b/x-pack/plugins/fleet/tsconfig.json
index 7cc16fe654268..320843546a305 100644
--- a/x-pack/plugins/fleet/tsconfig.json
+++ b/x-pack/plugins/fleet/tsconfig.json
@@ -36,6 +36,7 @@
{ "path": "../../../src/plugins/home/tsconfig.json" },
// requiredBundles from ./kibana.json
+ { "path": "../cloud_integrations/cloud_chat/tsconfig.json" },
{ "path": "../../../src/plugins/kibana_react/tsconfig.json" },
{ "path": "../../../src/plugins/es_ui_shared/tsconfig.json" },
{ "path": "../infra/tsconfig.json" },
diff --git a/x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/features/node_allocation/cloud_aware_behavior.test.ts b/x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/features/node_allocation/cloud_aware_behavior.test.ts
index 820f8a4f9100a..75db772ec0926 100644
--- a/x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/features/node_allocation/cloud_aware_behavior.test.ts
+++ b/x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/features/node_allocation/cloud_aware_behavior.test.ts
@@ -11,6 +11,7 @@ import {
CloudNodeAllocationTestBed,
setupCloudNodeAllocation,
} from './cloud_aware_behavior.helpers';
+import { cloudMock } from '@kbn/cloud-plugin/public/mocks';
describe(' node allocation cloud-aware behavior', () => {
let testBed: CloudNodeAllocationTestBed;
@@ -28,7 +29,7 @@ describe(' node allocation cloud-aware behavior', () => {
await act(async () => {
if (Boolean(isOnCloud)) {
testBed = await setupCloudNodeAllocation(httpSetup, {
- appServicesContext: { cloud: { isCloudEnabled: true } },
+ appServicesContext: { cloud: { ...cloudMock.createSetup(), isCloudEnabled: true } },
});
} else {
testBed = await setupCloudNodeAllocation(httpSetup);
diff --git a/x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/features/searchable_snapshots.test.ts b/x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/features/searchable_snapshots.test.ts
index 68e74e23a781c..fbe724c881af3 100644
--- a/x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/features/searchable_snapshots.test.ts
+++ b/x-pack/plugins/index_lifecycle_management/integration_tests/edit_policy/features/searchable_snapshots.test.ts
@@ -15,6 +15,7 @@ import {
SearchableSnapshotsTestBed,
setupSearchableSnapshotsTestBed,
} from './searchable_snapshots.helpers';
+import { cloudMock } from '@kbn/cloud-plugin/public/mocks';
describe(' searchable snapshots', () => {
let testBed: SearchableSnapshotsTestBed;
@@ -142,7 +143,7 @@ describe(' searchable snapshots', () => {
await act(async () => {
testBed = await setupSearchableSnapshotsTestBed(httpSetup, {
- appServicesContext: { cloud: { isCloudEnabled: true } },
+ appServicesContext: { cloud: { ...cloudMock.createSetup(), isCloudEnabled: true } },
});
});
@@ -171,7 +172,7 @@ describe(' searchable snapshots', () => {
await act(async () => {
testBed = await setupSearchableSnapshotsTestBed(httpSetup, {
- appServicesContext: { cloud: { isCloudEnabled: true } },
+ appServicesContext: { cloud: { ...cloudMock.createSetup(), isCloudEnabled: true } },
});
});
diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/components/hosts_table.tsx b/x-pack/plugins/infra/public/pages/metrics/hosts/components/hosts_table.tsx
index 74f2468eb4c45..e92ac801e8612 100644
--- a/x-pack/plugins/infra/public/pages/metrics/hosts/components/hosts_table.tsx
+++ b/x-pack/plugins/infra/public/pages/metrics/hosts/components/hosts_table.tsx
@@ -8,8 +8,10 @@
import { useKibana } from '@kbn/kibana-react-plugin/public';
import { TypedLensByValueInput } from '@kbn/lens-plugin/public';
import type { Query, TimeRange } from '@kbn/es-query';
-import React from 'react';
+import React, { useState } from 'react';
import type { DataView } from '@kbn/data-views-plugin/public';
+import { i18n } from '@kbn/i18n';
+import { NoData } from '../../../../components/empty_states';
import { InfraClientStartDeps } from '../../../../types';
const getLensHostsTable = (
@@ -498,23 +500,54 @@ interface Props {
timeRange: TimeRange;
query: Query;
searchSessionId: string;
+ onRefetch: () => void;
+ onLoading: (isLoading: boolean) => void;
+ isLensLoading: boolean;
}
export const HostsTable: React.FunctionComponent = ({
dataView,
timeRange,
query,
searchSessionId,
+ onRefetch,
+ onLoading,
+ isLensLoading,
}) => {
const {
services: { lens },
} = useKibana();
const LensComponent = lens?.EmbeddableComponent;
+ const [noData, setNoData] = useState(false);
+
+ if (noData && !isLensLoading) {
+ return (
+
+ );
+ }
return (
{
+ if (!isLoading && adapters?.tables) {
+ setNoData(adapters?.tables.tables.default?.rows.length === 0);
+ onLoading(false);
+ }
+ }}
/>
);
};
diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/hosts_content.tsx b/x-pack/plugins/infra/public/pages/metrics/hosts/hosts_content.tsx
index 63e95a19f1c7b..7bf087db39eb5 100644
--- a/x-pack/plugins/infra/public/pages/metrics/hosts/hosts_content.tsx
+++ b/x-pack/plugins/infra/public/pages/metrics/hosts/hosts_content.tsx
@@ -27,6 +27,7 @@ export const HostsContent: React.FunctionComponent = () => {
useMetricsDataViewContext();
// needed to refresh the lens table when filters havent changed
const [searchSessionId, setSearchSessionId] = useState(data.search.session.start());
+ const [isLensLoading, setIsLensLoading] = useState(false);
const onQuerySubmit = useCallback(
(payload: { dateRange: TimeRange; query?: Query }) => {
@@ -34,11 +35,26 @@ export const HostsContent: React.FunctionComponent = () => {
if (payload.query) {
setQuery(payload.query);
}
+ setIsLensLoading(true);
setSearchSessionId(data.search.session.start());
},
[setDateRange, setQuery, data.search.session]
);
+ const onLoading = useCallback(
+ (isLoading: boolean) => {
+ if (isLensLoading) {
+ setIsLensLoading(isLoading);
+ }
+ },
+ [setIsLensLoading, isLensLoading]
+ );
+
+ const onRefetch = useCallback(() => {
+ setIsLensLoading(true);
+ setSearchSessionId(data.search.session.start());
+ }, [data.search.session]);
+
return (
{metricsDataView ? (
@@ -61,6 +77,9 @@ export const HostsContent: React.FunctionComponent = () => {
timeRange={dateRange}
query={query}
searchSessionId={searchSessionId}
+ onRefetch={onRefetch}
+ onLoading={onLoading}
+ isLensLoading={isLensLoading}
/>
>
) : hasFailedCreatingDataView || hasFailedFetchingDataView ? (
diff --git a/x-pack/plugins/infra/public/plugin.ts b/x-pack/plugins/infra/public/plugin.ts
index 6eb6663b10eeb..c14a13d1a7ea1 100644
--- a/x-pack/plugins/infra/public/plugin.ts
+++ b/x-pack/plugins/infra/public/plugin.ts
@@ -5,12 +5,17 @@
* 2.0.
*/
+import {
+ AppMountParameters,
+ AppUpdater,
+ CoreStart,
+ DEFAULT_APP_CATEGORIES,
+ PluginInitializerContext,
+} from '@kbn/core/public';
import { i18n } from '@kbn/i18n';
-import { AppMountParameters, PluginInitializerContext } from '@kbn/core/public';
-import { from } from 'rxjs';
-import { map } from 'rxjs/operators';
-import { DEFAULT_APP_CATEGORIES } from '@kbn/core/public';
import { enableInfrastructureHostsView } from '@kbn/observability-plugin/public';
+import { BehaviorSubject, combineLatest, from } from 'rxjs';
+import { map } from 'rxjs/operators';
import { defaultLogViewsStaticConfig } from '../common/log_views';
import { InfraPublicConfig } from '../common/plugin_config_types';
import { createInventoryMetricRuleType } from './alerting/inventory';
@@ -38,6 +43,7 @@ import { getLogsHasDataFetcher, getLogsOverviewDataFetcher } from './utils/logs_
export class Plugin implements InfraClientPluginClass {
public config: InfraPublicConfig;
private logViews: LogViewsService;
+ private readonly appUpdater$ = new BehaviorSubject
(() => ({}));
constructor(context: PluginInitializerContext) {
this.config = context.config.get();
@@ -74,6 +80,11 @@ export class Plugin implements InfraClientPluginClass {
fetchData: createMetricsFetchData(core.getStartServices),
});
+ const startDep$AndHostViewFlag$ = combineLatest([
+ from(core.getStartServices()),
+ core.uiSettings.get$(enableInfrastructureHostsView),
+ ]);
+
/** !! Need to be kept in sync with the deepLinks in x-pack/plugins/infra/public/plugin.ts */
const infraEntries = [
{ label: 'Inventory', app: 'metrics', path: '/inventory' },
@@ -81,12 +92,15 @@ export class Plugin implements InfraClientPluginClass {
];
const hostInfraEntry = { label: 'Hosts', app: 'metrics', path: '/hosts' };
pluginsSetup.observability.navigation.registerSections(
- from(core.getStartServices()).pipe(
+ startDep$AndHostViewFlag$.pipe(
map(
([
- {
- application: { capabilities },
- },
+ [
+ {
+ application: { capabilities },
+ },
+ ],
+ isInfrastructureHostsViewEnabled,
]) => [
...(capabilities.logs.show
? [
@@ -106,7 +120,7 @@ export class Plugin implements InfraClientPluginClass {
{
label: 'Infrastructure',
sortKey: 300,
- entries: core.uiSettings.get(enableInfrastructureHostsView)
+ entries: isInfrastructureHostsViewEnabled
? [hostInfraEntry, ...infraEntries]
: infraEntries,
},
@@ -171,6 +185,7 @@ export class Plugin implements InfraClientPluginClass {
},
});
+ // !! Need to be kept in sync with the routes in x-pack/plugins/infra/public/pages/metrics/index.tsx
const infraDeepLinks = [
{
id: 'inventory',
@@ -210,8 +225,8 @@ export class Plugin implements InfraClientPluginClass {
order: 8200,
appRoute: '/app/metrics',
category: DEFAULT_APP_CATEGORIES.observability,
- // !! Need to be kept in sync with the routes in x-pack/plugins/infra/public/pages/metrics/index.tsx
- deepLinks: core.uiSettings.get(enableInfrastructureHostsView)
+ updater$: this.appUpdater$,
+ deepLinks: core.uiSettings.get(enableInfrastructureHostsView)
? [hostInfraDeepLink, ...infraDeepLinks]
: infraDeepLinks,
mount: async (params: AppMountParameters) => {
@@ -223,6 +238,19 @@ export class Plugin implements InfraClientPluginClass {
},
});
+ startDep$AndHostViewFlag$.subscribe(
+ ([_startServices, isInfrastructureHostsViewEnabled]: [
+ [CoreStart, InfraClientStartDeps, InfraClientStartExports],
+ boolean
+ ]) => {
+ this.appUpdater$.next(() => ({
+ deepLinks: isInfrastructureHostsViewEnabled
+ ? [hostInfraDeepLink, ...infraDeepLinks]
+ : infraDeepLinks,
+ }));
+ }
+ );
+
/* This exists purely to facilitate URL redirects from the old App ID ("infra"),
to our new App IDs ("metrics" and "logs"). With version 8.0.0 we can remove this. */
core.application.register({
diff --git a/x-pack/plugins/profiling/public/components/topn_functions.tsx b/x-pack/plugins/profiling/public/components/topn_functions.tsx
index 3ad540983d903..4d8522913a245 100644
--- a/x-pack/plugins/profiling/public/components/topn_functions.tsx
+++ b/x-pack/plugins/profiling/public/components/topn_functions.tsx
@@ -219,7 +219,7 @@ export const TopNFunctionsTable = ({
: row[sortField];
},
[sortDirection]
- ).slice(0, 100);
+ );
return (
<>
diff --git a/x-pack/plugins/security/kibana.json b/x-pack/plugins/security/kibana.json
index 881f67dcf22cb..6804a5ba52253 100644
--- a/x-pack/plugins/security/kibana.json
+++ b/x-pack/plugins/security/kibana.json
@@ -9,7 +9,7 @@
"kibanaVersion": "kibana",
"configPath": ["xpack", "security"],
"requiredPlugins": ["features", "licensing", "taskManager"],
- "optionalPlugins": ["dataViews", "home", "management", "usageCollection", "spaces", "share"],
+ "optionalPlugins": ["cloud", "dataViews", "home", "management", "usageCollection", "spaces", "share"],
"server": true,
"ui": true,
"enabledOnAnonymousPages": true,
diff --git a/x-pack/plugins/security/public/analytics/analytics_service.test.ts b/x-pack/plugins/security/public/analytics/analytics_service.test.ts
index 8174fffc250d9..28a272c12f9ec 100644
--- a/x-pack/plugins/security/public/analytics/analytics_service.test.ts
+++ b/x-pack/plugins/security/public/analytics/analytics_service.test.ts
@@ -11,6 +11,8 @@ import { coreMock } from '@kbn/core/public/mocks';
import { nextTick } from '@kbn/test-jest-helpers';
import { licenseMock } from '../../common/licensing/index.mock';
+import { authenticationMock } from '../authentication/index.mock';
+import { securityMock } from '../mocks';
import { AnalyticsService } from './analytics_service';
describe('AnalyticsService', () => {
@@ -29,7 +31,14 @@ describe('AnalyticsService', () => {
expect(localStorage.getItem(AnalyticsService.AuthTypeInfoStorageKey)).toBeNull();
- analyticsService.setup({ securityLicense: licenseMock.create({ allowLogin: true }) });
+ const authc = authenticationMock.createSetup();
+ authc.getCurrentUser.mockResolvedValue(securityMock.createMockAuthenticatedUser());
+
+ analyticsService.setup({
+ authc,
+ analytics: coreMock.createSetup().analytics,
+ securityLicense: licenseMock.create({ allowLogin: true }),
+ });
analyticsService.start({ http: mockCore.http });
await nextTick();
@@ -51,7 +60,12 @@ describe('AnalyticsService', () => {
mockCore.http.post.mockResolvedValue({ signature: 'some-signature', timestamp: 1234 });
const licenseFeatures$ = new BehaviorSubject({ allowLogin: true });
+ const authc = authenticationMock.createSetup();
+ authc.getCurrentUser.mockResolvedValue(securityMock.createMockAuthenticatedUser());
+
analyticsService.setup({
+ authc,
+ analytics: coreMock.createSetup().analytics,
securityLicense: licenseMock.create(licenseFeatures$.asObservable()),
});
analyticsService.start({ http: mockCore.http });
@@ -99,7 +113,14 @@ describe('AnalyticsService', () => {
});
localStorage.setItem(AnalyticsService.AuthTypeInfoStorageKey, mockCurrentAuthTypeInfo);
- analyticsService.setup({ securityLicense: licenseMock.create({ allowLogin: true }) });
+ const authc = authenticationMock.createSetup();
+ authc.getCurrentUser.mockResolvedValue(securityMock.createMockAuthenticatedUser());
+
+ analyticsService.setup({
+ authc,
+ analytics: coreMock.createSetup().analytics,
+ securityLicense: licenseMock.create({ allowLogin: true }),
+ });
analyticsService.start({ http: mockCore.http });
await nextTick();
@@ -117,7 +138,14 @@ describe('AnalyticsService', () => {
it('does not report authentication type if security is not available', async () => {
const mockCore = coreMock.createStart();
- analyticsService.setup({ securityLicense: licenseMock.create({ allowLogin: false }) });
+ const authc = authenticationMock.createSetup();
+ authc.getCurrentUser.mockResolvedValue(securityMock.createMockAuthenticatedUser());
+
+ analyticsService.setup({
+ authc,
+ analytics: coreMock.createSetup().analytics,
+ securityLicense: licenseMock.create({ allowLogin: false }),
+ });
analyticsService.start({ http: mockCore.http });
await nextTick();
@@ -136,7 +164,14 @@ describe('AnalyticsService', () => {
});
localStorage.setItem(AnalyticsService.AuthTypeInfoStorageKey, mockCurrentAuthTypeInfo);
- analyticsService.setup({ securityLicense: licenseMock.create({ allowLogin: true }) });
+ const authc = authenticationMock.createSetup();
+ authc.getCurrentUser.mockResolvedValue(securityMock.createMockAuthenticatedUser());
+
+ analyticsService.setup({
+ authc,
+ analytics: coreMock.createSetup().analytics,
+ securityLicense: licenseMock.create({ allowLogin: true }),
+ });
analyticsService.start({ http: mockCore.http });
await nextTick();
diff --git a/x-pack/plugins/security/public/analytics/analytics_service.ts b/x-pack/plugins/security/public/analytics/analytics_service.ts
index 1db3a9566f6fd..87c402e9983a0 100644
--- a/x-pack/plugins/security/public/analytics/analytics_service.ts
+++ b/x-pack/plugins/security/public/analytics/analytics_service.ts
@@ -9,12 +9,20 @@ import type { Subscription } from 'rxjs';
import { filter } from 'rxjs';
import { throttleTime } from 'rxjs/operators';
-import type { HttpStart } from '@kbn/core/public';
+import type {
+ AnalyticsServiceSetup as CoreAnalyticsServiceSetup,
+ HttpStart,
+} from '@kbn/core/public';
+import type { AuthenticationServiceSetup } from '..';
import type { SecurityLicense } from '../../common';
+import { registerUserContext } from './register_user_context';
interface AnalyticsServiceSetupParams {
securityLicense: SecurityLicense;
+ analytics: CoreAnalyticsServiceSetup;
+ authc: AuthenticationServiceSetup;
+ cloudId?: string;
}
interface AnalyticsServiceStartParams {
@@ -35,8 +43,9 @@ export class AnalyticsService {
private securityLicense!: SecurityLicense;
private securityFeaturesSubscription?: Subscription;
- public setup({ securityLicense }: AnalyticsServiceSetupParams) {
+ public setup({ analytics, authc, cloudId, securityLicense }: AnalyticsServiceSetupParams) {
this.securityLicense = securityLicense;
+ registerUserContext(analytics, authc, cloudId);
}
public start({ http }: AnalyticsServiceStartParams) {
diff --git a/x-pack/plugins/security/public/analytics/register_user_context.test.ts b/x-pack/plugins/security/public/analytics/register_user_context.test.ts
new file mode 100644
index 0000000000000..0654042059649
--- /dev/null
+++ b/x-pack/plugins/security/public/analytics/register_user_context.test.ts
@@ -0,0 +1,126 @@
+/*
+ * 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 { firstValueFrom } from 'rxjs';
+
+import type { AnalyticsServiceSetup } from '@kbn/core/public';
+import { coreMock } from '@kbn/core/public/mocks';
+import { Sha256 } from '@kbn/crypto-browser';
+
+import type { AuthenticationServiceSetup } from '..';
+import { authenticationMock } from '../authentication/index.mock';
+import { securityMock } from '../mocks';
+import { registerUserContext } from './register_user_context';
+
+describe('registerUserContext', () => {
+ const username = '1234';
+ const expectedHashedPlainUsername = new Sha256().update(username, 'utf8').digest('hex');
+
+ let analytics: jest.Mocked;
+ let authentication: jest.Mocked;
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ analytics = coreMock.createSetup().analytics;
+ authentication = authenticationMock.createSetup();
+ authentication.getCurrentUser.mockResolvedValue(securityMock.createMockAuthenticatedUser());
+ });
+
+ test('register the context provider for the cloud user with hashed user ID when security is available', async () => {
+ registerUserContext(analytics, authentication, 'cloudId');
+
+ expect(analytics.registerContextProvider).toHaveBeenCalled();
+
+ const [{ context$ }] = analytics.registerContextProvider.mock.calls.find(
+ ([{ name }]) => name === 'user_id'
+ )!;
+
+ await expect(firstValueFrom(context$)).resolves.toEqual({
+ userId: '7a3e98632e2c878671da5d5c49e625dd84fb4ba85758feae9a5fd5ec57724753',
+ isElasticCloudUser: false,
+ });
+ });
+
+ it('user hash includes cloud id', async () => {
+ authentication.getCurrentUser.mockResolvedValue(
+ securityMock.createMockAuthenticatedUser({ username })
+ );
+ const analytics1 = coreMock.createSetup().analytics;
+ registerUserContext(analytics1, authentication, 'esOrg1');
+
+ const [{ context$: context1$ }] = analytics1.registerContextProvider.mock.calls.find(
+ ([{ name }]) => name === 'user_id'
+ )!;
+
+ const { userId: hashId1 } = (await firstValueFrom(context1$)) as { userId: string };
+ expect(hashId1).not.toEqual(expectedHashedPlainUsername);
+
+ const analytics2 = coreMock.createSetup().analytics;
+ registerUserContext(analytics2, authentication, 'esOrg2');
+ const [{ context$: context2$ }] = analytics2.registerContextProvider.mock.calls.find(
+ ([{ name }]) => name === 'user_id'
+ )!;
+
+ const { userId: hashId2 } = (await firstValueFrom(context2$)) as { userId: string };
+ expect(hashId2).not.toEqual(expectedHashedPlainUsername);
+
+ expect(hashId1).not.toEqual(hashId2);
+ });
+
+ test('user hash does not include cloudId when user is an Elastic Cloud user', async () => {
+ authentication.getCurrentUser.mockResolvedValue(
+ securityMock.createMockAuthenticatedUser({ username, elastic_cloud_user: true })
+ );
+ registerUserContext(analytics, authentication, 'cloudDeploymentId');
+
+ expect(analytics.registerContextProvider).toHaveBeenCalled();
+
+ const [{ context$ }] = analytics.registerContextProvider.mock.calls.find(
+ ([{ name }]) => name === 'user_id'
+ )!;
+
+ await expect(firstValueFrom(context$)).resolves.toEqual({
+ userId: expectedHashedPlainUsername,
+ isElasticCloudUser: true,
+ });
+ });
+
+ test('user hash does not include cloudId when not provided', async () => {
+ authentication.getCurrentUser.mockResolvedValue(
+ securityMock.createMockAuthenticatedUser({ username })
+ );
+ registerUserContext(analytics, authentication);
+
+ expect(analytics.registerContextProvider).toHaveBeenCalled();
+
+ const [{ context$ }] = analytics.registerContextProvider.mock.calls.find(
+ ([{ name }]) => name === 'user_id'
+ )!;
+
+ await expect(firstValueFrom(context$)).resolves.toEqual({
+ userId: expectedHashedPlainUsername,
+ isElasticCloudUser: false,
+ });
+ });
+
+ test('user hash is undefined when failed to fetch a user', async () => {
+ authentication.getCurrentUser.mockRejectedValue(new Error('failed to fetch a user'));
+
+ registerUserContext(analytics, authentication);
+
+ expect(analytics.registerContextProvider).toHaveBeenCalled();
+
+ const [{ context$ }] = analytics.registerContextProvider.mock.calls.find(
+ ([{ name }]) => name === 'user_id'
+ )!;
+
+ await expect(firstValueFrom(context$)).resolves.toEqual({
+ userId: undefined,
+ isElasticCloudUser: false,
+ });
+ });
+});
diff --git a/x-pack/plugins/security/public/analytics/register_user_context.ts b/x-pack/plugins/security/public/analytics/register_user_context.ts
new file mode 100644
index 0000000000000..bc48846913d02
--- /dev/null
+++ b/x-pack/plugins/security/public/analytics/register_user_context.ts
@@ -0,0 +1,67 @@
+/*
+ * 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 { catchError, from, map, of } from 'rxjs';
+
+import type { AnalyticsServiceSetup } from '@kbn/core/public';
+import { Sha256 } from '@kbn/crypto-browser';
+
+import type { AuthenticationServiceSetup } from '..';
+
+/**
+ * Set up the Analytics context provider for the User information.
+ * @param analytics Core's Analytics service. The Setup contract.
+ * @param authc {@link AuthenticationServiceSetup} used to get the current user's information
+ * @param cloudId The Cloud Org ID.
+ * @private
+ */
+export function registerUserContext(
+ analytics: AnalyticsServiceSetup,
+ authc: AuthenticationServiceSetup,
+ cloudId?: string
+) {
+ analytics.registerContextProvider({
+ name: 'user_id',
+ context$: from(authc.getCurrentUser()).pipe(
+ map((user) => {
+ if (user.elastic_cloud_user) {
+ // If the user is managed by ESS, use the plain username as the user ID:
+ // The username is expected to be unique for these users,
+ // and it matches how users are identified in the Cloud UI, so it allows us to correlate them.
+ return { userId: user.username, isElasticCloudUser: true };
+ }
+
+ return {
+ // For the rest of the authentication providers, we want to add the cloud deployment ID to make it unique.
+ // Especially in the case of Elasticsearch-backed authentication, where users are commonly repeated
+ // across multiple deployments (i.e.: `elastic` superuser).
+ userId: cloudId ? `${cloudId}:${user.username}` : user.username,
+ isElasticCloudUser: false,
+ };
+ }),
+ // The hashing here is to keep it at clear as possible in our source code that we do not send literal user IDs
+ map(({ userId, isElasticCloudUser }) => ({ userId: sha256(userId), isElasticCloudUser })),
+ catchError(() => of({ userId: undefined, isElasticCloudUser: false }))
+ ),
+ schema: {
+ userId: {
+ type: 'keyword',
+ _meta: { description: 'The user id scoped as seen by Cloud (hashed)' },
+ },
+ isElasticCloudUser: {
+ type: 'boolean',
+ _meta: {
+ description: '`true` if the user is managed by ESS.',
+ },
+ },
+ },
+ });
+}
+
+function sha256(str: string) {
+ return new Sha256().update(str, 'utf8').digest('hex');
+}
diff --git a/x-pack/plugins/security/public/plugin.tsx b/x-pack/plugins/security/public/plugin.tsx
index a7ce4e855962f..2a91479824062 100644
--- a/x-pack/plugins/security/public/plugin.tsx
+++ b/x-pack/plugins/security/public/plugin.tsx
@@ -5,6 +5,7 @@
* 2.0.
*/
+import type { CloudSetup, CloudStart } from '@kbn/cloud-plugin/public';
import type {
CoreSetup,
CoreStart,
@@ -43,6 +44,7 @@ export interface PluginSetupDependencies {
home?: HomePublicPluginSetup;
management?: ManagementSetup;
share?: SharePluginSetup;
+ cloud?: CloudSetup;
}
export interface PluginStartDependencies {
@@ -51,6 +53,7 @@ export interface PluginStartDependencies {
management?: ManagementStart;
spaces?: SpacesPluginStart;
share?: SharePluginStart;
+ cloud?: CloudStart;
}
export class SecurityPlugin
@@ -81,7 +84,7 @@ export class SecurityPlugin
public setup(
core: CoreSetup,
- { home, licensing, management, share }: PluginSetupDependencies
+ { cloud, home, licensing, management, share }: PluginSetupDependencies
): SecurityPluginSetup {
const { license } = this.securityLicenseService.setup({ license$: licensing.license$ });
@@ -106,7 +109,12 @@ export class SecurityPlugin
securityApiClients: this.securityApiClients,
});
- this.analyticsService.setup({ securityLicense: license });
+ this.analyticsService.setup({
+ analytics: core.analytics,
+ authc: this.authc,
+ cloudId: cloud?.cloudId,
+ securityLicense: license,
+ });
accountManagementApp.create({
authc: this.authc,
diff --git a/x-pack/plugins/security/server/plugin.test.ts b/x-pack/plugins/security/server/plugin.test.ts
index 6c8b97b1e4b9b..10e3b99484f52 100644
--- a/x-pack/plugins/security/server/plugin.test.ts
+++ b/x-pack/plugins/security/server/plugin.test.ts
@@ -124,18 +124,9 @@ describe('Security Plugin', () => {
"privilegeDeprecationsService": Object {
"getKibanaRolesByFeatureId": [Function],
},
- "setIsElasticCloudDeployment": [Function],
}
`);
});
-
- it('#setIsElasticCloudDeployment cannot be called twice', () => {
- const { setIsElasticCloudDeployment } = plugin.setup(mockCoreSetup, mockSetupDependencies);
- setIsElasticCloudDeployment();
- expect(() => setIsElasticCloudDeployment()).toThrowErrorMatchingInlineSnapshot(
- `"The Elastic Cloud deployment flag has been set already!"`
- );
- });
});
describe('start()', () => {
diff --git a/x-pack/plugins/security/server/plugin.ts b/x-pack/plugins/security/server/plugin.ts
index 682c2eba56da9..9e5724b6a393b 100644
--- a/x-pack/plugins/security/server/plugin.ts
+++ b/x-pack/plugins/security/server/plugin.ts
@@ -8,6 +8,7 @@
import type { Subscription } from 'rxjs';
import { map } from 'rxjs/operators';
+import type { CloudStart } from '@kbn/cloud-plugin/public';
import type { TypeOf } from '@kbn/config-schema';
import type {
CoreSetup,
@@ -88,12 +89,6 @@ export interface SecurityPluginSetup {
* Exposes services to access kibana roles per feature id with the GetDeprecationsContext
*/
privilegeDeprecationsService: PrivilegeDeprecationsService;
-
- /**
- * Sets the flag to indicate that Kibana is running inside an Elastic Cloud deployment. This flag is supposed to be
- * set by the Cloud plugin and can be only once.
- */
- setIsElasticCloudDeployment: () => void;
}
/**
@@ -123,6 +118,7 @@ export interface PluginSetupDependencies {
}
export interface PluginStartDependencies {
+ cloud?: CloudStart;
features: FeaturesPluginStart;
licensing: LicensingPluginStart;
taskManager: TaskManagerStartContract;
@@ -206,21 +202,6 @@ export class SecurityPlugin
return this.userProfileStart;
};
- /**
- * Indicates whether Kibana is running inside an Elastic Cloud deployment. Since circular plugin dependencies are
- * forbidden, this flag is supposed to be set by the Cloud plugin that already depends on the Security plugin.
- * @private
- */
- private isElasticCloudDeployment?: boolean;
- private readonly getIsElasticCloudDeployment = () => this.isElasticCloudDeployment === true;
- private readonly setIsElasticCloudDeployment = () => {
- if (this.isElasticCloudDeployment !== undefined) {
- throw new Error(`The Elastic Cloud deployment flag has been set already!`);
- }
-
- this.isElasticCloudDeployment = true;
- };
-
constructor(private readonly initializerContext: PluginInitializerContext) {
this.logger = this.initializerContext.logger.get();
@@ -374,13 +355,12 @@ export class SecurityPlugin
license,
logger: this.logger.get('deprecations'),
}),
- setIsElasticCloudDeployment: this.setIsElasticCloudDeployment,
});
}
public start(
core: CoreStart,
- { features, licensing, taskManager, spaces }: PluginStartDependencies
+ { cloud, features, licensing, taskManager, spaces }: PluginStartDependencies
) {
this.logger.debug('Starting plugin');
@@ -413,7 +393,7 @@ export class SecurityPlugin
session,
applicationName: this.authorizationSetup!.applicationName,
kibanaFeatures: features.getKibanaFeatures(),
- isElasticCloudDeployment: this.getIsElasticCloudDeployment,
+ isElasticCloudDeployment: () => cloud?.isCloudEnabled === true,
});
this.authorizationService.start({
diff --git a/x-pack/plugins/security/server/routes/authorization/spaces/share_saved_object_permissions.test.ts b/x-pack/plugins/security/server/routes/authorization/spaces/share_saved_object_permissions.test.ts
index e5e3135a68024..a8fa3888efeb9 100644
--- a/x-pack/plugins/security/server/routes/authorization/spaces/share_saved_object_permissions.test.ts
+++ b/x-pack/plugins/security/server/routes/authorization/spaces/share_saved_object_permissions.test.ts
@@ -36,6 +36,7 @@ describe('Share Saved Object Permissions', () => {
describe('GET /internal/security/_share_saved_object_permissions', () => {
let routeHandler: RequestHandler;
let routeConfig: RouteConfig;
+
beforeEach(() => {
const [shareRouteConfig, shareRouteHandler] = router.get.mock.calls.find(
([{ path }]) => path === '/internal/security/_share_saved_object_permissions'
@@ -50,6 +51,24 @@ describe('Share Saved Object Permissions', () => {
expect(routeConfig.validate).toHaveProperty('query');
});
+ it('returns `not found` when security is diabled', async () => {
+ routeParamsMock.license.isEnabled = jest.fn().mockReturnValue(false);
+
+ const request = httpServerMock.createKibanaRequest({
+ query: {
+ type: 'foo-type',
+ },
+ });
+
+ await expect(
+ routeHandler(mockContext, request, kibanaResponseFactory)
+ ).resolves.toMatchObject({
+ status: 404,
+ });
+
+ expect(routeParamsMock.license.isEnabled).toHaveBeenCalled();
+ });
+
it('returns `true` when the user is authorized globally', async () => {
const checkPrivilegesWithRequest = jest.fn().mockResolvedValue({ hasAllRequested: true });
diff --git a/x-pack/plugins/security/server/routes/authorization/spaces/share_saved_object_permissions.ts b/x-pack/plugins/security/server/routes/authorization/spaces/share_saved_object_permissions.ts
index 574be3ce37a01..536220eff03da 100644
--- a/x-pack/plugins/security/server/routes/authorization/spaces/share_saved_object_permissions.ts
+++ b/x-pack/plugins/security/server/routes/authorization/spaces/share_saved_object_permissions.ts
@@ -11,7 +11,11 @@ import type { RouteDefinitionParams } from '../..';
import { wrapIntoCustomErrorResponse } from '../../../errors';
import { createLicensedRouteHandler } from '../../licensed_route_handler';
-export function defineShareSavedObjectPermissionRoutes({ router, authz }: RouteDefinitionParams) {
+export function defineShareSavedObjectPermissionRoutes({
+ router,
+ authz,
+ license,
+}: RouteDefinitionParams) {
router.get(
{
path: '/internal/security/_share_saved_object_permissions',
@@ -21,6 +25,10 @@ export function defineShareSavedObjectPermissionRoutes({ router, authz }: RouteD
let shareToAllSpaces = true;
const { type } = request.query;
+ if (!license.isEnabled()) {
+ return response.notFound();
+ }
+
try {
const checkPrivileges = authz.checkPrivilegesWithRequest(request);
shareToAllSpaces = (
diff --git a/x-pack/plugins/security/tsconfig.json b/x-pack/plugins/security/tsconfig.json
index e4566248efc46..68c43cf64e6b6 100644
--- a/x-pack/plugins/security/tsconfig.json
+++ b/x-pack/plugins/security/tsconfig.json
@@ -8,6 +8,7 @@
},
"include": ["common/**/*", "public/**/*", "server/**/*"],
"references": [
+ { "path": "../cloud/tsconfig.json" },
{ "path": "../features/tsconfig.json" },
{ "path": "../licensing/tsconfig.json" },
{ "path": "../spaces/tsconfig.json" },
diff --git a/x-pack/plugins/security_solution/common/endpoint/schema/resolver.ts b/x-pack/plugins/security_solution/common/endpoint/schema/resolver.ts
index 15c89c8cd9c28..6de81d3e95a55 100644
--- a/x-pack/plugins/security_solution/common/endpoint/schema/resolver.ts
+++ b/x-pack/plugins/security_solution/common/endpoint/schema/resolver.ts
@@ -58,10 +58,12 @@ export const validateEvents = {
afterEvent: schema.maybe(schema.string()),
}),
body: schema.object({
- timeRange: schema.object({
- from: schema.string(),
- to: schema.string(),
- }),
+ timeRange: schema.maybe(
+ schema.object({
+ from: schema.string(),
+ to: schema.string(),
+ })
+ ),
indexPatterns: schema.arrayOf(schema.string()),
filter: schema.maybe(schema.string()),
entityType: schema.maybe(schema.string()),
diff --git a/x-pack/plugins/security_solution/common/endpoint/service/authz/authz.ts b/x-pack/plugins/security_solution/common/endpoint/service/authz/authz.ts
index dde2a7f92b1e0..d25fd440d1c24 100644
--- a/x-pack/plugins/security_solution/common/endpoint/service/authz/authz.ts
+++ b/x-pack/plugins/security_solution/common/endpoint/service/authz/authz.ts
@@ -160,7 +160,7 @@ export const calculateEndpointAuthz = (
canWritePolicyManagement,
canReadPolicyManagement,
canWriteActionsLogManagement,
- canReadActionsLogManagement,
+ canReadActionsLogManagement: canReadActionsLogManagement && isPlatinumPlusLicense,
// Response Actions
canIsolateHost: canIsolateHost && isPlatinumPlusLicense,
canUnIsolateHost: canIsolateHost,
diff --git a/x-pack/plugins/security_solution/cypress/e2e/detection_alerts/resolver.cy.ts b/x-pack/plugins/security_solution/cypress/e2e/detection_alerts/resolver.cy.ts
index aa2263b9b518c..c2436f3f2de9a 100644
--- a/x-pack/plugins/security_solution/cypress/e2e/detection_alerts/resolver.cy.ts
+++ b/x-pack/plugins/security_solution/cypress/e2e/detection_alerts/resolver.cy.ts
@@ -28,12 +28,12 @@ describe('Analyze events view for alerts', () => {
waitForAlertsToPopulate();
});
- it('should render analyzer when button is clicked', () => {
+ it('should render when button is clicked', () => {
openAnalyzerForFirstAlertInTimeline();
cy.get(ANALYZER_NODE).first().should('be.visible');
});
- it(`should render an analyzer view and display
+ it(`should display
a toast indicating the date range of found events when a time range has 0 events in it`, () => {
const dateContainingZeroEvents = 'Jul 27, 2022 @ 00:00:00.000';
setStartDate(dateContainingZeroEvents);
diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/insights/related_alerts_by_process_ancestry.test.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/insights/related_alerts_by_process_ancestry.test.tsx
index f95bf9234cc16..bd91f55d704da 100644
--- a/x-pack/plugins/security_solution/public/common/components/event_details/insights/related_alerts_by_process_ancestry.test.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/event_details/insights/related_alerts_by_process_ancestry.test.tsx
@@ -116,7 +116,7 @@ describe('RelatedAlertsByProcessAncestry', () => {
});
});
- it('renders a special message when there are no alerts to display', async () => {
+ it('renders a special message when there are no alerts to display (empty response)', async () => {
mockUseAlertPrevalenceFromProcessTree.mockReturnValue({
loading: false,
error: false,
@@ -134,4 +134,23 @@ describe('RelatedAlertsByProcessAncestry', () => {
expect(screen.getByText(PROCESS_ANCESTRY_EMPTY)).toBeInTheDocument();
});
});
+
+ it('renders a special message when there are no alerts to display (undefined case)', async () => {
+ mockUseAlertPrevalenceFromProcessTree.mockReturnValue({
+ loading: false,
+ error: false,
+ alertIds: undefined,
+ });
+
+ render(
+
+
+
+ );
+
+ userEvent.click(screen.getByText(PROCESS_ANCESTRY));
+ await waitFor(() => {
+ expect(screen.getByText(PROCESS_ANCESTRY_EMPTY)).toBeInTheDocument();
+ });
+ });
});
diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/insights/related_alerts_by_process_ancestry.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/insights/related_alerts_by_process_ancestry.tsx
index 330cb7ae113b3..28737c60f4e07 100644
--- a/x-pack/plugins/security_solution/public/common/components/event_details/insights/related_alerts_by_process_ancestry.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/event_details/insights/related_alerts_by_process_ancestry.tsx
@@ -70,15 +70,12 @@ export const RelatedAlertsByProcessAncestry = React.memo(
const [cache, setCache] = useState>({});
const onToggle = useCallback((isOpen: boolean) => setShowContent(isOpen), []);
- const isEmpty = !!cache.alertIds && cache.alertIds.length === 0;
// Makes sure the component is not fetching data before the accordion
// has been openend.
const renderContent = useCallback(() => {
if (!showContent) {
return null;
- } else if (isEmpty) {
- return PROCESS_ANCESTRY_EMPTY;
} else if (cache.alertIds) {
return (
(
onCacheLoad={setCache}
/>
);
- }, [showContent, cache, data, eventId, timelineId, index, originalDocumentId, isEmpty]);
+ }, [showContent, cache, data, eventId, timelineId, index, originalDocumentId]);
return (
{
- if (alertIds) {
+ if (alertIds && alertIds.length !== 0) {
onCacheLoad({ alertIds });
}
}, [alertIds, onCacheLoad]);
@@ -152,6 +149,8 @@ const FetchAndNotifyCachedAlertsByProcessAncestry: React.FC<{
return ;
} else if (error) {
return <>{PROCESS_ANCESTRY_ERROR}>;
+ } else if (!alertIds || alertIds.length === 0) {
+ return <>{PROCESS_ANCESTRY_EMPTY}>;
}
return null;
diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/types.ts b/x-pack/plugins/security_solution/public/common/components/navigation/types.ts
index ebfae21d5a5e5..5a2d192b9fd48 100644
--- a/x-pack/plugins/security_solution/public/common/components/navigation/types.ts
+++ b/x-pack/plugins/security_solution/public/common/components/navigation/types.ts
@@ -68,7 +68,6 @@ export interface NavTab {
}
export const securityNavKeys = [
SecurityPageName.alerts,
- SecurityPageName.responseActionsHistory,
SecurityPageName.blocklist,
SecurityPageName.detectionAndResponse,
SecurityPageName.case,
@@ -81,6 +80,7 @@ export const securityNavKeys = [
SecurityPageName.hosts,
SecurityPageName.network,
SecurityPageName.overview,
+ SecurityPageName.responseActionsHistory,
SecurityPageName.rules,
SecurityPageName.timelines,
SecurityPageName.trustedApps,
diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/index.test.tsx
index 1055c98835d56..5a99df01e5328 100644
--- a/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/index.test.tsx
@@ -17,6 +17,7 @@ import { TestProviders } from '../../../mock';
import { CASES_FEATURE_ID } from '../../../../../common/constants';
import { useCanSeeHostIsolationExceptionsMenu } from '../../../../management/pages/host_isolation_exceptions/view/hooks';
import { useTourContext } from '../../guided_onboarding';
+import { useUserPrivileges } from '../../user_privileges';
import {
noCasesPermissions,
readCasesCapabilities,
@@ -38,6 +39,9 @@ jest.mock('../../../hooks/use_experimental_features');
jest.mock('../../../utils/route/use_route_spy');
jest.mock('../../../../management/pages/host_isolation_exceptions/view/hooks');
jest.mock('../../guided_onboarding');
+jest.mock('../../user_privileges');
+
+const mockUseUserPrivileges = useUserPrivileges as jest.Mock;
describe('useSecuritySolutionNavigation', () => {
const mockRouteSpy = [
@@ -56,6 +60,9 @@ describe('useSecuritySolutionNavigation', () => {
(useIsExperimentalFeatureEnabled as jest.Mock).mockReturnValue(false);
(useRouteSpy as jest.Mock).mockReturnValue(mockRouteSpy);
(useCanSeeHostIsolationExceptionsMenu as jest.Mock).mockReturnValue(true);
+ mockUseUserPrivileges.mockImplementation(() => ({
+ endpointPrivileges: { canReadActionsLogManagement: true },
+ }));
(useTourContext as jest.Mock).mockReturnValue({ isTourShown: false });
const cases = mockCasesContract();
@@ -83,6 +90,10 @@ describe('useSecuritySolutionNavigation', () => {
});
});
+ afterEach(() => {
+ mockUseUserPrivileges.mockReset();
+ });
+
it('should create navigation config', async () => {
const { result } = renderHook<{}, KibanaPageTemplateProps['solutionNav']>(
() => useSecuritySolutionNavigation(),
@@ -117,6 +128,23 @@ describe('useSecuritySolutionNavigation', () => {
).toBeUndefined();
});
+ it('should omit response actions history if hook reports false', () => {
+ mockUseUserPrivileges.mockImplementation(() => ({
+ endpointPrivileges: { canReadActionsLogManagement: false },
+ }));
+ const { result } = renderHook<{}, KibanaPageTemplateProps['solutionNav']>(
+ () => useSecuritySolutionNavigation(),
+ { wrapper: TestProviders }
+ );
+ const items = result.current?.items;
+ expect(items).toBeDefined();
+ expect(
+ items!
+ .find((item) => item.id === 'manage')
+ ?.items?.find((item) => item.id === 'response_actions_history')
+ ).toBeUndefined();
+ });
+
describe('Permission gated routes', () => {
describe('cases', () => {
it('should display the cases navigation item when the user has read permissions', () => {
diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/use_navigation_items.tsx b/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/use_navigation_items.tsx
index dc15e371ba630..a4364c8564529 100644
--- a/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/use_navigation_items.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/use_navigation_items.tsx
@@ -21,6 +21,7 @@ import { SecurityPageName } from '../../../../../common/constants';
import { useCanSeeHostIsolationExceptionsMenu } from '../../../../management/pages/host_isolation_exceptions/view/hooks';
import { useIsExperimentalFeatureEnabled } from '../../../hooks/use_experimental_features';
import { useGlobalQueryString } from '../../../utils/global_query_string';
+import { useUserPrivileges } from '../../user_privileges';
export const usePrimaryNavigationItems = ({
navTabs,
@@ -71,6 +72,8 @@ export const usePrimaryNavigationItems = ({
function usePrimaryNavigationItemsToDisplay(navTabs: Record) {
const hasCasesReadPermissions = useGetUserCasesPermissions().read;
const canSeeHostIsolationExceptions = useCanSeeHostIsolationExceptionsMenu();
+ const canSeeResponseActionsHistory =
+ useUserPrivileges().endpointPrivileges.canReadActionsLogManagement;
const isPolicyListEnabled = useIsExperimentalFeatureEnabled('policyListEnabled');
const uiCapabilities = useKibana().services.application.capabilities;
@@ -138,7 +141,9 @@ function usePrimaryNavigationItemsToDisplay(navTabs: Record) {
? [navTabs[SecurityPageName.hostIsolationExceptions]]
: []),
navTabs[SecurityPageName.blocklist],
- navTabs[SecurityPageName.responseActionsHistory],
+ ...(canSeeResponseActionsHistory
+ ? [navTabs[SecurityPageName.responseActionsHistory]]
+ : []),
navTabs[SecurityPageName.cloudSecurityPostureBenchmarks],
],
},
@@ -156,6 +161,7 @@ function usePrimaryNavigationItemsToDisplay(navTabs: Record) {
navTabs,
hasCasesReadPermissions,
canSeeHostIsolationExceptions,
+ canSeeResponseActionsHistory,
isPolicyListEnabled,
]
);
diff --git a/x-pack/plugins/security_solution/public/common/containers/alerts/use_alert_prevalence_from_process_tree.ts b/x-pack/plugins/security_solution/public/common/containers/alerts/use_alert_prevalence_from_process_tree.ts
index 1a59271614c57..e3bc22ec2decb 100644
--- a/x-pack/plugins/security_solution/public/common/containers/alerts/use_alert_prevalence_from_process_tree.ts
+++ b/x-pack/plugins/security_solution/public/common/containers/alerts/use_alert_prevalence_from_process_tree.ts
@@ -18,7 +18,7 @@ interface UserAlertPrevalenceFromProcessTreeResult {
}
interface ProcessTreeAlertPrevalenceResponse {
- alertIds: string[];
+ alertIds: string[] | undefined;
}
interface EntityResponse {
diff --git a/x-pack/plugins/security_solution/public/management/links.test.ts b/x-pack/plugins/security_solution/public/management/links.test.ts
index 09c47bc70095c..c8166563428ab 100644
--- a/x-pack/plugins/security_solution/public/management/links.test.ts
+++ b/x-pack/plugins/security_solution/public/management/links.test.ts
@@ -80,13 +80,30 @@ describe('links', () => {
expect(filteredLinks).toEqual(links);
});
+ it('it returns all but response actions history when no access privilege to either response actions history or HIE but have at least one HIE entry', async () => {
+ fakeHttpServices.get.mockResolvedValue({ total: 1 });
+ const filteredLinks = await getManagementFilteredLinks(
+ coreMockStarted,
+ getPlugins(['superuser'])
+ );
+ (licenseService.isPlatinumPlus as jest.Mock).mockReturnValue(false);
+ expect(filteredLinks).toEqual({
+ ...links,
+ links: links.links?.filter((link) => link.id !== SecurityPageName.responseActionsHistory),
+ });
+ });
+
it('it returns filtered links when not having isolation permissions and no host isolation exceptions entry', async () => {
fakeHttpServices.get.mockResolvedValue({ total: 0 });
(licenseService.isPlatinumPlus as jest.Mock).mockReturnValue(false);
const filteredLinks = await getManagementFilteredLinks(coreMockStarted, getPlugins([]));
expect(filteredLinks).toEqual({
...links,
- links: links.links?.filter((link) => link.id !== SecurityPageName.hostIsolationExceptions),
+ links: links.links?.filter(
+ (link) =>
+ link.id !== SecurityPageName.hostIsolationExceptions &&
+ link.id !== SecurityPageName.responseActionsHistory
+ ),
});
});
});
diff --git a/x-pack/plugins/security_solution/public/management/links.ts b/x-pack/plugins/security_solution/public/management/links.ts
index 03cfee736def3..12a904201a9c5 100644
--- a/x-pack/plugins/security_solution/public/management/links.ts
+++ b/x-pack/plugins/security_solution/public/management/links.ts
@@ -226,7 +226,7 @@ export const links: LinkItem = {
],
};
-const getFilteredLinks = (linkIds: SecurityPageName[]) => ({
+const excludeLinks = (linkIds: SecurityPageName[]) => ({
...links,
links: links.links?.filter((link) => !linkIds.includes(link.id)),
});
@@ -249,19 +249,26 @@ export const getManagementFilteredLinks = async (
)
: getEndpointAuthzInitialState();
if (!privileges.canAccessEndpointManagement) {
- return getFilteredLinks([SecurityPageName.hostIsolationExceptions]);
+ return excludeLinks([
+ SecurityPageName.hostIsolationExceptions,
+ SecurityPageName.responseActionsHistory,
+ ]);
}
- if (!privileges.canIsolateHost) {
+ if (!privileges.canIsolateHost || !privileges.canReadActionsLogManagement) {
const hostIsolationExceptionsApiClientInstance = HostIsolationExceptionsApiClient.getInstance(
core.http
);
const summaryResponse = await hostIsolationExceptionsApiClientInstance.summary();
if (!summaryResponse.total) {
- return getFilteredLinks([SecurityPageName.hostIsolationExceptions]);
+ return excludeLinks([
+ SecurityPageName.hostIsolationExceptions,
+ SecurityPageName.responseActionsHistory,
+ ]);
}
+ return excludeLinks([SecurityPageName.responseActionsHistory]);
}
} catch {
- return getFilteredLinks([SecurityPageName.hostIsolationExceptions]);
+ return excludeLinks([SecurityPageName.hostIsolationExceptions]);
}
return links;
diff --git a/x-pack/plugins/security_solution/public/management/pages/index.tsx b/x-pack/plugins/security_solution/public/management/pages/index.tsx
index dd06a838a26cb..590b3786ece15 100644
--- a/x-pack/plugins/security_solution/public/management/pages/index.tsx
+++ b/x-pack/plugins/security_solution/public/management/pages/index.tsx
@@ -76,7 +76,8 @@ const ResponseActionsTelemetry = () => (
);
export const ManagementContainer = memo(() => {
- const { loading, canAccessEndpointManagement } = useUserPrivileges().endpointPrivileges;
+ const { loading, canAccessEndpointManagement, canReadActionsLogManagement } =
+ useUserPrivileges().endpointPrivileges;
// Lets wait until we can verify permissions
if (loading) {
@@ -103,10 +104,12 @@ export const ManagementContainer = memo(() => {
component={HostIsolationExceptionsTelemetry}
/>
-
+ {canReadActionsLogManagement && (
+
+ )}
diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/components/policy_form_layout.test.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/components/policy_form_layout.test.tsx
index 49a2e8173476a..60dc7bd29895a 100644
--- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/components/policy_form_layout.test.tsx
+++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/components/policy_form_layout.test.tsx
@@ -135,6 +135,13 @@ describe('Policy Form Layout', () => {
expect(saveButton).toHaveLength(1);
expect(saveButton.text()).toEqual('Save');
});
+ it('should display beta badge', async () => {
+ await asyncActions;
+ policyFormLayoutView.update();
+ const saveButton = policyFormLayoutView.find('EuiBetaBadge');
+ expect(saveButton).toHaveLength(1);
+ expect(saveButton.text()).toEqual('beta');
+ });
describe('when the save button is clicked', () => {
let saveButton: FindReactWrapperResponse;
let confirmModal: FindReactWrapperResponse;
diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/events/linux.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/events/linux.tsx
index 1b8e4f2040150..984bc53a014e3 100644
--- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/events/linux.tsx
+++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/events/linux.tsx
@@ -88,7 +88,7 @@ const SUPPLEMENTAL_OPTIONS: ReadonlyArray {
return !config.linux.events.session_data;
},
- beta: false,
+ beta: true,
},
];
diff --git a/x-pack/plugins/security_solution/public/resolver/data_access_layer/factory.ts b/x-pack/plugins/security_solution/public/resolver/data_access_layer/factory.ts
index 04e694b2cedbb..719fdedb73546 100644
--- a/x-pack/plugins/security_solution/public/resolver/data_access_layer/factory.ts
+++ b/x-pack/plugins/security_solution/public/resolver/data_access_layer/factory.ts
@@ -17,6 +17,17 @@ import type {
ResolverSchema,
} from '../../../common/endpoint/types';
+function getRangeFilter(timeRange: TimeRange | undefined) {
+ return timeRange
+ ? {
+ timeRange: {
+ from: timeRange.from,
+ to: timeRange.to,
+ },
+ }
+ : [];
+}
+
/**
* The data access layer for resolver. All communication with the Kibana server is done through this object. This object is provided to Resolver. In tests, a mock data access layer can be used instead.
*/
@@ -34,7 +45,7 @@ export function dataAccessLayerFactory(
indexPatterns,
}: {
entityID: string;
- timeRange: TimeRange;
+ timeRange?: TimeRange;
indexPatterns: string[];
}): Promise {
const response: ResolverPaginatedEvents = await context.services.http.post(
@@ -43,10 +54,7 @@ export function dataAccessLayerFactory(
query: {},
body: JSON.stringify({
indexPatterns,
- timeRange: {
- from: timeRange.from,
- to: timeRange.to,
- },
+ ...getRangeFilter(timeRange),
filter: JSON.stringify({
bool: {
filter: [
@@ -76,16 +84,13 @@ export function dataAccessLayerFactory(
entityID: string;
category: string;
after?: string;
- timeRange: TimeRange;
+ timeRange?: TimeRange;
indexPatterns: string[];
}): Promise {
const commonFields = {
query: { afterEvent: after, limit: 25 },
body: {
- timeRange: {
- from: timeRange.from,
- to: timeRange.to,
- },
+ ...getRangeFilter(timeRange),
indexPatterns,
},
};
@@ -127,30 +132,28 @@ export function dataAccessLayerFactory(
limit,
}: {
ids: string[];
- timeRange: TimeRange;
+ timeRange?: TimeRange;
indexPatterns: string[];
limit: number;
}): Promise {
- const response: ResolverPaginatedEvents = await context.services.http.post(
- '/api/endpoint/resolver/events',
- {
- query: { limit },
- body: JSON.stringify({
- timeRange: {
- from: timeRange.from,
- to: timeRange.to,
+ const query = {
+ query: { limit },
+ body: JSON.stringify({
+ indexPatterns,
+ ...getRangeFilter(timeRange),
+ filter: JSON.stringify({
+ bool: {
+ filter: [
+ { terms: { 'process.entity_id': ids } },
+ { term: { 'event.category': 'process' } },
+ ],
},
- indexPatterns,
- filter: JSON.stringify({
- bool: {
- filter: [
- { terms: { 'process.entity_id': ids } },
- { term: { 'event.category': 'process' } },
- ],
- },
- }),
}),
- }
+ }),
+ };
+ const response: ResolverPaginatedEvents = await context.services.http.post(
+ '/api/endpoint/resolver/events',
+ query
);
return response.events;
},
@@ -172,7 +175,7 @@ export function dataAccessLayerFactory(
eventTimestamp: string;
eventID?: string | number;
winlogRecordID: string;
- timeRange: TimeRange;
+ timeRange?: TimeRange;
indexPatterns: string[];
}): Promise {
/** @description - eventID isn't provided by winlog. This can be removed once runtime fields are available */
@@ -200,10 +203,7 @@ export function dataAccessLayerFactory(
query: { limit: 1 },
body: JSON.stringify({
indexPatterns,
- timeRange: {
- from: timeRange.from,
- to: timeRange.to,
- },
+ ...getRangeFilter(timeRange),
filter: JSON.stringify(filter),
}),
}
@@ -217,10 +217,7 @@ export function dataAccessLayerFactory(
query: { limit: 1 },
body: JSON.stringify({
indexPatterns,
- timeRange: {
- from: timeRange.from,
- to: timeRange.to,
- },
+ ...getRangeFilter(timeRange),
entityType: 'alertDetail',
eventID,
}),
@@ -250,7 +247,7 @@ export function dataAccessLayerFactory(
}: {
dataId: string;
schema: ResolverSchema;
- timeRange: TimeRange;
+ timeRange?: TimeRange;
indices: string[];
ancestors: number;
descendants: number;
diff --git a/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/generator_tree.ts b/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/generator_tree.ts
index 130b81c5622b2..6b833c93704b4 100644
--- a/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/generator_tree.ts
+++ b/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/generator_tree.ts
@@ -63,7 +63,7 @@ export function generateTreeWithDAL(
indexPatterns,
}: {
entityID: string;
- timeRange: TimeRange;
+ timeRange?: TimeRange;
indexPatterns: string[];
}): Promise {
const node = allNodes.get(entityID);
@@ -88,7 +88,7 @@ export function generateTreeWithDAL(
entityID: string;
category: string;
after?: string;
- timeRange: TimeRange;
+ timeRange?: TimeRange;
indexPatterns: string[];
}): Promise<{ events: SafeResolverEvent[]; nextEvent: string | null }> {
const node = allNodes.get(entityID);
@@ -119,7 +119,7 @@ export function generateTreeWithDAL(
eventCategory: string[];
eventTimestamp: string;
eventID?: string | number;
- timeRange: TimeRange;
+ timeRange?: TimeRange;
indexPatterns: string[];
}): Promise {
return null;
@@ -135,7 +135,7 @@ export function generateTreeWithDAL(
limit,
}: {
ids: string[];
- timeRange: TimeRange;
+ timeRange?: TimeRange;
indexPatterns: string[];
limit: number;
}): Promise {
diff --git a/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/no_ancestors_two_children.ts b/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/no_ancestors_two_children.ts
index 000d08b4e15c7..e883a96b162e8 100644
--- a/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/no_ancestors_two_children.ts
+++ b/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/no_ancestors_two_children.ts
@@ -59,7 +59,7 @@ export function noAncestorsTwoChildren(): { dataAccessLayer: DataAccessLayer; me
indexPatterns,
}: {
entityID: string;
- timeRange: TimeRange;
+ timeRange?: TimeRange;
indexPatterns: string[];
}): Promise {
return Promise.resolve({
@@ -83,7 +83,7 @@ export function noAncestorsTwoChildren(): { dataAccessLayer: DataAccessLayer; me
entityID: string;
category: string;
after?: string;
- timeRange: TimeRange;
+ timeRange?: TimeRange;
indexPatterns: string[];
}): Promise<{
events: SafeResolverEvent[];
@@ -110,7 +110,7 @@ export function noAncestorsTwoChildren(): { dataAccessLayer: DataAccessLayer; me
eventTimestamp: string;
eventID?: string | number;
winlogRecordID: string;
- timeRange: TimeRange;
+ timeRange?: TimeRange;
indexPatterns: string[];
}): Promise {
return null;
diff --git a/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/no_ancestors_two_children_in_index_called_awesome_index.ts b/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/no_ancestors_two_children_in_index_called_awesome_index.ts
index 808c4463f3a89..c4c7fda097e8f 100644
--- a/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/no_ancestors_two_children_in_index_called_awesome_index.ts
+++ b/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/no_ancestors_two_children_in_index_called_awesome_index.ts
@@ -64,7 +64,7 @@ export function noAncestorsTwoChildenInIndexCalledAwesomeIndex(): {
indexPatterns,
}: {
entityID: string;
- timeRange: TimeRange;
+ timeRange?: TimeRange;
indexPatterns: string[];
}): Promise {
return Promise.resolve({
@@ -90,7 +90,7 @@ export function noAncestorsTwoChildenInIndexCalledAwesomeIndex(): {
entityID: string;
category: string;
after?: string;
- timeRange: TimeRange;
+ timeRange?: TimeRange;
indexPatterns: string[];
}): Promise<{
events: SafeResolverEvent[];
@@ -121,7 +121,7 @@ export function noAncestorsTwoChildenInIndexCalledAwesomeIndex(): {
eventTimestamp: string;
eventID?: string | number;
winlogRecordID: string;
- timeRange: TimeRange;
+ timeRange?: TimeRange;
indexPatterns: string[];
}): Promise {
return mockEndpointEvent({
diff --git a/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/no_ancestors_two_children_with_related_events_on_origin.ts b/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/no_ancestors_two_children_with_related_events_on_origin.ts
index 774111baf165d..30f7e07bf041a 100644
--- a/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/no_ancestors_two_children_with_related_events_on_origin.ts
+++ b/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/no_ancestors_two_children_with_related_events_on_origin.ts
@@ -67,7 +67,7 @@ export function noAncestorsTwoChildrenWithRelatedEventsOnOrigin(): {
indexPatterns,
}: {
entityID: string;
- timeRange: TimeRange;
+ timeRange?: TimeRange;
indexPatterns: string[];
}): Promise {
/**
@@ -97,7 +97,7 @@ export function noAncestorsTwoChildrenWithRelatedEventsOnOrigin(): {
entityID: string;
category: string;
after?: string;
- timeRange: TimeRange;
+ timeRange?: TimeRange;
indexPatterns: string[];
}): Promise<{ events: SafeResolverEvent[]; nextEvent: string | null }> {
const events =
@@ -129,7 +129,7 @@ export function noAncestorsTwoChildrenWithRelatedEventsOnOrigin(): {
eventTimestamp: string;
eventID?: string | number;
winlogRecordID: string;
- timeRange: TimeRange;
+ timeRange?: TimeRange;
indexPatterns: string[];
}): Promise {
return relatedEvents.events.find((event) => eventModel.eventID(event) === eventID) ?? null;
diff --git a/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/one_node_with_paginated_related_events.ts b/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/one_node_with_paginated_related_events.ts
index 7eb8c28a433e3..dc7031acdbd91 100644
--- a/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/one_node_with_paginated_related_events.ts
+++ b/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/one_node_with_paginated_related_events.ts
@@ -58,7 +58,7 @@ export function oneNodeWithPaginatedEvents(): {
indexPatterns,
}: {
entityID: string;
- timeRange: TimeRange;
+ timeRange?: TimeRange;
indexPatterns: string[];
}): Promise {
/**
@@ -86,7 +86,7 @@ export function oneNodeWithPaginatedEvents(): {
entityID: string;
category: string;
after?: string;
- timeRange: TimeRange;
+ timeRange?: TimeRange;
indexPatterns: string[];
}): Promise<{ events: SafeResolverEvent[]; nextEvent: string | null }> {
let events: SafeResolverEvent[] = [];
@@ -121,7 +121,7 @@ export function oneNodeWithPaginatedEvents(): {
eventTimestamp: string;
eventID?: string | number;
winlogRecordID: string;
- timeRange: TimeRange;
+ timeRange?: TimeRange;
indexPatterns: string[];
}): Promise {
return mockTree.events.find((event) => eventModel.eventID(event) === eventID) ?? null;
diff --git a/x-pack/plugins/security_solution/public/resolver/store/middleware/current_related_event_fetcher.ts b/x-pack/plugins/security_solution/public/resolver/store/middleware/current_related_event_fetcher.ts
index 6b58dd4e8e62e..cd4119f9569e7 100644
--- a/x-pack/plugins/security_solution/public/resolver/store/middleware/current_related_event_fetcher.ts
+++ b/x-pack/plugins/security_solution/public/resolver/store/middleware/current_related_event_fetcher.ts
@@ -48,8 +48,9 @@ export function CurrentRelatedEventFetcher(
api.dispatch({
type: 'appRequestedCurrentRelatedEventData',
});
- const timeRangeFilters = selectors.timeRangeFilters(state);
-
+ const detectedBounds = selectors.detectedBounds(state);
+ const timeRangeFilters =
+ detectedBounds !== undefined ? undefined : selectors.timeRangeFilters(state);
let result: SafeResolverEvent | null = null;
try {
result = await dataAccessLayer.event({
diff --git a/x-pack/plugins/security_solution/public/resolver/store/middleware/node_data_fetcher.ts b/x-pack/plugins/security_solution/public/resolver/store/middleware/node_data_fetcher.ts
index c3173b3238737..9a3a9eb3450fd 100644
--- a/x-pack/plugins/security_solution/public/resolver/store/middleware/node_data_fetcher.ts
+++ b/x-pack/plugins/security_solution/public/resolver/store/middleware/node_data_fetcher.ts
@@ -60,7 +60,9 @@ export function NodeDataFetcher(
let results: SafeResolverEvent[] | undefined;
try {
- const timeRangeFilters = selectors.timeRangeFilters(state);
+ const detectedBounds = selectors.detectedBounds(state);
+ const timeRangeFilters =
+ detectedBounds !== undefined ? undefined : selectors.timeRangeFilters(state);
results = await dataAccessLayer.nodeData({
ids: Array.from(newIDsToRequest),
timeRange: timeRangeFilters,
diff --git a/x-pack/plugins/security_solution/public/resolver/store/middleware/related_events_fetcher.ts b/x-pack/plugins/security_solution/public/resolver/store/middleware/related_events_fetcher.ts
index ec0f068b5425c..ab8f71940104e 100644
--- a/x-pack/plugins/security_solution/public/resolver/store/middleware/related_events_fetcher.ts
+++ b/x-pack/plugins/security_solution/public/resolver/store/middleware/related_events_fetcher.ts
@@ -30,7 +30,9 @@ export function RelatedEventsFetcher(
const indices = selectors.eventIndices(state);
const oldParams = last;
- const timeRangeFilters = selectors.timeRangeFilters(state);
+ const detectedBounds = selectors.detectedBounds(state);
+ const timeRangeFilters =
+ detectedBounds !== undefined ? undefined : selectors.timeRangeFilters(state);
// Update this each time before fetching data (or even if we don't fetch data) so that subsequent actions that call this (concurrently) will have up to date info.
last = newParams;
diff --git a/x-pack/plugins/security_solution/public/resolver/store/middleware/resolver_tree_fetcher.ts b/x-pack/plugins/security_solution/public/resolver/store/middleware/resolver_tree_fetcher.ts
index e4da1af5f4d79..61319158fccc2 100644
--- a/x-pack/plugins/security_solution/public/resolver/store/middleware/resolver_tree_fetcher.ts
+++ b/x-pack/plugins/security_solution/public/resolver/store/middleware/resolver_tree_fetcher.ts
@@ -93,9 +93,9 @@ export function ResolverTreeFetcher(
descendants: descendantsRequestAmount(),
});
if (unboundedTree.length > 0) {
- const timestamps = unboundedTree.map((event) =>
- firstNonNullValue(event.data['@timestamp'])
- );
+ const timestamps = unboundedTree
+ .map((event) => firstNonNullValue(event.data['@timestamp']))
+ .sort();
const oldestTimestamp = timestamps[0];
const newestTimestamp = timestamps.slice(-1);
api.dispatch({
diff --git a/x-pack/plugins/security_solution/public/resolver/types.ts b/x-pack/plugins/security_solution/public/resolver/types.ts
index 00ecd995176eb..88e97f416dc49 100644
--- a/x-pack/plugins/security_solution/public/resolver/types.ts
+++ b/x-pack/plugins/security_solution/public/resolver/types.ts
@@ -692,7 +692,7 @@ export interface DataAccessLayer {
indexPatterns,
}: {
entityID: string;
- timeRange: TimeRange;
+ timeRange?: TimeRange;
indexPatterns: string[];
}) => Promise;
@@ -710,7 +710,7 @@ export interface DataAccessLayer {
entityID: string;
category: string;
after?: string;
- timeRange: TimeRange;
+ timeRange?: TimeRange;
indexPatterns: string[];
}) => Promise;
@@ -725,7 +725,7 @@ export interface DataAccessLayer {
limit,
}: {
ids: string[];
- timeRange: TimeRange;
+ timeRange?: TimeRange;
indexPatterns: string[];
limit: number;
}): Promise;
@@ -747,7 +747,7 @@ export interface DataAccessLayer {
eventTimestamp: string;
eventID?: string | number;
winlogRecordID: string;
- timeRange: TimeRange;
+ timeRange?: TimeRange;
indexPatterns: string[];
}) => Promise;
diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/events.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/events.ts
index ba4f682423670..869ae911ad890 100644
--- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/events.ts
+++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/events.ts
@@ -11,31 +11,22 @@ import type { JsonObject, JsonValue } from '@kbn/utility-types';
import { parseFilterQuery } from '../../../../utils/serialized_query';
import type { SafeResolverEvent } from '../../../../../common/endpoint/types';
import type { PaginationBuilder } from '../utils/pagination';
-
-interface TimeRange {
- from: string;
- to: string;
-}
+import { BaseResolverQuery } from '../tree/queries/base';
+import type { ResolverQueryParams } from '../tree/queries/base';
/**
* Builds a query for retrieving events.
*/
-export class EventsQuery {
- private readonly pagination: PaginationBuilder;
- private readonly indexPatterns: string | string[];
- private readonly timeRange: TimeRange;
+export class EventsQuery extends BaseResolverQuery {
+ readonly pagination: PaginationBuilder;
constructor({
- pagination,
indexPatterns,
timeRange,
- }: {
- pagination: PaginationBuilder;
- indexPatterns: string | string[];
- timeRange: TimeRange;
- }) {
+ isInternalRequest,
+ pagination,
+ }: ResolverQueryParams & { pagination: PaginationBuilder }) {
+ super({ indexPatterns, timeRange, isInternalRequest });
this.pagination = pagination;
- this.indexPatterns = indexPatterns;
- this.timeRange = timeRange;
}
private query(filters: JsonObject[]): JsonObject {
@@ -44,15 +35,7 @@ export class EventsQuery {
bool: {
filter: [
...filters,
- {
- range: {
- '@timestamp': {
- gte: this.timeRange.from,
- lte: this.timeRange.to,
- format: 'strict_date_optional_time',
- },
- },
- },
+ ...this.getRangeFilter(),
{
term: { 'event.kind': 'event' },
},
@@ -71,15 +54,7 @@ export class EventsQuery {
{
term: { 'event.id': id },
},
- {
- range: {
- '@timestamp': {
- gte: this.timeRange.from,
- lte: this.timeRange.to,
- format: 'strict_date_optional_time',
- },
- },
- },
+ ...this.getRangeFilter(),
],
},
},
@@ -97,15 +72,7 @@ export class EventsQuery {
{
term: { 'process.entity_id': id },
},
- {
- range: {
- '@timestamp': {
- gte: this.timeRange.from,
- lte: this.timeRange.to,
- format: 'strict_date_optional_time',
- },
- },
- },
+ ...this.getRangeFilter(),
],
},
},
diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/tree/queries/base.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/tree/queries/base.ts
index 6637e7931b056..256f2b58b6864 100644
--- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/tree/queries/base.ts
+++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/tree/queries/base.ts
@@ -11,10 +11,10 @@ import type { TimeRange } from '../utils';
import { resolverFields } from '../utils';
export interface ResolverQueryParams {
- readonly schema: ResolverSchema;
+ readonly schema?: ResolverSchema;
readonly indexPatterns: string | string[];
readonly timeRange: TimeRange | undefined;
- readonly isInternalRequest: boolean;
+ readonly isInternalRequest?: boolean;
readonly resolverFields?: JsonValue[];
getRangeFilter?: () => Array<{
range: { '@timestamp': { gte: string; lte: string; format: string } };
@@ -25,12 +25,18 @@ export class BaseResolverQuery implements ResolverQueryParams {
readonly schema: ResolverSchema;
readonly indexPatterns: string | string[];
readonly timeRange: TimeRange | undefined;
- readonly isInternalRequest: boolean;
+ readonly isInternalRequest?: boolean;
readonly resolverFields?: JsonValue[];
constructor({ schema, indexPatterns, timeRange, isInternalRequest }: ResolverQueryParams) {
- this.resolverFields = resolverFields(schema);
- this.schema = schema;
+ const schemaOrDefault = schema
+ ? schema
+ : {
+ id: 'process.entity_id',
+ parent: 'process.parent.entity_id',
+ };
+ this.resolverFields = resolverFields(schemaOrDefault);
+ this.schema = schemaOrDefault;
this.indexPatterns = indexPatterns;
this.timeRange = timeRange;
this.isInternalRequest = isInternalRequest;
diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/state/index_status/actions.ts b/x-pack/plugins/synthetics/public/apps/synthetics/state/index_status/actions.ts
index 36e2e2514910e..e522af3bfed7c 100644
--- a/x-pack/plugins/synthetics/public/apps/synthetics/state/index_status/actions.ts
+++ b/x-pack/plugins/synthetics/public/apps/synthetics/state/index_status/actions.ts
@@ -5,10 +5,11 @@
* 2.0.
*/
-import type { IHttpFetchError } from '@kbn/core-http-browser';
import { createAction } from '@reduxjs/toolkit';
import { StatesIndexStatus } from '../../../../../common/runtime_types';
+import { IHttpSerializedFetchError } from '../utils/http_error';
export const getIndexStatus = createAction('[INDEX STATUS] GET');
export const getIndexStatusSuccess = createAction('[INDEX STATUS] GET SUCCESS');
-export const getIndexStatusFail = createAction('[INDEX STATUS] GET FAIL');
+export const getIndexStatusFail =
+ createAction('[INDEX STATUS] GET FAIL');
diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/state/index_status/index.ts b/x-pack/plugins/synthetics/public/apps/synthetics/state/index_status/index.ts
index f5351c65d0d6b..19ef8f94938a3 100644
--- a/x-pack/plugins/synthetics/public/apps/synthetics/state/index_status/index.ts
+++ b/x-pack/plugins/synthetics/public/apps/synthetics/state/index_status/index.ts
@@ -6,7 +6,7 @@
*/
import { createReducer } from '@reduxjs/toolkit';
-import { IHttpSerializedFetchError, serializeHttpFetchError } from '../utils/http_error';
+import { IHttpSerializedFetchError } from '../utils/http_error';
import { StatesIndexStatus } from '../../../../../common/runtime_types';
import { getIndexStatus, getIndexStatusSuccess, getIndexStatusFail } from './actions';
@@ -33,7 +33,7 @@ export const indexStatusReducer = createReducer(initialState, (builder) => {
state.loading = false;
})
.addCase(getIndexStatusFail, (state, action) => {
- state.error = serializeHttpFetchError(action.payload);
+ state.error = action.payload;
state.loading = false;
});
});
diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/state/monitor_details/index.ts b/x-pack/plugins/synthetics/public/apps/synthetics/state/monitor_details/index.ts
index a2d9379df778e..b1fb95d5d5ee4 100644
--- a/x-pack/plugins/synthetics/public/apps/synthetics/state/monitor_details/index.ts
+++ b/x-pack/plugins/synthetics/public/apps/synthetics/state/monitor_details/index.ts
@@ -5,9 +5,8 @@
* 2.0.
*/
-import { IHttpFetchError, ResponseErrorBody } from '@kbn/core/public';
import { createReducer } from '@reduxjs/toolkit';
-import { IHttpSerializedFetchError, serializeHttpFetchError } from '../utils/http_error';
+import { IHttpSerializedFetchError } from '../utils/http_error';
import {
getMonitorRecentPingsAction,
setMonitorDetailsLocationAction,
@@ -47,7 +46,7 @@ export const monitorDetailsReducer = createReducer(initialState, (builder) => {
state.loading = false;
})
.addCase(getMonitorRecentPingsAction.fail, (state, action) => {
- state.error = serializeHttpFetchError(action.payload as IHttpFetchError);
+ state.error = action.payload;
state.loading = false;
})
@@ -59,7 +58,7 @@ export const monitorDetailsReducer = createReducer(initialState, (builder) => {
state.syntheticsMonitorLoading = false;
})
.addCase(getMonitorAction.fail, (state, action) => {
- state.error = serializeHttpFetchError(action.payload as IHttpFetchError);
+ state.error = action.payload;
state.syntheticsMonitorLoading = false;
});
});
diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/state/monitor_list/actions.ts b/x-pack/plugins/synthetics/public/apps/synthetics/state/monitor_list/actions.ts
index fcfc3d4f22cf7..5a8c38284e034 100644
--- a/x-pack/plugins/synthetics/public/apps/synthetics/state/monitor_list/actions.ts
+++ b/x-pack/plugins/synthetics/public/apps/synthetics/state/monitor_list/actions.ts
@@ -5,13 +5,13 @@
* 2.0.
*/
-import { IHttpFetchError } from '@kbn/core-http-browser';
import { createAction } from '@reduxjs/toolkit';
import {
EncryptedSyntheticsMonitor,
MonitorManagementListResult,
} from '../../../../../common/runtime_types';
import { createAsyncAction } from '../utils/actions';
+import { IHttpSerializedFetchError } from '../utils/http_error';
import { MonitorListPageState } from './models';
@@ -29,7 +29,8 @@ export const fetchUpsertSuccessAction = createAction<{
id: string;
attributes: { enabled: boolean };
}>('fetchUpsertMonitorSuccess');
-export const fetchUpsertFailureAction = createAction<{ id: string; error: IHttpFetchError }>(
- 'fetchUpsertMonitorFailure'
-);
+export const fetchUpsertFailureAction = createAction<{
+ id: string;
+ error: IHttpSerializedFetchError;
+}>('fetchUpsertMonitorFailure');
export const clearMonitorUpsertStatus = createAction('clearMonitorUpsertStatus');
diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/state/monitor_list/effects.ts b/x-pack/plugins/synthetics/public/apps/synthetics/state/monitor_list/effects.ts
index 0dee2edfd7903..67aaa4ec982ed 100644
--- a/x-pack/plugins/synthetics/public/apps/synthetics/state/monitor_list/effects.ts
+++ b/x-pack/plugins/synthetics/public/apps/synthetics/state/monitor_list/effects.ts
@@ -5,10 +5,10 @@
* 2.0.
*/
-import { IHttpFetchError } from '@kbn/core-http-browser';
import { PayloadAction } from '@reduxjs/toolkit';
import { call, put, takeEvery, takeLeading } from 'redux-saga/effects';
import { fetchEffectFactory } from '../utils/fetch_effect';
+import { serializeHttpFetchError } from '../utils/http_error';
import {
fetchMonitorListAction,
fetchUpsertFailureAction,
@@ -40,7 +40,7 @@ export function* upsertMonitorEffect() {
);
} catch (error) {
yield put(
- fetchUpsertFailureAction({ id: action.payload.id, error: error as IHttpFetchError })
+ fetchUpsertFailureAction({ id: action.payload.id, error: serializeHttpFetchError(error) })
);
}
}
diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/state/monitor_list/index.ts b/x-pack/plugins/synthetics/public/apps/synthetics/state/monitor_list/index.ts
index e1f564c0d0a3f..997f853c9bfc5 100644
--- a/x-pack/plugins/synthetics/public/apps/synthetics/state/monitor_list/index.ts
+++ b/x-pack/plugins/synthetics/public/apps/synthetics/state/monitor_list/index.ts
@@ -10,7 +10,7 @@ import { FETCH_STATUS } from '@kbn/observability-plugin/public';
import { ConfigKey, MonitorManagementListResult } from '../../../../../common/runtime_types';
-import { IHttpSerializedFetchError, serializeHttpFetchError } from '../utils/http_error';
+import { IHttpSerializedFetchError } from '../utils/http_error';
import { MonitorListPageState } from './models';
import {
@@ -58,7 +58,7 @@ export const monitorListReducer = createReducer(initialState, (builder) => {
})
.addCase(fetchMonitorListAction.fail, (state, action) => {
state.loading = false;
- state.error = serializeHttpFetchError(action.payload);
+ state.error = action.payload;
})
.addCase(fetchUpsertMonitorAction, (state, action) => {
state.monitorUpsertStatuses[action.payload.id] = {
diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/state/overview/index.ts b/x-pack/plugins/synthetics/public/apps/synthetics/state/overview/index.ts
index 49159b29ef461..82272638ffb11 100644
--- a/x-pack/plugins/synthetics/public/apps/synthetics/state/overview/index.ts
+++ b/x-pack/plugins/synthetics/public/apps/synthetics/state/overview/index.ts
@@ -9,7 +9,7 @@ import { createReducer } from '@reduxjs/toolkit';
import { MonitorOverviewResult, OverviewStatus } from '../../../../../common/runtime_types';
-import { IHttpSerializedFetchError, serializeHttpFetchError } from '../utils/http_error';
+import { IHttpSerializedFetchError } from '../utils/http_error';
import { MonitorOverviewPageState } from './models';
import {
@@ -60,13 +60,13 @@ export const monitorOverviewReducer = createReducer(initialState, (builder) => {
})
.addCase(fetchMonitorOverviewAction.fail, (state, action) => {
state.loading = false;
- state.error = serializeHttpFetchError(action.payload);
+ state.error = action.payload;
})
.addCase(quietFetchOverviewAction.success, (state, action) => {
state.data = action.payload;
})
.addCase(quietFetchOverviewAction.fail, (state, action) => {
- state.error = serializeHttpFetchError(action.payload);
+ state.error = action.payload;
})
.addCase(setOverviewPerPageAction, (state, action) => {
state.pageState = {
@@ -79,7 +79,7 @@ export const monitorOverviewReducer = createReducer(initialState, (builder) => {
state.status = action.payload;
})
.addCase(fetchOverviewStatusAction.fail, (state, action) => {
- state.statusError = serializeHttpFetchError(action.payload);
+ state.statusError = action.payload;
})
.addCase(clearOverviewStatusErrorAction, (state) => {
state.statusError = null;
diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/state/service_locations/actions.ts b/x-pack/plugins/synthetics/public/apps/synthetics/state/service_locations/actions.ts
index 794e16d0292c5..dbdd53d4cbcb7 100644
--- a/x-pack/plugins/synthetics/public/apps/synthetics/state/service_locations/actions.ts
+++ b/x-pack/plugins/synthetics/public/apps/synthetics/state/service_locations/actions.ts
@@ -7,10 +7,13 @@
import { createAction } from '@reduxjs/toolkit';
import { ServiceLocations, ThrottlingOptions } from '../../../../../common/runtime_types';
+import { IHttpSerializedFetchError } from '../utils/http_error';
export const getServiceLocations = createAction('[SERVICE LOCATIONS] GET');
export const getServiceLocationsSuccess = createAction<{
throttling: ThrottlingOptions | undefined;
locations: ServiceLocations;
}>('[SERVICE LOCATIONS] GET SUCCESS');
-export const getServiceLocationsFailure = createAction('[SERVICE LOCATIONS] GET FAILURE');
+export const getServiceLocationsFailure = createAction(
+ '[SERVICE LOCATIONS] GET FAILURE'
+);
diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/state/service_locations/index.ts b/x-pack/plugins/synthetics/public/apps/synthetics/state/service_locations/index.ts
index e13fe756ec7fd..9a338458e603f 100644
--- a/x-pack/plugins/synthetics/public/apps/synthetics/state/service_locations/index.ts
+++ b/x-pack/plugins/synthetics/public/apps/synthetics/state/service_locations/index.ts
@@ -11,6 +11,7 @@ import {
ServiceLocations,
ThrottlingOptions,
} from '../../../../../common/runtime_types';
+import { IHttpSerializedFetchError } from '../utils/http_error';
import {
getServiceLocations,
@@ -22,7 +23,7 @@ export interface ServiceLocationsState {
locations: ServiceLocations;
throttling: ThrottlingOptions | null;
loading: boolean;
- error: Error | null;
+ error: IHttpSerializedFetchError | null;
locationsLoaded?: boolean;
}
diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/state/synthetics_enablement/actions.ts b/x-pack/plugins/synthetics/public/apps/synthetics/state/synthetics_enablement/actions.ts
index c38fadc0952a6..0c7abffd1b289 100644
--- a/x-pack/plugins/synthetics/public/apps/synthetics/state/synthetics_enablement/actions.ts
+++ b/x-pack/plugins/synthetics/public/apps/synthetics/state/synthetics_enablement/actions.ts
@@ -7,23 +7,24 @@
import { createAction } from '@reduxjs/toolkit';
import { MonitorManagementEnablementResult } from '../../../../../common/runtime_types';
+import { IHttpSerializedFetchError } from '../utils/http_error';
export const getSyntheticsEnablement = createAction('[SYNTHETICS_ENABLEMENT] GET');
export const getSyntheticsEnablementSuccess = createAction(
'[SYNTHETICS_ENABLEMENT] GET SUCCESS'
);
-export const getSyntheticsEnablementFailure = createAction(
+export const getSyntheticsEnablementFailure = createAction(
'[SYNTHETICS_ENABLEMENT] GET FAILURE'
);
export const disableSynthetics = createAction('[SYNTHETICS_ENABLEMENT] DISABLE');
export const disableSyntheticsSuccess = createAction<{}>('[SYNTHETICS_ENABLEMENT] DISABLE SUCCESS');
-export const disableSyntheticsFailure = createAction(
+export const disableSyntheticsFailure = createAction(
'[SYNTHETICS_ENABLEMENT] DISABLE FAILURE'
);
export const enableSynthetics = createAction('[SYNTHETICS_ENABLEMENT] ENABLE');
export const enableSyntheticsSuccess = createAction<{}>('[SYNTHETICS_ENABLEMENT] ENABLE SUCCESS');
-export const enableSyntheticsFailure = createAction(
+export const enableSyntheticsFailure = createAction(
'[SYNTHETICS_ENABLEMENT] ENABLE FAILURE'
);
diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/state/synthetics_enablement/index.ts b/x-pack/plugins/synthetics/public/apps/synthetics/state/synthetics_enablement/index.ts
index 62ed85ad17e86..3bf9ff69bf005 100644
--- a/x-pack/plugins/synthetics/public/apps/synthetics/state/synthetics_enablement/index.ts
+++ b/x-pack/plugins/synthetics/public/apps/synthetics/state/synthetics_enablement/index.ts
@@ -18,10 +18,11 @@ import {
getSyntheticsEnablementFailure,
} from './actions';
import { MonitorManagementEnablementResult } from '../../../../../common/runtime_types';
+import { IHttpSerializedFetchError } from '../utils/http_error';
export interface SyntheticsEnablementState {
loading: boolean;
- error: Error | null;
+ error: IHttpSerializedFetchError | null;
enablement: MonitorManagementEnablementResult | null;
}
diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/state/utils/actions.ts b/x-pack/plugins/synthetics/public/apps/synthetics/state/utils/actions.ts
index 416c3134d6034..35e93fd91484e 100644
--- a/x-pack/plugins/synthetics/public/apps/synthetics/state/utils/actions.ts
+++ b/x-pack/plugins/synthetics/public/apps/synthetics/state/utils/actions.ts
@@ -6,13 +6,13 @@
*/
import { createAction } from '@reduxjs/toolkit';
-import type { IHttpFetchError } from '@kbn/core-http-browser';
+import type { IHttpSerializedFetchError } from './http_error';
export function createAsyncAction(actionStr: string) {
return {
get: createAction(actionStr),
success: createAction(`${actionStr}_SUCCESS`),
- fail: createAction(`${actionStr}_FAIL`),
+ fail: createAction(`${actionStr}_FAIL`),
};
}
diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/state/utils/fetch_effect.ts b/x-pack/plugins/synthetics/public/apps/synthetics/state/utils/fetch_effect.ts
index b07f1fa542633..294da718a6fd3 100644
--- a/x-pack/plugins/synthetics/public/apps/synthetics/state/utils/fetch_effect.ts
+++ b/x-pack/plugins/synthetics/public/apps/synthetics/state/utils/fetch_effect.ts
@@ -8,6 +8,7 @@
import { call, put } from 'redux-saga/effects';
import { PayloadAction } from '@reduxjs/toolkit';
import type { IHttpFetchError } from '@kbn/core-http-browser';
+import { IHttpSerializedFetchError, serializeHttpFetchError } from './http_error';
/**
* Factory function for a fetch effect. It expects three action creators,
@@ -23,7 +24,7 @@ import type { IHttpFetchError } from '@kbn/core-http-browser';
export function fetchEffectFactory(
fetch: (request: T) => Promise,
success: (response: R) => PayloadAction,
- fail: (error: IHttpFetchError) => PayloadAction
+ fail: (error: IHttpSerializedFetchError) => PayloadAction
) {
return function* (action: PayloadAction): Generator {
try {
@@ -32,14 +33,14 @@ export function fetchEffectFactory(
// eslint-disable-next-line no-console
console.error(response);
- yield put(fail(response as IHttpFetchError));
+ yield put(fail(serializeHttpFetchError(response as IHttpFetchError)));
} else {
yield put(success(response as R));
}
} catch (error) {
// eslint-disable-next-line no-console
console.error(error);
- yield put(fail(error as IHttpFetchError));
+ yield put(fail(serializeHttpFetchError(error)));
}
};
}
diff --git a/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor/ping_list/columns/ping_timestamp/ping_timestamp.tsx b/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor/ping_list/columns/ping_timestamp/ping_timestamp.tsx
index 4cee2eb9bfca8..ed3f5499ba0ab 100644
--- a/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor/ping_list/columns/ping_timestamp/ping_timestamp.tsx
+++ b/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor/ping_list/columns/ping_timestamp/ping_timestamp.tsx
@@ -41,7 +41,7 @@ export const PingTimestamp = ({
label,
checkGroup,
stepStatus,
- allStepsLoaded,
+ allStepsLoaded = true,
initialStepNo = 1,
}: Props) => {
const [stepNumber, setStepNumber] = useState(initialStepNo);
diff --git a/x-pack/plugins/synthetics/public/legacy_uptime/state/private_locations/index.ts b/x-pack/plugins/synthetics/public/legacy_uptime/state/private_locations/index.ts
index 0ff45023143ec..831f8a9cbf6bb 100644
--- a/x-pack/plugins/synthetics/public/legacy_uptime/state/private_locations/index.ts
+++ b/x-pack/plugins/synthetics/public/legacy_uptime/state/private_locations/index.ts
@@ -5,9 +5,9 @@
* 2.0.
*/
-import { IHttpFetchError, ResponseErrorBody } from '@kbn/core/public';
import { createReducer } from '@reduxjs/toolkit';
import { AgentPolicy } from '@kbn/fleet-plugin/common';
+import { IHttpSerializedFetchError } from '../../../apps/synthetics/state';
import {
getAgentPoliciesAction,
setAddingNewPrivateLocation,
@@ -24,7 +24,7 @@ export interface AgentPoliciesList {
export interface AgentPoliciesState {
data: AgentPoliciesList | null;
loading: boolean;
- error: IHttpFetchError | null;
+ error: IHttpSerializedFetchError | null;
isManageFlyoutOpen?: boolean;
isAddingNewPrivateLocation?: boolean;
}
@@ -47,7 +47,7 @@ export const agentPoliciesReducer = createReducer(initialState, (builder) => {
state.loading = false;
})
.addCase(getAgentPoliciesAction.fail, (state, action) => {
- state.error = action.payload as IHttpFetchError;
+ state.error = action.payload;
state.loading = false;
})
.addCase(setManageFlyoutOpen, (state, action) => {
diff --git a/x-pack/plugins/synthetics/server/routes/monitor_cruds/add_monitor_project.ts b/x-pack/plugins/synthetics/server/routes/monitor_cruds/add_monitor_project.ts
index 668d97a0819e3..ea269d87413e7 100644
--- a/x-pack/plugins/synthetics/server/routes/monitor_cruds/add_monitor_project.ts
+++ b/x-pack/plugins/synthetics/server/routes/monitor_cruds/add_monitor_project.ts
@@ -13,6 +13,8 @@ import { API_URLS } from '../../../common/constants';
import { getAllLocations } from '../../synthetics_service/get_all_locations';
import { ProjectMonitorFormatter } from '../../synthetics_service/project_monitor/project_monitor_formatter';
+const MAX_PAYLOAD_SIZE = 1048576 * 20; // 20MiB
+
export const addSyntheticsProjectMonitorRoute: SyntheticsStreamingRouteFactory = (
libs: UMServerLibs
) => ({
@@ -25,6 +27,11 @@ export const addSyntheticsProjectMonitorRoute: SyntheticsStreamingRouteFactory =
monitors: schema.arrayOf(schema.any()),
}),
},
+ options: {
+ body: {
+ maxBytes: MAX_PAYLOAD_SIZE,
+ },
+ },
handler: async ({
request,
savedObjectsClient,
diff --git a/x-pack/plugins/synthetics/server/server.ts b/x-pack/plugins/synthetics/server/server.ts
index 12844c9cb9223..7f667e0fb264d 100644
--- a/x-pack/plugins/synthetics/server/server.ts
+++ b/x-pack/plugins/synthetics/server/server.ts
@@ -57,7 +57,7 @@ export const initSyntheticsServer = (
});
syntheticsAppStreamingApiRoutes.forEach((route) => {
- const { method, streamHandler, path } = syntheticsRouteWrapper(
+ const { method, streamHandler, path, options } = syntheticsRouteWrapper(
createSyntheticsRouteWithAuth(libs, route),
server,
syntheticsMonitorClient
@@ -82,7 +82,8 @@ export const initSyntheticsServer = (
};
},
method,
- server.router
+ server.router,
+ options
);
});
};
diff --git a/x-pack/plugins/synthetics/server/synthetics_route_wrapper.ts b/x-pack/plugins/synthetics/server/synthetics_route_wrapper.ts
index 8706735fa9256..fc1376e157607 100644
--- a/x-pack/plugins/synthetics/server/synthetics_route_wrapper.ts
+++ b/x-pack/plugins/synthetics/server/synthetics_route_wrapper.ts
@@ -19,6 +19,7 @@ export const syntheticsRouteWrapper: SyntheticsRouteWrapper = (
...uptimeRoute,
options: {
tags: ['access:uptime-read', ...(uptimeRoute?.writeAccess ? ['access:uptime-write'] : [])],
+ ...(uptimeRoute.options ?? {}),
},
streamHandler: async (context, request, subject) => {
const coreContext = await context.core;
diff --git a/x-pack/plugins/threat_intelligence/cypress/e2e/indicators.cy.ts b/x-pack/plugins/threat_intelligence/cypress/e2e/indicators.cy.ts
index e52effa09ab3b..c5d67894aa0ff 100644
--- a/x-pack/plugins/threat_intelligence/cypress/e2e/indicators.cy.ts
+++ b/x-pack/plugins/threat_intelligence/cypress/e2e/indicators.cy.ts
@@ -185,9 +185,7 @@ describe('Indicators', () => {
it('should render the inspector flyout', () => {
cy.get(INSPECTOR_BUTTON).last().click({ force: true });
- cy.get(INSPECTOR_PANEL).should('be.visible');
-
- cy.get(INSPECTOR_PANEL).contains('Index patterns');
+ cy.get(INSPECTOR_PANEL).contains('Indicators search requests');
});
});
});
diff --git a/x-pack/plugins/threat_intelligence/cypress/screens/indicators.ts b/x-pack/plugins/threat_intelligence/cypress/screens/indicators.ts
index 2bc1b704e8159..0464e57c6749b 100644
--- a/x-pack/plugins/threat_intelligence/cypress/screens/indicators.ts
+++ b/x-pack/plugins/threat_intelligence/cypress/screens/indicators.ts
@@ -31,7 +31,7 @@ export const FILTERS_GLOBAL_CONTAINER = '[data-test-subj="filters-global-contain
export const TIME_RANGE_PICKER = `[data-test-subj="superDatePickerToggleQuickMenuButton"]`;
-export const QUERY_INPUT = `[data-test-subj="iocListPageQueryInput"]`;
+export const QUERY_INPUT = `[data-test-subj="queryInput"]`;
export const EMPTY_STATE = '[data-test-subj="indicatorsTableEmptyState"]';
diff --git a/x-pack/plugins/threat_intelligence/public/components/layout/layout.tsx b/x-pack/plugins/threat_intelligence/public/components/layout/layout.tsx
index 6c7621977b8dc..04ee12819d988 100644
--- a/x-pack/plugins/threat_intelligence/public/components/layout/layout.tsx
+++ b/x-pack/plugins/threat_intelligence/public/components/layout/layout.tsx
@@ -6,17 +6,23 @@
*/
import { EuiPageHeader, EuiPageHeaderSection, EuiSpacer, EuiText } from '@elastic/eui';
-import React, { FC } from 'react';
+import React, { FC, ReactNode } from 'react';
import { SecuritySolutionPageWrapper } from '../../containers/security_solution_page_wrapper';
export interface LayoutProps {
pageTitle?: string;
border?: boolean;
+ subHeader?: ReactNode;
}
export const TITLE_TEST_ID = 'tiDefaultPageLayoutTitle';
-export const DefaultPageLayout: FC = ({ children, pageTitle, border = true }) => {
+export const DefaultPageLayout: FC = ({
+ children,
+ pageTitle,
+ border = true,
+ subHeader,
+}) => {
return (
@@ -26,6 +32,12 @@ export const DefaultPageLayout: FC = ({ children, pageTitle, border
{pageTitle}
)}
+ {subHeader ? (
+ <>
+
+ {subHeader}
+ >
+ ) : null}
diff --git a/x-pack/plugins/threat_intelligence/public/components/update_status/index.ts b/x-pack/plugins/threat_intelligence/public/components/update_status/index.ts
new file mode 100644
index 0000000000000..f83c0e64fda23
--- /dev/null
+++ b/x-pack/plugins/threat_intelligence/public/components/update_status/index.ts
@@ -0,0 +1,8 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+export * from './update_status';
diff --git a/x-pack/plugins/threat_intelligence/public/components/update_status/update_status.test.tsx b/x-pack/plugins/threat_intelligence/public/components/update_status/update_status.test.tsx
new file mode 100644
index 0000000000000..2ed1503d89a78
--- /dev/null
+++ b/x-pack/plugins/threat_intelligence/public/components/update_status/update_status.test.tsx
@@ -0,0 +1,63 @@
+/*
+ * 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 { render } from '@testing-library/react';
+import React from 'react';
+import { TestProvidersComponent } from '../../common/mocks/test_providers';
+import { UpdateStatus } from './update_status';
+
+describe('', () => {
+ it('should render Updated now', () => {
+ const result = render(, {
+ wrapper: TestProvidersComponent,
+ });
+
+ expect(result.asFragment()).toMatchInlineSnapshot(`
+
+
+
+ `);
+ });
+
+ it('should render Updating when isUpdating', () => {
+ const result = render(, {
+ wrapper: TestProvidersComponent,
+ });
+
+ expect(result.asFragment()).toMatchInlineSnapshot(`
+
+
+
+ `);
+ });
+});
diff --git a/x-pack/plugins/threat_intelligence/public/components/update_status/update_status.tsx b/x-pack/plugins/threat_intelligence/public/components/update_status/update_status.tsx
new file mode 100644
index 0000000000000..02f43481186dd
--- /dev/null
+++ b/x-pack/plugins/threat_intelligence/public/components/update_status/update_status.tsx
@@ -0,0 +1,43 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React from 'react';
+
+import { EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+import { FormattedRelative } from '@kbn/i18n-react';
+
+interface UpdateStatusProps {
+ updatedAt: number;
+ isUpdating: boolean;
+}
+
+const UPDATING = i18n.translate('xpack.threatIntelligence.updateStatus.updating', {
+ defaultMessage: 'Updating...',
+});
+
+const UPDATED = i18n.translate('xpack.threatIntelligence.updateStatus.updated', {
+ defaultMessage: 'Updated',
+});
+
+export const UpdateStatus: React.FC = ({ isUpdating, updatedAt }) => (
+
+
+
+ {isUpdating ? (
+ UPDATING
+ ) : (
+ <>
+ {UPDATED}
+
+
+ >
+ )}
+
+
+
+);
diff --git a/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_table/hooks/__snapshots__/use_toolbar_options.test.tsx.snap b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_table/hooks/__snapshots__/use_toolbar_options.test.tsx.snap
new file mode 100644
index 0000000000000..4b58689023333
--- /dev/null
+++ b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_table/hooks/__snapshots__/use_toolbar_options.test.tsx.snap
@@ -0,0 +1,120 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`useToolbarOptions() should return correct value for 0 indicators total 1`] = `
+Object {
+ "additionalControls": Object {
+ "left": Object {
+ "append": ,
+ "prepend":
+
+ -
+
+ ,
+ },
+ "right": ,
+ },
+ "showDisplaySelector": false,
+ "showFullScreenSelector": true,
+}
+`;
+
+exports[`useToolbarOptions() should return correct value for 25 indicators total 1`] = `
+Object {
+ "additionalControls": Object {
+ "left": Object {
+ "append": ,
+ "prepend":
+
+ Showing
+ 1
+ -
+ 25
+ of
+
+ 25
+ indicators
+
+ ,
+ },
+ "right": ,
+ },
+ "showDisplaySelector": false,
+ "showFullScreenSelector": true,
+}
+`;
+
+exports[`useToolbarOptions() should return correct value for 50 indicators total 1`] = `
+Object {
+ "additionalControls": Object {
+ "left": Object {
+ "append": ,
+ "prepend":
+
+ Showing
+ 26
+ -
+ 50
+ of
+
+ 50
+ indicators
+
+ ,
+ },
+ "right": ,
+ },
+ "showDisplaySelector": false,
+ "showFullScreenSelector": true,
+}
+`;
diff --git a/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_table/hooks/use_toolbar_options.test.tsx b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_table/hooks/use_toolbar_options.test.tsx
index 084279fe8353a..ecf1cbf0a477a 100644
--- a/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_table/hooks/use_toolbar_options.test.tsx
+++ b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_table/hooks/use_toolbar_options.test.tsx
@@ -25,40 +25,7 @@ describe('useToolbarOptions()', () => {
{ wrapper: TestProvidersComponent }
);
- expect(result.result.current).toMatchInlineSnapshot(`
- Object {
- "additionalControls": Object {
- "left": Object {
- "append": ,
- "prepend":
-
- -
-
- ,
- },
- "right": ,
- },
- "showDisplaySelector": false,
- "showFullScreenSelector": false,
- }
- `);
+ expect(result.result.current).toMatchSnapshot();
});
it('should return correct value for 25 indicators total', () => {
@@ -76,47 +43,7 @@ describe('useToolbarOptions()', () => {
{ wrapper: TestProvidersComponent }
);
- expect(result.result.current).toMatchInlineSnapshot(`
- Object {
- "additionalControls": Object {
- "left": Object {
- "append": ,
- "prepend":
-
- Showing
- 1
- -
- 25
- of
-
- 25
- indicators
-
- ,
- },
- "right": ,
- },
- "showDisplaySelector": false,
- "showFullScreenSelector": false,
- }
- `);
+ expect(result.result.current).toMatchSnapshot();
});
it('should return correct value for 50 indicators total', () => {
@@ -134,46 +61,6 @@ describe('useToolbarOptions()', () => {
{ wrapper: TestProvidersComponent }
);
- expect(result.result.current).toMatchInlineSnapshot(`
- Object {
- "additionalControls": Object {
- "left": Object {
- "append": ,
- "prepend":
-
- Showing
- 26
- -
- 50
- of
-
- 50
- indicators
-
- ,
- },
- "right": ,
- },
- "showDisplaySelector": false,
- "showFullScreenSelector": false,
- }
- `);
+ expect(result.result.current).toMatchSnapshot();
});
});
diff --git a/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_table/hooks/use_toolbar_options.tsx b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_table/hooks/use_toolbar_options.tsx
index b19d6df71463e..12bd94951e33c 100644
--- a/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_table/hooks/use_toolbar_options.tsx
+++ b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_table/hooks/use_toolbar_options.tsx
@@ -41,7 +41,7 @@ export const useToolbarOptions = ({
return useMemo(
() => ({
showDisplaySelector: false,
- showFullScreenSelector: false,
+ showFullScreenSelector: true,
additionalControls: {
left: {
prepend: (
diff --git a/x-pack/plugins/threat_intelligence/public/modules/indicators/hooks/use_indicators.test.tsx b/x-pack/plugins/threat_intelligence/public/modules/indicators/hooks/use_indicators.test.tsx
index 42f6a4eb1fdb7..40d64636fa346 100644
--- a/x-pack/plugins/threat_intelligence/public/modules/indicators/hooks/use_indicators.test.tsx
+++ b/x-pack/plugins/threat_intelligence/public/modules/indicators/hooks/use_indicators.test.tsx
@@ -105,6 +105,7 @@ describe('useIndicators()', () => {
expect(hookResult.result.current).toMatchInlineSnapshot(`
Object {
+ "dataUpdatedAt": 0,
"handleRefresh": [Function],
"indicatorCount": 0,
"indicators": Array [],
diff --git a/x-pack/plugins/threat_intelligence/public/modules/indicators/hooks/use_indicators.ts b/x-pack/plugins/threat_intelligence/public/modules/indicators/hooks/use_indicators.ts
index 2352f302a1d4d..e2e0aaddf07aa 100644
--- a/x-pack/plugins/threat_intelligence/public/modules/indicators/hooks/use_indicators.ts
+++ b/x-pack/plugins/threat_intelligence/public/modules/indicators/hooks/use_indicators.ts
@@ -47,6 +47,8 @@ export interface UseIndicatorsValue {
* Data loading is in progress (see docs on `isFetching` here: https://tanstack.com/query/v4/docs/guides/queries)
*/
isFetching: boolean;
+
+ dataUpdatedAt: number;
}
export const useIndicators = ({
@@ -95,7 +97,7 @@ export const useIndicators = ({
[inspectorAdapters, searchService]
);
- const { isLoading, isFetching, data, refetch } = useQuery(
+ const { isLoading, isFetching, data, refetch, dataUpdatedAt } = useQuery(
[
'indicatorsTable',
{
@@ -132,5 +134,6 @@ export const useIndicators = ({
isLoading,
isFetching,
handleRefresh,
+ dataUpdatedAt,
};
};
diff --git a/x-pack/plugins/threat_intelligence/public/modules/indicators/indicators_page.test.tsx b/x-pack/plugins/threat_intelligence/public/modules/indicators/indicators_page.test.tsx
index e46c605d1a90a..7f4db9fa75262 100644
--- a/x-pack/plugins/threat_intelligence/public/modules/indicators/indicators_page.test.tsx
+++ b/x-pack/plugins/threat_intelligence/public/modules/indicators/indicators_page.test.tsx
@@ -42,6 +42,7 @@ describe('', () => {
onChangeItemsPerPage: stub,
onChangePage: stub,
handleRefresh: stub,
+ dataUpdatedAt: Date.now(),
});
(useFilters as jest.MockedFunction).mockReturnValue({
diff --git a/x-pack/plugins/threat_intelligence/public/modules/indicators/indicators_page.tsx b/x-pack/plugins/threat_intelligence/public/modules/indicators/indicators_page.tsx
index 511faaa73a7a0..fcf690631d740 100644
--- a/x-pack/plugins/threat_intelligence/public/modules/indicators/indicators_page.tsx
+++ b/x-pack/plugins/threat_intelligence/public/modules/indicators/indicators_page.tsx
@@ -20,6 +20,7 @@ import { useColumnSettings } from './components/indicators_table/hooks/use_colum
import { useAggregatedIndicators } from './hooks/use_aggregated_indicators';
import { IndicatorsFilters } from './containers/indicators_filters';
import { useSecurityContext } from '../../hooks/use_security_context';
+import { UpdateStatus } from '../../components/update_status';
const queryClient = new QueryClient();
@@ -48,6 +49,7 @@ const IndicatorsPageContent: VFC = () => {
pagination,
isLoading: isLoadingIndicators,
isFetching: isFetchingIndicators,
+ dataUpdatedAt,
} = useIndicators({
filters,
filterQuery,
@@ -72,10 +74,14 @@ const IndicatorsPageContent: VFC = () => {
return (
-
+ }
+ >
+
{
+ describe('disable', () => {
const objectRemover = new ObjectRemover(supertest);
after(() => objectRemover.removeAll());
@@ -110,21 +109,23 @@ export default function createDisableAlertTests({ getService }: FtrProviderConte
expect(response.body).to.eql('');
// task should still exist but be disabled
- const taskRecord2 = await getScheduledTask(createdAlert.scheduled_task_id);
- expect(taskRecord2.type).to.eql('task');
- expect(taskRecord2.task.taskType).to.eql('alerting:test.noop');
- expect(JSON.parse(taskRecord2.task.params)).to.eql({
- alertId: createdAlert.id,
- spaceId: space.id,
- consumer: 'alertsFixture',
- });
- expect(taskRecord2.task.enabled).to.eql(false);
- // Ensure AAD isn't broken
- await checkAAD({
- supertest,
- spaceId: space.id,
- type: 'alert',
- id: createdAlert.id,
+ await retry.try(async () => {
+ const taskRecord2 = await getScheduledTask(createdAlert.scheduled_task_id);
+ expect(taskRecord2.type).to.eql('task');
+ expect(taskRecord2.task.taskType).to.eql('alerting:test.noop');
+ expect(JSON.parse(taskRecord2.task.params)).to.eql({
+ alertId: createdAlert.id,
+ spaceId: space.id,
+ consumer: 'alertsFixture',
+ });
+ expect(taskRecord2.task.enabled).to.eql(false);
+ // Ensure AAD isn't broken
+ await checkAAD({
+ supertest,
+ spaceId: space.id,
+ type: 'alert',
+ id: createdAlert.id,
+ });
});
break;
default:
@@ -295,15 +296,17 @@ export default function createDisableAlertTests({ getService }: FtrProviderConte
expect(response.statusCode).to.eql(204);
expect(response.body).to.eql('');
// task should still exist but be disabled
- const taskRecord = await getScheduledTask(createdAlert.scheduled_task_id);
- expect(taskRecord.type).to.eql('task');
- expect(taskRecord.task.taskType).to.eql('alerting:test.noop');
- expect(JSON.parse(taskRecord.task.params)).to.eql({
- alertId: createdAlert.id,
- spaceId: space.id,
- consumer: 'alerts',
+ await retry.try(async () => {
+ const taskRecord = await getScheduledTask(createdAlert.scheduled_task_id);
+ expect(taskRecord.type).to.eql('task');
+ expect(taskRecord.task.taskType).to.eql('alerting:test.noop');
+ expect(JSON.parse(taskRecord.task.params)).to.eql({
+ alertId: createdAlert.id,
+ spaceId: space.id,
+ consumer: 'alerts',
+ });
+ expect(taskRecord.task.enabled).to.eql(false);
});
- expect(taskRecord.task.enabled).to.eql(false);
break;
default:
throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`);
@@ -366,15 +369,17 @@ export default function createDisableAlertTests({ getService }: FtrProviderConte
expect(response.statusCode).to.eql(204);
expect(response.body).to.eql('');
// task should still exist but be disabled
- const taskRecord2 = await getScheduledTask(createdAlert.scheduled_task_id);
- expect(taskRecord2.type).to.eql('task');
- expect(taskRecord2.task.taskType).to.eql('alerting:test.noop');
- expect(JSON.parse(taskRecord2.task.params)).to.eql({
- alertId: createdAlert.id,
- spaceId: space.id,
- consumer: 'alertsFixture',
+ await retry.try(async () => {
+ const taskRecord2 = await getScheduledTask(createdAlert.scheduled_task_id);
+ expect(taskRecord2.type).to.eql('task');
+ expect(taskRecord2.task.taskType).to.eql('alerting:test.noop');
+ expect(JSON.parse(taskRecord2.task.params)).to.eql({
+ alertId: createdAlert.id,
+ spaceId: space.id,
+ consumer: 'alertsFixture',
+ });
+ expect(taskRecord2.task.enabled).to.eql(false);
});
- expect(taskRecord2.task.enabled).to.eql(false);
break;
default:
throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`);
diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/telemetry/alerting_and_actions_telemetry.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/telemetry/alerting_and_actions_telemetry.ts
index c89e5b48b236b..b4cb36ab59d85 100644
--- a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/telemetry/alerting_and_actions_telemetry.ts
+++ b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/telemetry/alerting_and_actions_telemetry.ts
@@ -235,8 +235,9 @@ export default function createAlertingAndActionsTelemetryTests({ getService }: F
// number of action executions broken down by connector type
expect(telemetry.count_actions_executions_by_type_per_day['test.throw'] > 0).to.be(true);
- // average execution time - just checking for non-zero as we can't set an exact number
- expect(telemetry.avg_execution_time_per_day > 0).to.be(true);
+ // average execution time - just checking for a positive number as we can't set an exact number
+ // if the time is less than 1ms it will round down to 0
+ expect(telemetry.avg_execution_time_per_day >= 0).to.be(true);
// average execution time broken down by rule type
expect(telemetry.avg_execution_time_by_type_per_day['test.throw'] > 0).to.be(true);
diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/run_soon.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/run_soon.ts
index 050c220ab1b0f..f32665a5a1fac 100644
--- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/run_soon.ts
+++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/run_soon.ts
@@ -18,7 +18,8 @@ export default function createRunSoonTests({ getService }: FtrProviderContext) {
const es = getService('es');
const esArchiver = getService('esArchiver');
- describe('runSoon', () => {
+ // Failing: See https://github.com/elastic/kibana/issues/142564
+ describe.skip('runSoon', () => {
const objectRemover = new ObjectRemover(supertest);
before(async () => {
diff --git a/x-pack/test/api_integration/apis/aiops/explain_log_rate_spikes.ts b/x-pack/test/api_integration/apis/aiops/explain_log_rate_spikes.ts
index b4bdf9f50beb5..a2e1f158a73e2 100644
--- a/x-pack/test/api_integration/apis/aiops/explain_log_rate_spikes.ts
+++ b/x-pack/test/api_integration/apis/aiops/explain_log_rate_spikes.ts
@@ -222,9 +222,7 @@ export default ({ getService }: FtrProviderContext) => {
const errorActions = data.filter((d) => d.type === expected.errorFilter);
expect(errorActions.length).to.be(1);
- expect(errorActions[0].payload).to.be(
- 'ResponseError: index_not_found_exception: [index_not_found_exception] Reason: no such index [does_not_exist]'
- );
+ expect(errorActions[0].payload).to.be('Failed to fetch field candidates.');
});
});
};
diff --git a/x-pack/test/api_integration/apis/uptime/feature_controls.ts b/x-pack/test/api_integration/apis/uptime/feature_controls.ts
index 8185cb0f03a20..39d7406636353 100644
--- a/x-pack/test/api_integration/apis/uptime/feature_controls.ts
+++ b/x-pack/test/api_integration/apis/uptime/feature_controls.ts
@@ -145,8 +145,7 @@ export default function featureControlsTests({ getService }: FtrProviderContext)
}
});
- // FLAKY: https://github.com/elastic/kibana/issues/136542
- describe.skip('spaces', () => {
+ describe('spaces', () => {
// the following tests create a user_1 which has uptime read access to space_1 and dashboard all access to space_2
const space1Id = 'space_1';
const space2Id = 'space_2';
diff --git a/x-pack/test/api_integration/apis/uptime/rest/add_monitor_project.ts b/x-pack/test/api_integration/apis/uptime/rest/add_monitor_project.ts
index 9dce7e7d8fdaa..a8eec4c568dc9 100644
--- a/x-pack/test/api_integration/apis/uptime/rest/add_monitor_project.ts
+++ b/x-pack/test/api_integration/apis/uptime/rest/add_monitor_project.ts
@@ -19,7 +19,8 @@ import { PrivateLocationTestService } from './services/private_location_test_ser
import { comparePolicies, getTestProjectSyntheticsPolicy } from './sample_data/test_policy';
export default function ({ getService }: FtrProviderContext) {
- describe('AddProjectMonitors', function () {
+ // FLAKY: https://github.com/elastic/kibana/issues/142110
+ describe.skip('AddProjectMonitors', function () {
this.tags('skipCloud');
const supertest = getService('supertest');
diff --git a/x-pack/test/spaces_api_integration/common/lib/test_data_loader.ts b/x-pack/test/common/lib/test_data_loader.ts
similarity index 79%
rename from x-pack/test/spaces_api_integration/common/lib/test_data_loader.ts
rename to x-pack/test/common/lib/test_data_loader.ts
index 4b25c722603c8..61c8ff4c1bf52 100644
--- a/x-pack/test/spaces_api_integration/common/lib/test_data_loader.ts
+++ b/x-pack/test/common/lib/test_data_loader.ts
@@ -5,16 +5,14 @@
* 2.0.
*/
-import { FtrProviderContext } from '../ftr_provider_context';
-
-const SPACE_1 = {
+export const SPACE_1 = {
id: 'space_1',
name: 'Space 1',
description: 'This is the first test space',
disabledFeatures: [],
};
-const SPACE_2 = {
+export const SPACE_2 = {
id: 'space_2',
name: 'Space 2',
description: 'This is the second test space',
@@ -64,36 +62,38 @@ const OBJECTS_TO_SHARE: Array<{
},
];
-export function getTestDataLoader({ getService }: FtrProviderContext) {
+// @ts-ignore
+export function getTestDataLoader({ getService }) {
const spacesService = getService('spaces');
const kbnServer = getService('kibanaServer');
const supertest = getService('supertest');
const log = getService('log');
return {
- before: async () => {
+ createFtrSpaces: async () => {
await Promise.all([await spacesService.create(SPACE_1), await spacesService.create(SPACE_2)]);
},
- after: async () => {
+ deleteFtrSpaces: async () => {
await Promise.all([spacesService.delete(SPACE_1.id), spacesService.delete(SPACE_2.id)]);
},
- beforeEach: async () => {
+ createFtrSavedObjectsData: async (
+ spaceData: Array<{ spaceName: string | null; dataUrl: string }>
+ ) => {
log.debug('Loading test data for the following spaces: default, space_1 and space_2');
- await Promise.all([
- kbnServer.importExport.load(
- 'x-pack/test/spaces_api_integration/common/fixtures/kbn_archiver/default_space.json'
- ),
- kbnServer.importExport.load(
- 'x-pack/test/spaces_api_integration/common/fixtures/kbn_archiver/space_1.json',
- { space: SPACE_1.id }
- ),
- kbnServer.importExport.load(
- 'x-pack/test/spaces_api_integration/common/fixtures/kbn_archiver/space_2.json',
- { space: SPACE_2.id }
- ),
- ]);
+
+ await Promise.all(
+ spaceData.map((spaceDataObj) => {
+ if (spaceDataObj.spaceName) {
+ return kbnServer.importExport.load(spaceDataObj.dataUrl, {
+ space: spaceDataObj.spaceName,
+ });
+ } else {
+ return kbnServer.importExport.load(spaceDataObj.dataUrl);
+ }
+ })
+ );
// Adjust spaces for the imported saved objects.
for (const { objects, spacesToAdd = [], spacesToRemove = [] } of OBJECTS_TO_SHARE) {
@@ -111,9 +111,9 @@ export function getTestDataLoader({ getService }: FtrProviderContext) {
}
},
- afterEach: async () => {
+ deleteFtrSavedObjectsData: async () => {
const allSpacesIds = [
- ...(await spacesService.getAll()).map((space) => space.id),
+ ...(await spacesService.getAll()).map((space: { id: string }) => space.id),
'non_existent_space',
];
log.debug(`Removing data from the following spaces: ${allSpacesIds.join(', ')}`);
diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/details.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/details.ts
index d32c5bd58a94c..45147118b93aa 100644
--- a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/details.ts
+++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/details.ts
@@ -238,7 +238,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
});
it('should snooze the rule', async () => {
- const snoozeBadge = await testSubjects.find('rulesListNotifyBadge-unsnoozed');
+ let snoozeBadge = await testSubjects.find('rulesListNotifyBadge-unsnoozed');
await snoozeBadge.click();
const snoozeIndefinite = await testSubjects.find('ruleSnoozeIndefiniteApply');
@@ -247,18 +247,64 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
await retry.try(async () => {
await testSubjects.existOrFail('rulesListNotifyBadge-snoozedIndefinitely');
});
+
+ // Unsnooze the rule for the next test
+ snoozeBadge = await testSubjects.find('rulesListNotifyBadge-snoozedIndefinitely');
+ await snoozeBadge.click();
+
+ const snoozeCancel = await testSubjects.find('ruleSnoozeCancel');
+ await snoozeCancel.click();
+ await pageObjects.header.waitUntilLoadingHasFinished();
});
- it('should unsnooze the rule', async () => {
- const snoozeBadge = await testSubjects.find('rulesListNotifyBadge-snoozedIndefinitely');
+ it('should snooze the rule for a set duration', async () => {
+ let snoozeBadge = await testSubjects.find('rulesListNotifyBadge-unsnoozed');
+ await snoozeBadge.click();
+
+ const snooze8h = await testSubjects.find('linkSnooze8h');
+ await snooze8h.click();
+
+ await pageObjects.header.waitUntilLoadingHasFinished();
+
+ await retry.try(async () => {
+ await testSubjects.existOrFail('rulesListNotifyBadge-snoozed');
+ });
+
+ // Unsnooze the rule for the next test
+ snoozeBadge = await testSubjects.find('rulesListNotifyBadge-snoozed');
await snoozeBadge.click();
const snoozeCancel = await testSubjects.find('ruleSnoozeCancel');
await snoozeCancel.click();
+ await pageObjects.header.waitUntilLoadingHasFinished();
+ });
+
+ it('should add snooze schedule', async () => {
+ let snoozeBadge = await testSubjects.find('rulesListNotifyBadge-unsnoozed');
+ await snoozeBadge.click();
+
+ const addScheduleButton = await testSubjects.find('ruleAddSchedule');
+ await addScheduleButton.click();
+
+ const saveScheduleButton = await testSubjects.find('scheduler-saveSchedule');
+ await saveScheduleButton.click();
+
+ await pageObjects.header.waitUntilLoadingHasFinished();
await retry.try(async () => {
- await testSubjects.existOrFail('rulesListNotifyBadge-unsnoozed');
+ await testSubjects.existOrFail('rulesListNotifyBadge-scheduled');
});
+
+ // Unsnooze the rule for the next test
+ snoozeBadge = await testSubjects.find('rulesListNotifyBadge-scheduled');
+ await snoozeBadge.click();
+
+ const snoozeCancel = await testSubjects.find('ruleRemoveAllSchedules');
+ await snoozeCancel.click();
+
+ const confirmButton = await testSubjects.find('confirmModalConfirmButton');
+ await confirmButton.click();
+ await pageObjects.header.waitUntilLoadingHasFinished();
});
});
diff --git a/x-pack/test/saved_object_api_integration/common/fixtures/kbn_archiver/default_space.json b/x-pack/test/saved_object_api_integration/common/fixtures/kbn_archiver/default_space.json
new file mode 100644
index 0000000000000..9a2713fc61872
--- /dev/null
+++ b/x-pack/test/saved_object_api_integration/common/fixtures/kbn_archiver/default_space.json
@@ -0,0 +1,163 @@
+{
+ "attributes": {
+ "title": "logstash-*"
+ },
+ "coreMigrationVersion": "8.4.0",
+ "id": "defaultspace-index-pattern-id",
+ "migrationVersion": {
+ "index-pattern": "8.0.0"
+ },
+ "originId": "cts_ip_1",
+ "references": [],
+ "type": "index-pattern",
+ "updated_at": "2017-09-21T18:49:16.270Z",
+ "version": "WzUyOCwxXQ=="
+}
+
+{
+ "attributes": {
+ "title": "Count of requests",
+ "uiStateJSON": "{\"spy\":{\"mode\":{\"name\":null,\"fill\":false}}}",
+ "version": 1,
+ "visState": "{\"title\":\"Count of requests\",\"type\":\"area\",\"params\":{\"type\":\"area\",\"grid\":{\"categoryLines\":false,\"style\":{\"color\":\"#eee\"}},\"categoryAxes\":[{\"id\":\"CategoryAxis-1\",\"type\":\"category\",\"position\":\"bottom\",\"show\":true,\"style\":{},\"scale\":{\"type\":\"linear\"},\"labels\":{\"show\":true,\"truncate\":100},\"title\":{}}],\"valueAxes\":[{\"id\":\"ValueAxis-1\",\"name\":\"LeftAxis-1\",\"type\":\"value\",\"position\":\"left\",\"show\":true,\"style\":{},\"scale\":{\"type\":\"linear\",\"mode\":\"normal\"},\"labels\":{\"show\":true,\"rotate\":0,\"filter\":false,\"truncate\":100},\"title\":{\"text\":\"Count\"}}],\"seriesParams\":[{\"show\":\"true\",\"type\":\"area\",\"mode\":\"stacked\",\"data\":{\"label\":\"Count\",\"id\":\"1\"},\"drawLinesBetweenPoints\":true,\"showCircles\":true,\"interpolate\":\"linear\",\"valueAxis\":\"ValueAxis-1\"}],\"addTooltip\":true,\"addLegend\":true,\"legendPosition\":\"right\",\"times\":[],\"addTimeMarker\":false},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"enabled\":true,\"type\":\"date_histogram\",\"schema\":\"segment\",\"params\":{\"field\":\"@timestamp\",\"interval\":\"auto\",\"customInterval\":\"2h\",\"min_doc_count\":1,\"extended_bounds\":{}}}]}",
+ "description": "",
+ "kibanaSavedObjectMeta": {
+ "searchSourceJSON": "{\"index\":\"defaultspace-index-pattern-id\",\"filter\":[],\"query\":{\"query\":\"\",\"language\":\"lucene\"}}"
+ }
+ },
+ "id": "defaultspace-isolatedtype-id",
+ "references": [],
+ "type": "isolatedtype",
+ "updated_at": "2017-09-21T18:51:23.794Z",
+ "version": "WzQ4NywxXQ=="
+}
+
+{
+ "attributes": {
+ "title": "Requests",
+ "kibanaSavedObjectMeta": {
+ "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"filter\":[],\"highlightAll\":true,\"version\":true}"
+ }
+ },
+ "coreMigrationVersion": "8.4.0",
+ "id": "defaultspace-dashboard-id",
+ "migrationVersion": {
+ "dashboard": "8.4.0"
+ },
+ "type": "dashboard",
+ "updated_at": "2017-09-21T18:49:16.270Z",
+ "version": "WzUyMCwxXQ=="
+}
+
+{
+ "attributes": {
+ "title": "A share-capable (isolated) saved-object only in the default space"
+ },
+ "id": "only_default_space",
+ "type": "sharecapabletype",
+ "updated_at": "2017-09-21T18:59:16.270Z",
+ "version": "WzQ4OCwxXQ=="
+}
+
+{
+ "attributes": {
+ "title": "A shared saved-object in all spaces"
+ },
+ "id": "all_spaces",
+ "type": "sharedtype",
+ "references": [],
+ "updated_at": "2017-09-21T18:59:16.270Z",
+ "version": "WzQ5NywxXQ=="
+}
+
+{
+ "attributes": {
+ "title": "My favorite global object"
+ },
+ "id": "globaltype-id",
+ "references": [],
+ "type": "globaltype",
+ "updated_at": "2017-09-21T18:49:16.270Z",
+ "version": "WzQ4NywxXQ=="
+}
+
+{
+ "attributes": {
+ "title": "A shared saved-object in the default and space_1 spaces"
+ },
+ "id": "default_and_space_1",
+ "type": "sharedtype",
+ "updated_at": "2017-09-21T18:59:16.270Z",
+ "version": "WzQ4OCwxXQ=="
+}
+
+{
+ "attributes": {
+ "title": "A sharedtype saved-object with id: conflict_1"
+ },
+ "id": "conflict_1",
+ "type": "sharedtype",
+ "updated_at": "2017-09-21T18:59:16.270Z",
+ "version": "WzQ4OCwxXQ=="
+}
+
+{
+ "attributes": {
+ "title": "A sharedtype saved-object with id: conflict_2a"
+ },
+ "id": "conflict_2a",
+ "type": "sharedtype",
+ "updated_at": "2017-09-21T18:59:16.270Z",
+ "version": "WzQ4OCwxXQ=="
+}
+
+{
+ "attributes": {
+ "title": "A sharedtype saved-object with id: conflict_2b"
+ },
+ "id": "conflict_2b",
+ "type": "sharedtype",
+ "updated_at": "2017-09-21T18:59:16.270Z",
+ "version": "WzQ4OCwxXQ=="
+}
+
+{
+ "attributes": {
+ "title": "A sharedtype saved-object with id: conflict_3"
+ },
+ "id": "conflict_3",
+ "type": "sharedtype",
+ "updated_at": "2017-09-21T18:59:16.270Z",
+ "version": "WzQ4OCwxXQ=="
+}
+
+{
+ "attributes": {
+ "title": "A sharedtype saved-object with id: conflict_4a"
+ },
+ "id": "conflict_4a",
+ "type": "sharedtype",
+ "updated_at": "2017-09-21T18:59:16.270Z",
+ "version": "WzQ4OCwxXQ=="
+}
+
+{
+ "attributes": {
+ "title": "Resolve outcome exactMatch"
+ },
+ "id": "exact-match",
+ "type": "resolvetype",
+ "updated_at": "2017-09-21T18:59:16.270Z",
+ "version": "WzQ4OCwxXQ=="
+}
+
+{
+ "attributes": {
+ "title": "Resolve outcome aliasMatch"
+ },
+ "id": "alias-match-newid",
+ "type": "resolvetype",
+ "updated_at": "2017-09-21T18:59:16.270Z",
+ "version": "WzQ4OCwxXQ=="
+}
+
diff --git a/x-pack/test/saved_object_api_integration/common/fixtures/kbn_archiver/space_1.json b/x-pack/test/saved_object_api_integration/common/fixtures/kbn_archiver/space_1.json
new file mode 100644
index 0000000000000..6356d5c01989b
--- /dev/null
+++ b/x-pack/test/saved_object_api_integration/common/fixtures/kbn_archiver/space_1.json
@@ -0,0 +1,72 @@
+
+
+{
+ "attributes": {
+ "title": "logstash-*"
+ },
+ "coreMigrationVersion": "8.4.0",
+ "id": "space1-index-pattern-id",
+ "migrationVersion": {
+ "index-pattern": "8.0.0"
+ },
+ "references": [],
+ "type": "index-pattern",
+ "updated_at": "2017-09-21T18:49:16.270Z",
+ "version": "WzUyOSwxXQ=="
+}
+
+{
+ "attributes": {
+ "description": "",
+ "kibanaSavedObjectMeta": {
+ "searchSourceJSON": "{\"index\":\"space1-index-pattern-id\",\"filter\":[],\"query\":{\"query\":\"\",\"language\":\"lucene\"}}"
+ },
+ "title": "Count of requests",
+ "uiStateJSON": "{\"spy\":{\"mode\":{\"name\":null,\"fill\":false}}}",
+ "version": 1,
+ "visState": "{\"title\":\"Count of requests\",\"type\":\"area\",\"params\":{\"type\":\"area\",\"grid\":{\"categoryLines\":false,\"style\":{\"color\":\"#eee\"}},\"categoryAxes\":[{\"id\":\"CategoryAxis-1\",\"type\":\"category\",\"position\":\"bottom\",\"show\":true,\"style\":{},\"scale\":{\"type\":\"linear\"},\"labels\":{\"show\":true,\"truncate\":100},\"title\":{}}],\"valueAxes\":[{\"id\":\"ValueAxis-1\",\"name\":\"LeftAxis-1\",\"type\":\"value\",\"position\":\"left\",\"show\":true,\"style\":{},\"scale\":{\"type\":\"linear\",\"mode\":\"normal\"},\"labels\":{\"show\":true,\"rotate\":0,\"filter\":false,\"truncate\":100},\"title\":{\"text\":\"Count\"}}],\"seriesParams\":[{\"show\":\"true\",\"type\":\"area\",\"mode\":\"stacked\",\"data\":{\"label\":\"Count\",\"id\":\"1\"},\"drawLinesBetweenPoints\":true,\"showCircles\":true,\"interpolate\":\"linear\",\"valueAxis\":\"ValueAxis-1\"}],\"addTooltip\":true,\"addLegend\":true,\"legendPosition\":\"right\",\"times\":[],\"addTimeMarker\":false},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"enabled\":true,\"type\":\"date_histogram\",\"schema\":\"segment\",\"params\":{\"field\":\"@timestamp\",\"interval\":\"auto\",\"customInterval\":\"2h\",\"min_doc_count\":1,\"extended_bounds\":{}}}]}"
+ },
+ "id": "space1-isolatedtype-id",
+ "references": [],
+ "type": "isolatedtype",
+ "updated_at": "2017-09-21T18:49:16.270Z",
+ "version": "WzQ4NywxXQ=="
+}
+
+{
+ "attributes": {
+ "title": "Requests",
+ "kibanaSavedObjectMeta": {
+ "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"filter\":[],\"highlightAll\":true,\"version\":true}"
+ },
+ "version": 1
+ },
+ "coreMigrationVersion": "8.4.0",
+ "id": "space1-dashboard-id",
+ "migrationVersion": {
+ "dashboard": "8.4.0"
+ },
+ "type": "dashboard",
+ "updated_at": "2017-09-21T18:49:16.270Z",
+ "version": "WzUyMCwxXQ=="
+}
+
+{
+ "attributes": {
+ "title": "A shared saved-object only in space_1"
+ },
+ "id": "only_space_1",
+ "type": "sharedtype",
+ "updated_at": "2017-09-21T18:59:16.270Z",
+ "version": "WzQ4OCwxXQ=="
+}
+
+{
+ "attributes": {
+ "title": "A share-capable (isolated) saved-object only in space_1"
+ },
+ "id": "only_space_1",
+ "type": "sharecapabletype",
+ "updated_at": "2017-09-21T18:59:16.270Z",
+ "version": "WzQ4OCwxXQ=="
+}
diff --git a/x-pack/test/saved_object_api_integration/common/fixtures/kbn_archiver/space_2.json b/x-pack/test/saved_object_api_integration/common/fixtures/kbn_archiver/space_2.json
new file mode 100644
index 0000000000000..9715a5f54d2b4
--- /dev/null
+++ b/x-pack/test/saved_object_api_integration/common/fixtures/kbn_archiver/space_2.json
@@ -0,0 +1,58 @@
+{
+ "attributes": {
+ "title": "logstash-*"
+ },
+ "coreMigrationVersion": "8.4.0",
+ "id": "space2-index-pattern-id",
+ "migrationVersion": {
+ "index-pattern": "8.0.0"
+ },
+ "references": [],
+ "type": "index-pattern",
+ "updated_at": "2017-09-21T18:49:16.270Z",
+ "version": "WzUyOSwxXQ=="
+}
+
+{
+ "attributes": {
+ "description": "",
+ "kibanaSavedObjectMeta": {
+ "searchSourceJSON": "{\"index\":\"space2-index-pattern-id\",\"filter\":[],\"query\":{\"query\":\"\",\"language\":\"lucene\"}}"
+ },
+ "title": "Count of requests",
+ "version": 1
+ },
+ "id": "space2-isolatedtype-id",
+ "references": [],
+ "type": "isolatedtype",
+ "updated_at": "2017-09-21T18:49:16.270Z",
+ "version": "WzQ4NywxXQ=="
+}
+
+{
+ "attributes": {
+ "title": "Requests",
+ "kibanaSavedObjectMeta": {
+ "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"filter\":[],\"highlightAll\":true,\"version\":true}"
+ },
+ "version": 1
+ },
+ "coreMigrationVersion": "8.4.0",
+ "id": "space2-dashboard-id",
+ "migrationVersion": {
+ "dashboard": "8.4.0"
+ },
+ "type": "dashboard",
+ "updated_at": "2017-09-21T18:49:16.270Z",
+ "version": "WzUyMCwxXQ=="
+}
+
+{
+ "attributes": {
+ "title": "A shared saved-object only in space_2"
+ },
+ "id": "only_space_2",
+ "type": "sharedtype",
+ "updated_at": "2017-09-21T18:59:16.270Z",
+ "version": "WzQ4OCwxXQ=="
+}
diff --git a/x-pack/test/saved_object_api_integration/common/suites/bulk_get.ts b/x-pack/test/saved_object_api_integration/common/suites/bulk_get.ts
index 10709a6f20916..c9cb3b9739eee 100644
--- a/x-pack/test/saved_object_api_integration/common/suites/bulk_get.ts
+++ b/x-pack/test/saved_object_api_integration/common/suites/bulk_get.ts
@@ -6,11 +6,12 @@
*/
import expect from '@kbn/expect';
-import { SuperTest } from 'supertest';
+import { getTestDataLoader, SPACE_1, SPACE_2 } from '../../../common/lib/test_data_loader';
import { SAVED_OBJECT_TEST_CASES as CASES } from '../lib/saved_object_test_cases';
import { SPACES } from '../lib/spaces';
import { expectResponses, getUrlPrefix, getTestTitle } from '../lib/saved_object_test_utils';
import { ExpectResponseBody, TestCase, TestDefinition, TestSuite } from '../lib/types';
+import type { FtrProviderContext } from '../ftr_provider_context';
export interface BulkGetTestDefinition extends TestDefinition {
request: Array<{ type: string; id: string }>;
@@ -33,7 +34,10 @@ const createRequest = ({ type, id, namespaces }: BulkGetTestCase) => ({
...(namespaces && { namespaces }), // individual "object namespaces" string array
});
-export function bulkGetTestSuiteFactory(esArchiver: any, supertest: SuperTest) {
+export function bulkGetTestSuiteFactory(context: FtrProviderContext) {
+ const testDataLoader = getTestDataLoader(context);
+ const supertest = context.getService('supertestWithoutAuth');
+
const expectSavedObjectForbidden = expectResponses.forbiddenTypes('bulk_get');
const expectResponseBody =
(testCases: BulkGetTestCase | BulkGetTestCase[], statusCode: 200 | 403): ExpectResponseBody =>
@@ -91,16 +95,31 @@ export function bulkGetTestSuiteFactory(esArchiver: any, supertest: SuperTest {
- before(() =>
- esArchiver.load(
- 'x-pack/test/saved_object_api_integration/common/fixtures/es_archiver/saved_objects/spaces'
- )
- );
- after(() =>
- esArchiver.unload(
- 'x-pack/test/saved_object_api_integration/common/fixtures/es_archiver/saved_objects/spaces'
- )
- );
+ before(async () => {
+ await testDataLoader.createFtrSpaces();
+ await testDataLoader.createFtrSavedObjectsData([
+ {
+ spaceName: null,
+ dataUrl:
+ 'x-pack/test/saved_object_api_integration/common/fixtures/kbn_archiver/default_space.json',
+ },
+ {
+ spaceName: SPACE_1.id,
+ dataUrl:
+ 'x-pack/test/saved_object_api_integration/common/fixtures/kbn_archiver/space_1.json',
+ },
+ {
+ spaceName: SPACE_2.id,
+ dataUrl:
+ 'x-pack/test/saved_object_api_integration/common/fixtures/kbn_archiver/space_2.json',
+ },
+ ]);
+ });
+
+ after(async () => {
+ await testDataLoader.deleteFtrSpaces();
+ await testDataLoader.deleteFtrSavedObjectsData();
+ });
for (const test of tests) {
it(`should return ${test.responseStatusCode} ${test.title}`, async () => {
diff --git a/x-pack/test/saved_object_api_integration/security_and_spaces/apis/bulk_get.ts b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/bulk_get.ts
index 2c1fbf442b0ec..ed251440d361a 100644
--- a/x-pack/test/saved_object_api_integration/security_and_spaces/apis/bulk_get.ts
+++ b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/bulk_get.ts
@@ -67,14 +67,9 @@ const createTestCases = (spaceId: string) => {
return { normalTypes, crossNamespace, hiddenType, allTypes };
};
-export default function ({ getService }: FtrProviderContext) {
- const supertest = getService('supertestWithoutAuth');
- const esArchiver = getService('esArchiver');
-
- const { addTests, createTestDefinitions, expectSavedObjectForbidden } = bulkGetTestSuiteFactory(
- esArchiver,
- supertest
- );
+export default function (context: FtrProviderContext) {
+ const { addTests, createTestDefinitions, expectSavedObjectForbidden } =
+ bulkGetTestSuiteFactory(context);
const createTests = (spaceId: string) => {
const { normalTypes, crossNamespace, hiddenType, allTypes } = createTestCases(spaceId);
// use singleRequest to reduce execution time and/or test combined cases
diff --git a/x-pack/test/saved_object_api_integration/spaces_only/apis/bulk_get.ts b/x-pack/test/saved_object_api_integration/spaces_only/apis/bulk_get.ts
index 41fa4749cc48e..30ed220ea9ae3 100644
--- a/x-pack/test/saved_object_api_integration/spaces_only/apis/bulk_get.ts
+++ b/x-pack/test/saved_object_api_integration/spaces_only/apis/bulk_get.ts
@@ -55,11 +55,8 @@ const createTestCases = (spaceId: string) => [
{ ...CASES.MULTI_NAMESPACE_ONLY_SPACE_1, namespaces: [ALL_SPACES_ID] },
];
-export default function ({ getService }: FtrProviderContext) {
- const supertest = getService('supertest');
- const esArchiver = getService('esArchiver');
-
- const { addTests, createTestDefinitions } = bulkGetTestSuiteFactory(esArchiver, supertest);
+export default function (context: FtrProviderContext) {
+ const { addTests, createTestDefinitions } = bulkGetTestSuiteFactory(context);
const createTests = (spaceId: string) => {
const testCases = createTestCases(spaceId);
return createTestDefinitions(testCases, false, { singleRequest: true });
diff --git a/x-pack/test/spaces_api_integration/common/suites/copy_to_space.ts b/x-pack/test/spaces_api_integration/common/suites/copy_to_space.ts
index c781eff6d3272..4c5ae878bbf6e 100644
--- a/x-pack/test/spaces_api_integration/common/suites/copy_to_space.ts
+++ b/x-pack/test/spaces_api_integration/common/suites/copy_to_space.ts
@@ -14,8 +14,8 @@ import {
} from '@kbn/core/server';
import { getAggregatedSpaceData, getUrlPrefix } from '../lib/space_test_utils';
import { DescribeFn, TestDefinitionAuthentication } from '../lib/types';
-import { getTestDataLoader } from '../lib/test_data_loader';
-import { FtrProviderContext } from '../ftr_provider_context';
+import { getTestDataLoader, SPACE_1, SPACE_2 } from '../../../common/lib/test_data_loader';
+import type { FtrProviderContext } from '../ftr_provider_context';
type TestResponse = Record;
@@ -74,6 +74,21 @@ const UUID_PATTERN = new RegExp(
/^[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$/i
);
+const SPACE_DATA_TO_LOAD: Array<{ spaceName: string | null; dataUrl: string }> = [
+ {
+ spaceName: null,
+ dataUrl: 'x-pack/test/spaces_api_integration/common/fixtures/kbn_archiver/default_space.json',
+ },
+ {
+ spaceName: SPACE_1.id,
+ dataUrl: 'x-pack/test/spaces_api_integration/common/fixtures/kbn_archiver/space_1.json',
+ },
+ {
+ spaceName: SPACE_2.id,
+ dataUrl: 'x-pack/test/spaces_api_integration/common/fixtures/kbn_archiver/space_2.json',
+ },
+];
+
const getDestinationWithoutConflicts = () => 'space_2';
const getDestinationWithConflicts = (originSpaceId?: string) =>
!originSpaceId || originSpaceId === DEFAULT_SPACE_ID ? 'space_1' : DEFAULT_SPACE_ID;
@@ -748,16 +763,19 @@ export function copyToSpaceTestSuiteFactory(context: FtrProviderContext) {
// test data only allows for the following spaces as the copy origin
expect(['default', 'space_1']).to.contain(spaceId);
- await testDataLoader.before();
+ await testDataLoader.createFtrSpaces();
});
after(async () => {
- await testDataLoader.after();
+ await testDataLoader.deleteFtrSpaces();
});
describe('single-namespace types', () => {
- beforeEach(async () => await testDataLoader.beforeEach());
- afterEach(async () => await testDataLoader.afterEach());
+ beforeEach(async () => {
+ await testDataLoader.createFtrSavedObjectsData(SPACE_DATA_TO_LOAD);
+ });
+
+ afterEach(async () => await testDataLoader.deleteFtrSavedObjectsData());
const dashboardObject = { type: 'dashboard', id: `cts_dashboard_${spaceId}` };
@@ -898,8 +916,8 @@ export function copyToSpaceTestSuiteFactory(context: FtrProviderContext) {
const spaces = ['space_2'];
const includeReferences = false;
describe(`multi-namespace types with overwrite=${overwrite} and createNewCopies=${createNewCopies}`, () => {
- before(async () => await testDataLoader.beforeEach());
- after(async () => await testDataLoader.afterEach());
+ before(async () => await testDataLoader.createFtrSavedObjectsData(SPACE_DATA_TO_LOAD));
+ after(async () => await testDataLoader.deleteFtrSavedObjectsData());
const testCases = tests.multiNamespaceTestCases(overwrite, createNewCopies);
testCases.forEach(({ testTitle, objects, statusCode, response }) => {
diff --git a/x-pack/test/spaces_api_integration/common/suites/resolve_copy_to_space_conflicts.ts b/x-pack/test/spaces_api_integration/common/suites/resolve_copy_to_space_conflicts.ts
index 58a434bd0ca91..5f2c361714c49 100644
--- a/x-pack/test/spaces_api_integration/common/suites/resolve_copy_to_space_conflicts.ts
+++ b/x-pack/test/spaces_api_integration/common/suites/resolve_copy_to_space_conflicts.ts
@@ -11,8 +11,8 @@ import { DEFAULT_SPACE_ID } from '@kbn/spaces-plugin/common/constants';
import { CopyResponse } from '@kbn/spaces-plugin/server/lib/copy_to_spaces';
import { getUrlPrefix } from '../lib/space_test_utils';
import { DescribeFn, TestDefinitionAuthentication } from '../lib/types';
-import { FtrProviderContext } from '../ftr_provider_context';
-import { getTestDataLoader } from '../lib/test_data_loader';
+import type { FtrProviderContext } from '../ftr_provider_context';
+import { getTestDataLoader, SPACE_1, SPACE_2 } from '../../../common/lib/test_data_loader';
type TestResponse = Record;
@@ -44,6 +44,21 @@ interface ResolveCopyToSpaceTestDefinition {
const NON_EXISTENT_SPACE_ID = 'non_existent_space';
+const SPACE_DATA_TO_LOAD: Array<{ spaceName: string | null; dataUrl: string }> = [
+ {
+ spaceName: null,
+ dataUrl: 'x-pack/test/spaces_api_integration/common/fixtures/kbn_archiver/default_space.json',
+ },
+ {
+ spaceName: SPACE_1.id,
+ dataUrl: 'x-pack/test/spaces_api_integration/common/fixtures/kbn_archiver/space_1.json',
+ },
+ {
+ spaceName: SPACE_2.id,
+ dataUrl: 'x-pack/test/spaces_api_integration/common/fixtures/kbn_archiver/space_2.json',
+ },
+];
+
const getDestinationSpace = (originSpaceId?: string) => {
if (!originSpaceId || originSpaceId === DEFAULT_SPACE_ID) {
return 'space_1';
@@ -487,8 +502,10 @@ export function resolveCopyToSpaceConflictsSuite(context: FtrProviderContext) {
});
describe('single-namespace types', () => {
- beforeEach(async () => await testDataLoader.beforeEach());
- afterEach(async () => await testDataLoader.afterEach());
+ beforeEach(
+ async () => await testDataLoader.createFtrSavedObjectsData(SPACE_DATA_TO_LOAD)
+ );
+ afterEach(async () => await testDataLoader.deleteFtrSavedObjectsData());
const dashboardObject = { type: 'dashboard', id: `cts_dashboard_${spaceId}` };
const visualizationObject = { type: 'visualization', id: `cts_vis_3_${spaceId}` };
@@ -630,8 +647,8 @@ export function resolveCopyToSpaceConflictsSuite(context: FtrProviderContext) {
const includeReferences = false;
const createNewCopies = false;
describe(`multi-namespace types with "overwrite" retry`, () => {
- before(async () => await testDataLoader.beforeEach());
- after(async () => await testDataLoader.afterEach());
+ before(async () => await testDataLoader.createFtrSavedObjectsData(SPACE_DATA_TO_LOAD));
+ after(async () => await testDataLoader.deleteFtrSavedObjectsData());
const testCases = tests.multiNamespaceTestCases();
testCases.forEach(({ testTitle, objects, retries, statusCode, response }) => {