[Security Solution] Remove usage of deprecated modules for mounting React, Part II (#182061)

## Summary

Partially addresses https://github.com/elastic/kibana-team/issues/805

These changes come up from searching in the code and finding where
certain kinds of deprecated AppEx-SharedUX modules are imported.
**Reviewers: Please interact with critical paths through the UI
components touched in this PR, ESPECIALLY in terms of testing dark mode
and i18n.**

This is the **2nd** PR to focus on code within **Security Solution**,
following https://github.com/elastic/kibana/pull/181099.

<img width="1196" alt="image"
src="7f8d3707-94f0-4746-8dd5-dd858ce027f9">

Note: this also makes inclusion of `i18n` and `analytics` dependencies
consistent. Analytics is an optional dependency for the SharedUX
modules, which wrap `KibanaErrorBoundaryProvider` and is designed to
capture telemetry about errors that are caught in the error boundary.

### Checklist

Delete any items that are not applicable to this PR.

- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
- [ ] This renders correctly on smaller devices using a responsive
layout. (You can test this [in your
browser](https://www.browserstack.com/guide/responsive-testing-on-local-server))
- [ ] This was checked for [cross-browser
compatibility](https://www.elastic.co/support/matrix#matrix_browsers)
This commit is contained in:
Tim Sullivan 2024-05-06 17:40:21 -07:00 committed by GitHub
parent b7057ce083
commit a6ddf518e2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
26 changed files with 330 additions and 271 deletions

View file

@ -36,14 +36,14 @@ export const DetectionRuleCounter = ({ tags, createRuleFn }: DetectionRuleCounte
const [isCreateRuleLoading, setIsCreateRuleLoading] = useState(false);
const queryClient = useQueryClient();
const { http, notifications } = useKibana().services;
const { http, notifications, analytics, i18n, theme } = useKibana().services;
const history = useHistory();
const [, setRulesTable] = useSessionStorage(RULES_TABLE_SESSION_STORAGE_KEY);
const rulePageNavigation = useCallback(async () => {
await setRulesTable({
setRulesTable({
tags,
});
history.push({
@ -58,14 +58,15 @@ export const DetectionRuleCounter = ({ tags, createRuleFn }: DetectionRuleCounte
}, [history]);
const createDetectionRuleOnClick = useCallback(async () => {
const startServices = { analytics, notifications, i18n, theme };
setIsCreateRuleLoading(true);
const ruleResponse = await createRuleFn(http);
setIsCreateRuleLoading(false);
showCreateDetectionRuleSuccessToast(notifications, http, ruleResponse);
showCreateDetectionRuleSuccessToast(startServices, http, ruleResponse);
// Triggering a refetch of rules and alerts to update the UI
queryClient.invalidateQueries([DETECTION_ENGINE_RULES_KEY]);
queryClient.invalidateQueries([DETECTION_ENGINE_ALERTS_KEY]);
}, [createRuleFn, http, notifications, queryClient]);
}, [createRuleFn, http, analytics, notifications, i18n, theme, queryClient]);
return (
<EuiSkeletonText

View file

@ -16,14 +16,15 @@ import {
EuiText,
useGeneratedHtmlId,
} from '@elastic/eui';
import { toMountPoint } from '@kbn/kibana-react-plugin/public';
import type { HttpSetup, NotificationsStart } from '@kbn/core/public';
import { toMountPoint } from '@kbn/react-kibana-mount';
import type { HttpSetup } from '@kbn/core/public';
import { FormattedMessage } from '@kbn/i18n-react';
import { QueryClient, useQueryClient } from '@tanstack/react-query';
import type { RuleResponse } from '../common/types';
import { CREATE_RULE_ACTION_SUBJ, TAKE_ACTION_SUBJ } from './test_subjects';
import { useKibana } from '../common/hooks/use_kibana';
import { DETECTION_ENGINE_ALERTS_KEY, DETECTION_ENGINE_RULES_KEY } from '../common/constants';
import { CloudSecurityPostureStartServices } from '../types';
const RULE_PAGE_PATH = '/app/security/rules/id/';
@ -35,10 +36,13 @@ interface TakeActionProps {
}
export const showCreateDetectionRuleSuccessToast = (
notifications: NotificationsStart,
cloudSecurityStartServices: CloudSecurityPostureStartServices,
http: HttpSetup,
ruleResponse: RuleResponse
) => {
const { notifications, analytics, i18n, theme } = cloudSecurityStartServices;
const startServices = { analytics, i18n, theme };
return notifications.toasts.addSuccess({
toastLifeTimeMs: 10000,
color: 'success',
@ -60,7 +64,8 @@ export const showCreateDetectionRuleSuccessToast = (
defaultMessage="Add rule actions to get notified when alerts are generated."
/>
</EuiText>
</div>
</div>,
startServices
),
text: toMountPoint(
<div>
@ -78,19 +83,23 @@ export const showCreateDetectionRuleSuccessToast = (
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
</div>
</div>,
startServices
),
});
};
export const showChangeBenchmarkRuleStatesSuccessToast = (
notifications: NotificationsStart,
cloudSecurityStartServices: CloudSecurityPostureStartServices,
isBenchmarkRuleMuted: boolean,
data: {
numberOfRules: number;
numberOfDetectionRules: number;
}
) => {
const { notifications, analytics, i18n, theme } = cloudSecurityStartServices;
const startServices = { analytics, i18n, theme };
return notifications.toasts.addSuccess({
toastLifeTimeMs: 10000,
color: 'success',
@ -111,7 +120,8 @@ export const showChangeBenchmarkRuleStatesSuccessToast = (
/>
)}
</strong>
</EuiText>
</EuiText>,
startServices
),
text: toMountPoint(
<div>
@ -145,7 +155,8 @@ export const showChangeBenchmarkRuleStatesSuccessToast = (
)}
</>
)}
</div>
</div>,
startServices
),
});
};
@ -171,8 +182,6 @@ export const TakeAction = ({
prefix: 'smallContextMenuPopover',
});
const { http, notifications } = useKibana().services;
const button = (
<EuiButton
isLoading={isLoading}
@ -193,8 +202,6 @@ export const TakeAction = ({
createRuleFn={createRuleFn}
setIsLoading={setIsLoading}
closePopover={closePopover}
notifications={notifications}
http={http}
queryClient={queryClient}
isCreateDetectionRuleDisabled={isCreateDetectionRuleDisabled}
/>
@ -206,9 +213,6 @@ export const TakeAction = ({
enableBenchmarkRuleFn={enableBenchmarkRuleFn}
setIsLoading={setIsLoading}
closePopover={closePopover}
notifications={notifications}
http={http}
queryClient={queryClient}
/>
);
if (disableBenchmarkRuleFn)
@ -218,9 +222,6 @@ export const TakeAction = ({
disableBenchmarkRuleFn={disableBenchmarkRuleFn}
setIsLoading={setIsLoading}
closePopover={closePopover}
notifications={notifications}
http={http}
queryClient={queryClient}
/>
);
@ -243,19 +244,17 @@ const CreateDetectionRule = ({
createRuleFn,
setIsLoading,
closePopover,
notifications,
http,
queryClient,
isCreateDetectionRuleDisabled = false,
}: {
createRuleFn: (http: HttpSetup) => Promise<RuleResponse>;
setIsLoading: (isLoading: boolean) => void;
closePopover: () => void;
notifications: NotificationsStart;
http: HttpSetup;
queryClient: QueryClient;
isCreateDetectionRuleDisabled: boolean;
}) => {
const { http, ...startServices } = useKibana().services;
return (
<EuiContextMenuItem
key="createRule"
@ -265,7 +264,7 @@ const CreateDetectionRule = ({
setIsLoading(true);
const ruleResponse = await createRuleFn(http);
setIsLoading(false);
showCreateDetectionRuleSuccessToast(notifications, http, ruleResponse);
showCreateDetectionRuleSuccessToast(startServices, http, ruleResponse);
// Triggering a refetch of rules and alerts to update the UI
queryClient.invalidateQueries([DETECTION_ENGINE_RULES_KEY]);
queryClient.invalidateQueries([DETECTION_ENGINE_ALERTS_KEY]);
@ -284,14 +283,10 @@ const EnableBenchmarkRule = ({
enableBenchmarkRuleFn,
setIsLoading,
closePopover,
notifications,
}: {
enableBenchmarkRuleFn: () => Promise<void>;
setIsLoading: (isLoading: boolean) => void;
closePopover: () => void;
notifications: NotificationsStart;
http: HttpSetup;
queryClient: QueryClient;
}) => {
return (
<EuiContextMenuItem
@ -313,14 +308,10 @@ const DisableBenchmarkRule = ({
disableBenchmarkRuleFn,
setIsLoading,
closePopover,
notifications,
}: {
disableBenchmarkRuleFn: () => Promise<void>;
setIsLoading: (isLoading: boolean) => void;
closePopover: () => void;
notifications: NotificationsStart;
http: HttpSetup;
queryClient: QueryClient;
}) => {
return (
<EuiContextMenuItem

View file

@ -71,8 +71,10 @@ export const RuleFlyout = ({ onClose, rule, refetchRulesStates }: RuleFlyoutProp
const { data: rulesData } = useFetchDetectionRulesByTags(
getFindingsDetectionRuleSearchTags(rule.metadata)
);
const { notifications, analytics, i18n: i18nStart, theme } = useKibana().services;
const startServices = { notifications, analytics, i18n: i18nStart, theme };
const isRuleMuted = rule?.state === 'muted';
const { notifications } = useKibana().services;
const switchRuleStates = async () => {
if (rule.metadata.benchmark.rule_number) {
const rulesObjectRequest = {
@ -83,8 +85,8 @@ export const RuleFlyout = ({ onClose, rule, refetchRulesStates }: RuleFlyoutProp
};
const nextRuleStates = isRuleMuted ? 'unmute' : 'mute';
await postRequestChangeRulesStates(nextRuleStates, [rulesObjectRequest]);
await refetchRulesStates();
await showChangeBenchmarkRuleStatesSuccessToast(notifications, isRuleMuted, {
refetchRulesStates();
showChangeBenchmarkRuleStatesSuccessToast(startServices, isRuleMuted, {
numberOfRules: 1,
numberOfDetectionRules: rulesData?.total || 0,
});

View file

@ -20,8 +20,9 @@ import {
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { uniqBy } from 'lodash';
import { CoreStart, HttpSetup, NotificationsStart } from '@kbn/core/public';
import { useKibana } from '@kbn/kibana-react-plugin/public';
import { HttpSetup } from '@kbn/core/public';
import { CloudSecurityPostureStartServices } from '../../types';
import { useKibana } from '../../common/hooks/use_kibana';
import { getFindingsDetectionRuleSearchTags } from '../../../common/utils/detection_rules';
import { ColumnNameWithTooltip } from '../../components/column_name_with_tooltip';
import type { CspBenchmarkRulesWithStates, RulesState } from './rules_container';
@ -61,8 +62,8 @@ type GetColumnProps = Pick<
currentPageRulesArray: CspBenchmarkRulesWithStates[],
selectedRulesArray: CspBenchmarkRulesWithStates[]
) => boolean;
notifications: NotificationsStart;
http: HttpSetup;
startServices: CloudSecurityPostureStartServices;
};
export const RulesTable = ({
@ -132,40 +133,42 @@ export const RulesTable = ({
return true;
};
const { http, notifications } = useKibana<CoreStart>().services;
const { http, notifications, analytics, i18n: i18nStart, theme } = useKibana().services;
useEffect(() => {
if (selectedRules.length >= items.length && items.length > 0 && selectedRules.length > 0)
setIsAllRulesSelectedThisPage(true);
else setIsAllRulesSelectedThisPage(false);
}, [items.length, selectedRules.length]);
const columns = useMemo(
() =>
getColumns({
refetchRulesStates,
postRequestChangeRulesStates,
selectedRules,
setSelectedRules,
items,
setIsAllRulesSelectedThisPage,
isAllRulesSelectedThisPage,
isCurrentPageRulesASubset,
onRuleClick,
notifications,
http,
}),
[
const columns = useMemo(() => {
const startServices = { notifications, analytics, i18n: i18nStart, theme };
return getColumns({
refetchRulesStates,
postRequestChangeRulesStates,
selectedRules,
setSelectedRules,
items,
setIsAllRulesSelectedThisPage,
isAllRulesSelectedThisPage,
isCurrentPageRulesASubset,
onRuleClick,
notifications,
http,
]
);
startServices,
});
}, [
refetchRulesStates,
postRequestChangeRulesStates,
selectedRules,
setSelectedRules,
items,
isAllRulesSelectedThisPage,
onRuleClick,
notifications,
http,
analytics,
i18nStart,
theme,
]);
return (
<>
@ -194,8 +197,8 @@ const getColumns = ({
isAllRulesSelectedThisPage,
isCurrentPageRulesASubset,
onRuleClick,
notifications,
http,
startServices,
}: GetColumnProps): Array<EuiTableFieldDataColumnType<CspBenchmarkRulesWithStates>> => [
{
field: 'action',
@ -203,7 +206,7 @@ const getColumns = ({
<EuiCheckbox
id={RULES_ROW_SELECT_ALL_CURRENT_PAGE}
checked={isCurrentPageRulesASubset(items, selectedRules) && isAllRulesSelectedThisPage}
onChange={(e) => {
onChange={() => {
const uniqueSelectedRules = uniqBy([...selectedRules, ...items], 'metadata.id');
const onChangeSelectAllThisPageFn = () => {
setSelectedRules(uniqueSelectedRules);
@ -227,7 +230,7 @@ const getColumns = ({
),
width: '40px',
sortable: false,
render: (rules, item: CspBenchmarkRulesWithStates) => {
render: (_rules, item: CspBenchmarkRulesWithStates) => {
return (
<EuiCheckbox
checked={selectedRules.some(
@ -300,7 +303,7 @@ const getColumns = ({
align: 'right',
width: '100px',
truncateText: true,
render: (name, rule: CspBenchmarkRulesWithStates) => {
render: (_name, rule: CspBenchmarkRulesWithStates) => {
const rulesObjectRequest = {
benchmark_id: rule?.metadata.benchmark.id,
benchmark_version: rule?.metadata.benchmark.version,
@ -320,9 +323,9 @@ const getColumns = ({
http
)
).total;
await postRequestChangeRulesStates(nextRuleState, [rulesObjectRequest]);
await refetchRulesStates();
await showChangeBenchmarkRuleStatesSuccessToast(notifications, isRuleMuted, {
postRequestChangeRulesStates(nextRuleState, [rulesObjectRequest]);
refetchRulesStates();
showChangeBenchmarkRuleStatesSuccessToast(startServices, isRuleMuted, {
numberOfRules: 1,
numberOfDetectionRules: detectionRuleCount || 0,
});

View file

@ -254,7 +254,8 @@ const CurrentPageOfTotal = ({
{ match: 'any' }
);
const { notifications } = useKibana().services;
const { notifications, analytics, i18n: i18nStart, theme } = useKibana().services;
const startServices = { notifications, analytics, i18n: i18nStart, theme };
const postRequestChangeRulesState = useChangeCspRuleState();
const changeRulesState = async (state: 'mute' | 'unmute') => {
@ -269,9 +270,9 @@ const CurrentPageOfTotal = ({
// Only do the API Call IF there are no undefined value for rule number in the selected rules
if (!bulkSelectedRules.some((rule) => rule.rule_number === undefined)) {
await postRequestChangeRulesState(state, bulkSelectedRules);
await refetchRulesStates();
await setIsPopoverOpen(false);
await showChangeBenchmarkRuleStatesSuccessToast(notifications, state !== 'mute', {
refetchRulesStates();
setIsPopoverOpen(false);
showChangeBenchmarkRuleStatesSuccessToast(startServices, state !== 'mute', {
numberOfRules: bulkSelectedRules.length,
numberOfDetectionRules: rulesData?.total || 0,
});

View file

@ -47,7 +47,7 @@ export class CspPlugin
private isCloudEnabled?: boolean;
public setup(
core: CoreSetup<CspClientPluginStartDeps, CspClientPluginStart>,
_core: CoreSetup<CspClientPluginStartDeps, CspClientPluginStart>,
plugins: CspClientPluginSetupDeps
): CspClientPluginSetup {
this.isCloudEnabled = plugins.cloud.isCloudEnabled;

View file

@ -14,7 +14,7 @@ import { UiActionsSetup, UiActionsStart } from '@kbn/ui-actions-plugin/public';
import { FieldFormatsStart } from '@kbn/field-formats-plugin/public';
import { IndexPatternFieldEditorStart } from '@kbn/data-view-field-editor-plugin/public';
import type { DataPublicPluginSetup, DataPublicPluginStart } from '@kbn/data-plugin/public';
import { ToastsStart } from '@kbn/core/public';
import { CoreStart, ToastsStart } from '@kbn/core/public';
import { Storage } from '@kbn/kibana-utils-plugin/public';
import type { ChartsPluginStart } from '@kbn/charts-plugin/public';
@ -84,3 +84,8 @@ export interface CspSecuritySolutionContext {
state?: Record<string, string | undefined>;
}>;
}
export type CloudSecurityPostureStartServices = Pick<
CoreStart,
'notifications' | 'analytics' | 'i18n' | 'theme'
>;

View file

@ -63,7 +63,8 @@
"@kbn/alerting-plugin",
"@kbn/code-editor",
"@kbn/code-editor-mock",
"@kbn/search-types"
"@kbn/search-types",
"@kbn/react-kibana-mount"
],
"exclude": ["target/**/*"]
}

View file

@ -5,11 +5,9 @@
* 2.0.
*/
import { EuiErrorBoundary } from '@elastic/eui';
import React from 'react';
import ReactDOM from 'react-dom';
import { Router } from '@kbn/shared-ux-router';
import { I18nProvider } from '@kbn/i18n-react';
import { QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
@ -20,17 +18,17 @@ import { OsqueryApp } from './components/app';
import { PLUGIN_NAME } from '../common';
import { KibanaContextProvider } from './common/lib/kibana';
import { queryClient } from './query_client';
import { KibanaThemeProvider } from './shared_imports';
import { KibanaRenderContextProvider } from './shared_imports';
export const renderApp = (
core: CoreStart,
services: AppPluginStartDependencies,
{ element, history, theme$ }: AppMountParameters,
{ element, history }: AppMountParameters,
storage: Storage,
kibanaVersion: string
) => {
ReactDOM.render(
<KibanaThemeProvider theme$={theme$}>
<KibanaRenderContextProvider {...core}>
<KibanaContextProvider
// eslint-disable-next-line react-perf/jsx-no-new-object-as-prop
services={{
@ -41,18 +39,14 @@ export const renderApp = (
storage,
}}
>
<EuiErrorBoundary>
<Router history={history}>
<I18nProvider>
<QueryClientProvider client={queryClient}>
<OsqueryApp />
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
</I18nProvider>
</Router>
</EuiErrorBoundary>
<Router history={history}>
<QueryClientProvider client={queryClient}>
<OsqueryApp />
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
</Router>
</KibanaContextProvider>
</KibanaThemeProvider>,
</KibanaRenderContextProvider>,
element
);

View file

@ -5,12 +5,13 @@
* 2.0.
*/
import { EuiComment, EuiErrorBoundary } from '@elastic/eui';
import { EuiComment } from '@elastic/eui';
import React, { useState, useEffect } from 'react';
import { FormattedRelative } from '@kbn/i18n-react';
import type { CoreStart } from '@kbn/core-lifecycle-browser';
import { KibanaContextProvider, KibanaThemeProvider } from '@kbn/kibana-react-plugin/public';
import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
import { KibanaRenderContextProvider } from '@kbn/react-kibana-context-render';
import { QueryClientProvider } from '@tanstack/react-query';
import { EmptyPrompt } from '../../routes/components/empty_prompt';
import { useKibana } from '../../common/lib/kibana';
@ -72,15 +73,13 @@ const OsqueryActionResultWrapperComponent: React.FC<OsqueryActionResultsWrapperP
services,
...restProps
}) => (
<KibanaThemeProvider theme$={services.theme.theme$}>
<KibanaRenderContextProvider {...services}>
<KibanaContextProvider services={services}>
<EuiErrorBoundary>
<QueryClientProvider client={queryClient}>
<OsqueryActionResult {...restProps} />
</QueryClientProvider>
</EuiErrorBoundary>
<QueryClientProvider client={queryClient}>
<OsqueryActionResult {...restProps} />
</QueryClientProvider>
</KibanaContextProvider>
</KibanaThemeProvider>
</KibanaRenderContextProvider>
);
const OsqueryActionResultWrapper = React.memo(OsqueryActionResultWrapperComponent);

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import { EuiErrorBoundary, EuiSpacer } from '@elastic/eui';
import { EuiSpacer } from '@elastic/eui';
import React from 'react';
import { QueryClientProvider } from '@tanstack/react-query';
import type { CoreStart } from '@kbn/core/public';
@ -14,7 +14,7 @@ import { EmptyPrompt } from '../../routes/components/empty_prompt';
import { KibanaContextProvider, useKibana } from '../../common/lib/kibana';
import { queryClient } from '../../query_client';
import { KibanaThemeProvider } from '../../shared_imports';
import { KibanaRenderContextProvider } from '../../shared_imports';
import type { StartPlugins } from '../../types';
import type { OsqueryActionResultsProps } from './types';
import { OsqueryResult } from './osquery_result';
@ -61,15 +61,13 @@ const OsqueryActionResultsWrapperComponent: React.FC<OsqueryActionResultsWrapper
services,
...restProps
}) => (
<KibanaThemeProvider theme$={services.theme.theme$}>
<KibanaRenderContextProvider {...services}>
<KibanaContextProvider services={services}>
<EuiErrorBoundary>
<QueryClientProvider client={queryClient}>
<OsqueryActionResults {...restProps} />
</QueryClientProvider>
</EuiErrorBoundary>
<QueryClientProvider client={queryClient}>
<OsqueryActionResults {...restProps} />
</QueryClientProvider>
</KibanaContextProvider>
</KibanaThemeProvider>
</KibanaRenderContextProvider>
);
const OsqueryActionResultsWrapper = React.memo(OsqueryActionResultsWrapperComponent);

View file

@ -5,14 +5,13 @@
* 2.0.
*/
import { EuiErrorBoundary } from '@elastic/eui';
import React from 'react';
import { QueryClientProvider } from '@tanstack/react-query';
import type { CoreStart } from '@kbn/core/public';
import { KibanaContextProvider } from '../common/lib/kibana';
import { queryClient } from '../query_client';
import { KibanaThemeProvider } from '../shared_imports';
import { KibanaRenderContextProvider } from '../shared_imports';
import type { StartPlugins } from '../types';
export interface ServicesWrapperProps {
@ -21,13 +20,11 @@ export interface ServicesWrapperProps {
}
const ServicesWrapperComponent: React.FC<ServicesWrapperProps> = ({ services, children }) => (
<KibanaThemeProvider theme$={services.theme.theme$}>
<KibanaRenderContextProvider {...services}>
<KibanaContextProvider services={services}>
<EuiErrorBoundary>
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
</EuiErrorBoundary>
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
</KibanaContextProvider>
</KibanaThemeProvider>
</KibanaRenderContextProvider>
);
const ServicesWrapper = React.memo(ServicesWrapperComponent);

View file

@ -45,4 +45,5 @@ export {
export { fieldValidators } from '@kbn/es-ui-shared-plugin/static/forms/helpers';
export type { ERROR_CODE } from '@kbn/es-ui-shared-plugin/static/forms/helpers/field_validators/types';
export { useUiSetting$, KibanaThemeProvider } from '@kbn/kibana-react-plugin/public';
export { useUiSetting$ } from '@kbn/kibana-react-plugin/public';
export { KibanaRenderContextProvider } from '@kbn/react-kibana-context-render';

View file

@ -22,7 +22,8 @@ export interface AddToTimelineButtonProps {
export const SECURITY_APP_NAME = 'Security';
export const AddToTimelineButton = (props: AddToTimelineButtonProps) => {
const { timelines, appName } = useKibana().services;
const { timelines, appName, analytics, i18n, theme } = useKibana().services;
const startServices = { analytics, i18n, theme };
const { field, value, isIcon, iconProps } = props;
const queryIds = isArray(value) ? value : [value];
@ -60,5 +61,6 @@ export const AddToTimelineButton = (props: AddToTimelineButtonProps) => {
...(isIcon
? { showTooltip: true, Component: TimelineIconComponent }
: { Component: TimelineComponent }),
startServices,
});
};

View file

@ -76,6 +76,7 @@
"@kbn/shared-ux-page-kibana-template",
"@kbn/openapi-generator",
"@kbn/code-editor",
"@kbn/search-types"
"@kbn/search-types",
"@kbn/react-kibana-context-render"
]
}

View file

@ -82,7 +82,7 @@ export const useHoverActionItems = ({
}: UseHoverActionItemsProps): UseHoverActionItems => {
const kibana = useKibana();
const dispatch = useDispatch();
const { timelines, timelineFilterManager } = kibana.services;
const { timelines, timelineFilterManager, analytics, i18n, theme } = kibana.services;
const dataViewId = useDataViewId(getSourcererScopeId(scopeId ?? ''));
// Common actions used by the alert table and alert flyout
@ -168,127 +168,130 @@ export const useHoverActionItems = ({
]
);
const allItems = useMemo(
() =>
[
showFilters ? (
<div data-test-subj="hover-actions-filter-for" key="hover-actions-filter-for">
{getFilterForValueButton({
defaultFocusedButtonRef,
field,
filterManager,
keyboardEvent: stKeyboardEvent,
onClick: handleHoverActionClicked,
onFilterAdded,
ownFocus,
showTooltip: enableOverflowButton ? false : true,
value: values,
dataViewId,
})}
</div>
) : null,
showFilters ? (
<div data-test-subj="hover-actions-filter-out" key="hover-actions-filter-out">
{getFilterOutValueButton({
field,
filterManager,
keyboardEvent: stKeyboardEvent,
onFilterAdded,
ownFocus,
onClick: handleHoverActionClicked,
showTooltip: enableOverflowButton ? false : true,
value: values,
dataViewId,
})}
</div>
) : null,
toggleColumn && !shouldDisableColumnToggle ? (
<div data-test-subj="hover-actions-toggle-column" key="hover-actions-toggle-column">
{getColumnToggleButton({
Component: enableOverflowButton ? EuiContextMenuItem : undefined,
field,
isDisabled: isObjectArray && dataType !== 'geo_point',
isObjectArray,
keyboardEvent: stKeyboardEvent,
ownFocus,
onClick: handleHoverActionClicked,
showTooltip: enableOverflowButton ? false : true,
toggleColumn,
value: values,
})}
</div>
) : null,
values != null && (draggableId != null || !isEmpty(dataProvider)) && !hideAddToTimeline ? (
<div data-test-subj="hover-actions-add-timeline" key="hover-actions-add-timeline">
{getAddToTimelineButton({
Component: enableOverflowButton ? EuiContextMenuItem : undefined,
dataProvider,
draggableId,
field,
keyboardEvent: stKeyboardEvent,
ownFocus,
onClick: onAddToTimelineClicked,
showTooltip: enableOverflowButton ? false : true,
value: values,
})}
</div>
) : null,
allowTopN({
fieldType,
isAggregatable,
fieldName: field,
hideTopN,
})
? showTopNBtn
: null,
field != null ? (
<div data-test-subj="hover-actions-copy-button" key="hover-actions-copy-button">
{getCopyButton({
Component: enableOverflowButton ? EuiContextMenuItem : undefined,
field,
isHoverAction: true,
keyboardEvent: stKeyboardEvent,
ownFocus,
onClick: handleHoverActionClicked,
showTooltip: enableOverflowButton ? false : true,
value: values,
})}
</div>
) : null,
].filter((item) => {
return item != null;
}),
[
dataProvider,
dataType,
defaultFocusedButtonRef,
draggableId,
enableOverflowButton,
field,
fieldType,
isAggregatable,
filterManager,
getAddToTimelineButton,
getColumnToggleButton,
getCopyButton,
getFilterForValueButton,
getFilterOutValueButton,
handleHoverActionClicked,
onAddToTimelineClicked,
hideAddToTimeline,
hideTopN,
isObjectArray,
onFilterAdded,
ownFocus,
shouldDisableColumnToggle,
showFilters,
showTopNBtn,
stKeyboardEvent,
toggleColumn,
values,
dataViewId,
]
) as JSX.Element[];
const allItems = useMemo(() => {
const startServices = { analytics, i18n, theme };
return [
showFilters ? (
<div data-test-subj="hover-actions-filter-for" key="hover-actions-filter-for">
{getFilterForValueButton({
defaultFocusedButtonRef,
field,
filterManager,
keyboardEvent: stKeyboardEvent,
onClick: handleHoverActionClicked,
onFilterAdded,
ownFocus,
showTooltip: enableOverflowButton ? false : true,
value: values,
dataViewId,
})}
</div>
) : null,
showFilters ? (
<div data-test-subj="hover-actions-filter-out" key="hover-actions-filter-out">
{getFilterOutValueButton({
field,
filterManager,
keyboardEvent: stKeyboardEvent,
onFilterAdded,
ownFocus,
onClick: handleHoverActionClicked,
showTooltip: enableOverflowButton ? false : true,
value: values,
dataViewId,
})}
</div>
) : null,
toggleColumn && !shouldDisableColumnToggle ? (
<div data-test-subj="hover-actions-toggle-column" key="hover-actions-toggle-column">
{getColumnToggleButton({
Component: enableOverflowButton ? EuiContextMenuItem : undefined,
field,
isDisabled: isObjectArray && dataType !== 'geo_point',
isObjectArray,
keyboardEvent: stKeyboardEvent,
ownFocus,
onClick: handleHoverActionClicked,
showTooltip: enableOverflowButton ? false : true,
toggleColumn,
value: values,
})}
</div>
) : null,
values != null && (draggableId != null || !isEmpty(dataProvider)) && !hideAddToTimeline ? (
<div data-test-subj="hover-actions-add-timeline" key="hover-actions-add-timeline">
{getAddToTimelineButton({
Component: enableOverflowButton ? EuiContextMenuItem : undefined,
dataProvider,
draggableId,
field,
keyboardEvent: stKeyboardEvent,
ownFocus,
onClick: onAddToTimelineClicked,
showTooltip: enableOverflowButton ? false : true,
value: values,
startServices,
})}
</div>
) : null,
allowTopN({
fieldType,
isAggregatable,
fieldName: field,
hideTopN,
})
? showTopNBtn
: null,
field != null ? (
<div data-test-subj="hover-actions-copy-button" key="hover-actions-copy-button">
{getCopyButton({
Component: enableOverflowButton ? EuiContextMenuItem : undefined,
field,
isHoverAction: true,
keyboardEvent: stKeyboardEvent,
ownFocus,
onClick: handleHoverActionClicked,
showTooltip: enableOverflowButton ? false : true,
value: values,
})}
</div>
) : null,
].filter((item) => {
return item != null;
});
}, [
dataProvider,
dataType,
defaultFocusedButtonRef,
draggableId,
enableOverflowButton,
field,
fieldType,
isAggregatable,
filterManager,
getAddToTimelineButton,
getColumnToggleButton,
getCopyButton,
getFilterForValueButton,
getFilterOutValueButton,
handleHoverActionClicked,
onAddToTimelineClicked,
hideAddToTimeline,
hideTopN,
isObjectArray,
onFilterAdded,
ownFocus,
shouldDisableColumnToggle,
showFilters,
showTopNBtn,
stKeyboardEvent,
toggleColumn,
values,
dataViewId,
analytics,
i18n,
theme,
]) as JSX.Element[];
const overflowActionItems = useMemo(
() =>

View file

@ -29,8 +29,8 @@ jest.mock('../../lib/kibana', () => {
useKibana: () => mockedUseKibana,
};
});
jest.mock('@kbn/kibana-react-plugin/public', () => {
const original = jest.requireActual('@kbn/kibana-react-plugin/public');
jest.mock('@kbn/react-kibana-mount', () => {
const original = jest.requireActual('@kbn/react-kibana-mount');
return {
...original,

View file

@ -878,7 +878,7 @@ export interface ResolverPluginSetup {
/**
* The Resolver component without the required Providers.
* You must wrap this component in: `I18nProvider`, `Router` (from react-router,) `KibanaContextProvider`,
* You must wrap this component in: `KibanaRenderContextProvider`, `Router` (from react-router,) `KibanaContextProvider`,
* and the `Provider` component provided by this object.
*/
ResolverWithoutProviders: React.MemoExoticComponent<

View file

@ -94,8 +94,9 @@ export const AddToTimelineButtonEmpty: VFC<AddToTimelineProps> = ({
const buttonRef = useRef<HTMLButtonElement>(null);
const addToTimelineButton =
useKibana().services.timelines.getHoverActions().getAddToTimelineButton;
const { timelines, analytics, i18n: i18nStart, theme } = useKibana().services;
const startServices = { analytics, i18n: i18nStart, theme };
const addToTimelineButton = timelines.getHoverActions().getAddToTimelineButton;
const { key, value } =
typeof data === 'string' ? { key: field, value: data } : getIndicatorFieldAndValue(data, field);
@ -110,6 +111,7 @@ export const AddToTimelineButtonEmpty: VFC<AddToTimelineProps> = ({
dataProvider,
field: key,
ownFocus: false,
startServices,
};
// Use case is for the barchart legend (for example).
@ -153,8 +155,10 @@ export const AddToTimelineContextMenu: VFC<AddToTimelineProps> = ({
const contextMenuRef = useRef<HTMLButtonElement>(null);
const addToTimelineButton =
useKibana().services.timelines.getHoverActions().getAddToTimelineButton;
const { timelines, analytics, i18n: i18nStart, theme } = useKibana().services;
const startServices = { analytics, i18n: i18nStart, theme };
const addToTimelineButton = timelines.getHoverActions().getAddToTimelineButton;
const { key, value } =
typeof data === 'string' ? { key: field, value: data } : getIndicatorFieldAndValue(data, field);
@ -169,6 +173,7 @@ export const AddToTimelineContextMenu: VFC<AddToTimelineProps> = ({
dataProvider,
field: key,
ownFocus: false,
startServices,
};
// Use case is for the barchart legend (for example).

View file

@ -7,6 +7,7 @@
import { DataProvider } from '@kbn/timelines-plugin/common';
import { AddToTimelineButtonProps } from '@kbn/timelines-plugin/public';
import { useKibana } from '../../../hooks/use_kibana';
import { generateDataProvider } from '../utils/data_provider';
import { fieldAndValueValid, getIndicatorFieldAndValue } from '../../indicators/utils/field_value';
import { Indicator } from '../../../../common/types/indicator';
@ -38,6 +39,9 @@ export const useAddToTimeline = ({
indicator,
field,
}: UseAddToTimelineParam): UseAddToTimelineValue => {
const { analytics, i18n, theme } = useKibana().services;
const startServices = { analytics, i18n, theme };
const { key, value } =
typeof indicator === 'string'
? { key: field, value: indicator }
@ -53,6 +57,7 @@ export const useAddToTimeline = ({
dataProvider,
field: key,
ownFocus: false,
startServices,
};
return {

View file

@ -56,7 +56,7 @@ export const createApp =
export class ThreatIntelligencePlugin implements Plugin<void, void> {
public async setup(
core: CoreSetup,
_core: CoreSetup,
plugins: SetupPlugins
): Promise<ThreatIntelligencePluginSetup> {
const externalAttachmentType: ExternalReferenceAttachmentType = generateAttachmentType();
@ -73,11 +73,11 @@ export class ThreatIntelligencePlugin implements Plugin<void, void> {
storage: new Storage(localStorage),
};
const services = {
const services: Services = {
...localPluginServices,
...core,
...plugins,
} as Services;
};
return {
getComponent: createApp(services),

View file

@ -45,18 +45,17 @@ export interface ThreatIntelligencePluginStart {
export interface ThreatIntelligencePluginStartDeps {
data: DataPublicPluginStart;
}
export type Services = {
cases: CasesPublicStart;
data: DataPublicPluginStart;
storage: Storage;
dataViews: DataViewsPublicPluginStart;
triggersActionsUi: TriggersActionsStart;
timelines: TimelinesUIStart;
securityLayout: any;
inspector: InspectorPluginStart;
} & CoreStart;
}
export interface Services extends CoreStart, ThreatIntelligencePluginStartDeps {
storage: Storage;
}
export interface LicenseAware {
isEnterprise(): boolean;

View file

@ -7,6 +7,7 @@
import { EuiButtonEmpty } from '@elastic/eui';
import { act, fireEvent, render, screen } from '@testing-library/react';
import { coreMock } from '@kbn/core/public/mocks';
import React from 'react';
import AddToTimelineButton, {
@ -18,6 +19,8 @@ import { DataProvider, IS_OPERATOR } from '../../../../common/types';
import { TestProviders } from '../../../mock';
import * as i18n from './translations';
const coreStart = coreMock.createStart();
const mockAddSuccess = jest.fn();
jest.mock('../../../hooks/use_app_toasts', () => ({
useAppToasts: () => ({
@ -93,7 +96,7 @@ describe('add to timeline', () => {
beforeEach(() => {
render(
<TestProviders>
<AddToTimelineButton field={field} ownFocus={false} />
<AddToTimelineButton field={field} ownFocus={false} startServices={coreStart} />
</TestProviders>
);
});
@ -111,7 +114,12 @@ describe('add to timeline', () => {
beforeEach(() => {
render(
<TestProviders>
<AddToTimelineButton Component={EuiButtonEmpty} field={field} ownFocus={false} />
<AddToTimelineButton
Component={EuiButtonEmpty}
field={field}
ownFocus={false}
startServices={coreStart}
/>
</TestProviders>
);
});
@ -128,7 +136,12 @@ describe('add to timeline', () => {
test('it renders a tooltip when `showTooltip` is true', () => {
const { container } = render(
<TestProviders>
<AddToTimelineButton field={field} ownFocus={false} showTooltip={true} />
<AddToTimelineButton
field={field}
ownFocus={false}
showTooltip={true}
startServices={coreStart}
/>
</TestProviders>
);
@ -138,7 +151,7 @@ describe('add to timeline', () => {
test('it does NOT render a tooltip when `showTooltip` is false (default)', () => {
const { container } = render(
<TestProviders>
<AddToTimelineButton field={field} ownFocus={false} />
<AddToTimelineButton field={field} ownFocus={false} startServices={coreStart} />
</TestProviders>
);
@ -151,7 +164,12 @@ describe('add to timeline', () => {
test('it starts dragging to timeline when a `draggableId` is provided', () => {
render(
<TestProviders>
<AddToTimelineButton draggableId={draggableId} field={field} ownFocus={false} />
<AddToTimelineButton
draggableId={draggableId}
field={field}
ownFocus={false}
startServices={coreStart}
/>
</TestProviders>
);
@ -163,7 +181,7 @@ describe('add to timeline', () => {
test('it does NOT start dragging to timeline when a `draggableId` is NOT provided', () => {
render(
<TestProviders>
<AddToTimelineButton field={field} ownFocus={false} />
<AddToTimelineButton field={field} ownFocus={false} startServices={coreStart} />
</TestProviders>
);
@ -175,7 +193,12 @@ describe('add to timeline', () => {
test('it dispatches a single `addProviderToTimeline` action when a single, non-array `dataProvider` is provided', () => {
render(
<TestProviders>
<AddToTimelineButton dataProvider={providerA} field={field} ownFocus={false} />
<AddToTimelineButton
dataProvider={providerA}
field={field}
ownFocus={false}
startServices={coreStart}
/>
</TestProviders>
);
@ -209,6 +232,7 @@ describe('add to timeline', () => {
dataProvider={[providerA, providerB]}
field={field}
ownFocus={false}
startServices={coreStart}
/>
</TestProviders>
);
@ -217,7 +241,7 @@ describe('add to timeline', () => {
expect(mockDispatch).toHaveBeenCalledTimes(2);
providers.forEach((p, i) =>
providers.forEach((_p, i) =>
expect(mockDispatch).toHaveBeenNthCalledWith(i + 1, {
payload: {
dataProvider: {
@ -241,7 +265,12 @@ describe('add to timeline', () => {
render(
<TestProviders>
<AddToTimelineButton field={field} onClick={onClick} ownFocus={false} />
<AddToTimelineButton
field={field}
onClick={onClick}
ownFocus={false}
startServices={coreStart}
/>
</TestProviders>
);
@ -273,6 +302,7 @@ describe('add to timeline', () => {
keyboardEvent={keyboardEvent}
ownFocus={true}
showTooltip={true}
startServices={coreStart}
/>
</TestProviders>
);
@ -290,6 +320,7 @@ describe('add to timeline', () => {
keyboardEvent={keyboardEvent}
ownFocus={true}
showTooltip={true}
startServices={coreStart}
/>
</TestProviders>
);
@ -308,6 +339,7 @@ describe('add to timeline', () => {
keyboardEvent={keyboardEvent}
ownFocus={true}
showTooltip={true}
startServices={coreStart}
/>
</TestProviders>
);
@ -338,6 +370,7 @@ describe('add to timeline', () => {
keyboardEvent={keyboardEvent}
ownFocus={true}
showTooltip={true}
startServices={coreStart}
/>
</TestProviders>
);
@ -355,6 +388,7 @@ describe('add to timeline', () => {
keyboardEvent={keyboardEvent}
ownFocus={true}
showTooltip={true}
startServices={coreStart}
/>
</TestProviders>
);
@ -373,6 +407,7 @@ describe('add to timeline', () => {
keyboardEvent={keyboardEvent}
ownFocus={true}
showTooltip={true}
startServices={coreStart}
/>
</TestProviders>
);
@ -387,7 +422,12 @@ describe('add to timeline', () => {
test('Add success is called with "timeline" if timeline type is timeline', () => {
render(
<TestProviders>
<AddToTimelineButton dataProvider={providerA} field={field} ownFocus={false} />
<AddToTimelineButton
dataProvider={providerA}
field={field}
ownFocus={false}
startServices={coreStart}
/>
</TestProviders>
);
@ -408,6 +448,7 @@ describe('add to timeline', () => {
field={field}
ownFocus={false}
timelineType={'template'}
startServices={coreStart}
/>
</TestProviders>
);

View file

@ -11,7 +11,8 @@ import { DraggableId } from '@hello-pangea/dnd';
import { isEmpty } from 'lodash';
import { useDispatch } from 'react-redux';
import { toMountPoint } from '@kbn/kibana-react-plugin/public';
import { toMountPoint } from '@kbn/react-kibana-mount';
import { TimelinesStartServices } from '../../..';
import { TimelineId } from '../../../store/timeline';
import { addProviderToTimeline } from '../../../store/timeline/actions';
import { stopPropagationAndPreventDefault } from '../../../../common/utils/accessibility';
@ -63,6 +64,7 @@ export interface AddToTimelineButtonProps extends HoverActionComponentProps {
draggableId?: DraggableId;
dataProvider?: DataProvider[] | DataProvider;
timelineType?: string;
startServices: TimelinesStartServices;
}
const AddToTimelineButton: React.FC<AddToTimelineButtonProps> = React.memo(
@ -78,6 +80,7 @@ const AddToTimelineButton: React.FC<AddToTimelineButtonProps> = React.memo(
showTooltip = false,
value,
timelineType = 'default',
startServices,
}) => {
const dispatch = useDispatch();
const { addSuccess } = useAppToasts();
@ -103,7 +106,8 @@ const AddToTimelineButton: React.FC<AddToTimelineButtonProps> = React.memo(
provider.name,
timelineType === 'default'
)}
</AddSuccessMessage>
</AddSuccessMessage>,
startServices
),
});
}
@ -121,6 +125,7 @@ const AddToTimelineButton: React.FC<AddToTimelineButtonProps> = React.memo(
onClick,
startDragToTimeline,
timelineType,
startServices,
]);
useEffect(() => {

View file

@ -14,6 +14,8 @@
// first download since the other plugins/areas of your code can directly pull from the package in their async imports.
// See: https://docs.elastic.dev/kibana-dev-docs/key-concepts/platform-intro#public-plugin-api
import type { CoreStart } from '@kbn/core/public';
import { TimelinesPlugin } from './plugin';
export type { TimelinesUIStart } from './types';
@ -49,3 +51,5 @@ export function plugin() {
}
export type { AddToTimelineButtonProps } from './components/hover_actions/actions/add_to_timeline';
export type TimelinesStartServices = Pick<CoreStart, 'analytics' | 'i18n' | 'theme'>;

View file

@ -2,7 +2,7 @@
{
"extends": "../../../tsconfig.base.json",
"compilerOptions": {
"outDir": "target/types",
"outDir": "target/types"
},
"include": [
"common/**/*",
@ -36,8 +36,9 @@
"@kbn/logging",
"@kbn/search-errors",
"@kbn/search-types",
"@kbn/react-kibana-mount"
],
"exclude": [
"target/**/*",
"target/**/*"
]
}