Skip to content

Commit

Permalink
[Security Solution] One Discover - Enable Security Solution Expandabl…
Browse files Browse the repository at this point in the history
…e Flyout in One Discover entities (elastic#189633)

>[!Note]
> This Change is only applicable to Serverless Security Solution as of
now. In follow-up PRs, support will be added to ESS as well based
data-sources such as index or intergrations.
## Summary

Resolves elastic#189151

This PR is foundation for the work described in
elastic#186783. This just enables
expandable flyout for entity details, which is currently only used in
security solution, in discover as well.

As a part of **One Discover** work, we need to make sure that cell
rendering in Discover should behave exactly like it does in security
solution.

To enable this, a new `shared-browser` package
`@kbn/security-solution-common` in `x-pack/packages/security-solution`
has been created which can used to share components between `security
solution` and `discover`. Below is the usage pattern

```mermaid
flowchart TD
    disc-utils[@kbn/discover-utils] --> sscommon
    sscommon[@kbn/security-solution-common] --> ssplugin[security_solution]
    sscommon[@kbn/security-solution-common] --> discover[discover]
    disc-utils[@kbn/discover-utils] --> discover
```


## Desk Testing Guide.

1. Enable Security profile in serverless by adding below to `kibana.yml`

```yaml
discover.experimental.enabledProfiles: ['security-root-profile']
```

2. Load Some data

4. Navigate to discover and add `host.name` as one of the column.

5. Should open an expandable flyout as shown below.


https://github.com/user-attachments/assets/92b84c89-8769-45dd-bf7e-a9fe527fdcf0

## Code Review Guide

Most of the changes in the PR are code-organization. There are NO
changes in security solution but only the changes to import statements.

You can focus regarding the changes in below packages:

- x-pack/packages/security-solution/common
- packages/kbn-discover-utils
- packages/kbn-expandable-flyout

---------

Co-authored-by: kibanamachine <[email protected]>
  • Loading branch information
logeekal and kibanamachine authored Aug 27, 2024
1 parent 5075b04 commit 9293bc1
Show file tree
Hide file tree
Showing 160 changed files with 966 additions and 317 deletions.
4 changes: 3 additions & 1 deletion .github/CODEOWNERS
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
Expand Up @@ -762,6 +762,7 @@ x-pack/packages/security/plugin_types_common @elastic/kibana-security
x-pack/packages/security/plugin_types_public @elastic/kibana-security
x-pack/packages/security/plugin_types_server @elastic/kibana-security
x-pack/packages/security/role_management_model @elastic/kibana-security
x-pack/packages/security-solution/common @elastic/security-threat-hunting-investigations
x-pack/packages/security-solution/distribution_bar @elastic/kibana-cloud-security-posture
x-pack/plugins/security_solution_ess @elastic/security-solution
x-pack/packages/security-solution/features @elastic/security-threat-hunting-explore
Expand Down Expand Up @@ -931,7 +932,7 @@ test/plugin_functional/plugins/ui_settings_plugin @elastic/kibana-core
packages/kbn-ui-shared-deps-npm @elastic/kibana-operations
packages/kbn-ui-shared-deps-src @elastic/kibana-operations
packages/kbn-ui-theme @elastic/kibana-operations
packages/kbn-unified-data-table @elastic/kibana-data-discovery
packages/kbn-unified-data-table @elastic/kibana-data-discovery @elastic/security-threat-hunting-investigations
packages/kbn-unified-doc-viewer @elastic/kibana-data-discovery
examples/unified_doc_viewer @elastic/kibana-core
src/plugins/unified_doc_viewer @elastic/kibana-data-discovery
Expand Down Expand Up @@ -1036,6 +1037,7 @@ packages/kbn-zod-helpers @elastic/security-detection-rule-management
/x-pack/test_serverless/functional/test_suites/common/examples/search_examples @elastic/kibana-data-discovery
/x-pack/test_serverless/functional/test_suites/common/examples/unified_field_list_examples @elastic/kibana-data-discovery
/x-pack/test_serverless/functional/test_suites/common/management/data_views @elastic/kibana-data-discovery
src/plugins/discover/public/context_awareness/profile_providers/security @elastic/kibana-data-discovery @elastic/security-threat-hunting-investigations

# Visualizations
/src/plugins/visualize/ @elastic/kibana-visualizations
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -778,6 +778,7 @@
"@kbn/security-plugin-types-public": "link:x-pack/packages/security/plugin_types_public",
"@kbn/security-plugin-types-server": "link:x-pack/packages/security/plugin_types_server",
"@kbn/security-role-management-model": "link:x-pack/packages/security/role_management_model",
"@kbn/security-solution-common": "link:x-pack/packages/security-solution/common",
"@kbn/security-solution-distribution-bar": "link:x-pack/packages/security-solution/distribution_bar",
"@kbn/security-solution-ess": "link:x-pack/plugins/security_solution_ess",
"@kbn/security-solution-features": "link:x-pack/packages/security-solution/features",
Expand Down
1 change: 1 addition & 0 deletions packages/kbn-discover-utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ export {
getLogLevelCoalescedValueLabel,
LogLevelCoalescedValue,
LogLevelBadge,
getFieldValue,
} from './src';

export type { LogsContextService } from './src';
Expand Down
33 changes: 33 additions & 0 deletions packages/kbn-discover-utils/src/utils/get_field_value.test.ts
Original file line number Diff line number Diff line change
@@ -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 { DataTableRecord } from '../types';
import { getFieldValue } from './get_field_value';

const dataTableRecord: DataTableRecord = {
id: '1',
raw: {},
flattened: {
'field1.value': 'value1',
'field2.value': ['value2'],
},
};

describe('getFieldValue', () => {
it('should return the value of field correctly', () => {
expect(getFieldValue(dataTableRecord, 'field1.value')).toBe('value1');
});

it('should return the first value of field correctly if field has a value of Array type', () => {
expect(getFieldValue(dataTableRecord, 'field2.value')).toBe('value2');
});

it('should return undefined when field is not available', () => {
expect(getFieldValue(dataTableRecord, 'field3.value')).toBeUndefined();
});
});
14 changes: 14 additions & 0 deletions packages/kbn-discover-utils/src/utils/get_field_value.ts
Original file line number Diff line number Diff line change
@@ -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 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 { DataTableRecord } from '../types';

export const getFieldValue = (record: DataTableRecord, field: string) => {
const value = record.flattened[field];
return Array.isArray(value) ? value[0] : value;
};
1 change: 1 addition & 0 deletions packages/kbn-discover-utils/src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,6 @@ export * from './get_log_document_overview';
export * from './get_message_field_with_fallbacks';
export * from './get_should_show_field_handler';
export * from './nested_fields';
export * from './get_field_value';
export * from './calc_field_counts';
export { isLegacyTableEnabled } from './is_legacy_table_enabled';
39 changes: 39 additions & 0 deletions packages/kbn-expandable-flyout/__mocks__/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

import React from 'react';

export const useExpandableFlyoutApi = jest.fn(() => ({
openFlyout: jest.fn(),
closeFlyout: jest.fn(),
openPanels: jest.fn(),
openRightPanel: jest.fn(),
openLeftPanel: jest.fn(),
openPreviewPanel: jest.fn(),
closeRightPanel: jest.fn(),
closeLeftPanel: jest.fn(),
closePreviewPanel: jest.fn(),
closePanels: jest.fn(),
previousPreviewPanel: jest.fn(),
}));

export const useExpandableFlyoutState = jest.fn();

export const ExpandableFlyoutProvider = jest.fn(({ children }: React.PropsWithChildren<{}>) => {
return <>{children}</>;
});

export const withExpandableFlyoutProvider = <T extends object>(
Component: React.ComponentType<T>
) => {
return (props: T) => {
return <Component {...props} />;
};
};

export const ExpandableFlyout = jest.fn();
1 change: 1 addition & 0 deletions packages/kbn-expandable-flyout/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export { useExpandableFlyoutState } from './src/hooks/use_expandable_flyout_stat
export { type FlyoutState as ExpandableFlyoutState } from './src/state';

export { ExpandableFlyoutProvider } from './src/provider';
export { withExpandableFlyoutProvider } from './src/with_provider';

export type { ExpandableFlyoutProps } from './src';
export type { FlyoutPanelProps, PanelPath, ExpandableFlyoutApi } from './src/types';
23 changes: 23 additions & 0 deletions packages/kbn-expandable-flyout/src/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -110,4 +110,27 @@ describe('ExpandableFlyout', () => {

expect(getByTestId(PREVIEW_SECTION_TEST_ID)).toBeInTheDocument();
});

it('should not render flyout when right has value but does not matches registered panels', () => {
const state = {
byId: {
[id]: {
right: {
id: 'key1',
},
left: undefined,
preview: undefined,
},
},
};

const { queryByTestId } = render(
<TestProvider state={state}>
<ExpandableFlyout data-test-subj="my-test-flyout" registeredPanels={registeredPanels} />
</TestProvider>
);

expect(queryByTestId('my-test-flyout')).toBeNull();
expect(queryByTestId(RIGHT_SECTION_TEST_ID)).toBeNull();
});
});
4 changes: 3 additions & 1 deletion packages/kbn-expandable-flyout/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -86,14 +86,16 @@ export const ExpandableFlyout: React.FC<ExpandableFlyoutProps> = ({
showPreview,
});

const hideFlyout = !left && !right && !preview?.length;
const hideFlyout = !(left && leftSection) && !(right && rightSection) && !preview?.length;

if (hideFlyout) {
return null;
}

return (
<EuiFlyout
{...flyoutProps}
data-panel-id={right?.id ?? ''}
size={flyoutWidth}
ownFocus={false}
onClose={(e) => {
Expand Down
29 changes: 29 additions & 0 deletions packages/kbn-expandable-flyout/src/with_provider.test.tsx
Original file line number Diff line number Diff line change
@@ -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 { render } from '@testing-library/react';
import { useExpandableFlyoutApi } from './hooks/use_expandable_flyout_api';
import React from 'react';
import { withExpandableFlyoutProvider } from './with_provider';

const TestComponent = () => {
useExpandableFlyoutApi();

return <div data-test-subj="test-comp" />;
};

describe('withExpandableFlyoutProvider', () => {
it('should throw when rendered without Expandable Provider', () => {
expect(() => render(<TestComponent />)).toThrow();
});

it('should not throw when rendered with Expandable Provider', () => {
const TestComponentWithProvider = withExpandableFlyoutProvider(TestComponent);
expect(() => render(<TestComponentWithProvider />)).not.toThrow();
});
});
25 changes: 25 additions & 0 deletions packages/kbn-expandable-flyout/src/with_provider.tsx
Original file line number Diff line number Diff line change
@@ -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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

import React from 'react';
import { ComponentType } from 'react';
import { ExpandableFlyoutContextProviderProps } from './context';
import { ExpandableFlyoutProvider } from './provider';

export const withExpandableFlyoutProvider = <Props extends {}>(
Component: ComponentType<Props>,
expandableProviderProps?: ExpandableFlyoutContextProviderProps
) => {
return (props: Props) => {
return (
<ExpandableFlyoutProvider {...(expandableProviderProps ?? {})}>
<Component {...props} />
</ExpandableFlyoutProvider>
);
};
};
5 changes: 4 additions & 1 deletion packages/kbn-unified-data-table/kibana.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,8 @@
"type": "shared-browser",
"id": "@kbn/unified-data-table",
"description": "Contains functionality for the unified data table which can be integrated into apps",
"owner": "@elastic/kibana-data-discovery"
"owner": [
"@elastic/kibana-data-discovery",
"@elastic/security-threat-hunting-investigations"
]
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
*/

import { EuiBadge } from '@elastic/eui';
import type { DataTableRecord } from '@kbn/discover-utils';
import { getFieldValue } from '@kbn/discover-utils';
import type { RowControlColumn } from '@kbn/unified-data-table';
import { isOfAggregateQueryType } from '@kbn/es-query';
import { getIndexPatternFromESQLQuery } from '@kbn/esql-utils';
Expand All @@ -19,6 +19,7 @@ import { DataSourceCategory, DataSourceProfileProvider } from '../../profiles';

export const exampleDataSourceProfileProvider: DataSourceProfileProvider = {
profileId: 'example-data-source-profile',
isExperimental: true,
profile: {
getCellRenderers: (prev) => () => ({
...prev(),
Expand Down Expand Up @@ -137,8 +138,3 @@ export const exampleDataSourceProfileProvider: DataSourceProfileProvider = {
};
},
};

const getFieldValue = (record: DataTableRecord, field: string) => {
const value = record.flattened[field];
return Array.isArray(value) ? value[0] : value;
};
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,12 @@
* Side Public License, v 1.
*/

import type { DataTableRecord } from '@kbn/discover-utils';
import { getFieldValue } from '@kbn/discover-utils';
import { DocumentProfileProvider, DocumentType } from '../../profiles';

export const exampleDocumentProfileProvider: DocumentProfileProvider = {
profileId: 'example-document-profile',
isExperimental: true,
profile: {},
resolve: (params) => {
if (getFieldValue(params.record, 'data_stream.type') !== 'example') {
Expand All @@ -25,8 +26,3 @@ export const exampleDocumentProfileProvider: DocumentProfileProvider = {
};
},
};

const getFieldValue = (record: DataTableRecord, field: string) => {
const value = record.flattened[field];
return Array.isArray(value) ? value[0] : value;
};
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { RootProfileProvider, SolutionType } from '../../profiles';

export const exampleRootProfileProvider: RootProfileProvider = {
profileId: 'example-root-profile',
isExperimental: true,
profile: {
getCellRenderers: (prev) => () => ({
...prev(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,34 +12,51 @@ import { exampleDataSourceProfileProvider } from './example_data_source_profile'
import { exampleDocumentProfileProvider } from './example_document_profile';
import { exampleRootProfileProvider } from './example_root_pofile';
import {
registerEnabledProfileProviders,
registerProfileProviders,
registerEnabledProfileProviders,
} from './register_profile_providers';

describe('registerEnabledProfileProviders', () => {
it('should register enabled profile providers', async () => {
it('should register all profile providers', async () => {
const { rootProfileServiceMock, rootProfileProviderMock } = createContextAwarenessMocks({
shouldRegisterProviders: false,
});
registerEnabledProfileProviders({
profileService: rootProfileServiceMock,
availableProviders: [rootProfileProviderMock],
enabledProfileIds: ['root-profile'],
providers: [rootProfileProviderMock],
enabledExperimentalProfileIds: [],
});
const context = await rootProfileServiceMock.resolve({ solutionNavId: null });
expect(rootProfileServiceMock.getProfile(context)).toBe(rootProfileProviderMock.profile);
});

it('should not register disabled profile providers', async () => {
it('should not register experimental profile providers by default', async () => {
const { rootProfileServiceMock } = createContextAwarenessMocks({
shouldRegisterProviders: false,
});

registerEnabledProfileProviders({
profileService: rootProfileServiceMock,
providers: [exampleRootProfileProvider],
enabledExperimentalProfileIds: [],
});
const context = await rootProfileServiceMock.resolve({ solutionNavId: null });
expect(rootProfileServiceMock.getProfile(context)).not.toBe(exampleRootProfileProvider.profile);
expect(rootProfileServiceMock.getProfile(context)).toMatchObject({});
});

it('should register experimental profile providers when enabled by config', async () => {
const { rootProfileServiceMock, rootProfileProviderMock } = createContextAwarenessMocks({
shouldRegisterProviders: false,
});

registerEnabledProfileProviders({
profileService: rootProfileServiceMock,
availableProviders: [rootProfileProviderMock],
enabledProfileIds: [],
providers: [exampleRootProfileProvider],
enabledExperimentalProfileIds: [exampleRootProfileProvider.profileId],
});
const context = await rootProfileServiceMock.resolve({ solutionNavId: null });
expect(rootProfileServiceMock.getProfile(context)).toBe(exampleRootProfileProvider.profile);
expect(rootProfileServiceMock.getProfile(context)).not.toBe(rootProfileProviderMock.profile);
});
});
Expand All @@ -54,7 +71,7 @@ describe('registerProfileProviders', () => {
rootProfileService: rootProfileServiceMock,
dataSourceProfileService: dataSourceProfileServiceMock,
documentProfileService: documentProfileServiceMock,
experimentalProfileIds: [
enabledExperimentalProfileIds: [
exampleRootProfileProvider.profileId,
exampleDataSourceProfileProvider.profileId,
exampleDocumentProfileProvider.profileId,
Expand Down Expand Up @@ -93,7 +110,7 @@ describe('registerProfileProviders', () => {
rootProfileService: rootProfileServiceMock,
dataSourceProfileService: dataSourceProfileServiceMock,
documentProfileService: documentProfileServiceMock,
experimentalProfileIds: [],
enabledExperimentalProfileIds: [],
});
const rootContext = await rootProfileServiceMock.resolve({ solutionNavId: null });
const dataSourceContext = await dataSourceProfileServiceMock.resolve({
Expand Down
Loading

0 comments on commit 9293bc1

Please sign in to comment.