mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
decouple 'Add to case' action from legacy embeddable framework (#187293)
Part of https://github.com/elastic/kibana/issues/175138 PR decouples "Add to case" action from legacy embeddable framework. PR also cleans up page load bundle size by moving `isCompatible` and `execute` implemenations behind async imports. --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
parent
129ad61c76
commit
e3cc2e0cc0
19 changed files with 412 additions and 495 deletions
|
@ -85,6 +85,7 @@ export {
|
|||
export { apiHasUniqueId, type HasUniqueId } from './interfaces/has_uuid';
|
||||
export {
|
||||
apiPublishesBlockingError,
|
||||
hasBlockingError,
|
||||
type PublishesBlockingError,
|
||||
} from './interfaces/publishes_blocking_error';
|
||||
export {
|
||||
|
|
|
@ -17,3 +17,7 @@ export const apiPublishesBlockingError = (
|
|||
): unknownApi is PublishesBlockingError => {
|
||||
return Boolean(unknownApi && (unknownApi as PublishesBlockingError)?.blockingError !== undefined);
|
||||
};
|
||||
|
||||
export function hasBlockingError(api: unknown) {
|
||||
return apiPublishesBlockingError(api) && api.blockingError?.value !== undefined;
|
||||
}
|
||||
|
|
|
@ -11,7 +11,8 @@ import { SECURITY_SOLUTION_OWNER } from '../../../../common';
|
|||
import { canUseCases } from '../../../client/helpers/can_use_cases';
|
||||
import CasesProvider from '../../cases_context';
|
||||
import { ActionWrapper } from './action_wrapper';
|
||||
import { getMockCaseUiActionProps } from './mocks';
|
||||
import { getMockServices } from './mocks';
|
||||
import type { CasesActionContextProps } from './types';
|
||||
|
||||
jest.mock('../../cases_context', () =>
|
||||
jest.fn().mockImplementation(({ children, ...props }) => <div {...props}>{children}</div>)
|
||||
|
@ -28,7 +29,11 @@ jest.mock('../../../client/helpers/can_use_cases', () => {
|
|||
const mockCasePermissions = jest.fn().mockReturnValue({ create: true, update: true });
|
||||
|
||||
describe('ActionWrapper', () => {
|
||||
const props = { ...getMockCaseUiActionProps(), currentAppId: 'securitySolutionUI' };
|
||||
const props = {
|
||||
casesActionContextProps: {} as unknown as CasesActionContextProps,
|
||||
currentAppId: 'securitySolutionUI',
|
||||
services: getMockServices(),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
|
|
@ -11,7 +11,7 @@ import { Router } from '@kbn/shared-ux-router';
|
|||
import { KibanaRenderContextProvider } from '@kbn/react-kibana-context-render';
|
||||
|
||||
import { SECURITY_SOLUTION_OWNER } from '../../../../common';
|
||||
import type { CasesUIActionProps } from './types';
|
||||
import type { CasesActionContextProps, Services } from './types';
|
||||
import { KibanaContextProvider, useKibana } from '../../../common/lib/kibana';
|
||||
import CasesProvider from '../../cases_context';
|
||||
import { getCaseOwnerByAppId } from '../../../../common/utils/owner';
|
||||
|
@ -20,13 +20,13 @@ import { canUseCases } from '../../../client/helpers/can_use_cases';
|
|||
export const DEFAULT_DARK_MODE = 'theme:darkMode' as const;
|
||||
|
||||
interface Props {
|
||||
caseContextProps: CasesUIActionProps['caseContextProps'];
|
||||
casesActionContextProps: CasesActionContextProps;
|
||||
currentAppId?: string;
|
||||
}
|
||||
|
||||
const ActionWrapperWithContext: React.FC<PropsWithChildren<Props>> = ({
|
||||
children,
|
||||
caseContextProps,
|
||||
casesActionContextProps,
|
||||
currentAppId,
|
||||
}) => {
|
||||
const { application, i18n, theme } = useKibana().services;
|
||||
|
@ -40,7 +40,7 @@ const ActionWrapperWithContext: React.FC<PropsWithChildren<Props>> = ({
|
|||
<KibanaRenderContextProvider i18n={i18n} theme={theme}>
|
||||
<CasesProvider
|
||||
value={{
|
||||
...caseContextProps,
|
||||
...casesActionContextProps,
|
||||
owner: owner ? [owner] : [],
|
||||
permissions: casePermissions,
|
||||
features: { alerts: { sync: syncAlerts } },
|
||||
|
@ -54,29 +54,31 @@ const ActionWrapperWithContext: React.FC<PropsWithChildren<Props>> = ({
|
|||
|
||||
ActionWrapperWithContext.displayName = 'ActionWrapperWithContext';
|
||||
|
||||
type ActionWrapperComponentProps = PropsWithChildren<
|
||||
CasesUIActionProps & { currentAppId?: string }
|
||||
>;
|
||||
type ActionWrapperComponentProps = PropsWithChildren<{
|
||||
casesActionContextProps: CasesActionContextProps;
|
||||
currentAppId?: string;
|
||||
services: Services;
|
||||
}>;
|
||||
|
||||
const ActionWrapperComponent: React.FC<ActionWrapperComponentProps> = ({
|
||||
core,
|
||||
plugins,
|
||||
storage,
|
||||
history,
|
||||
casesActionContextProps,
|
||||
children,
|
||||
caseContextProps,
|
||||
currentAppId,
|
||||
services,
|
||||
}) => {
|
||||
return (
|
||||
<KibanaContextProvider
|
||||
services={{
|
||||
...core,
|
||||
...plugins,
|
||||
storage,
|
||||
...services.core,
|
||||
...services.plugins,
|
||||
storage: services.storage,
|
||||
}}
|
||||
>
|
||||
<Router history={history}>
|
||||
<ActionWrapperWithContext caseContextProps={caseContextProps} currentAppId={currentAppId}>
|
||||
<Router history={services.history}>
|
||||
<ActionWrapperWithContext
|
||||
casesActionContextProps={casesActionContextProps}
|
||||
currentAppId={currentAppId}
|
||||
>
|
||||
{children}
|
||||
</ActionWrapperWithContext>
|
||||
</Router>
|
||||
|
|
|
@ -1,221 +0,0 @@
|
|||
/*
|
||||
* 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 { LENS_EMBEDDABLE_TYPE, type Embeddable as LensEmbeddable } from '@kbn/lens-plugin/public';
|
||||
import { ErrorEmbeddable } from '@kbn/embeddable-plugin/public';
|
||||
import type { Action } from '@kbn/ui-actions-plugin/public';
|
||||
import ReactDOM, { unmountComponentAtNode } from 'react-dom';
|
||||
|
||||
import { createAddToExistingCaseLensAction } from './add_to_existing_case';
|
||||
import type { ActionContext } from './types';
|
||||
import { useCasesAddToExistingCaseModal } from '../../all_cases/selector_modal/use_cases_add_to_existing_case_modal';
|
||||
import type { PropsWithChildren } from 'react';
|
||||
import React from 'react';
|
||||
import { toMountPoint } from '@kbn/react-kibana-mount';
|
||||
import {
|
||||
getMockApplications$,
|
||||
getMockCaseUiActionProps,
|
||||
getMockCurrentAppId$,
|
||||
mockAttributes,
|
||||
MockEmbeddable,
|
||||
mockTimeRange,
|
||||
} from './mocks';
|
||||
import { useKibana } from '../../../common/lib/kibana';
|
||||
import { waitFor } from '@testing-library/react';
|
||||
import { canUseCases } from '../../../client/helpers/can_use_cases';
|
||||
import { getCaseOwnerByAppId } from '../../../../common/utils/owner';
|
||||
|
||||
const element = document.createElement('div');
|
||||
document.body.appendChild(element);
|
||||
|
||||
jest.mock('../../all_cases/selector_modal/use_cases_add_to_existing_case_modal', () => ({
|
||||
useCasesAddToExistingCaseModal: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('../../../client/helpers/can_use_cases', () => {
|
||||
const actual = jest.requireActual('../../../client/helpers/can_use_cases');
|
||||
return {
|
||||
...actual,
|
||||
canUseCases: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock('@kbn/kibana-react-plugin/public', () => ({
|
||||
KibanaThemeProvider: jest
|
||||
.fn()
|
||||
.mockImplementation(({ children }: PropsWithChildren<unknown>) => <>{children}</>),
|
||||
}));
|
||||
|
||||
jest.mock('@kbn/react-kibana-mount', () => ({
|
||||
toMountPoint: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('../../../common/lib/kibana', () => {
|
||||
return {
|
||||
useKibana: jest.fn(),
|
||||
KibanaContextProvider: jest
|
||||
.fn()
|
||||
.mockImplementation(({ children, ...props }) => <div {...props}>{children}</div>),
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock('react-dom', () => {
|
||||
const original = jest.requireActual('react-dom');
|
||||
return { ...original, unmountComponentAtNode: jest.fn() };
|
||||
});
|
||||
|
||||
jest.mock('./action_wrapper');
|
||||
|
||||
jest.mock('../../../../common/utils/owner', () => ({
|
||||
getCaseOwnerByAppId: jest.fn().mockReturnValue('securitySolution'),
|
||||
}));
|
||||
|
||||
describe('createAddToExistingCaseLensAction', () => {
|
||||
const mockEmbeddable = new MockEmbeddable(LENS_EMBEDDABLE_TYPE, {
|
||||
id: 'mockId',
|
||||
attributes: mockAttributes,
|
||||
timeRange: mockTimeRange,
|
||||
}) as unknown as LensEmbeddable;
|
||||
|
||||
const context = {
|
||||
embeddable: mockEmbeddable,
|
||||
} as unknown as ActionContext;
|
||||
|
||||
const caseUiActionProps = getMockCaseUiActionProps();
|
||||
|
||||
const mockUseCasesAddToExistingCaseModal = useCasesAddToExistingCaseModal as jest.Mock;
|
||||
const mockOpenModal = jest.fn();
|
||||
const mockMount = jest.fn();
|
||||
let action: Action<ActionContext>;
|
||||
const mockCasePermissions = jest.fn();
|
||||
beforeEach(() => {
|
||||
mockUseCasesAddToExistingCaseModal.mockReturnValue({
|
||||
open: mockOpenModal,
|
||||
});
|
||||
(useKibana as jest.Mock).mockReturnValue({
|
||||
services: {
|
||||
application: {
|
||||
currentAppId$: getMockCurrentAppId$(),
|
||||
applications$: getMockApplications$(),
|
||||
},
|
||||
},
|
||||
});
|
||||
(canUseCases as jest.Mock).mockReturnValue(
|
||||
mockCasePermissions.mockReturnValue({ create: true, update: true })
|
||||
);
|
||||
(toMountPoint as jest.Mock).mockImplementation((node) => {
|
||||
ReactDOM.render(node, element);
|
||||
return mockMount;
|
||||
});
|
||||
jest.clearAllMocks();
|
||||
action = createAddToExistingCaseLensAction(caseUiActionProps);
|
||||
});
|
||||
|
||||
test('it should return display name', () => {
|
||||
expect(action.getDisplayName(context)).toEqual('Add to case');
|
||||
});
|
||||
|
||||
it('should return icon type', () => {
|
||||
expect(action.getIconType(context)).toEqual('casesApp');
|
||||
});
|
||||
|
||||
describe('isCompatible', () => {
|
||||
it('should return false if error embeddable', async () => {
|
||||
expect(
|
||||
await action.isCompatible({
|
||||
...context,
|
||||
embeddable: new ErrorEmbeddable('some error', {
|
||||
id: '123',
|
||||
}) as unknown as LensEmbeddable,
|
||||
})
|
||||
).toEqual(false);
|
||||
});
|
||||
|
||||
it('should return false if not lens embeddable', async () => {
|
||||
expect(
|
||||
await action.isCompatible({
|
||||
...context,
|
||||
embeddable: new MockEmbeddable('not_lens') as unknown as LensEmbeddable,
|
||||
})
|
||||
).toEqual(false);
|
||||
});
|
||||
|
||||
it('should return false if no permission', async () => {
|
||||
mockCasePermissions.mockReturnValue({ create: false, update: false });
|
||||
expect(await action.isCompatible(context)).toEqual(false);
|
||||
});
|
||||
|
||||
it('should return true if is lens embeddable', async () => {
|
||||
expect(await action.isCompatible(context)).toEqual(true);
|
||||
});
|
||||
|
||||
it('should check permission with undefined if owner is not found', async () => {
|
||||
(getCaseOwnerByAppId as jest.Mock).mockReturnValue(undefined);
|
||||
await action.isCompatible(context);
|
||||
expect(mockCasePermissions).toBeCalledWith(undefined);
|
||||
});
|
||||
});
|
||||
|
||||
describe('execute', () => {
|
||||
beforeEach(async () => {
|
||||
await action.execute(context);
|
||||
});
|
||||
|
||||
it('should execute', () => {
|
||||
expect(toMountPoint).toHaveBeenCalled();
|
||||
expect(mockMount).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Add to existing case modal', () => {
|
||||
beforeEach(async () => {
|
||||
await action.execute(context);
|
||||
});
|
||||
|
||||
it('should open modal with an attachment', async () => {
|
||||
await waitFor(() => {
|
||||
expect(mockOpenModal).toHaveBeenCalled();
|
||||
|
||||
const getAttachments = mockOpenModal.mock.calls[0][0].getAttachments;
|
||||
expect(getAttachments()).toEqual([
|
||||
{
|
||||
persistableStateAttachmentState: {
|
||||
attributes: mockAttributes,
|
||||
timeRange: mockTimeRange,
|
||||
},
|
||||
persistableStateAttachmentTypeId: '.lens',
|
||||
type: 'persistableState',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
it('should have correct onClose handler - when close modal clicked', () => {
|
||||
const onClose = mockUseCasesAddToExistingCaseModal.mock.calls[0][0].onClose;
|
||||
onClose();
|
||||
expect(unmountComponentAtNode as jest.Mock).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should have correct onClose handler - when case selected', () => {
|
||||
const onClose = mockUseCasesAddToExistingCaseModal.mock.calls[0][0].onClose;
|
||||
onClose({ id: 'case-id', title: 'case-title' });
|
||||
expect(unmountComponentAtNode as jest.Mock).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should have correct onClose handler - when case created', () => {
|
||||
const onClose = mockUseCasesAddToExistingCaseModal.mock.calls[0][0].onClose;
|
||||
onClose(null, true);
|
||||
expect(unmountComponentAtNode as jest.Mock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should have correct onSuccess handler', () => {
|
||||
const onSuccess = mockUseCasesAddToExistingCaseModal.mock.calls[0][0].onSuccess;
|
||||
onSuccess();
|
||||
expect(unmountComponentAtNode as jest.Mock).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -4,130 +4,39 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import React, { useEffect, useMemo } from 'react';
|
||||
import { unmountComponentAtNode } from 'react-dom';
|
||||
import type { Embeddable as LensEmbeddable } from '@kbn/lens-plugin/public';
|
||||
import { createAction } from '@kbn/ui-actions-plugin/public';
|
||||
import { isErrorEmbeddable } from '@kbn/embeddable-plugin/public';
|
||||
|
||||
import { toMountPoint } from '@kbn/react-kibana-mount';
|
||||
import { createAction, IncompatibleActionError } from '@kbn/ui-actions-plugin/public';
|
||||
|
||||
import type { CaseUI } from '../../../../common';
|
||||
import { isLensEmbeddable, hasInput, getLensCaseAttachment } from './utils';
|
||||
|
||||
import type { ActionContext, CasesUIActionProps } from './types';
|
||||
import { useCasesAddToExistingCaseModal } from '../../all_cases/selector_modal/use_cases_add_to_existing_case_modal';
|
||||
import type { EmbeddableApiContext } from '@kbn/presentation-publishing';
|
||||
import type { CasesActionContextProps, Services } from './types';
|
||||
import { ADD_TO_EXISTING_CASE_DISPLAYNAME } from './translations';
|
||||
import { ActionWrapper } from './action_wrapper';
|
||||
import { canUseCases } from '../../../client/helpers/can_use_cases';
|
||||
import { getCaseOwnerByAppId } from '../../../../common/utils/owner';
|
||||
|
||||
export const ACTION_ID = 'embeddable_addToExistingCase';
|
||||
export const DEFAULT_DARK_MODE = 'theme:darkMode' as const;
|
||||
|
||||
interface Props {
|
||||
embeddable: LensEmbeddable;
|
||||
onSuccess: () => void;
|
||||
onClose: (theCase?: CaseUI) => void;
|
||||
}
|
||||
|
||||
const AddExistingCaseModalWrapper: React.FC<Props> = ({ embeddable, onClose, onSuccess }) => {
|
||||
const modal = useCasesAddToExistingCaseModal({
|
||||
onClose,
|
||||
onSuccess,
|
||||
});
|
||||
|
||||
const attachments = useMemo(() => {
|
||||
const { timeRange } = embeddable.getInput();
|
||||
const attributes = embeddable.getFullAttributes();
|
||||
// we've checked attributes exists before rendering (isCompatible), attributes should not be undefined here
|
||||
return attributes != null ? [getLensCaseAttachment({ attributes, timeRange })] : [];
|
||||
}, [embeddable]);
|
||||
useEffect(() => {
|
||||
modal.open({ getAttachments: () => attachments });
|
||||
}, [attachments, modal]);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
AddExistingCaseModalWrapper.displayName = 'AddExistingCaseModalWrapper';
|
||||
|
||||
export const createAddToExistingCaseLensAction = ({
|
||||
core,
|
||||
plugins,
|
||||
storage,
|
||||
history,
|
||||
caseContextProps,
|
||||
}: CasesUIActionProps) => {
|
||||
const { application: applicationService, i18n, theme } = core;
|
||||
|
||||
export const createAddToExistingCaseLensAction = (
|
||||
casesActionContextProps: CasesActionContextProps,
|
||||
services: Services
|
||||
) => {
|
||||
let currentAppId: string | undefined;
|
||||
|
||||
applicationService?.currentAppId$.subscribe((appId) => {
|
||||
services.core.application?.currentAppId$.subscribe((appId) => {
|
||||
currentAppId = appId;
|
||||
});
|
||||
|
||||
return createAction<ActionContext>({
|
||||
return createAction<EmbeddableApiContext>({
|
||||
id: ACTION_ID,
|
||||
type: 'actionButton',
|
||||
getIconType: () => 'casesApp',
|
||||
getDisplayName: () => ADD_TO_EXISTING_CASE_DISPLAYNAME,
|
||||
isCompatible: async ({ embeddable }) => {
|
||||
const owner = getCaseOwnerByAppId(currentAppId);
|
||||
const casePermissions = canUseCases(applicationService.capabilities)(
|
||||
owner ? [owner] : undefined
|
||||
);
|
||||
|
||||
return (
|
||||
!isErrorEmbeddable(embeddable) &&
|
||||
isLensEmbeddable(embeddable) &&
|
||||
casePermissions.update &&
|
||||
casePermissions.create &&
|
||||
hasInput(embeddable)
|
||||
);
|
||||
const { isCompatible } = await import('./is_compatible');
|
||||
return isCompatible(embeddable, currentAppId, services.core);
|
||||
},
|
||||
execute: async ({ embeddable }) => {
|
||||
const targetDomElement = document.createElement('div');
|
||||
|
||||
const cleanupDom = (shouldCleanup?: boolean) => {
|
||||
if (targetDomElement != null && shouldCleanup) {
|
||||
unmountComponentAtNode(targetDomElement);
|
||||
}
|
||||
};
|
||||
|
||||
const onClose = (theCase?: CaseUI, isCreateCase?: boolean) => {
|
||||
const closeModalClickedScenario = theCase == null && !isCreateCase;
|
||||
const caseSelectedScenario = theCase != null;
|
||||
// When `Creating` a case from the `add to existing case modal`,
|
||||
// we close the modal and then open the flyout.
|
||||
// If we clean up dom when closing the modal, then the flyout won't open.
|
||||
// Thus we do not clean up dom when `Creating` a case.
|
||||
const shouldCleanup = closeModalClickedScenario || caseSelectedScenario;
|
||||
cleanupDom(shouldCleanup);
|
||||
};
|
||||
|
||||
const onSuccess = () => {
|
||||
cleanupDom(true);
|
||||
};
|
||||
const mount = toMountPoint(
|
||||
<ActionWrapper
|
||||
core={core}
|
||||
caseContextProps={caseContextProps}
|
||||
storage={storage}
|
||||
plugins={plugins}
|
||||
history={history}
|
||||
currentAppId={currentAppId}
|
||||
>
|
||||
<AddExistingCaseModalWrapper
|
||||
embeddable={embeddable}
|
||||
onClose={onClose}
|
||||
onSuccess={onSuccess}
|
||||
/>
|
||||
</ActionWrapper>,
|
||||
{ i18n, theme }
|
||||
);
|
||||
|
||||
mount(targetDomElement);
|
||||
const { isLensApi } = await import('@kbn/lens-plugin/public');
|
||||
if (!isLensApi(embeddable)) throw new IncompatibleActionError();
|
||||
const { openModal } = await import('./open_modal');
|
||||
openModal(embeddable, currentAppId, casesActionContextProps, services);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
|
|
@ -0,0 +1,58 @@
|
|||
/*
|
||||
* 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 { BehaviorSubject } from 'rxjs';
|
||||
import { coreMock } from '@kbn/core/public/mocks';
|
||||
import { isCompatible } from './is_compatible';
|
||||
import { canUseCases } from '../../../client/helpers/can_use_cases';
|
||||
import { mockLensApi } from './mocks';
|
||||
|
||||
jest.mock('../../../../common/utils/owner', () => ({
|
||||
getCaseOwnerByAppId: () => 'securitySolution',
|
||||
}));
|
||||
|
||||
jest.mock('../../../client/helpers/can_use_cases', () => {
|
||||
const actual = jest.requireActual('../../../client/helpers/can_use_cases');
|
||||
return {
|
||||
...actual,
|
||||
canUseCases: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
describe('isCompatible', () => {
|
||||
const appId = 'myAppId';
|
||||
const mockCoreStart = coreMock.createStart();
|
||||
|
||||
const mockCasePermissions = jest.fn();
|
||||
beforeEach(() => {
|
||||
(canUseCases as jest.Mock).mockReturnValue(
|
||||
mockCasePermissions.mockReturnValue({ create: true, update: true })
|
||||
);
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
test('should return false if error embeddable', async () => {
|
||||
const errorApi = {
|
||||
...mockLensApi,
|
||||
blockingError: new BehaviorSubject<Error | undefined>(new Error('Simulated blocking error')),
|
||||
};
|
||||
expect(isCompatible(errorApi, appId, mockCoreStart)).toBe(false);
|
||||
});
|
||||
|
||||
test('should return false if not lens embeddable', async () => {
|
||||
expect(isCompatible({}, appId, mockCoreStart)).toBe(false);
|
||||
});
|
||||
|
||||
test('should return false if no permission', async () => {
|
||||
mockCasePermissions.mockReturnValue({ create: false, update: false });
|
||||
expect(isCompatible(mockLensApi, appId, mockCoreStart)).toBe(false);
|
||||
});
|
||||
|
||||
test('should return true if is lens embeddable', async () => {
|
||||
expect(isCompatible(mockLensApi, appId, mockCoreStart)).toBe(true);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,30 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { CoreStart } from '@kbn/core-lifecycle-browser';
|
||||
import { isLensApi } from '@kbn/lens-plugin/public';
|
||||
import { hasBlockingError } from '@kbn/presentation-publishing';
|
||||
import { canUseCases } from '../../../client/helpers/can_use_cases';
|
||||
import { getCaseOwnerByAppId } from '../../../../common/utils/owner';
|
||||
|
||||
export function isCompatible(
|
||||
embeddable: unknown,
|
||||
currentAppId: string | undefined,
|
||||
core: CoreStart
|
||||
) {
|
||||
if (!isLensApi(embeddable) || hasBlockingError(embeddable)) return false;
|
||||
if (!embeddable.getFullAttributes()) {
|
||||
return false;
|
||||
}
|
||||
const timeRange = embeddable.timeRange$?.value ?? embeddable.parentApi?.timeRange$?.value;
|
||||
if (!timeRange) {
|
||||
return false;
|
||||
}
|
||||
const owner = getCaseOwnerByAppId(currentAppId);
|
||||
const casePermissions = canUseCases(core.application.capabilities)(owner ? [owner] : undefined);
|
||||
return casePermissions.update && casePermissions.create;
|
||||
}
|
|
@ -10,36 +10,13 @@ import { BehaviorSubject } from 'rxjs';
|
|||
|
||||
import type { PublicAppInfo } from '@kbn/core/public';
|
||||
import { coreMock } from '@kbn/core/public/mocks';
|
||||
import type { TypedLensByValueInput } from '@kbn/lens-plugin/public';
|
||||
import type { CasesUIActionProps } from './types';
|
||||
import type { LensApi, LensSavedObjectAttributes } from '@kbn/lens-plugin/public';
|
||||
import type { AggregateQuery, Filter, Query, TimeRange } from '@kbn/es-query';
|
||||
import type { Services } from './types';
|
||||
|
||||
const coreStart = coreMock.createStart();
|
||||
|
||||
export class MockEmbeddable {
|
||||
public type;
|
||||
private input;
|
||||
constructor(
|
||||
type: string,
|
||||
input?: {
|
||||
attributes: TypedLensByValueInput['attributes'];
|
||||
id: string;
|
||||
timeRange: { from: string; to: string; fromStr: string; toStr: string };
|
||||
}
|
||||
) {
|
||||
this.type = type;
|
||||
this.input = input;
|
||||
}
|
||||
getFilters() {}
|
||||
getQuery() {}
|
||||
getInput() {
|
||||
return this.input;
|
||||
}
|
||||
getFullAttributes() {
|
||||
return this.input?.attributes;
|
||||
}
|
||||
}
|
||||
|
||||
export const mockAttributes = {
|
||||
export const mockLensAttributes = {
|
||||
title: 'mockTitle',
|
||||
description: 'mockDescription',
|
||||
references: [],
|
||||
|
@ -57,9 +34,26 @@ export const mockAttributes = {
|
|||
},
|
||||
},
|
||||
},
|
||||
} as unknown as TypedLensByValueInput['attributes'];
|
||||
} as unknown as LensSavedObjectAttributes;
|
||||
|
||||
export const mockTimeRange = { from: '', to: '', fromStr: '', toStr: '' };
|
||||
export const mockLensApi = {
|
||||
type: 'lens',
|
||||
getSavedVis: () => {},
|
||||
canViewUnderlyingData: () => {},
|
||||
getViewUnderlyingDataArgs: () => {},
|
||||
getFullAttributes: () => {
|
||||
return mockLensAttributes;
|
||||
},
|
||||
panelTitle: new BehaviorSubject('myPanel'),
|
||||
hidePanelTitle: new BehaviorSubject('false'),
|
||||
timeslice$: new BehaviorSubject<[number, number] | undefined>(undefined),
|
||||
timeRange$: new BehaviorSubject<TimeRange | undefined>({
|
||||
from: 'now-24h',
|
||||
to: 'now',
|
||||
}),
|
||||
filters$: new BehaviorSubject<Filter[] | undefined>(undefined),
|
||||
query$: new BehaviorSubject<Query | AggregateQuery | undefined>(undefined),
|
||||
} as unknown as LensApi;
|
||||
|
||||
export const getMockCurrentAppId$ = () => new BehaviorSubject<string>('securitySolutionUI');
|
||||
export const getMockApplications$ = () =>
|
||||
|
@ -67,26 +61,17 @@ export const getMockApplications$ = () =>
|
|||
new Map([['securitySolutionUI', { category: { label: 'Test' } } as unknown as PublicAppInfo]])
|
||||
);
|
||||
|
||||
export const getMockCaseUiActionProps = () => {
|
||||
const core = {
|
||||
...coreStart,
|
||||
application: { currentAppId$: getMockCurrentAppId$(), capabilities: {} },
|
||||
uiSettings: {
|
||||
get: jest.fn().mockReturnValue(true),
|
||||
export const getMockServices = () => {
|
||||
return {
|
||||
core: {
|
||||
...coreStart,
|
||||
application: { currentAppId$: getMockCurrentAppId$(), capabilities: {} },
|
||||
uiSettings: {
|
||||
get: jest.fn().mockReturnValue(true),
|
||||
},
|
||||
},
|
||||
};
|
||||
const plugins = {};
|
||||
const storage = {};
|
||||
const history = createBrowserHistory();
|
||||
const caseContextProps = {};
|
||||
|
||||
const caseUiActionProps = {
|
||||
core,
|
||||
plugins,
|
||||
storage,
|
||||
history,
|
||||
caseContextProps,
|
||||
} as unknown as CasesUIActionProps;
|
||||
|
||||
return caseUiActionProps;
|
||||
plugins: {},
|
||||
storage: {},
|
||||
history: createBrowserHistory(),
|
||||
} as unknown as Services;
|
||||
};
|
||||
|
|
|
@ -0,0 +1,126 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import ReactDOM, { unmountComponentAtNode } from 'react-dom';
|
||||
import { useCasesAddToExistingCaseModal } from '../../all_cases/selector_modal/use_cases_add_to_existing_case_modal';
|
||||
import type { PropsWithChildren } from 'react';
|
||||
import React from 'react';
|
||||
import { toMountPoint } from '@kbn/react-kibana-mount';
|
||||
import {
|
||||
getMockApplications$,
|
||||
getMockCurrentAppId$,
|
||||
mockLensApi,
|
||||
mockLensAttributes,
|
||||
getMockServices,
|
||||
} from './mocks';
|
||||
import { useKibana } from '../../../common/lib/kibana';
|
||||
import { waitFor } from '@testing-library/react';
|
||||
import { openModal } from './open_modal';
|
||||
import type { CasesActionContextProps } from './types';
|
||||
|
||||
const element = document.createElement('div');
|
||||
document.body.appendChild(element);
|
||||
|
||||
jest.mock('../../all_cases/selector_modal/use_cases_add_to_existing_case_modal', () => ({
|
||||
useCasesAddToExistingCaseModal: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('@kbn/kibana-react-plugin/public', () => ({
|
||||
KibanaThemeProvider: jest
|
||||
.fn()
|
||||
.mockImplementation(({ children }: PropsWithChildren<unknown>) => <>{children}</>),
|
||||
}));
|
||||
|
||||
jest.mock('@kbn/react-kibana-mount', () => ({
|
||||
toMountPoint: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('../../../common/lib/kibana', () => {
|
||||
return {
|
||||
useKibana: jest.fn(),
|
||||
KibanaContextProvider: jest
|
||||
.fn()
|
||||
.mockImplementation(({ children, ...props }) => <div {...props}>{children}</div>),
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock('react-dom', () => {
|
||||
const original = jest.requireActual('react-dom');
|
||||
return { ...original, unmountComponentAtNode: jest.fn() };
|
||||
});
|
||||
|
||||
jest.mock('./action_wrapper');
|
||||
|
||||
describe('openModal', () => {
|
||||
const mockUseCasesAddToExistingCaseModal = useCasesAddToExistingCaseModal as jest.Mock;
|
||||
const mockOpenModal = jest.fn();
|
||||
const mockMount = jest.fn();
|
||||
beforeEach(() => {
|
||||
mockUseCasesAddToExistingCaseModal.mockReturnValue({
|
||||
open: mockOpenModal,
|
||||
});
|
||||
(useKibana as jest.Mock).mockReturnValue({
|
||||
services: {
|
||||
application: {
|
||||
currentAppId$: getMockCurrentAppId$(),
|
||||
applications$: getMockApplications$(),
|
||||
},
|
||||
},
|
||||
});
|
||||
(toMountPoint as jest.Mock).mockImplementation((node) => {
|
||||
ReactDOM.render(node, element);
|
||||
return mockMount;
|
||||
});
|
||||
jest.clearAllMocks();
|
||||
openModal(mockLensApi, 'myAppId', {} as unknown as CasesActionContextProps, getMockServices());
|
||||
});
|
||||
|
||||
test('should open modal with an attachment', async () => {
|
||||
await waitFor(() => {
|
||||
expect(mockOpenModal).toHaveBeenCalled();
|
||||
|
||||
const getAttachments = mockOpenModal.mock.calls[0][0].getAttachments;
|
||||
expect(getAttachments()).toEqual([
|
||||
{
|
||||
persistableStateAttachmentState: {
|
||||
attributes: mockLensAttributes,
|
||||
timeRange: {
|
||||
from: 'now-24h',
|
||||
to: 'now',
|
||||
},
|
||||
},
|
||||
persistableStateAttachmentTypeId: '.lens',
|
||||
type: 'persistableState',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
test('should have correct onClose handler - when close modal clicked', () => {
|
||||
const onClose = mockUseCasesAddToExistingCaseModal.mock.calls[0][0].onClose;
|
||||
onClose();
|
||||
expect(unmountComponentAtNode as jest.Mock).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('should have correct onClose handler - when case selected', () => {
|
||||
const onClose = mockUseCasesAddToExistingCaseModal.mock.calls[0][0].onClose;
|
||||
onClose({ id: 'case-id', title: 'case-title' });
|
||||
expect(unmountComponentAtNode as jest.Mock).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('should have correct onClose handler - when case created', () => {
|
||||
const onClose = mockUseCasesAddToExistingCaseModal.mock.calls[0][0].onClose;
|
||||
onClose(null, true);
|
||||
expect(unmountComponentAtNode as jest.Mock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('should have correct onSuccess handler', () => {
|
||||
const onSuccess = mockUseCasesAddToExistingCaseModal.mock.calls[0][0].onSuccess;
|
||||
onSuccess();
|
||||
expect(unmountComponentAtNode as jest.Mock).toHaveBeenCalled();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,89 @@
|
|||
/*
|
||||
* 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, { useEffect, useMemo } from 'react';
|
||||
import { unmountComponentAtNode } from 'react-dom';
|
||||
import type { LensApi } from '@kbn/lens-plugin/public';
|
||||
import { toMountPoint } from '@kbn/react-kibana-mount';
|
||||
import { useStateFromPublishingSubject } from '@kbn/presentation-publishing';
|
||||
import { ActionWrapper } from './action_wrapper';
|
||||
import type { CasesActionContextProps, Services } from './types';
|
||||
import type { CaseUI } from '../../../../common';
|
||||
import { getLensCaseAttachment } from './utils';
|
||||
import { useCasesAddToExistingCaseModal } from '../../all_cases/selector_modal/use_cases_add_to_existing_case_modal';
|
||||
|
||||
interface Props {
|
||||
lensApi: LensApi;
|
||||
onSuccess: () => void;
|
||||
onClose: (theCase?: CaseUI) => void;
|
||||
}
|
||||
|
||||
const AddExistingCaseModalWrapper: React.FC<Props> = ({ lensApi, onClose, onSuccess }) => {
|
||||
const modal = useCasesAddToExistingCaseModal({
|
||||
onClose,
|
||||
onSuccess,
|
||||
});
|
||||
|
||||
const timeRange = useStateFromPublishingSubject(lensApi.timeRange$);
|
||||
const parentTimeRange = useStateFromPublishingSubject(lensApi.parentApi?.timeRange$);
|
||||
|
||||
const attachments = useMemo(() => {
|
||||
const appliedTimeRange = timeRange ?? parentTimeRange;
|
||||
const attributes = lensApi.getFullAttributes();
|
||||
return !attributes || !appliedTimeRange
|
||||
? []
|
||||
: [getLensCaseAttachment({ attributes, timeRange: appliedTimeRange })];
|
||||
}, [lensApi, timeRange, parentTimeRange]);
|
||||
useEffect(() => {
|
||||
modal.open({ getAttachments: () => attachments });
|
||||
}, [attachments, modal]);
|
||||
|
||||
return null;
|
||||
};
|
||||
AddExistingCaseModalWrapper.displayName = 'AddExistingCaseModalWrapper';
|
||||
|
||||
export function openModal(
|
||||
lensApi: LensApi,
|
||||
currentAppId: string | undefined,
|
||||
casesActionContextProps: CasesActionContextProps,
|
||||
services: Services
|
||||
) {
|
||||
const targetDomElement = document.createElement('div');
|
||||
|
||||
const cleanupDom = (shouldCleanup?: boolean) => {
|
||||
if (targetDomElement != null && shouldCleanup) {
|
||||
unmountComponentAtNode(targetDomElement);
|
||||
}
|
||||
};
|
||||
|
||||
const onClose = (theCase?: CaseUI, isCreateCase?: boolean) => {
|
||||
const closeModalClickedScenario = theCase == null && !isCreateCase;
|
||||
const caseSelectedScenario = theCase != null;
|
||||
// When `Creating` a case from the `add to existing case modal`,
|
||||
// we close the modal and then open the flyout.
|
||||
// If we clean up dom when closing the modal, then the flyout won't open.
|
||||
// Thus we do not clean up dom when `Creating` a case.
|
||||
const shouldCleanup = closeModalClickedScenario || caseSelectedScenario;
|
||||
cleanupDom(shouldCleanup);
|
||||
};
|
||||
|
||||
const onSuccess = () => {
|
||||
cleanupDom(true);
|
||||
};
|
||||
const mount = toMountPoint(
|
||||
<ActionWrapper
|
||||
casesActionContextProps={casesActionContextProps}
|
||||
currentAppId={currentAppId}
|
||||
services={services}
|
||||
>
|
||||
<AddExistingCaseModalWrapper lensApi={lensApi} onClose={onClose} onSuccess={onSuccess} />
|
||||
</ActionWrapper>,
|
||||
{ i18n: services.core.i18n, theme: services.core.theme }
|
||||
);
|
||||
|
||||
mount(targetDomElement);
|
||||
}
|
|
@ -8,31 +8,15 @@
|
|||
import { CONTEXT_MENU_TRIGGER } from '@kbn/embeddable-plugin/public';
|
||||
|
||||
import { createAddToExistingCaseLensAction } from './add_to_existing_case';
|
||||
import type { CasesUIActionProps } from './types';
|
||||
import type { CasesActionContextProps, Services } from './types';
|
||||
|
||||
export const registerUIActions = ({
|
||||
core,
|
||||
plugins,
|
||||
caseContextProps,
|
||||
history,
|
||||
storage,
|
||||
}: CasesUIActionProps) => {
|
||||
registerLensActions({ core, plugins, caseContextProps, history, storage });
|
||||
};
|
||||
|
||||
const registerLensActions = ({
|
||||
core,
|
||||
plugins,
|
||||
caseContextProps,
|
||||
history,
|
||||
storage,
|
||||
}: CasesUIActionProps) => {
|
||||
const addToExistingCaseAction = createAddToExistingCaseLensAction({
|
||||
core,
|
||||
plugins,
|
||||
caseContextProps,
|
||||
history,
|
||||
storage,
|
||||
});
|
||||
plugins.uiActions.addTriggerAction(CONTEXT_MENU_TRIGGER, addToExistingCaseAction);
|
||||
export const registerUIActions = (
|
||||
casesActionContextProps: CasesActionContextProps,
|
||||
services: Services
|
||||
) => {
|
||||
const addToExistingCaseAction = createAddToExistingCaseLensAction(
|
||||
casesActionContextProps,
|
||||
services
|
||||
);
|
||||
services.plugins.uiActions.addTriggerAction(CONTEXT_MENU_TRIGGER, addToExistingCaseAction);
|
||||
};
|
||||
|
|
|
@ -5,30 +5,23 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { CoreStart } from '@kbn/core-lifecycle-browser';
|
||||
import type { Embeddable } from '@kbn/lens-plugin/public';
|
||||
import type { CoreStart } from '@kbn/core/public';
|
||||
import type * as H from 'history';
|
||||
import type { Storage } from '@kbn/kibana-utils-plugin/public';
|
||||
|
||||
import type { ActionExecutionContext } from '@kbn/ui-actions-plugin/public';
|
||||
import type { CasesPublicStartDependencies } from '../../../types';
|
||||
import type { CasesContextProps } from '../../cases_context';
|
||||
|
||||
export type CasesUIActionContextProps = Pick<
|
||||
export type CasesActionContextProps = Pick<
|
||||
CasesContextProps,
|
||||
| 'externalReferenceAttachmentTypeRegistry'
|
||||
| 'persistableStateAttachmentTypeRegistry'
|
||||
| 'getFilesClient'
|
||||
>;
|
||||
|
||||
export interface CasesUIActionProps {
|
||||
export interface Services {
|
||||
core: CoreStart;
|
||||
plugins: CasesPublicStartDependencies;
|
||||
caseContextProps: CasesUIActionContextProps;
|
||||
history: H.History;
|
||||
storage: Storage;
|
||||
}
|
||||
|
||||
export type ActionContext = ActionExecutionContext<{
|
||||
embeddable: Embeddable;
|
||||
}>;
|
||||
|
|
|
@ -5,51 +5,9 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { LENS_EMBEDDABLE_TYPE } from '@kbn/lens-plugin/public';
|
||||
import { isLensEmbeddable, hasInput, getLensCaseAttachment } from './utils';
|
||||
import { getLensCaseAttachment } from './utils';
|
||||
|
||||
describe('utils', () => {
|
||||
describe('isLensEmbeddable', () => {
|
||||
it('return true if it is a lens embeddable', () => {
|
||||
// @ts-expect-error: extra attributes are not needed
|
||||
expect(isLensEmbeddable({ type: LENS_EMBEDDABLE_TYPE })).toBe(true);
|
||||
});
|
||||
|
||||
it('return false if it is not a lens embeddable', () => {
|
||||
// @ts-expect-error: extra attributes are not needed
|
||||
expect(isLensEmbeddable({ type: 'not-exist' })).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('hasInput', () => {
|
||||
it('return true if it has correct input', () => {
|
||||
const embeddable = {
|
||||
getInput: () => ({ timeRange: {} }),
|
||||
getFullAttributes: jest.fn().mockReturnValue({}),
|
||||
};
|
||||
|
||||
// @ts-expect-error: extra attributes are not needed
|
||||
expect(hasInput(embeddable)).toBe(true);
|
||||
});
|
||||
|
||||
it('return false if attributes are null', () => {
|
||||
const embeddable = {
|
||||
getInput: () => ({ timeRange: {} }),
|
||||
getFullAttributes: jest.fn().mockReturnValue(null),
|
||||
};
|
||||
|
||||
// @ts-expect-error: extra attributes are not needed
|
||||
expect(hasInput(embeddable)).toBe(false);
|
||||
});
|
||||
|
||||
it('return false if timeRange is null', () => {
|
||||
const embeddable = { getInput: () => ({ timeRange: null }), getFullAttributes: () => ({}) };
|
||||
|
||||
// @ts-expect-error: extra attributes are not needed
|
||||
expect(hasInput(embeddable)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getLensCaseAttachment', () => {
|
||||
it('create a case lens attachment correctly', () => {
|
||||
const embeddable = { attributes: {}, timeRange: {} };
|
||||
|
|
|
@ -4,24 +4,12 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import type { IEmbeddable } from '@kbn/embeddable-plugin/public';
|
||||
import type { LensSavedObjectAttributes } from '@kbn/lens-plugin/public';
|
||||
import { LENS_EMBEDDABLE_TYPE, type Embeddable as LensEmbeddable } from '@kbn/lens-plugin/public';
|
||||
import { LENS_ATTACHMENT_TYPE } from '../../../../common/constants/visualizations';
|
||||
import type { PersistableStateAttachmentPayload } from '../../../../common/types/domain';
|
||||
import { AttachmentType } from '../../../../common/types/domain';
|
||||
import type { LensProps } from '../types';
|
||||
|
||||
export const isLensEmbeddable = (embeddable: IEmbeddable): embeddable is LensEmbeddable => {
|
||||
return embeddable.type === LENS_EMBEDDABLE_TYPE;
|
||||
};
|
||||
|
||||
export const hasInput = (embeddable: LensEmbeddable) => {
|
||||
const { timeRange } = embeddable.getInput();
|
||||
const attributes = embeddable.getFullAttributes();
|
||||
return attributes != null && timeRange != null;
|
||||
};
|
||||
|
||||
type PersistableStateAttachmentWithoutOwner = Omit<PersistableStateAttachmentPayload, 'owner'>;
|
||||
|
||||
export const getLensCaseAttachment = ({
|
||||
|
|
|
@ -149,17 +149,19 @@ export class CasesUiPlugin
|
|||
getFilesClient: plugins.files.filesClientFactory.asScoped,
|
||||
});
|
||||
|
||||
registerActions({
|
||||
core,
|
||||
plugins,
|
||||
caseContextProps: {
|
||||
registerActions(
|
||||
{
|
||||
externalReferenceAttachmentTypeRegistry: this.externalReferenceAttachmentTypeRegistry,
|
||||
persistableStateAttachmentTypeRegistry: this.persistableStateAttachmentTypeRegistry,
|
||||
getFilesClient: plugins.files.filesClientFactory.asScoped,
|
||||
},
|
||||
history: createBrowserHistory(),
|
||||
storage: this.storage,
|
||||
});
|
||||
{
|
||||
core,
|
||||
plugins,
|
||||
history: createBrowserHistory(),
|
||||
storage: this.storage,
|
||||
}
|
||||
);
|
||||
|
||||
return {
|
||||
api: createClientAPI({ http: core.http }),
|
||||
|
|
|
@ -75,6 +75,7 @@
|
|||
"@kbn/core-logging-browser-mocks",
|
||||
"@kbn/data-views-plugin",
|
||||
"@kbn/core-http-router-server-internal",
|
||||
"@kbn/presentation-publishing",
|
||||
],
|
||||
"exclude": [
|
||||
"target/**/*",
|
||||
|
|
|
@ -22,12 +22,13 @@ export type HasLensConfig = HasType<'lens'> & {
|
|||
getSavedVis: () => Readonly<LensSavedObjectAttributes | undefined>;
|
||||
canViewUnderlyingData: () => Promise<boolean>;
|
||||
getViewUnderlyingDataArgs: () => ViewUnderlyingDataArgs;
|
||||
getFullAttributes: () => LensSavedObjectAttributes | undefined;
|
||||
};
|
||||
|
||||
export type LensApi = HasLensConfig &
|
||||
PublishesPanelTitle &
|
||||
PublishesUnifiedSearch &
|
||||
Partial<HasParentApi<unknown>>;
|
||||
Partial<HasParentApi<Partial<PublishesUnifiedSearch>>>;
|
||||
|
||||
export const isLensApi = (api: unknown): api is LensApi => {
|
||||
return Boolean(
|
||||
|
@ -36,6 +37,7 @@ export const isLensApi = (api: unknown): api is LensApi => {
|
|||
typeof (api as HasLensConfig).getSavedVis === 'function' &&
|
||||
typeof (api as HasLensConfig).canViewUnderlyingData === 'function' &&
|
||||
typeof (api as HasLensConfig).getViewUnderlyingDataArgs === 'function' &&
|
||||
typeof (api as HasLensConfig).getFullAttributes === 'function' &&
|
||||
apiPublishesPanelTitle(api) &&
|
||||
apiPublishesUnifiedSearch(api)
|
||||
);
|
||||
|
|
|
@ -23,6 +23,7 @@ describe('open in discover action', () => {
|
|||
timeRange$: new BehaviorSubject({ from: 'now-15m', to: 'now' }),
|
||||
getSavedVis: jest.fn(() => undefined),
|
||||
canViewUnderlyingData: () => Promise.resolve(true),
|
||||
getFullAttributes: jest.fn(() => undefined),
|
||||
getViewUnderlyingDataArgs: jest.fn(() => ({
|
||||
dataViewSpec: { id: 'index-pattern-id' },
|
||||
timeRange: { from: 'now-7d', to: 'now' },
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue