(
+ handler: RequestHandler
+): RequestHandler
=> {
+ return async (context, req, res) => {
+ const { isAvailable } = await context.integrationAssistant;
+ if (!isAvailable()) {
+ return res.notFound({
+ body: { message: 'This API route is not available using your current license/tier.' },
+ });
+ }
+ return handler(context, req, res);
+ };
+};
diff --git a/x-pack/plugins/integration_assistant/server/types.ts b/x-pack/plugins/integration_assistant/server/types.ts
index 54713df18b0d8..503c318648bad 100644
--- a/x-pack/plugins/integration_assistant/server/types.ts
+++ b/x-pack/plugins/integration_assistant/server/types.ts
@@ -5,11 +5,21 @@
* 2.0.
*/
-// eslint-disable-next-line @typescript-eslint/no-empty-interface
-export interface IntegrationAssistantPluginSetup {}
+import type { LicensingPluginSetup, LicensingPluginStart } from '@kbn/licensing-plugin/server';
+
+export interface IntegrationAssistantPluginSetup {
+ setIsAvailable: (isAvailable: boolean) => void;
+}
// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface IntegrationAssistantPluginStart {}
+export interface IntegrationAssistantPluginSetupDependencies {
+ licensing: LicensingPluginSetup;
+}
+export interface IntegrationAssistantPluginStartDependencies {
+ licensing: LicensingPluginStart;
+}
+
export interface CategorizationState {
rawSamples: string[];
samples: string[];
diff --git a/x-pack/plugins/integration_assistant/tsconfig.json b/x-pack/plugins/integration_assistant/tsconfig.json
index ec7a7e094997d..47087c83731a0 100644
--- a/x-pack/plugins/integration_assistant/tsconfig.json
+++ b/x-pack/plugins/integration_assistant/tsconfig.json
@@ -33,6 +33,7 @@
"@kbn/stack-connectors-plugin",
"@kbn/core-analytics-browser",
"@kbn/logging-mocks",
+ "@kbn/licensing-plugin",
"@kbn/core-http-request-handler-context-server",
"@kbn/core-http-router-server-mocks",
"@kbn/core-http-server"
diff --git a/x-pack/plugins/security_solution/public/common/components/landing_page/onboarding/step_links/add_integration_buttons.tsx b/x-pack/plugins/security_solution/public/common/components/landing_page/onboarding/step_links/add_integration_buttons.tsx
index 4bbf13d617b8c..2ee5928a4c786 100644
--- a/x-pack/plugins/security_solution/public/common/components/landing_page/onboarding/step_links/add_integration_buttons.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/landing_page/onboarding/step_links/add_integration_buttons.tsx
@@ -156,7 +156,7 @@ AddIntegrationPanel.displayName = 'AddIntegrationPanel';
export const AddIntegrationButtons: React.FC = React.memo(() => {
const { integrationAssistant } = useKibana().services;
- const CreateIntegrationCardButton = integrationAssistant?.CreateIntegrationCardButton;
+ const { CreateIntegrationCardButton } = integrationAssistant?.components ?? {};
return (
diff --git a/x-pack/plugins/security_solution_serverless/common/pli/pli_config.ts b/x-pack/plugins/security_solution_serverless/common/pli/pli_config.ts
index f4365787b9189..9a6eb9ab743ca 100644
--- a/x-pack/plugins/security_solution_serverless/common/pli/pli_config.ts
+++ b/x-pack/plugins/security_solution_serverless/common/pli/pli_config.ts
@@ -27,6 +27,7 @@ export const PLI_PRODUCT_FEATURES: PliProductFeatures = {
ProductFeatureKey.threatIntelligence,
ProductFeatureKey.casesConnectors,
ProductFeatureKey.externalRuleActions,
+ ProductFeatureKey.integrationAssistant,
],
},
endpoint: {
diff --git a/x-pack/plugins/security_solution_serverless/kibana.jsonc b/x-pack/plugins/security_solution_serverless/kibana.jsonc
index 76fb90ec236c6..1829503bfe988 100644
--- a/x-pack/plugins/security_solution_serverless/kibana.jsonc
+++ b/x-pack/plugins/security_solution_serverless/kibana.jsonc
@@ -24,7 +24,8 @@
"discover"
],
"optionalPlugins": [
- "securitySolutionEss"
+ "securitySolutionEss",
+ "integrationAssistant"
],
}
}
diff --git a/x-pack/plugins/security_solution_serverless/public/plugin.ts b/x-pack/plugins/security_solution_serverless/public/plugin.ts
index 679884a0c1406..8ea73d406cb3e 100644
--- a/x-pack/plugins/security_solution_serverless/public/plugin.ts
+++ b/x-pack/plugins/security_solution_serverless/public/plugin.ts
@@ -64,10 +64,9 @@ export class SecuritySolutionServerlessPlugin
): SecuritySolutionServerlessPluginStart {
const { securitySolution } = startDeps;
const { productTypes } = this.config;
-
const services = createServices(core, startDeps, this.experimentalFeatures);
- registerUpsellings(securitySolution.getUpselling(), productTypes, services);
+ registerUpsellings(productTypes, services);
securitySolution.setComponents({
DashboardsLandingCallout: getDashboardsLandingCallout(services),
diff --git a/x-pack/plugins/security_solution_serverless/public/types.ts b/x-pack/plugins/security_solution_serverless/public/types.ts
index 11ce38c77642b..0e47917f8122c 100644
--- a/x-pack/plugins/security_solution_serverless/public/types.ts
+++ b/x-pack/plugins/security_solution_serverless/public/types.ts
@@ -14,6 +14,7 @@ import type { ServerlessPluginSetup, ServerlessPluginStart } from '@kbn/serverle
import type { ManagementSetup, ManagementStart } from '@kbn/management-plugin/public';
import type { CloudStart } from '@kbn/cloud-plugin/public';
import type { DiscoverSetup } from '@kbn/discover-plugin/public';
+import type { IntegrationAssistantPluginStart } from '@kbn/integration-assistant-plugin/public';
import type { ServerlessSecurityConfigSchema } from '../common/config';
// eslint-disable-next-line @typescript-eslint/no-empty-interface
@@ -36,6 +37,7 @@ export interface SecuritySolutionServerlessPluginStartDeps {
serverless: ServerlessPluginStart;
management: ManagementStart;
cloud: CloudStart;
+ integrationAssistant?: IntegrationAssistantPluginStart;
}
export type ServerlessSecurityPublicConfig = Pick<
diff --git a/x-pack/plugins/security_solution_serverless/public/upselling/hooks/use_product_type_by_pli.ts b/x-pack/plugins/security_solution_serverless/public/upselling/hooks/use_product_type_by_pli.ts
index 46d09cbae6c26..da98c3003c2a2 100644
--- a/x-pack/plugins/security_solution_serverless/public/upselling/hooks/use_product_type_by_pli.ts
+++ b/x-pack/plugins/security_solution_serverless/public/upselling/hooks/use_product_type_by_pli.ts
@@ -6,6 +6,7 @@
*/
import type { ProductFeatureKeyType } from '@kbn/security-solution-features';
+import { useMemo } from 'react';
import { PLI_PRODUCT_FEATURES } from '../../../common/pli/pli_config';
export const getProductTypeByPLI = (requiredPLI: ProductFeatureKeyType): string | null => {
@@ -29,3 +30,7 @@ export const getProductTypeByPLI = (requiredPLI: ProductFeatureKeyType): string
}
return null;
};
+
+export const useProductTypeByPLI = (requiredPLI: ProductFeatureKeyType): string => {
+ return useMemo(() => getProductTypeByPLI(requiredPLI) ?? '', [requiredPLI]);
+};
diff --git a/x-pack/plugins/security_solution_serverless/public/upselling/register_upsellings.test.tsx b/x-pack/plugins/security_solution_serverless/public/upselling/register_upsellings.test.tsx
index 542ab4cd11fb0..9eeafe816f09b 100644
--- a/x-pack/plugins/security_solution_serverless/public/upselling/register_upsellings.test.tsx
+++ b/x-pack/plugins/security_solution_serverless/public/upselling/register_upsellings.test.tsx
@@ -5,23 +5,31 @@
* 2.0.
*/
-import {
- registerUpsellings,
- upsellingMessages,
- upsellingPages,
- upsellingSections,
-} from './register_upsellings';
+import { registerUpsellings } from './register_upsellings';
+import { upsellingMessages, upsellingPages, upsellingSections } from './upsellings';
import { ProductLine, ProductTier } from '../../common/product';
import type { SecurityProductTypes } from '../../common/config';
import { ALL_PRODUCT_FEATURE_KEYS } from '@kbn/security-solution-features/keys';
import type { UpsellingService } from '@kbn/security-solution-upselling/service';
import { mockServices } from '../common/services/__mocks__/services.mock';
+import { of } from 'rxjs';
const mockGetProductProductFeatures = jest.fn();
jest.mock('../../common/pli/pli_features', () => ({
getProductProductFeatures: () => mockGetProductProductFeatures(),
}));
+const setPages = jest.fn();
+const setSections = jest.fn();
+const setMessages = jest.fn();
+const upselling = {
+ setPages,
+ setSections,
+ setMessages,
+ sections$: of([]),
+} as unknown as UpsellingService;
+mockServices.securitySolution.getUpselling = jest.fn(() => upselling);
+
const allProductTypes: SecurityProductTypes = [
{ product_line: ProductLine.security, product_tier: ProductTier.complete },
{ product_line: ProductLine.endpoint, product_tier: ProductTier.complete },
@@ -29,19 +37,14 @@ const allProductTypes: SecurityProductTypes = [
];
describe('registerUpsellings', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
it('should not register anything when all PLIs features are enabled', () => {
mockGetProductProductFeatures.mockReturnValue(ALL_PRODUCT_FEATURE_KEYS);
- const setPages = jest.fn();
- const setSections = jest.fn();
- const setMessages = jest.fn();
- const upselling = {
- setPages,
- setSections,
- setMessages,
- } as unknown as UpsellingService;
-
- registerUpsellings(upselling, allProductTypes, mockServices);
+ registerUpsellings(allProductTypes, mockServices);
expect(setPages).toHaveBeenCalledTimes(1);
expect(setPages).toHaveBeenCalledWith({});
@@ -56,17 +59,7 @@ describe('registerUpsellings', () => {
it('should register all upsellings pages, sections and messages when PLIs features are disabled', () => {
mockGetProductProductFeatures.mockReturnValue([]);
- const setPages = jest.fn();
- const setSections = jest.fn();
- const setMessages = jest.fn();
-
- const upselling = {
- setPages,
- setSections,
- setMessages,
- } as unknown as UpsellingService;
-
- registerUpsellings(upselling, allProductTypes, mockServices);
+ registerUpsellings(allProductTypes, mockServices);
const expectedPagesObject = Object.fromEntries(
upsellingPages.map(({ pageName }) => [pageName, expect.anything()])
diff --git a/x-pack/plugins/security_solution_serverless/public/upselling/register_upsellings.tsx b/x-pack/plugins/security_solution_serverless/public/upselling/register_upsellings.tsx
index ef4574424b42c..851bf6010cb44 100644
--- a/x-pack/plugins/security_solution_serverless/public/upselling/register_upsellings.tsx
+++ b/x-pack/plugins/security_solution_serverless/public/upselling/register_upsellings.tsx
@@ -4,63 +4,32 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
-import type { ProductFeatureKeyType } from '@kbn/security-solution-features';
-import { ProductFeatureKey } from '@kbn/security-solution-features/keys';
-import { SecurityPageName } from '@kbn/security-solution-plugin/common';
-import {
- UPGRADE_INVESTIGATION_GUIDE,
- UPGRADE_INVESTIGATION_GUIDE_INTERACTIONS,
-} from '@kbn/security-solution-upselling/messages';
import type { UpsellingService } from '@kbn/security-solution-upselling/service';
import type {
MessageUpsellings,
PageUpsellings,
SectionUpsellings,
- UpsellingMessageId,
- UpsellingSectionId,
} from '@kbn/security-solution-upselling/service/types';
-import React from 'react';
-import { CloudSecurityPostureIntegrationPliBlockLazy } from './sections/cloud_security_posture';
-import {
- EndpointAgentTamperProtectionLazy,
- EndpointPolicyProtectionsLazy,
- EndpointProtectionUpdatesLazy,
- RuleDetailsEndpointExceptionsLazy,
-} from './sections/endpoint_management';
import type { SecurityProductTypes } from '../../common/config';
import { getProductProductFeatures } from '../../common/pli/pli_features';
import type { Services } from '../common/services';
import { withServicesProvider } from '../common/services';
-import { getProductTypeByPLI } from './hooks/use_product_type_by_pli';
-import {
- EndpointExceptionsDetailsUpsellingLazy,
- EntityAnalyticsUpsellingPageLazy,
- EntityAnalyticsUpsellingSectionLazy,
- OsqueryResponseActionsUpsellingSectionLazy,
- ThreatIntelligencePaywallLazy,
-} from './lazy_upselling';
-import * as i18n from './translations';
+import { upsellingPages, upsellingSections, upsellingMessages } from './upsellings';
-interface UpsellingsConfig {
- pli: ProductFeatureKeyType;
- component: React.ComponentType;
-}
-
-interface UpsellingsMessageConfig {
- pli: ProductFeatureKeyType;
- message: string;
- id: UpsellingMessageId;
-}
-
-type UpsellingPages = Array;
-type UpsellingSections = Array;
-type UpsellingMessages = UpsellingsMessageConfig[];
+export const registerUpsellings = (productTypes: SecurityProductTypes, services: Services) => {
+ const upsellingService = registerSecuritySolutionUpsellings(productTypes, services);
+ configurePluginsUpsellings(upsellingService, services);
+};
-export const registerUpsellings = (
- upselling: UpsellingService,
+/**
+ * Registers the upsellings for the security solution.
+ */
+const registerSecuritySolutionUpsellings = (
productTypes: SecurityProductTypes,
services: Services
-) => {
+): UpsellingService => {
+ const upsellingService = services.securitySolution.getUpselling();
+
const enabledPLIsSet = new Set(getProductProductFeatures(productTypes));
const upsellingPagesToRegister = upsellingPages.reduce(
@@ -93,105 +62,20 @@ export const registerUpsellings = (
{}
);
- upselling.setPages(upsellingPagesToRegister);
- upselling.setSections(upsellingSectionsToRegister);
- upselling.setMessages(upsellingMessagesToRegister);
-};
-
-// Upselling for entire pages, linked to a SecurityPageName
-export const upsellingPages: UpsellingPages = [
- // It is highly advisable to make use of lazy loaded components to minimize bundle size.
- {
- pageName: SecurityPageName.entityAnalytics,
- pli: ProductFeatureKey.advancedInsights,
- component: () => (
-
- ),
- },
- {
- pageName: SecurityPageName.threatIntelligence,
- pli: ProductFeatureKey.threatIntelligence,
- component: () => (
-
- ),
- },
- {
- pageName: SecurityPageName.exceptions,
- pli: ProductFeatureKey.endpointExceptions,
- component: () => (
-
- ),
- },
-];
+ upsellingService.setPages(upsellingPagesToRegister);
+ upsellingService.setSections(upsellingSectionsToRegister);
+ upsellingService.setMessages(upsellingMessagesToRegister);
-const entityAnalyticsProductType = getProductTypeByPLI(ProductFeatureKey.advancedInsights) ?? '';
+ return upsellingService;
+};
-// Upselling for sections, linked by arbitrary ids
-export const upsellingSections: UpsellingSections = [
- // It is highly advisable to make use of lazy loaded components to minimize bundle size.
- {
- id: 'osquery_automated_response_actions',
- pli: ProductFeatureKey.osqueryAutomatedResponseActions,
- component: () => (
-
- ),
- },
- {
- id: 'endpoint_agent_tamper_protection',
- pli: ProductFeatureKey.endpointAgentTamperProtection,
- component: EndpointAgentTamperProtectionLazy,
- },
- {
- id: 'endpointPolicyProtections',
- pli: ProductFeatureKey.endpointPolicyProtections,
- component: EndpointPolicyProtectionsLazy,
- },
- {
- id: 'ruleDetailsEndpointExceptions',
- pli: ProductFeatureKey.endpointExceptions,
- component: RuleDetailsEndpointExceptionsLazy,
- },
- {
- id: 'endpoint_protection_updates',
- pli: ProductFeatureKey.endpointProtectionUpdates,
- component: EndpointProtectionUpdatesLazy,
- },
- {
- id: 'cloud_security_posture_integration_installation',
- pli: ProductFeatureKey.cloudSecurityPosture,
- component: CloudSecurityPostureIntegrationPliBlockLazy,
- },
- {
- id: 'entity_analytics_panel',
- pli: ProductFeatureKey.advancedInsights,
- component: () => (
-
- ),
- },
-];
+/**
+ * Configures the upsellings for other plugins.
+ */
+const configurePluginsUpsellings = (upsellingService: UpsellingService, services: Services) => {
+ const { integrationAssistant } = services;
-// Upselling for sections, linked by arbitrary ids
-export const upsellingMessages: UpsellingMessages = [
- {
- id: 'investigation_guide',
- pli: ProductFeatureKey.investigationGuide,
- message: UPGRADE_INVESTIGATION_GUIDE(
- getProductTypeByPLI(ProductFeatureKey.investigationGuide) ?? ''
- ),
- },
- {
- id: 'investigation_guide_interactions',
- pli: ProductFeatureKey.investigationGuideInteractions,
- message: UPGRADE_INVESTIGATION_GUIDE_INTERACTIONS(
- getProductTypeByPLI(ProductFeatureKey.investigationGuideInteractions) ?? ''
- ),
- },
-];
+ upsellingService.sections$.subscribe((sections) => {
+ integrationAssistant?.renderUpselling(sections.get('integration_assistant'));
+ });
+};
diff --git a/x-pack/plugins/security_solution_serverless/public/upselling/sections/integration_assistant/index.ts b/x-pack/plugins/security_solution_serverless/public/upselling/sections/integration_assistant/index.ts
new file mode 100644
index 0000000000000..320ed8be7ffc1
--- /dev/null
+++ b/x-pack/plugins/security_solution_serverless/public/upselling/sections/integration_assistant/index.ts
@@ -0,0 +1,14 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { lazy } from 'react';
+
+export const IntegrationsAssistantLazy = lazy(() =>
+ import('./integration_assistant').then(({ IntegrationsAssistant }) => ({
+ default: IntegrationsAssistant,
+ }))
+);
diff --git a/x-pack/plugins/security_solution_serverless/public/upselling/sections/integration_assistant/integration_assistant.tsx b/x-pack/plugins/security_solution_serverless/public/upselling/sections/integration_assistant/integration_assistant.tsx
new file mode 100644
index 0000000000000..7d1d797c1ecad
--- /dev/null
+++ b/x-pack/plugins/security_solution_serverless/public/upselling/sections/integration_assistant/integration_assistant.tsx
@@ -0,0 +1,87 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+import React from 'react';
+import {
+ EuiCard,
+ EuiIcon,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiText,
+ EuiTextColor,
+ EuiSpacer,
+} from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+import type { ProductFeatureKeyType } from '@kbn/security-solution-features';
+import { useProductTypeByPLI } from '../../hooks/use_product_type_by_pli';
+
+export const UPGRADE_PRODUCT_MESSAGE = (requiredProductType: string) =>
+ i18n.translate(
+ 'xpack.securitySolutionServerless.upselling.integrationAssistant.upgradeProductMessage',
+ {
+ defaultMessage:
+ 'To turn on the Integration Assistant feature, you must upgrade the product tier to {requiredProductType}',
+ values: {
+ requiredProductType,
+ },
+ }
+ );
+export const TIER_REQUIRED = (requiredProductType: string) =>
+ i18n.translate('xpack.securitySolutionServerless.upselling.integrationAssistant.tierRequired', {
+ defaultMessage: '{requiredProductType} tier required',
+ values: {
+ requiredProductType,
+ },
+ });
+export const CONTACT_ADMINISTRATOR = i18n.translate(
+ 'xpack.securitySolutionServerless.upselling.integrationAssistant.contactAdministrator',
+ {
+ defaultMessage: 'Contact your administrator for assistance.',
+ }
+);
+
+export interface IntegrationsAssistantProps {
+ requiredPLI: ProductFeatureKeyType;
+}
+export const IntegrationsAssistant = React.memo(({ requiredPLI }) => {
+ const requiredProductType = useProductTypeByPLI(requiredPLI);
+ return (
+ <>
+
+ }
+ title={
+
+ {TIER_REQUIRED(requiredProductType)}
+
+ }
+ description={false}
+ >
+
+
+
+
+
+
+ {UPGRADE_PRODUCT_MESSAGE(requiredProductType)}
+
+
+
+
+
+ {CONTACT_ADMINISTRATOR}
+
+
+
+ >
+ );
+});
+IntegrationsAssistant.displayName = 'IntegrationsAssistant';
diff --git a/x-pack/plugins/security_solution_serverless/public/upselling/upsellings.tsx b/x-pack/plugins/security_solution_serverless/public/upselling/upsellings.tsx
new file mode 100644
index 0000000000000..cb0e1514b1df5
--- /dev/null
+++ b/x-pack/plugins/security_solution_serverless/public/upselling/upsellings.tsx
@@ -0,0 +1,155 @@
+/*
+ * 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 type { ProductFeatureKeyType } from '@kbn/security-solution-features';
+import { ProductFeatureKey } from '@kbn/security-solution-features/keys';
+import { SecurityPageName } from '@kbn/security-solution-plugin/common';
+import {
+ UPGRADE_INVESTIGATION_GUIDE,
+ UPGRADE_INVESTIGATION_GUIDE_INTERACTIONS,
+} from '@kbn/security-solution-upselling/messages';
+import type {
+ UpsellingMessageId,
+ UpsellingSectionId,
+} from '@kbn/security-solution-upselling/service/types';
+import React from 'react';
+import { CloudSecurityPostureIntegrationPliBlockLazy } from './sections/cloud_security_posture';
+import {
+ EndpointAgentTamperProtectionLazy,
+ EndpointPolicyProtectionsLazy,
+ EndpointProtectionUpdatesLazy,
+ RuleDetailsEndpointExceptionsLazy,
+} from './sections/endpoint_management';
+import { getProductTypeByPLI } from './hooks/use_product_type_by_pli';
+import {
+ EndpointExceptionsDetailsUpsellingLazy,
+ EntityAnalyticsUpsellingPageLazy,
+ EntityAnalyticsUpsellingSectionLazy,
+ OsqueryResponseActionsUpsellingSectionLazy,
+ ThreatIntelligencePaywallLazy,
+} from './lazy_upselling';
+import * as i18n from './translations';
+import { IntegrationsAssistantLazy } from './sections/integration_assistant';
+
+interface UpsellingsConfig {
+ pli: ProductFeatureKeyType;
+ component: React.ComponentType;
+}
+
+interface UpsellingsMessageConfig {
+ pli: ProductFeatureKeyType;
+ message: string;
+ id: UpsellingMessageId;
+}
+
+type UpsellingPages = Array;
+type UpsellingSections = Array;
+type UpsellingMessages = UpsellingsMessageConfig[];
+
+// Upselling for entire pages, linked to a SecurityPageName
+export const upsellingPages: UpsellingPages = [
+ // It is highly advisable to make use of lazy loaded components to minimize bundle size.
+ {
+ pageName: SecurityPageName.entityAnalytics,
+ pli: ProductFeatureKey.advancedInsights,
+ component: () => (
+
+ ),
+ },
+ {
+ pageName: SecurityPageName.threatIntelligence,
+ pli: ProductFeatureKey.threatIntelligence,
+ component: () => (
+
+ ),
+ },
+ {
+ pageName: SecurityPageName.exceptions,
+ pli: ProductFeatureKey.endpointExceptions,
+ component: () => (
+
+ ),
+ },
+];
+
+const entityAnalyticsProductType = getProductTypeByPLI(ProductFeatureKey.advancedInsights) ?? '';
+
+// Upselling for sections, linked by arbitrary ids
+export const upsellingSections: UpsellingSections = [
+ // It is highly advisable to make use of lazy loaded components to minimize bundle size.
+ {
+ id: 'osquery_automated_response_actions',
+ pli: ProductFeatureKey.osqueryAutomatedResponseActions,
+ component: () => (
+
+ ),
+ },
+ {
+ id: 'endpoint_agent_tamper_protection',
+ pli: ProductFeatureKey.endpointAgentTamperProtection,
+ component: EndpointAgentTamperProtectionLazy,
+ },
+ {
+ id: 'endpointPolicyProtections',
+ pli: ProductFeatureKey.endpointPolicyProtections,
+ component: EndpointPolicyProtectionsLazy,
+ },
+ {
+ id: 'ruleDetailsEndpointExceptions',
+ pli: ProductFeatureKey.endpointExceptions,
+ component: RuleDetailsEndpointExceptionsLazy,
+ },
+ {
+ id: 'endpoint_protection_updates',
+ pli: ProductFeatureKey.endpointProtectionUpdates,
+ component: EndpointProtectionUpdatesLazy,
+ },
+ {
+ id: 'cloud_security_posture_integration_installation',
+ pli: ProductFeatureKey.cloudSecurityPosture,
+ component: CloudSecurityPostureIntegrationPliBlockLazy,
+ },
+ {
+ id: 'entity_analytics_panel',
+ pli: ProductFeatureKey.advancedInsights,
+ component: () => (
+
+ ),
+ },
+ {
+ id: 'integration_assistant',
+ pli: ProductFeatureKey.integrationAssistant,
+ component: () => (
+
+ ),
+ },
+];
+
+// Upselling for sections, linked by arbitrary ids
+export const upsellingMessages: UpsellingMessages = [
+ {
+ id: 'investigation_guide',
+ pli: ProductFeatureKey.investigationGuide,
+ message: UPGRADE_INVESTIGATION_GUIDE(
+ getProductTypeByPLI(ProductFeatureKey.investigationGuide) ?? ''
+ ),
+ },
+ {
+ id: 'investigation_guide_interactions',
+ pli: ProductFeatureKey.investigationGuideInteractions,
+ message: UPGRADE_INVESTIGATION_GUIDE_INTERACTIONS(
+ getProductTypeByPLI(ProductFeatureKey.investigationGuideInteractions) ?? ''
+ ),
+ },
+];
diff --git a/x-pack/plugins/security_solution_serverless/server/plugin.ts b/x-pack/plugins/security_solution_serverless/server/plugin.ts
index a7783ad37d53c..d63152f169490 100644
--- a/x-pack/plugins/security_solution_serverless/server/plugin.ts
+++ b/x-pack/plugins/security_solution_serverless/server/plugin.ts
@@ -26,13 +26,12 @@ import type {
} from './types';
import { SecurityUsageReportingTask } from './task_manager/usage_reporting_task';
import { cloudSecurityMetringTaskProperties } from './cloud_security/cloud_security_metering_task_config';
-import { getProductProductFeaturesConfigurator, getSecurityProductTier } from './product_features';
+import { registerProductFeatures, getSecurityProductTier } from './product_features';
import { METERING_TASK as ENDPOINT_METERING_TASK } from './endpoint/constants/metering';
import {
endpointMeteringService,
setEndpointPackagePolicyServerlessBillingFlags,
} from './endpoint/services';
-import { enableRuleActions } from './rules/enable_rule_actions';
import { NLPCleanupTask } from './task_manager/nlp_cleanup_task/nlp_cleanup_task';
import { telemetryEvents } from './telemetry/event_based_telemetry';
@@ -54,34 +53,25 @@ export class SecuritySolutionServerlessPlugin
constructor(private readonly initializerContext: PluginInitializerContext) {
this.config = this.initializerContext.config.get();
this.logger = this.initializerContext.logger.get();
+
+ const productTypesStr = JSON.stringify(this.config.productTypes, null, 2);
+ this.logger.info(`Security Solution running with product types:\n${productTypesStr}`);
}
public setup(coreSetup: CoreSetup, pluginsSetup: SecuritySolutionServerlessPluginSetupDeps) {
this.config = createConfig(this.initializerContext, pluginsSetup.securitySolution);
- const enabledProductFeatures = getProductProductFeatures(this.config.productTypes);
- // securitySolutionEss plugin should always be disabled when securitySolutionServerless is enabled.
- // This check is an additional layer of security to prevent double registrations when
- // `plugins.forceEnableAllPlugins` flag is enabled. Should never happen in real scenarios.
- const shouldRegister = pluginsSetup.securitySolutionEss == null;
- if (shouldRegister) {
- const productTypesStr = JSON.stringify(this.config.productTypes, null, 2);
- this.logger.info(`Security Solution running with product types:\n${productTypesStr}`);
- const productFeaturesConfigurator = getProductProductFeaturesConfigurator(
- enabledProductFeatures,
- this.config
- );
- pluginsSetup.securitySolution.setProductFeaturesConfigurator(productFeaturesConfigurator);
- }
+ // Register product features
+ const enabledProductFeatures = getProductProductFeatures(this.config.productTypes);
+ registerProductFeatures(pluginsSetup, enabledProductFeatures, this.config);
// Register telemetry events
telemetryEvents.forEach((eventConfig) => coreSetup.analytics.registerEventType(eventConfig));
- enableRuleActions({
- actions: pluginsSetup.actions,
- productFeatureKeys: enabledProductFeatures,
- });
+ // Setup project uiSettings whitelisting
+ pluginsSetup.serverless.setupProjectSettings(SECURITY_PROJECT_SETTINGS);
+ // Tasks
this.cloudSecurityUsageReportingTask = new SecurityUsageReportingTask({
core: coreSetup,
logFactory: this.initializerContext.logger,
@@ -113,8 +103,6 @@ export class SecuritySolutionServerlessPlugin
taskManager: pluginsSetup.taskManager,
});
- pluginsSetup.serverless.setupProjectSettings(SECURITY_PROJECT_SETTINGS);
-
return {};
}
diff --git a/x-pack/plugins/security_solution_serverless/server/product_features/index.ts b/x-pack/plugins/security_solution_serverless/server/product_features/index.ts
index 40ae2a4253cdd..6c0b2b9091c66 100644
--- a/x-pack/plugins/security_solution_serverless/server/product_features/index.ts
+++ b/x-pack/plugins/security_solution_serverless/server/product_features/index.ts
@@ -5,30 +5,56 @@
* 2.0.
*/
-import type { ProductFeatureKeys } from '@kbn/security-solution-features';
-import type { ProductFeaturesConfigurator } from '@kbn/security-solution-plugin/server/lib/product_features_service/types';
import type { Logger } from '@kbn/logging';
-import type { ServerlessSecurityConfig } from '../config';
+
+import { ProductFeatureKey } from '@kbn/security-solution-features/keys';
+import type { ProductFeatureKeys } from '@kbn/security-solution-features';
import { getCasesProductFeaturesConfigurator } from './cases_product_features_config';
import { getSecurityProductFeaturesConfigurator } from './security_product_features_config';
import { getSecurityAssistantProductFeaturesConfigurator } from './assistant_product_features_config';
-import type { Tier } from '../types';
+import { enableRuleActions } from '../rules/enable_rule_actions';
+import type { ServerlessSecurityConfig } from '../config';
+import type { Tier, SecuritySolutionServerlessPluginSetupDeps } from '../types';
import { ProductLine } from '../../common/product';
-export const getProductProductFeaturesConfigurator = (
+export const registerProductFeatures = (
+ pluginsSetup: SecuritySolutionServerlessPluginSetupDeps,
enabledProductFeatureKeys: ProductFeatureKeys,
config: ServerlessSecurityConfig
-): ProductFeaturesConfigurator => {
- return {
+): void => {
+ // securitySolutionEss plugin should always be disabled when securitySolutionServerless is enabled.
+ // This check is an additional layer of security to prevent double registrations when
+ // `plugins.forceEnableAllPlugins` flag is enabled. Should never happen in real scenarios.
+ const shouldRegister = pluginsSetup.securitySolutionEss == null;
+ if (!shouldRegister) {
+ return;
+ }
+
+ // register product features for the main security solution product features service
+ pluginsSetup.securitySolution.setProductFeaturesConfigurator({
security: getSecurityProductFeaturesConfigurator(
enabledProductFeatureKeys,
config.experimentalFeatures
),
cases: getCasesProductFeaturesConfigurator(enabledProductFeatureKeys),
securityAssistant: getSecurityAssistantProductFeaturesConfigurator(enabledProductFeatureKeys),
- };
+ });
+
+ // enable rule actions based on the enabled product features
+ enableRuleActions({
+ actions: pluginsSetup.actions,
+ productFeatureKeys: enabledProductFeatureKeys,
+ });
+
+ // set availability for the integration assistant plugin based on the product features
+ pluginsSetup.integrationAssistant?.setIsAvailable(
+ enabledProductFeatureKeys.includes(ProductFeatureKey.integrationAssistant)
+ );
};
+/**
+ * Get the security product tier from the security product type in the config
+ */
export const getSecurityProductTier = (config: ServerlessSecurityConfig, logger: Logger): Tier => {
const securityProductType = config.productTypes.find(
(productType) => productType.product_line === ProductLine.security
diff --git a/x-pack/plugins/security_solution_serverless/server/types.ts b/x-pack/plugins/security_solution_serverless/server/types.ts
index 7e84b418cf4cf..eec8185a28199 100644
--- a/x-pack/plugins/security_solution_serverless/server/types.ts
+++ b/x-pack/plugins/security_solution_serverless/server/types.ts
@@ -21,6 +21,7 @@ import type { FleetStartContract } from '@kbn/fleet-plugin/server';
import type { PluginSetupContract as ActionsPluginSetupContract } from '@kbn/actions-plugin/server';
import type { ServerlessPluginSetup } from '@kbn/serverless/server';
+import type { IntegrationAssistantPluginSetup } from '@kbn/integration-assistant-plugin/server';
import type { ProductTier } from '../common/product';
import type { ServerlessSecurityConfig } from './config';
@@ -39,6 +40,7 @@ export interface SecuritySolutionServerlessPluginSetupDeps {
taskManager: TaskManagerSetupContract;
cloud: CloudSetup;
actions: ActionsPluginSetupContract;
+ integrationAssistant?: IntegrationAssistantPluginSetup;
}
export interface SecuritySolutionServerlessPluginStartDeps {
diff --git a/x-pack/plugins/security_solution_serverless/tsconfig.json b/x-pack/plugins/security_solution_serverless/tsconfig.json
index a1c6cefd396ca..b6bfbea1cc7be 100644
--- a/x-pack/plugins/security_solution_serverless/tsconfig.json
+++ b/x-pack/plugins/security_solution_serverless/tsconfig.json
@@ -44,5 +44,6 @@
"@kbn/management-cards-navigation",
"@kbn/discover-plugin",
"@kbn/logging",
+ "@kbn/integration-assistant-plugin",
]
}
From cf11c5fb3fb635e0a260a67fb3c7159d50232eaf Mon Sep 17 00:00:00 2001
From: Tomasz Ciecierski
Date: Mon, 1 Jul 2024 15:57:27 +0200
Subject: [PATCH 17/25] [EDR Workflows] change osquery pipeline (#187222)
---
.buildkite/scripts/pipelines/pull_request/pipeline.ts | 6 +++++-
1 file changed, 5 insertions(+), 1 deletion(-)
diff --git a/.buildkite/scripts/pipelines/pull_request/pipeline.ts b/.buildkite/scripts/pipelines/pull_request/pipeline.ts
index c6b28bc20c6f3..cd5d9aa470b3c 100644
--- a/.buildkite/scripts/pipelines/pull_request/pipeline.ts
+++ b/.buildkite/scripts/pipelines/pull_request/pipeline.ts
@@ -307,7 +307,11 @@ const getPipeline = (filename: string, removeSteps = true) => {
}
if (
- ((await doAnyChangesMatch([/^x-pack\/plugins\/osquery/, /^x-pack\/test\/osquery_cypress/])) ||
+ ((await doAnyChangesMatch([
+ /^x-pack\/plugins\/osquery/,
+ /^x-pack\/test\/osquery_cypress/,
+ /^x-pack\/plugins\/security_solution/,
+ ])) ||
GITHUB_PR_LABELS.includes('ci:all-cypress-suites')) &&
!GITHUB_PR_LABELS.includes('ci:skip-cypress-osquery')
) {
From c7789a4fc12967b6870c8dfe5b03ecbbcbaa89ba Mon Sep 17 00:00:00 2001
From: Miriam <31922082+MiriamAparicio@users.noreply.github.com>
Date: Mon, 1 Jul 2024 15:27:28 +0100
Subject: [PATCH 18/25] [ObsUX][Infra] Fix container image name not being
displayed (#187220)
## Summary
Small fix, container.image.name wasn't display on the metadata summary
fields
---
.../infra/common/http_api/metadata_api.ts | 1 -
.../tabs/overview/metadata_summary/metadata_summary_list.tsx | 2 +-
2 files changed, 1 insertion(+), 2 deletions(-)
diff --git a/x-pack/plugins/observability_solution/infra/common/http_api/metadata_api.ts b/x-pack/plugins/observability_solution/infra/common/http_api/metadata_api.ts
index f9db4df10ed1c..f2f5bc2c07dbe 100644
--- a/x-pack/plugins/observability_solution/infra/common/http_api/metadata_api.ts
+++ b/x-pack/plugins/observability_solution/infra/common/http_api/metadata_api.ts
@@ -48,7 +48,6 @@ export const InfraMetadataContainerRT = rt.partial({
name: rt.string,
id: rt.string,
runtime: rt.string,
- imageName: rt.string,
image: rt.partial({ name: rt.string }),
});
diff --git a/x-pack/plugins/observability_solution/infra/public/components/asset_details/tabs/overview/metadata_summary/metadata_summary_list.tsx b/x-pack/plugins/observability_solution/infra/public/components/asset_details/tabs/overview/metadata_summary/metadata_summary_list.tsx
index fdd5b0b8aeee5..0b369c90f1390 100644
--- a/x-pack/plugins/observability_solution/infra/public/components/asset_details/tabs/overview/metadata_summary/metadata_summary_list.tsx
+++ b/x-pack/plugins/observability_solution/infra/public/components/asset_details/tabs/overview/metadata_summary/metadata_summary_list.tsx
@@ -104,7 +104,7 @@ const containerMetadataData = (metadataInfo: InfraMetadata['info']): MetadataDat
},
{
field: 'containerImageName',
- value: metadataInfo?.container?.imageName,
+ value: metadataInfo?.container?.image?.name,
tooltipFieldLabel: 'container.image.name',
},
{
From 2b9d7fd607bb4c30e2bf6acb2ff89878cf8e24e2 Mon Sep 17 00:00:00 2001
From: Julia Rechkunova
Date: Mon, 1 Jul 2024 16:29:27 +0200
Subject: [PATCH 19/25] [Discover] Unskip ES|QL view flaky tests (#187218)
- Closes https://github.com/elastic/kibana/issues/183493
- Closes https://github.com/elastic/kibana/issues/183479
- Closes https://github.com/elastic/kibana/issues/183193
25x
https://buildkite.com/elastic/kibana-flaky-test-suite-runner/builds/6422
---
.../common/discover/esql/_esql_view.ts | 17 ++++++++++-------
1 file changed, 10 insertions(+), 7 deletions(-)
diff --git a/x-pack/test_serverless/functional/test_suites/common/discover/esql/_esql_view.ts b/x-pack/test_serverless/functional/test_suites/common/discover/esql/_esql_view.ts
index 6ce8021024366..8e7b921d9655d 100644
--- a/x-pack/test_serverless/functional/test_suites/common/discover/esql/_esql_view.ts
+++ b/x-pack/test_serverless/functional/test_suites/common/discover/esql/_esql_view.ts
@@ -35,8 +35,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
defaultIndex: 'logstash-*',
};
- // Failing: See https://github.com/elastic/kibana/issues/183493
- describe.skip('discover esql view', async function () {
+ describe('discover esql view', async function () {
before(async () => {
await kibanaServer.savedObjects.cleanStandardList();
log.debug('load kibana index with default index pattern');
@@ -50,8 +49,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await PageObjects.timePicker.setDefaultAbsoluteRange();
});
- // FLAKY: https://github.com/elastic/kibana/issues/183193
- describe.skip('test', () => {
+ describe('test', () => {
it('should render esql view correctly', async function () {
await PageObjects.unifiedFieldList.waitUntilSidebarHasLoaded();
@@ -133,11 +131,14 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
it('should query an index pattern that doesnt translate to a dataview correctly', async function () {
await PageObjects.discover.selectTextBaseLang();
- const testQuery = `from logstash* | limit 10 | stats countB = count(bytes) by geo.dest | sort countB`;
+ await PageObjects.header.waitUntilLoadingHasFinished();
+ await PageObjects.discover.waitUntilSearchingHasFinished();
+ const testQuery = `from logstash* | limit 10 | stats countB = count(bytes) by geo.dest | sort countB`;
await monacoEditor.setCodeEditorValue(testQuery);
await testSubjects.click('querySubmitButton');
await PageObjects.header.waitUntilLoadingHasFinished();
+ await PageObjects.discover.waitUntilSearchingHasFinished();
const cell = await dataGrid.getCellElement(0, 2);
expect(await cell.getVisibleText()).to.be('1');
@@ -175,10 +176,12 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
});
});
- // FLAKY: https://github.com/elastic/kibana/issues/183479
- describe.skip('errors', () => {
+ describe('errors', () => {
it('should show error messages for syntax errors in query', async function () {
await PageObjects.discover.selectTextBaseLang();
+ await PageObjects.header.waitUntilLoadingHasFinished();
+ await PageObjects.discover.waitUntilSearchingHasFinished();
+
const brokenQueries = [
'from logstash-* | limit 10*',
'from logstash-* | limit A',
From 9785ce0e59b6a9c6fa1241df273afe2de880883d Mon Sep 17 00:00:00 2001
From: Agustina Nahir Ruidiaz <61565784+agusruidiazgd@users.noreply.github.com>
Date: Mon, 1 Jul 2024 16:45:18 +0200
Subject: [PATCH 20/25] [Security Solution] Adding accordion to dashboard
security views (#186465)
## Summary
In this feature the following UI changes are required:
Add an accordion layout for wrap all the image link cards.
Change Layout of image cards.
Remove "Default" Header
Change second header label
### Checklist
Delete any items that are not applicable to this PR.
- [ ] Any text added follows [EUI's writing
guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses
sentence case text and includes [i18n
support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)
---------
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: Angela Chuang <6295984+angorayc@users.noreply.github.com>
---
.../landing_links_image_card.stories.tsx | 117 ++++++++++++++++
.../landing_links_image_card.test.tsx | 104 ++++++++++++++
.../landing_links_image_card.tsx | 119 ++++++++++++++++
.../landing_links_images_cards.test.tsx | 71 ++--------
.../landing_links_images_cards.tsx | 127 ++++++++----------
.../src/landing_links/translations.ts | 14 ++
.../dashboards/pages/landing_page/index.tsx | 6 +-
.../pages/landing_page/translations.ts | 9 +-
.../translations/translations/fr-FR.json | 1 -
.../translations/translations/ja-JP.json | 1 -
.../translations/translations/zh-CN.json | 1 -
.../ftr/cases/attachment_framework.ts | 1 +
12 files changed, 424 insertions(+), 147 deletions(-)
create mode 100644 x-pack/packages/security-solution/navigation/src/landing_links/landing_links_image_card.stories.tsx
create mode 100644 x-pack/packages/security-solution/navigation/src/landing_links/landing_links_image_card.test.tsx
create mode 100644 x-pack/packages/security-solution/navigation/src/landing_links/landing_links_image_card.tsx
create mode 100644 x-pack/packages/security-solution/navigation/src/landing_links/translations.ts
diff --git a/x-pack/packages/security-solution/navigation/src/landing_links/landing_links_image_card.stories.tsx b/x-pack/packages/security-solution/navigation/src/landing_links/landing_links_image_card.stories.tsx
new file mode 100644
index 0000000000000..ba54ff1ebd2cf
--- /dev/null
+++ b/x-pack/packages/security-solution/navigation/src/landing_links/landing_links_image_card.stories.tsx
@@ -0,0 +1,117 @@
+/*
+ * 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 type { CoreStart } from '@kbn/core/public';
+import type { NavigationLink } from '../types';
+import type { LandingLinksImagesProps } from './landing_links_images_cards';
+import { LandingLinksImageCard as LandingLinksImageCardComponent } from './landing_links_image_card';
+import { NavigationProvider } from '../context';
+
+const items: NavigationLink[] = [
+ {
+ id: 'link1',
+ title: 'link #1',
+ description: 'This is the description of the link #1',
+ landingImage: 'https://dummyimage.com/360x200/efefef/000',
+ },
+ {
+ id: 'link2',
+ title: 'link #2',
+ description: 'This is the description of the link #2',
+ isBeta: true,
+ landingImage: 'https://dummyimage.com/360x200/efefef/000',
+ },
+ {
+ id: 'link3',
+ title: 'link #3',
+ description: 'This is the description of the link #3',
+ landingImage: 'https://dummyimage.com/360x200/efefef/000',
+ },
+ {
+ id: 'link4',
+ title: 'link #4',
+ description: 'This is the description of the link #4',
+ landingImage: 'https://dummyimage.com/360x200/efefef/000',
+ },
+ {
+ id: 'link5',
+ title: 'link #5',
+ description: 'This is the description of the link #5',
+ landingImage: 'https://dummyimage.com/360x200/efefef/000',
+ },
+ {
+ id: 'link6',
+ title: 'link #6',
+ description: 'This is the description of the link #6',
+ landingImage: 'https://dummyimage.com/360x200/efefef/000',
+ },
+ {
+ id: 'link7',
+ title: 'link #7',
+ description: 'This is the description of the link #7',
+ landingImage: 'https://dummyimage.com/360x200/efefef/000',
+ },
+];
+
+export default {
+ title: 'Landing Links/Landing Links Image Card',
+ description: 'Renders the links with images in a horizontal layout',
+ decorators: [
+ (storyFn: Function) => (
+
+ {storyFn()}
+
+ ),
+ ],
+};
+
+const mockCore = {
+ application: {
+ navigateToApp: () => {},
+ getUrlForApp: () => '#',
+ },
+} as unknown as CoreStart;
+
+export const LandingLinksImageCards = (params: LandingLinksImagesProps) => (
+
+
+
+ {items.map((item) => {
+ const { id } = item;
+ return ;
+ })}
+
+
+
+);
+
+LandingLinksImageCards.argTypes = {
+ items: {
+ control: 'object',
+ defaultValue: items,
+ },
+};
+
+LandingLinksImageCards.parameters = {
+ layout: 'fullscreen',
+};
diff --git a/x-pack/packages/security-solution/navigation/src/landing_links/landing_links_image_card.test.tsx b/x-pack/packages/security-solution/navigation/src/landing_links/landing_links_image_card.test.tsx
new file mode 100644
index 0000000000000..b55527146bbad
--- /dev/null
+++ b/x-pack/packages/security-solution/navigation/src/landing_links/landing_links_image_card.test.tsx
@@ -0,0 +1,104 @@
+/*
+ * 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 { render } from '@testing-library/react';
+import { SecurityPageName } from '../constants';
+import { mockNavigateTo, mockGetAppUrl } from '../../mocks/navigation';
+import { LandingLinksImageCard } from './landing_links_image_card';
+import { BETA } from './beta_badge';
+
+jest.mock('../navigation');
+
+mockGetAppUrl.mockImplementation(({ deepLinkId }: { deepLinkId: string }) => `/${deepLinkId}`);
+const mockOnLinkClick = jest.fn();
+
+const DEFAULT_NAV_ITEM = {
+ id: SecurityPageName.overview,
+ title: 'TEST LABEL',
+ description: 'TEST DESCRIPTION',
+ landingImage: 'TEST_IMAGE.png',
+};
+
+describe('LandingLinksImageCard', () => {
+ it('should render', () => {
+ const title = 'test label';
+
+ const { queryByText } = render(
+
+ );
+
+ expect(queryByText(title)).toBeInTheDocument();
+ });
+
+ it('should render landingImage', () => {
+ const landingImage = 'test_image.jpeg';
+ const title = 'TEST_LABEL';
+
+ const { getByTestId } = render(
+
+ );
+
+ expect(getByTestId('LandingImageCard-image')).toHaveStyle({
+ backgroundImage: `url(${landingImage})`,
+ });
+ });
+
+ it('should render beta tag when isBeta is true', () => {
+ const { queryByText } = render(
+
+ );
+ expect(queryByText(BETA)).toBeInTheDocument();
+ });
+
+ it('should not render beta tag when isBeta is false', () => {
+ const { queryByText } = render();
+ expect(queryByText(BETA)).not.toBeInTheDocument();
+ });
+
+ it('should navigate link', () => {
+ const id = SecurityPageName.administration;
+ const title = 'test label 2';
+
+ const { getByText } = render(
+
+ );
+
+ getByText(title).click();
+
+ expect(mockGetAppUrl).toHaveBeenCalledWith({
+ deepLinkId: SecurityPageName.administration,
+ absolute: false,
+ path: '',
+ });
+ expect(mockNavigateTo).toHaveBeenCalledWith({ url: '/administration' });
+ });
+
+ it('should call onLinkClick', () => {
+ const id = SecurityPageName.administration;
+ const title = 'myTestLabel';
+
+ const { getByText } = render(
+
+ );
+
+ getByText(title).click();
+
+ expect(mockOnLinkClick).toHaveBeenCalledWith(id);
+ });
+});
diff --git a/x-pack/packages/security-solution/navigation/src/landing_links/landing_links_image_card.tsx b/x-pack/packages/security-solution/navigation/src/landing_links/landing_links_image_card.tsx
new file mode 100644
index 0000000000000..fb03cf5386115
--- /dev/null
+++ b/x-pack/packages/security-solution/navigation/src/landing_links/landing_links_image_card.tsx
@@ -0,0 +1,119 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+import { EuiFlexGroup, EuiFlexItem, EuiPanel, EuiText, EuiTitle, useEuiTheme } from '@elastic/eui';
+import React, { useMemo } from 'react';
+import { css } from '@emotion/react';
+import { withLink } from '../links';
+import type { NavigationLink } from '../types';
+import { BetaBadge } from './beta_badge';
+import { getKibanaLinkProps } from './utils';
+
+export interface LandingLinksImageCardProps {
+ item: NavigationLink;
+ urlState?: string;
+ onLinkClick?: (id: string) => void;
+}
+
+const CARD_HEIGHT = 116;
+const CARD_WIDTH = 370;
+const CARD_HEIGHT_IMAGE = 98;
+
+const useStyles = () => {
+ const { euiTheme } = useEuiTheme();
+ return {
+ card: css`
+ height: ${CARD_HEIGHT}px;
+ max-width: ${CARD_WIDTH}px;
+ `,
+ cardWrapper: css`
+ height: 100%;
+ `,
+ titleContainer: css`
+ height: ${euiTheme.size.l};
+ `,
+ title: css`
+ color: ${euiTheme.colors.primaryText};
+ font-weight: ${euiTheme.font.weight.semiBold};
+ `,
+ getImageContainer: (imageUrl: string | undefined) => css`
+ height: ${CARD_HEIGHT_IMAGE}px;
+ width: ${CARD_HEIGHT_IMAGE}px;
+ background-position: center center;
+ background-repeat: no-repeat;
+ background-image: url(${imageUrl ?? ''});
+ background-size: auto 98px;
+ `,
+ };
+};
+
+const EuiPanelWithLink = withLink(EuiPanel);
+
+export const LandingLinksImageCard: React.FC = React.memo(
+ function LandingLinksImageCard({ item, urlState, onLinkClick }) {
+ const styles = useStyles();
+
+ const linkProps = getKibanaLinkProps({ item, urlState, onLinkClick });
+ const { landingImage, title, description, isBeta, betaOptions } = item;
+
+ const imageBackground = useMemo(
+ () => styles.getImageContainer(landingImage),
+ [landingImage, styles]
+ );
+
+ return (
+
+
+
+
+ {landingImage && (
+
+ )}
+
+
+
+
+
+
+
+ {title}
+
+
+ {isBeta && }
+
+
+
+ {description}
+
+
+
+
+
+
+ );
+ }
+);
+
+// eslint-disable-next-line import/no-default-export
+export default LandingLinksImageCard;
diff --git a/x-pack/packages/security-solution/navigation/src/landing_links/landing_links_images_cards.test.tsx b/x-pack/packages/security-solution/navigation/src/landing_links/landing_links_images_cards.test.tsx
index e614c99a500d3..690739576c926 100644
--- a/x-pack/packages/security-solution/navigation/src/landing_links/landing_links_images_cards.test.tsx
+++ b/x-pack/packages/security-solution/navigation/src/landing_links/landing_links_images_cards.test.tsx
@@ -8,14 +8,12 @@
import React from 'react';
import { render } from '@testing-library/react';
import { SecurityPageName } from '../constants';
-import { mockNavigateTo, mockGetAppUrl } from '../../mocks/navigation';
+import { mockGetAppUrl } from '../../mocks/navigation';
import { LandingLinksImageCards } from './landing_links_images_cards';
-import { BETA } from './beta_badge';
jest.mock('../navigation');
mockGetAppUrl.mockImplementation(({ deepLinkId }: { deepLinkId: string }) => `/${deepLinkId}`);
-const mockOnLinkClick = jest.fn();
const DEFAULT_NAV_ITEM = {
id: SecurityPageName.overview,
@@ -25,70 +23,19 @@ const DEFAULT_NAV_ITEM = {
};
describe('LandingLinksImageCards', () => {
- it('should render', () => {
- const title = 'test label';
+ it('should render accordion', () => {
+ const landingLinksCardsAccordionTestId = 'LandingImageCards-accordion';
- const { queryByText } = render(
-
- );
+ const { queryByTestId } = render();
- expect(queryByText(title)).toBeInTheDocument();
+ expect(queryByTestId(landingLinksCardsAccordionTestId)).toBeInTheDocument();
});
- it('should render landingImage', () => {
- const landingImage = 'test_image.jpeg';
- const title = 'TEST_LABEL';
+ it('should render LandingLinksImageCard item', () => {
+ const landingLinksCardTestId = 'LandingImageCard-item';
- const { getByTestId } = render(
-
- );
+ const { queryByTestId } = render();
- expect(getByTestId('LandingImageCard-image')).toHaveAttribute('src', landingImage);
- });
-
- it('should render beta tag when isBeta is true', () => {
- const { queryByText } = render(
-
- );
- expect(queryByText(BETA)).toBeInTheDocument();
- });
-
- it('should not render beta tag when isBeta is false', () => {
- const { queryByText } = render();
- expect(queryByText(BETA)).not.toBeInTheDocument();
- });
-
- it('should navigate link', () => {
- const id = SecurityPageName.administration;
- const title = 'test label 2';
-
- const { getByText } = render(
-
- );
-
- getByText(title).click();
-
- expect(mockGetAppUrl).toHaveBeenCalledWith({
- deepLinkId: SecurityPageName.administration,
- absolute: false,
- path: '',
- });
- expect(mockNavigateTo).toHaveBeenCalledWith({ url: '/administration' });
- });
-
- it('should call onLinkClick', () => {
- const id = SecurityPageName.administration;
- const title = 'myTestLabel';
-
- const { getByText } = render(
-
- );
-
- getByText(title).click();
-
- expect(mockOnLinkClick).toHaveBeenCalledWith(id);
+ expect(queryByTestId(landingLinksCardTestId)).toBeInTheDocument();
});
});
diff --git a/x-pack/packages/security-solution/navigation/src/landing_links/landing_links_images_cards.tsx b/x-pack/packages/security-solution/navigation/src/landing_links/landing_links_images_cards.tsx
index ac8598c427026..6ba9b63dc7ca3 100644
--- a/x-pack/packages/security-solution/navigation/src/landing_links/landing_links_images_cards.tsx
+++ b/x-pack/packages/security-solution/navigation/src/landing_links/landing_links_images_cards.tsx
@@ -4,13 +4,21 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
-import { EuiCard, EuiFlexGroup, EuiFlexItem, EuiImage, EuiTitle, useEuiTheme } from '@elastic/eui';
+import {
+ EuiAccordion,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiIcon,
+ EuiPanel,
+ EuiSpacer,
+ EuiTitle,
+ useGeneratedHtmlId,
+} from '@elastic/eui';
import React from 'react';
import { css } from '@emotion/react';
-import { withLink } from '../links';
+import * as i18n from './translations';
import type { NavigationLink } from '../types';
-import { BetaBadge } from './beta_badge';
-import { getKibanaLinkProps } from './utils';
+import LandingLinksImageCard from './landing_links_image_card';
export interface LandingLinksImagesProps {
items: NavigationLink[];
@@ -18,83 +26,64 @@ export interface LandingLinksImagesProps {
onLinkClick?: (id: string) => void;
}
-const CARD_WIDTH = 320;
-
const useStyles = () => {
- const { euiTheme } = useEuiTheme();
return {
- container: css`
- max-width: ${CARD_WIDTH}px;
- `,
- card: css`
- // Needed to use the primary color in the title underlining on hover
- .euiCard__title {
- color: ${euiTheme.colors.primaryText};
+ accordion: css`
+ .euiAccordion__childWrapper {
+ overflow: visible;
}
`,
- titleContainer: css`
- display: flex;
- align-items: center;
- `,
- title: css`
- color: ${euiTheme.colors.primaryText};
- `,
- description: css`
- padding-top: ${euiTheme.size.xs};
- max-width: 550px;
- `,
};
};
-const EuiCardWithLink = withLink(EuiCard);
-
export const LandingLinksImageCards: React.FC = React.memo(
function LandingLinksImageCards({ items, urlState, onLinkClick }) {
+ const landingLinksAccordionId = useGeneratedHtmlId({ prefix: 'landingLinksAccordion' });
const styles = useStyles();
+
return (
-
- {items.map((item) => {
- const linkProps = getKibanaLinkProps({ item, urlState, onLinkClick });
- const { id, landingImage, title, description, isBeta, betaOptions } = item;
- return (
-
+
-
- )
- }
- title={
-
-
- {title}
-
- {isBeta && }
-
- }
- titleElement="span"
- description={{description}}
- />
-
- );
- })}
-
+
+
+
+
+
+
+ {i18n.LANDING_LINKS_ACCORDION_HEADER}
+
+
+
+ }
+ >
+
+
+ {items.map((item) => {
+ const { id } = item;
+ return (
+
+ );
+ })}
+
+
+
);
}
);
diff --git a/x-pack/packages/security-solution/navigation/src/landing_links/translations.ts b/x-pack/packages/security-solution/navigation/src/landing_links/translations.ts
new file mode 100644
index 0000000000000..d9409b46534db
--- /dev/null
+++ b/x-pack/packages/security-solution/navigation/src/landing_links/translations.ts
@@ -0,0 +1,14 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+import { i18n } from '@kbn/i18n';
+
+export const LANDING_LINKS_ACCORDION_HEADER = i18n.translate(
+ 'securitySolutionPackages.navigation.landingLinks',
+ {
+ defaultMessage: 'Security views',
+ }
+);
diff --git a/x-pack/plugins/security_solution/public/dashboards/pages/landing_page/index.tsx b/x-pack/plugins/security_solution/public/dashboards/pages/landing_page/index.tsx
index affe83b0682ca..b25b235213e60 100644
--- a/x-pack/plugins/security_solution/public/dashboards/pages/landing_page/index.tsx
+++ b/x-pack/plugins/security_solution/public/dashboards/pages/landing_page/index.tsx
@@ -128,10 +128,6 @@ export const DashboardsLandingPage = () => {
>
)}
-
- {i18n.DASHBOARDS_PAGE_SECTION_DEFAULT}
-
-
{
{canReadDashboard && securityTagsExist && initialFilter && (
<>
-
+
{i18n.DASHBOARDS_PAGE_SECTION_CUSTOM}
diff --git a/x-pack/plugins/security_solution/public/dashboards/pages/landing_page/translations.ts b/x-pack/plugins/security_solution/public/dashboards/pages/landing_page/translations.ts
index 71bafc6a80990..256c3a71a33e1 100644
--- a/x-pack/plugins/security_solution/public/dashboards/pages/landing_page/translations.ts
+++ b/x-pack/plugins/security_solution/public/dashboards/pages/landing_page/translations.ts
@@ -13,16 +13,9 @@ export const DASHBOARDS_PAGE_CREATE_BUTTON = i18n.translate(
}
);
-export const DASHBOARDS_PAGE_SECTION_DEFAULT = i18n.translate(
- 'xpack.securitySolution.dashboards.landing.section.default',
- {
- defaultMessage: 'DEFAULT',
- }
-);
-
export const DASHBOARDS_PAGE_SECTION_CUSTOM = i18n.translate(
'xpack.securitySolution.dashboards.landing.section.custom',
{
- defaultMessage: 'CUSTOM',
+ defaultMessage: 'Custom Dashboards',
}
);
diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json
index a1871e039fbb3..3619b6096c0f0 100644
--- a/x-pack/plugins/translations/translations/fr-FR.json
+++ b/x-pack/plugins/translations/translations/fr-FR.json
@@ -33700,7 +33700,6 @@
"xpack.securitySolution.dashboards.description": "Description",
"xpack.securitySolution.dashboards.landing.createButton": "Créer un tableau de bord",
"xpack.securitySolution.dashboards.landing.section.custom": "PERSONNALISÉ",
- "xpack.securitySolution.dashboards.landing.section.default": "PAR DÉFAUT",
"xpack.securitySolution.dashboards.pageTitle": "Tableaux de bord",
"xpack.securitySolution.dashboards.queryError": "Erreur lors de la récupération des tableaux de bord de sécurité",
"xpack.securitySolution.dashboards.title": "Titre",
diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json
index 7b1fb5c238f52..86790c7959135 100644
--- a/x-pack/plugins/translations/translations/ja-JP.json
+++ b/x-pack/plugins/translations/translations/ja-JP.json
@@ -33675,7 +33675,6 @@
"xpack.securitySolution.dashboards.description": "説明",
"xpack.securitySolution.dashboards.landing.createButton": "ダッシュボードを作成",
"xpack.securitySolution.dashboards.landing.section.custom": "カスタム",
- "xpack.securitySolution.dashboards.landing.section.default": "デフォルト",
"xpack.securitySolution.dashboards.pageTitle": "ダッシュボード",
"xpack.securitySolution.dashboards.queryError": "セキュリティダッシュボードの取得エラー",
"xpack.securitySolution.dashboards.title": "タイトル",
diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json
index c783af0ea5060..22ff7f4be1b70 100644
--- a/x-pack/plugins/translations/translations/zh-CN.json
+++ b/x-pack/plugins/translations/translations/zh-CN.json
@@ -33718,7 +33718,6 @@
"xpack.securitySolution.dashboards.description": "描述",
"xpack.securitySolution.dashboards.landing.createButton": "创建仪表板",
"xpack.securitySolution.dashboards.landing.section.custom": "定制",
- "xpack.securitySolution.dashboards.landing.section.default": "默认",
"xpack.securitySolution.dashboards.pageTitle": "仪表板",
"xpack.securitySolution.dashboards.queryError": "检索安全仪表板时出错",
"xpack.securitySolution.dashboards.title": "标题",
diff --git a/x-pack/test_serverless/functional/test_suites/security/ftr/cases/attachment_framework.ts b/x-pack/test_serverless/functional/test_suites/security/ftr/cases/attachment_framework.ts
index 99edcd7926905..4e3283c6600d6 100644
--- a/x-pack/test_serverless/functional/test_suites/security/ftr/cases/attachment_framework.ts
+++ b/x-pack/test_serverless/functional/test_suites/security/ftr/cases/attachment_framework.ts
@@ -101,6 +101,7 @@ export default ({ getPageObject, getService }: FtrProviderContext) => {
await common.navigateToApp('security', { path: 'dashboards' });
await header.waitUntilLoadingHasFinished();
+ await testSubjects.click('LandingImageCards-accordionButton');
if (await testSubjects.exists('edit-unsaved-New-Dashboard')) {
await testSubjects.click('edit-unsaved-New-Dashboard');
From bd92b18e407af4106e9308d87f5b85f421a03479 Mon Sep 17 00:00:00 2001
From: Tiago Costa
Date: Mon, 1 Jul 2024 16:19:30 +0100
Subject: [PATCH 21/25] skip flaky suite (#169894)
---
.../management/cypress/e2e/response_actions/responder.cy.ts | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/x-pack/plugins/security_solution/public/management/cypress/e2e/response_actions/responder.cy.ts b/x-pack/plugins/security_solution/public/management/cypress/e2e/response_actions/responder.cy.ts
index e72b5a42eaf1f..3a763fc0e8e23 100644
--- a/x-pack/plugins/security_solution/public/management/cypress/e2e/response_actions/responder.cy.ts
+++ b/x-pack/plugins/security_solution/public/management/cypress/e2e/response_actions/responder.cy.ts
@@ -43,7 +43,8 @@ describe(
login();
});
- describe('from Cases', () => {
+ // FLAKY: https://github.com/elastic/kibana/issues/169894
+ describe.skip('from Cases', () => {
let endpointData: ReturnTypeFromChainable;
let caseData: ReturnTypeFromChainable;
let alertData: ReturnTypeFromChainable;
From 3309c4f723e4ff2550ffa3771d1ca6b7025a3e19 Mon Sep 17 00:00:00 2001
From: Alexey Antonov
Date: Mon, 1 Jul 2024 18:28:36 +0300
Subject: [PATCH 22/25] fix: [Obs Applications > Services][SCREEN READER]:
Tooltips must be able to take keyboard focus: 0001 (#186884)
closes: https://github.com/elastic/observability-dev/issues/3595
## What was done?:
1. Replace `EuiToolTip` with `EuiIconTip` to improve accessibility
(a11y).
---
.../service_list/apm_services_table.tsx | 9 ++++-----
1 file changed, 4 insertions(+), 5 deletions(-)
diff --git a/x-pack/plugins/observability_solution/apm/public/components/app/service_inventory/apm_signal_inventory/service_list/apm_services_table.tsx b/x-pack/plugins/observability_solution/apm/public/components/app/service_inventory/apm_signal_inventory/service_list/apm_services_table.tsx
index 50f8a873b6c8c..de2c45862d30f 100644
--- a/x-pack/plugins/observability_solution/apm/public/components/app/service_inventory/apm_signal_inventory/service_list/apm_services_table.tsx
+++ b/x-pack/plugins/observability_solution/apm/public/components/app/service_inventory/apm_signal_inventory/service_list/apm_services_table.tsx
@@ -9,7 +9,6 @@ import {
EuiBadge,
EuiFlexGroup,
EuiFlexItem,
- EuiIcon,
EuiIconTip,
EuiLink,
EuiSpacer,
@@ -436,15 +435,15 @@ export function ApmServicesTable({
)}
{maxCountExceeded && (
-
-
-
+ />
)}
From 06a77a73b4a9963e94b0b47611f46594e6ccc4ce Mon Sep 17 00:00:00 2001
From: Alexey Antonov
Date: Mon, 1 Jul 2024 18:29:25 +0300
Subject: [PATCH 23/25] fix: Add `rowHeader` attribute for
`observability_solution -> managed_table` (#186625)
Closes: https://github.com/elastic/observability-dev/issues/3542
Closes: https://github.com/elastic/observability-dev/issues/3543
Closes: https://github.com/elastic/observability-dev/issues/3544
Closes: https://github.com/elastic/observability-dev/issues/3545
Closes: https://github.com/elastic/observability-dev/issues/3546
Closes: https://github.com/elastic/observability-dev/issues/3550
Closes: https://github.com/elastic/observability-dev/issues/3551
Closes: https://github.com/elastic/observability-dev/issues/3552
Closes: https://github.com/elastic/observability-dev/issues/3549
## Summary
This PR introduces support for the rowHeader attribute in
`ManagedTable`. By default, the first column is marked as
`TH[scope="row"]`. However, users now have the flexibility to either
override this value or set it to 'false' to disable this feature.
---
.../apm/public/components/shared/managed_table/index.tsx | 3 +++
1 file changed, 3 insertions(+)
diff --git a/x-pack/plugins/observability_solution/apm/public/components/shared/managed_table/index.tsx b/x-pack/plugins/observability_solution/apm/public/components/shared/managed_table/index.tsx
index e1d22482a30f3..dcc5a5ee9c677 100644
--- a/x-pack/plugins/observability_solution/apm/public/components/shared/managed_table/index.tsx
+++ b/x-pack/plugins/observability_solution/apm/public/components/shared/managed_table/index.tsx
@@ -66,6 +66,7 @@ export const shouldfetchServer = ({
function UnoptimizedManagedTable(props: {
items: T[];
columns: Array>;
+ rowHeader?: string | false;
noItemsMessage?: React.ReactNode;
isLoading?: boolean;
error?: boolean;
@@ -96,6 +97,7 @@ function UnoptimizedManagedTable(props: {
const {
items,
columns,
+ rowHeader,
noItemsMessage,
isLoading = false,
error = false,
@@ -285,6 +287,7 @@ function UnoptimizedManagedTable(props: {
}
items={renderedItems}
columns={columns as unknown as Array>} // EuiBasicTableColumn is stricter than ITableColumn
+ rowHeader={rowHeader === false ? undefined : rowHeader ?? columns[0]?.field}
sorting={sorting}
onChange={onTableChange}
{...(paginationProps ? { pagination: paginationProps } : {})}
From 3dddc52056820d88435ddc40c71cfd5192c1d1a3 Mon Sep 17 00:00:00 2001
From: Alexey Antonov
Date: Mon, 1 Jul 2024 18:30:12 +0300
Subject: [PATCH 24/25] fix: [Obs Synthetics > Monitor Test Run][SCREEN
READER]: Table rows need TH[scope="row"] for SR usability: 0017 (#186957)
Closes: https://github.com/elastic/observability-dev/issues/3570
## Summary
`rowHeader` attribute was added for the following table
---
.../apps/synthetics/components/common/components/stderr_logs.tsx | 1 +
1 file changed, 1 insertion(+)
diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/common/components/stderr_logs.tsx b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/common/components/stderr_logs.tsx
index 2b332ec78671f..ac9a313aaf1b1 100644
--- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/common/components/stderr_logs.tsx
+++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/common/components/stderr_logs.tsx
@@ -129,6 +129,7 @@ export const StdErrorLogs = ({
Date: Mon, 1 Jul 2024 18:30:49 +0300
Subject: [PATCH 25/25] fix: [Obs Infrastructure > Hosts, Kubernetes,
Docker][SCREEN READER]: Table rows need TH[scope="row"] for SR usability:
0016 (#186962)
Closes: https://github.com/elastic/observability-dev/issues/3569
## Summary
`rowHeader` attribute was added for the following table
---
.../metrics/inventory_view/components/table_view.tsx | 8 +++++++-
1 file changed, 7 insertions(+), 1 deletion(-)
diff --git a/x-pack/plugins/observability_solution/infra/public/pages/metrics/inventory_view/components/table_view.tsx b/x-pack/plugins/observability_solution/infra/public/pages/metrics/inventory_view/components/table_view.tsx
index 4791875958c67..f90176edddaf9 100644
--- a/x-pack/plugins/observability_solution/infra/public/pages/metrics/inventory_view/components/table_view.tsx
+++ b/x-pack/plugins/observability_solution/infra/public/pages/metrics/inventory_view/components/table_view.tsx
@@ -162,6 +162,12 @@ export const TableView = (props: Props) => {
);
return (
-
+
);
};