mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[Cases] Attach framework registry (#134744)
* Create external reference attachment registry * Pass externalReferenceAttachmentTypeRegistry to cases client * Better types * Show external references user action * Handle unregistered events * Add e2e tests * Fixe fixture plugin naming * Add cases fixture plugin to tsconfig * Fix types * Improvements * Fix types * Fixes * Fix bug * Add unit test
This commit is contained in:
parent
465e6f0abd
commit
65834334d3
41 changed files with 974 additions and 50 deletions
|
@ -417,6 +417,8 @@
|
|||
"@kbn/watcher-plugin/*": ["x-pack/plugins/watcher/*"],
|
||||
"@kbn/alerting-fixture-plugin": ["x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts"],
|
||||
"@kbn/alerting-fixture-plugin/*": ["x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/*"],
|
||||
"@kbn/cases-fixture-plugin": ["x-pack/test/functional_with_es_ssl/fixtures/plugins/cases"],
|
||||
"@kbn/cases-fixture-plugin/*": ["x-pack/test/functional_with_es_ssl/fixtures/plugins/cases/*"],
|
||||
"@kbn/test-feature-usage-plugin": ["x-pack/test/licensing_plugin/plugins/test_feature_usage"],
|
||||
"@kbn/test-feature-usage-plugin/*": ["x-pack/test/licensing_plugin/plugins/test_feature_usage/*"],
|
||||
"@kbn/elasticsearch-client-xpack-plugin": ["x-pack/test/plugin_api_integration/plugins/elasticsearch_client"],
|
||||
|
|
|
@ -21,6 +21,7 @@ import {
|
|||
CasesStatusResponse,
|
||||
CasesMetricsResponse,
|
||||
CaseSeverity,
|
||||
CommentResponseExternalReferenceType,
|
||||
} from '../api';
|
||||
import { SnakeToCamelCase } from '../types';
|
||||
|
||||
|
@ -65,6 +66,7 @@ export type CaseViewRefreshPropInterface = null | {
|
|||
|
||||
export type Comment = SnakeToCamelCase<CommentResponse>;
|
||||
export type AlertComment = SnakeToCamelCase<CommentResponseAlertsType>;
|
||||
export type ExternalReferenceComment = SnakeToCamelCase<CommentResponseExternalReferenceType>;
|
||||
export type CaseUserActions = SnakeToCamelCase<CaseUserActionResponse>;
|
||||
export type CaseExternalService = SnakeToCamelCase<CaseExternalServiceBasic>;
|
||||
export type Case = Omit<SnakeToCamelCase<CaseResponse>, 'comments'> & { comments: Comment[] };
|
||||
|
|
|
@ -19,6 +19,7 @@ import {
|
|||
import { EuiThemeProvider as StyledComponentsThemeProvider } from '@kbn/kibana-react-plugin/common';
|
||||
import { RenderAppProps } from './types';
|
||||
import { CasesApp } from './components/app';
|
||||
import { ExternalReferenceAttachmentTypeRegistry } from './client/attachment_framework/external_reference_registry';
|
||||
|
||||
export const renderApp = (deps: RenderAppProps) => {
|
||||
const { mountParams } = deps;
|
||||
|
@ -31,15 +32,23 @@ export const renderApp = (deps: RenderAppProps) => {
|
|||
};
|
||||
};
|
||||
|
||||
const CasesAppWithContext = () => {
|
||||
const [darkMode] = useUiSetting$<boolean>('theme:darkMode');
|
||||
interface CasesAppWithContextProps {
|
||||
externalReferenceAttachmentTypeRegistry: ExternalReferenceAttachmentTypeRegistry;
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledComponentsThemeProvider darkMode={darkMode}>
|
||||
<CasesApp />
|
||||
</StyledComponentsThemeProvider>
|
||||
);
|
||||
};
|
||||
const CasesAppWithContext: React.FC<CasesAppWithContextProps> = React.memo(
|
||||
({ externalReferenceAttachmentTypeRegistry }) => {
|
||||
const [darkMode] = useUiSetting$<boolean>('theme:darkMode');
|
||||
|
||||
return (
|
||||
<StyledComponentsThemeProvider darkMode={darkMode}>
|
||||
<CasesApp
|
||||
externalReferenceAttachmentTypeRegistry={externalReferenceAttachmentTypeRegistry}
|
||||
/>
|
||||
</StyledComponentsThemeProvider>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
CasesAppWithContext.displayName = 'CasesAppWithContext';
|
||||
|
||||
|
@ -60,7 +69,11 @@ export const App: React.FC<{ deps: RenderAppProps }> = ({ deps }) => {
|
|||
}}
|
||||
>
|
||||
<Router history={history}>
|
||||
<CasesAppWithContext />
|
||||
<CasesAppWithContext
|
||||
externalReferenceAttachmentTypeRegistry={
|
||||
deps.externalReferenceAttachmentTypeRegistry
|
||||
}
|
||||
/>
|
||||
</Router>
|
||||
</KibanaContextProvider>
|
||||
</KibanaThemeProvider>
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
/*
|
||||
* 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 { AttachmentTypeRegistry } from './registry';
|
||||
import { ExternalReferenceAttachmentType } from './types';
|
||||
|
||||
export class ExternalReferenceAttachmentTypeRegistry extends AttachmentTypeRegistry<ExternalReferenceAttachmentType> {}
|
|
@ -0,0 +1,86 @@
|
|||
/*
|
||||
* 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 { AttachmentTypeRegistry } from './registry';
|
||||
|
||||
export const ExpressionComponent: React.FunctionComponent = () => {
|
||||
return null;
|
||||
};
|
||||
|
||||
const getItem = (id: string = 'test') => {
|
||||
return { id };
|
||||
};
|
||||
|
||||
describe('AttachmentTypeRegistry', () => {
|
||||
beforeEach(() => jest.resetAllMocks());
|
||||
|
||||
describe('has()', () => {
|
||||
it('returns false for unregistered items', () => {
|
||||
const registry = new AttachmentTypeRegistry();
|
||||
|
||||
expect(registry.has('test')).toEqual(false);
|
||||
});
|
||||
|
||||
it('returns true after registering an item', () => {
|
||||
const registry = new AttachmentTypeRegistry();
|
||||
registry.register(getItem());
|
||||
|
||||
expect(registry.has('test'));
|
||||
});
|
||||
});
|
||||
|
||||
describe('register()', () => {
|
||||
it('able to register items', () => {
|
||||
const registry = new AttachmentTypeRegistry();
|
||||
registry.register(getItem());
|
||||
|
||||
expect(registry.has('test')).toEqual(true);
|
||||
});
|
||||
|
||||
it('throws error if item is already registered', () => {
|
||||
const registry = new AttachmentTypeRegistry();
|
||||
registry.register(getItem('test'));
|
||||
|
||||
expect(() => registry.register(getItem('test'))).toThrowErrorMatchingInlineSnapshot(
|
||||
`"Attachment type \\"test\\" is already registered."`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('get()', () => {
|
||||
it('returns item', () => {
|
||||
const registry = new AttachmentTypeRegistry();
|
||||
registry.register(getItem());
|
||||
const actionType = registry.get('test');
|
||||
|
||||
expect(actionType).toEqual({
|
||||
id: 'test',
|
||||
});
|
||||
});
|
||||
|
||||
it(`throw error when action type doesn't exist`, () => {
|
||||
const registry = new AttachmentTypeRegistry();
|
||||
expect(() => registry.get('not-exist-item')).toThrowErrorMatchingInlineSnapshot(
|
||||
`"Attachment type \\"not-exist-item\\" is not registered."`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('list()', () => {
|
||||
it('returns list of items', () => {
|
||||
const actionTypeRegistry = new AttachmentTypeRegistry();
|
||||
actionTypeRegistry.register(getItem());
|
||||
const actionTypes = actionTypeRegistry.list();
|
||||
|
||||
expect(actionTypes).toEqual([
|
||||
{
|
||||
id: 'test',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,65 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
interface BaseAttachmentType {
|
||||
id: string;
|
||||
}
|
||||
|
||||
export class AttachmentTypeRegistry<T extends BaseAttachmentType> {
|
||||
private readonly attachmentTypes: Map<string, T> = new Map();
|
||||
|
||||
/**
|
||||
* Returns true if the attachment type registry has the given type registered
|
||||
*/
|
||||
public has(id: string) {
|
||||
return this.attachmentTypes.has(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers an attachment type to the type registry
|
||||
*/
|
||||
public register(attachmentType: T) {
|
||||
if (this.has(attachmentType.id)) {
|
||||
throw new Error(
|
||||
i18n.translate('xpack.cases.typeRegistry.register.duplicateAttachmentTypeErrorMessage', {
|
||||
defaultMessage: 'Attachment type "{id}" is already registered.',
|
||||
values: {
|
||||
id: attachmentType.id,
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
this.attachmentTypes.set(attachmentType.id, attachmentType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an attachment type, throw error if not registered
|
||||
*/
|
||||
public get(id: string): T {
|
||||
const attachmentType = this.attachmentTypes.get(id);
|
||||
|
||||
if (!attachmentType) {
|
||||
throw new Error(
|
||||
i18n.translate('xpack.cases.typeRegistry.get.missingActionTypeErrorMessage', {
|
||||
defaultMessage: 'Attachment type "{id}" is not registered.',
|
||||
values: {
|
||||
id,
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
return attachmentType;
|
||||
}
|
||||
|
||||
public list() {
|
||||
return Array.from(this.attachmentTypes).map(([id, attachmentType]) => attachmentType);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
/*
|
||||
* 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 React from 'react';
|
||||
import { EuiCommentProps, IconType } from '@elastic/eui';
|
||||
import { CommentRequestExternalReferenceType } from '../../../common/api';
|
||||
import { Case } from '../../containers/types';
|
||||
|
||||
export interface ExternalReferenceAttachmentViewObject {
|
||||
type?: EuiCommentProps['type'];
|
||||
timelineIcon?: EuiCommentProps['timelineIcon'];
|
||||
actions?: EuiCommentProps['actions'];
|
||||
event?: EuiCommentProps['event'];
|
||||
children?: React.LazyExoticComponent<React.FC>;
|
||||
}
|
||||
|
||||
export interface ExternalReferenceAttachmentViewProps {
|
||||
externalReferenceId: CommentRequestExternalReferenceType['externalReferenceId'];
|
||||
externalReferenceMetadata: CommentRequestExternalReferenceType['externalReferenceMetadata'];
|
||||
caseData: Pick<Case, 'id' | 'title'>;
|
||||
}
|
||||
|
||||
export interface ExternalReferenceAttachmentType {
|
||||
id: string;
|
||||
icon: IconType;
|
||||
displayName: string;
|
||||
getAttachmentViewObject: (
|
||||
props: ExternalReferenceAttachmentViewProps
|
||||
) => ExternalReferenceAttachmentViewObject;
|
||||
}
|
||||
|
||||
export interface AttachmentFramework {
|
||||
registerExternalReference: (
|
||||
externalReferenceAttachmentType: ExternalReferenceAttachmentType
|
||||
) => void;
|
||||
}
|
|
@ -10,19 +10,24 @@ import { EuiLoadingSpinner } from '@elastic/eui';
|
|||
import { AllCasesSelectorModalProps } from '../../components/all_cases/selector_modal';
|
||||
import { CasesProvider, CasesContextProps } from '../../components/cases_context';
|
||||
|
||||
export type GetAllCasesSelectorModalProps = AllCasesSelectorModalProps & CasesContextProps;
|
||||
type GetAllCasesSelectorModalPropsInternal = AllCasesSelectorModalProps & CasesContextProps;
|
||||
export type GetAllCasesSelectorModalProps = Omit<
|
||||
GetAllCasesSelectorModalPropsInternal,
|
||||
'externalReferenceAttachmentTypeRegistry'
|
||||
>;
|
||||
|
||||
const AllCasesSelectorModalLazy: React.FC<AllCasesSelectorModalProps> = lazy(
|
||||
() => import('../../components/all_cases/selector_modal')
|
||||
);
|
||||
export const getAllCasesSelectorModalLazy = ({
|
||||
externalReferenceAttachmentTypeRegistry,
|
||||
owner,
|
||||
userCanCrud,
|
||||
hiddenStatuses,
|
||||
onRowClick,
|
||||
onClose,
|
||||
}: GetAllCasesSelectorModalProps) => (
|
||||
<CasesProvider value={{ owner, userCanCrud }}>
|
||||
}: GetAllCasesSelectorModalPropsInternal) => (
|
||||
<CasesProvider value={{ externalReferenceAttachmentTypeRegistry, owner, userCanCrud }}>
|
||||
<Suspense fallback={<EuiLoadingSpinner />}>
|
||||
<AllCasesSelectorModalLazy
|
||||
hiddenStatuses={hiddenStatuses}
|
||||
|
|
|
@ -10,11 +10,13 @@ import React, { lazy, Suspense } from 'react';
|
|||
import type { CasesProps } from '../../components/app';
|
||||
import { CasesProvider, CasesContextProps } from '../../components/cases_context';
|
||||
|
||||
export type GetCasesProps = CasesProps & CasesContextProps;
|
||||
type GetCasesPropsInternal = CasesProps & CasesContextProps;
|
||||
export type GetCasesProps = Omit<GetCasesPropsInternal, 'externalReferenceAttachmentTypeRegistry'>;
|
||||
|
||||
const CasesRoutesLazy: React.FC<CasesProps> = lazy(() => import('../../components/app/routes'));
|
||||
|
||||
export const getCasesLazy = ({
|
||||
externalReferenceAttachmentTypeRegistry,
|
||||
owner,
|
||||
userCanCrud,
|
||||
basePath,
|
||||
|
@ -27,8 +29,17 @@ export const getCasesLazy = ({
|
|||
timelineIntegration,
|
||||
features,
|
||||
releasePhase,
|
||||
}: GetCasesProps) => (
|
||||
<CasesProvider value={{ owner, userCanCrud, basePath, features, releasePhase }}>
|
||||
}: GetCasesPropsInternal) => (
|
||||
<CasesProvider
|
||||
value={{
|
||||
externalReferenceAttachmentTypeRegistry,
|
||||
owner,
|
||||
userCanCrud,
|
||||
basePath,
|
||||
features,
|
||||
releasePhase,
|
||||
}}
|
||||
>
|
||||
<Suspense fallback={<EuiLoadingSpinner />}>
|
||||
<CasesRoutesLazy
|
||||
onComponentInitialized={onComponentInitialized}
|
||||
|
|
|
@ -9,29 +9,62 @@ import { EuiLoadingSpinner } from '@elastic/eui';
|
|||
import React, { lazy, ReactNode, Suspense } from 'react';
|
||||
import { CasesContextProps } from '../../components/cases_context';
|
||||
|
||||
export type GetCasesContextProps = CasesContextProps;
|
||||
export type GetCasesContextPropsInternal = CasesContextProps;
|
||||
export type GetCasesContextProps = Omit<
|
||||
CasesContextProps,
|
||||
'externalReferenceAttachmentTypeRegistry'
|
||||
>;
|
||||
|
||||
const CasesProviderLazy: React.FC<{ value: GetCasesContextProps }> = lazy(
|
||||
const CasesProviderLazy: React.FC<{ value: GetCasesContextPropsInternal }> = lazy(
|
||||
() => import('../../components/cases_context')
|
||||
);
|
||||
|
||||
const CasesProviderLazyWrapper = ({
|
||||
externalReferenceAttachmentTypeRegistry,
|
||||
owner,
|
||||
userCanCrud,
|
||||
features,
|
||||
children,
|
||||
releasePhase,
|
||||
}: GetCasesContextProps & { children: ReactNode }) => {
|
||||
}: GetCasesContextPropsInternal & { children: ReactNode }) => {
|
||||
return (
|
||||
<Suspense fallback={<EuiLoadingSpinner />}>
|
||||
<CasesProviderLazy value={{ owner, userCanCrud, features, releasePhase }}>
|
||||
<CasesProviderLazy
|
||||
value={{
|
||||
externalReferenceAttachmentTypeRegistry,
|
||||
owner,
|
||||
userCanCrud,
|
||||
features,
|
||||
releasePhase,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</CasesProviderLazy>
|
||||
</Suspense>
|
||||
);
|
||||
};
|
||||
|
||||
CasesProviderLazyWrapper.displayName = 'CasesProviderLazyWrapper';
|
||||
|
||||
export const getCasesContextLazy = () => {
|
||||
return CasesProviderLazyWrapper;
|
||||
export const getCasesContextLazy = ({
|
||||
externalReferenceAttachmentTypeRegistry,
|
||||
}: Pick<
|
||||
GetCasesContextPropsInternal,
|
||||
'externalReferenceAttachmentTypeRegistry'
|
||||
>): (() => React.FC<GetCasesContextProps>) => {
|
||||
const CasesProviderLazyWrapperWithRegistry: React.FC<GetCasesContextProps> = ({
|
||||
children,
|
||||
...props
|
||||
}) => (
|
||||
<CasesProviderLazyWrapper
|
||||
{...props}
|
||||
externalReferenceAttachmentTypeRegistry={externalReferenceAttachmentTypeRegistry}
|
||||
>
|
||||
{children}
|
||||
</CasesProviderLazyWrapper>
|
||||
);
|
||||
|
||||
CasesProviderLazyWrapperWithRegistry.displayName = 'CasesProviderLazyWrapperWithRegistry';
|
||||
|
||||
return () => CasesProviderLazyWrapperWithRegistry;
|
||||
};
|
||||
|
|
|
@ -10,12 +10,17 @@ import { EuiLoadingSpinner } from '@elastic/eui';
|
|||
import type { CreateCaseFlyoutProps } from '../../components/create/flyout';
|
||||
import { CasesProvider, CasesContextProps } from '../../components/cases_context';
|
||||
|
||||
export type GetCreateCaseFlyoutProps = CreateCaseFlyoutProps & CasesContextProps;
|
||||
type GetCreateCaseFlyoutPropsInternal = CreateCaseFlyoutProps & CasesContextProps;
|
||||
export type GetCreateCaseFlyoutProps = Omit<
|
||||
GetCreateCaseFlyoutPropsInternal,
|
||||
'externalReferenceAttachmentTypeRegistry'
|
||||
>;
|
||||
|
||||
export const CreateCaseFlyoutLazy: React.FC<CreateCaseFlyoutProps> = lazy(
|
||||
() => import('../../components/create/flyout')
|
||||
);
|
||||
export const getCreateCaseFlyoutLazy = ({
|
||||
externalReferenceAttachmentTypeRegistry,
|
||||
owner,
|
||||
userCanCrud,
|
||||
features,
|
||||
|
@ -23,8 +28,8 @@ export const getCreateCaseFlyoutLazy = ({
|
|||
onClose,
|
||||
onSuccess,
|
||||
attachments,
|
||||
}: GetCreateCaseFlyoutProps) => (
|
||||
<CasesProvider value={{ owner, userCanCrud, features }}>
|
||||
}: GetCreateCaseFlyoutPropsInternal) => (
|
||||
<CasesProvider value={{ externalReferenceAttachmentTypeRegistry, owner, userCanCrud, features }}>
|
||||
<Suspense fallback={<EuiLoadingSpinner />}>
|
||||
<CreateCaseFlyoutLazy
|
||||
afterCaseCreated={afterCaseCreated}
|
||||
|
|
|
@ -10,13 +10,22 @@ import React, { lazy, Suspense } from 'react';
|
|||
import { CasesProvider, CasesContextProps } from '../../components/cases_context';
|
||||
import { RecentCasesProps } from '../../components/recent_cases';
|
||||
|
||||
export type GetRecentCasesProps = RecentCasesProps & CasesContextProps;
|
||||
type GetRecentCasesPropsInternal = RecentCasesProps & CasesContextProps;
|
||||
export type GetRecentCasesProps = Omit<
|
||||
GetRecentCasesPropsInternal,
|
||||
'externalReferenceAttachmentTypeRegistry'
|
||||
>;
|
||||
|
||||
const RecentCasesLazy: React.FC<RecentCasesProps> = lazy(
|
||||
() => import('../../components/recent_cases')
|
||||
);
|
||||
export const getRecentCasesLazy = ({ owner, userCanCrud, maxCasesToShow }: GetRecentCasesProps) => (
|
||||
<CasesProvider value={{ owner, userCanCrud }}>
|
||||
export const getRecentCasesLazy = ({
|
||||
externalReferenceAttachmentTypeRegistry,
|
||||
owner,
|
||||
userCanCrud,
|
||||
maxCasesToShow,
|
||||
}: GetRecentCasesPropsInternal) => (
|
||||
<CasesProvider value={{ externalReferenceAttachmentTypeRegistry, owner, userCanCrud }}>
|
||||
<Suspense fallback={<EuiLoadingSpinner />}>
|
||||
<RecentCasesLazy maxCasesToShow={maxCasesToShow} />
|
||||
</Suspense>
|
||||
|
|
|
@ -23,6 +23,9 @@ import {
|
|||
import { FieldHook } from '../shared_imports';
|
||||
import { StartServices } from '../../types';
|
||||
import { ReleasePhase } from '../../components/types';
|
||||
import { AttachmentTypeRegistry } from '../../client/attachment_framework/registry';
|
||||
import { ExternalReferenceAttachmentType } from '../../client/attachment_framework/types';
|
||||
import { ExternalReferenceAttachmentTypeRegistry } from '../../client/attachment_framework/external_reference_registry';
|
||||
|
||||
interface TestProviderProps {
|
||||
children: React.ReactNode;
|
||||
|
@ -30,6 +33,7 @@ interface TestProviderProps {
|
|||
features?: CasesFeatures;
|
||||
owner?: string[];
|
||||
releasePhase?: ReleasePhase;
|
||||
externalReferenceAttachmentTypeRegistry?: AttachmentTypeRegistry<ExternalReferenceAttachmentType>;
|
||||
}
|
||||
type UiRender = (ui: React.ReactElement, options?: RenderOptions) => RenderResult;
|
||||
|
||||
|
@ -43,6 +47,7 @@ const TestProvidersComponent: React.FC<TestProviderProps> = ({
|
|||
owner = [SECURITY_SOLUTION_OWNER],
|
||||
userCanCrud = true,
|
||||
releasePhase = 'ga',
|
||||
externalReferenceAttachmentTypeRegistry = new ExternalReferenceAttachmentTypeRegistry(),
|
||||
}) => {
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
|
@ -57,7 +62,11 @@ const TestProvidersComponent: React.FC<TestProviderProps> = ({
|
|||
<MockKibanaContextProvider>
|
||||
<ThemeProvider theme={() => ({ eui: euiDarkVars, darkMode: true })}>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<CasesProvider value={{ features, owner, userCanCrud }}>{children}</CasesProvider>
|
||||
<CasesProvider
|
||||
value={{ externalReferenceAttachmentTypeRegistry, features, owner, userCanCrud }}
|
||||
>
|
||||
{children}
|
||||
</CasesProvider>
|
||||
</QueryClientProvider>
|
||||
</ThemeProvider>
|
||||
</MockKibanaContextProvider>
|
||||
|
@ -69,6 +78,7 @@ TestProvidersComponent.displayName = 'TestProviders';
|
|||
export const TestProviders = React.memo(TestProvidersComponent);
|
||||
|
||||
export interface AppMockRenderer {
|
||||
externalReferenceAttachmentTypeRegistry: ExternalReferenceAttachmentTypeRegistry;
|
||||
render: UiRender;
|
||||
coreStart: StartServices;
|
||||
queryClient: QueryClient;
|
||||
|
@ -87,6 +97,7 @@ export const createAppMockRenderer = ({
|
|||
owner = [SECURITY_SOLUTION_OWNER],
|
||||
userCanCrud = true,
|
||||
releasePhase = 'ga',
|
||||
externalReferenceAttachmentTypeRegistry = new ExternalReferenceAttachmentTypeRegistry(),
|
||||
}: Omit<TestProviderProps, 'children'> = {}): AppMockRenderer => {
|
||||
const services = createStartServicesMock();
|
||||
const queryClient = new QueryClient({
|
||||
|
@ -102,7 +113,15 @@ export const createAppMockRenderer = ({
|
|||
<KibanaContextProvider services={services}>
|
||||
<ThemeProvider theme={() => ({ eui: euiDarkVars, darkMode: true })}>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<CasesProvider value={{ features, owner, userCanCrud, releasePhase }}>
|
||||
<CasesProvider
|
||||
value={{
|
||||
externalReferenceAttachmentTypeRegistry,
|
||||
features,
|
||||
owner,
|
||||
userCanCrud,
|
||||
releasePhase,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</CasesProvider>
|
||||
</QueryClientProvider>
|
||||
|
@ -110,18 +129,22 @@ export const createAppMockRenderer = ({
|
|||
</KibanaContextProvider>
|
||||
</I18nProvider>
|
||||
);
|
||||
|
||||
AppWrapper.displayName = 'AppWrapper';
|
||||
|
||||
const render: UiRender = (ui, options) => {
|
||||
return reactRender(ui, {
|
||||
wrapper: AppWrapper,
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
coreStart: services,
|
||||
queryClient,
|
||||
render,
|
||||
AppWrapper,
|
||||
externalReferenceAttachmentTypeRegistry,
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -17,6 +17,7 @@ import { alertComment } from '../../../containers/mock';
|
|||
import { useCreateAttachments } from '../../../containers/use_create_attachments';
|
||||
import { CasesContext } from '../../cases_context';
|
||||
import { CasesContextStoreActionsList } from '../../cases_context/cases_context_reducer';
|
||||
import { ExternalReferenceAttachmentTypeRegistry } from '../../../client/attachment_framework/external_reference_registry';
|
||||
import { useCasesAddToExistingCaseModal } from './use_cases_add_to_existing_case_modal';
|
||||
|
||||
jest.mock('../../../common/use_cases_toast');
|
||||
|
@ -45,6 +46,8 @@ const TestComponent: React.FC = () => {
|
|||
|
||||
const useCreateAttachmentsMock = useCreateAttachments as jest.Mock;
|
||||
|
||||
const externalReferenceAttachmentTypeRegistry = new ExternalReferenceAttachmentTypeRegistry();
|
||||
|
||||
describe('use cases add to existing case modal hook', () => {
|
||||
useCreateAttachmentsMock.mockReturnValue({
|
||||
createAttachments: jest.fn(),
|
||||
|
@ -56,6 +59,7 @@ describe('use cases add to existing case modal hook', () => {
|
|||
return (
|
||||
<CasesContext.Provider
|
||||
value={{
|
||||
externalReferenceAttachmentTypeRegistry,
|
||||
owner: ['test'],
|
||||
userCanCrud: true,
|
||||
appId: 'test',
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
import React from 'react';
|
||||
import { APP_OWNER } from '../../../common/constants';
|
||||
import { ExternalReferenceAttachmentTypeRegistry } from '../../client/attachment_framework/external_reference_registry';
|
||||
import { getCasesLazy } from '../../client/ui/get_cases';
|
||||
import { useApplicationCapabilities } from '../../common/lib/kibana';
|
||||
|
||||
|
@ -15,12 +16,19 @@ import { CasesRoutesProps } from './types';
|
|||
|
||||
export type CasesProps = CasesRoutesProps;
|
||||
|
||||
const CasesAppComponent: React.FC = () => {
|
||||
interface CasesAppProps {
|
||||
externalReferenceAttachmentTypeRegistry: ExternalReferenceAttachmentTypeRegistry;
|
||||
}
|
||||
|
||||
const CasesAppComponent: React.FC<CasesAppProps> = ({
|
||||
externalReferenceAttachmentTypeRegistry,
|
||||
}) => {
|
||||
const userCapabilities = useApplicationCapabilities();
|
||||
|
||||
return (
|
||||
<Wrapper data-test-subj="cases-app">
|
||||
{getCasesLazy({
|
||||
externalReferenceAttachmentTypeRegistry,
|
||||
owner: [APP_OWNER],
|
||||
useFetchAlertData: () => [false, {}],
|
||||
userCanCrud: userCapabilities.generalCases.crud,
|
||||
|
|
|
@ -18,10 +18,12 @@ import {
|
|||
import { CasesFeaturesAllRequired, CasesFeatures } from '../../containers/types';
|
||||
import { CasesGlobalComponents } from './cases_global_components';
|
||||
import { ReleasePhase } from '../types';
|
||||
import { ExternalReferenceAttachmentTypeRegistry } from '../../client/attachment_framework/external_reference_registry';
|
||||
|
||||
export type CasesContextValueDispatch = Dispatch<CasesContextStoreAction>;
|
||||
|
||||
export interface CasesContextValue {
|
||||
externalReferenceAttachmentTypeRegistry: ExternalReferenceAttachmentTypeRegistry;
|
||||
owner: string[];
|
||||
appId: string;
|
||||
appTitle: string;
|
||||
|
@ -32,7 +34,11 @@ export interface CasesContextValue {
|
|||
dispatch: CasesContextValueDispatch;
|
||||
}
|
||||
|
||||
export interface CasesContextProps extends Pick<CasesContextValue, 'owner' | 'userCanCrud'> {
|
||||
export interface CasesContextProps
|
||||
extends Pick<
|
||||
CasesContextValue,
|
||||
'owner' | 'userCanCrud' | 'externalReferenceAttachmentTypeRegistry'
|
||||
> {
|
||||
basePath?: string;
|
||||
features?: CasesFeatures;
|
||||
releasePhase?: ReleasePhase;
|
||||
|
@ -47,11 +53,19 @@ export interface CasesContextStateValue extends Omit<CasesContextValue, 'appId'
|
|||
|
||||
export const CasesProvider: React.FC<{ value: CasesContextProps }> = ({
|
||||
children,
|
||||
value: { owner, userCanCrud, basePath = DEFAULT_BASE_PATH, features = {}, releasePhase = 'ga' },
|
||||
value: {
|
||||
externalReferenceAttachmentTypeRegistry,
|
||||
owner,
|
||||
userCanCrud,
|
||||
basePath = DEFAULT_BASE_PATH,
|
||||
features = {},
|
||||
releasePhase = 'ga',
|
||||
},
|
||||
}) => {
|
||||
const { appId, appTitle } = useApplication();
|
||||
const [state, dispatch] = useReducer(casesContextReducer, getInitialCasesContextState());
|
||||
const [value, setValue] = useState<CasesContextStateValue>(() => ({
|
||||
externalReferenceAttachmentTypeRegistry,
|
||||
owner,
|
||||
userCanCrud,
|
||||
basePath,
|
||||
|
|
|
@ -13,8 +13,12 @@ import React from 'react';
|
|||
import { CasesContext } from '../../cases_context';
|
||||
import { CasesContextStoreActionsList } from '../../cases_context/cases_context_reducer';
|
||||
import { useCasesAddToNewCaseFlyout } from './use_cases_add_to_new_case_flyout';
|
||||
import { ExternalReferenceAttachmentTypeRegistry } from '../../../client/attachment_framework/external_reference_registry';
|
||||
|
||||
jest.mock('../../../common/use_cases_toast');
|
||||
|
||||
const externalReferenceAttachmentTypeRegistry = new ExternalReferenceAttachmentTypeRegistry();
|
||||
|
||||
describe('use cases add to new case flyout hook', () => {
|
||||
const dispatch = jest.fn();
|
||||
let wrapper: React.FC;
|
||||
|
@ -24,6 +28,7 @@ describe('use cases add to new case flyout hook', () => {
|
|||
return (
|
||||
<CasesContext.Provider
|
||||
value={{
|
||||
externalReferenceAttachmentTypeRegistry,
|
||||
owner: ['test'],
|
||||
userCanCrud: true,
|
||||
appId: 'test',
|
||||
|
|
|
@ -34,6 +34,9 @@ exports[`EditableTitle renders 1`] = `
|
|||
<CasesProvider
|
||||
value={
|
||||
Object {
|
||||
"externalReferenceAttachmentTypeRegistry": ExternalReferenceAttachmentTypeRegistry {
|
||||
"attachmentTypes": Map {},
|
||||
},
|
||||
"features": undefined,
|
||||
"owner": Array [
|
||||
"securitySolution",
|
||||
|
|
|
@ -34,6 +34,9 @@ exports[`HeaderPage it renders 1`] = `
|
|||
<CasesProvider
|
||||
value={
|
||||
Object {
|
||||
"externalReferenceAttachmentTypeRegistry": ExternalReferenceAttachmentTypeRegistry {
|
||||
"attachmentTypes": Map {},
|
||||
},
|
||||
"features": undefined,
|
||||
"owner": Array [
|
||||
"securitySolution",
|
||||
|
|
|
@ -13,15 +13,19 @@ import { Actions } from '../../../../common/api';
|
|||
import {
|
||||
alertComment,
|
||||
basicCase,
|
||||
externalReferenceAttachment,
|
||||
getAlertUserAction,
|
||||
getExternalReferenceAttachment,
|
||||
getExternalReferenceUserAction,
|
||||
getHostIsolationUserAction,
|
||||
getUserAction,
|
||||
hostIsolationComment,
|
||||
} from '../../../containers/mock';
|
||||
import { TestProviders } from '../../../common/mock';
|
||||
import { AppMockRenderer, createAppMockRenderer, TestProviders } from '../../../common/mock';
|
||||
import { createCommentUserActionBuilder } from './comment';
|
||||
import { getMockBuilderArgs } from '../mock';
|
||||
import { useCaseViewParams } from '../../../common/navigation';
|
||||
import { ExternalReferenceAttachmentTypeRegistry } from '../../../client/attachment_framework/external_reference_registry';
|
||||
|
||||
jest.mock('../../../common/lib/kibana');
|
||||
jest.mock('../../../common/navigation/hooks');
|
||||
|
@ -172,4 +176,91 @@ describe('createCommentUserActionBuilder', () => {
|
|||
expect(screen.getByText('host1')).toBeInTheDocument();
|
||||
expect(screen.getByText('I just isolated the host!')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
describe('External references', () => {
|
||||
let appMockRender: AppMockRenderer;
|
||||
|
||||
beforeEach(() => {
|
||||
appMockRender = createAppMockRenderer();
|
||||
});
|
||||
|
||||
it('renders correctly an external reference', async () => {
|
||||
const externalReferenceAttachmentTypeRegistry = new ExternalReferenceAttachmentTypeRegistry();
|
||||
externalReferenceAttachmentTypeRegistry.register(
|
||||
getExternalReferenceAttachment({ type: 'regular' })
|
||||
);
|
||||
|
||||
const userAction = getExternalReferenceUserAction();
|
||||
const builder = createCommentUserActionBuilder({
|
||||
...builderArgs,
|
||||
externalReferenceAttachmentTypeRegistry,
|
||||
caseData: {
|
||||
...builderArgs.caseData,
|
||||
comments: [externalReferenceAttachment],
|
||||
},
|
||||
userAction,
|
||||
});
|
||||
|
||||
const createdUserAction = builder.build();
|
||||
const result = appMockRender.render(<EuiCommentList comments={createdUserAction} />);
|
||||
|
||||
expect(result.getByTestId('comment-external-reference-.test')).toBeInTheDocument();
|
||||
expect(result.getByTestId('copy-link-external-reference-comment-id')).toBeInTheDocument();
|
||||
expect(result.getByTestId('user-action-username-with-avatar')).toBeInTheDocument();
|
||||
expect(screen.getByText('added a chart')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders correctly if the reference is not registered', async () => {
|
||||
const externalReferenceAttachmentTypeRegistry = new ExternalReferenceAttachmentTypeRegistry();
|
||||
|
||||
const userAction = getExternalReferenceUserAction();
|
||||
const builder = createCommentUserActionBuilder({
|
||||
...builderArgs,
|
||||
externalReferenceAttachmentTypeRegistry,
|
||||
caseData: {
|
||||
...builderArgs.caseData,
|
||||
comments: [externalReferenceAttachment],
|
||||
},
|
||||
userAction,
|
||||
});
|
||||
|
||||
const createdUserAction = builder.build();
|
||||
const result = appMockRender.render(<EuiCommentList comments={createdUserAction} />);
|
||||
|
||||
expect(result.getByTestId('comment-external-reference-not-found')).toBeInTheDocument();
|
||||
expect(screen.getByText('added an attachment of type')).toBeInTheDocument();
|
||||
expect(screen.getByText('Attachment type is not registered')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders correctly an external reference with actions', async () => {
|
||||
const ActionsView = () => {
|
||||
return <>{'Attachment actions'}</>;
|
||||
};
|
||||
|
||||
const attachment = getExternalReferenceAttachment({
|
||||
type: 'regular',
|
||||
actions: <ActionsView />,
|
||||
});
|
||||
|
||||
const externalReferenceAttachmentTypeRegistry = new ExternalReferenceAttachmentTypeRegistry();
|
||||
externalReferenceAttachmentTypeRegistry.register(attachment);
|
||||
|
||||
const userAction = getExternalReferenceUserAction();
|
||||
const builder = createCommentUserActionBuilder({
|
||||
...builderArgs,
|
||||
externalReferenceAttachmentTypeRegistry,
|
||||
caseData: {
|
||||
...builderArgs.caseData,
|
||||
comments: [externalReferenceAttachment],
|
||||
},
|
||||
userAction,
|
||||
});
|
||||
|
||||
const createdUserAction = builder.build();
|
||||
const result = appMockRender.render(<EuiCommentList comments={createdUserAction} />);
|
||||
|
||||
expect(result.getByTestId('comment-external-reference-.test')).toBeInTheDocument();
|
||||
expect(screen.getByText('Attachment actions')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -15,6 +15,7 @@ import * as i18n from '../translations';
|
|||
import { createUserAttachmentUserActionBuilder } from './user';
|
||||
import { createAlertAttachmentUserActionBuilder } from './alert';
|
||||
import { createActionAttachmentUserActionBuilder } from './actions';
|
||||
import { createExternalReferenceAttachmentUserActionBuilder } from './external_reference';
|
||||
|
||||
const getUpdateLabelTitle = () => `${i18n.EDITED_FIELD} ${i18n.COMMENT.toLowerCase()}`;
|
||||
const getDeleteLabelTitle = () => `${i18n.REMOVED_FIELD} ${i18n.COMMENT.toLowerCase()}`;
|
||||
|
@ -38,6 +39,8 @@ const getDeleteCommentUserAction = ({
|
|||
|
||||
const getCreateCommentUserAction = ({
|
||||
userAction,
|
||||
caseData,
|
||||
externalReferenceAttachmentTypeRegistry,
|
||||
comment,
|
||||
userCanCrud,
|
||||
commentRefs,
|
||||
|
@ -59,7 +62,7 @@ const getCreateCommentUserAction = ({
|
|||
comment: Comment;
|
||||
} & Omit<
|
||||
UserActionBuilderArgs,
|
||||
'caseData' | 'caseServices' | 'comments' | 'index' | 'handleOutlineComment'
|
||||
'caseServices' | 'comments' | 'index' | 'handleOutlineComment'
|
||||
>): EuiCommentProps[] => {
|
||||
switch (comment.type) {
|
||||
case CommentType.user:
|
||||
|
@ -96,6 +99,14 @@ const getCreateCommentUserAction = ({
|
|||
actionsNavigation,
|
||||
});
|
||||
return actionBuilder.build();
|
||||
case CommentType.externalReference:
|
||||
const externalReferenceBuilder = createExternalReferenceAttachmentUserActionBuilder({
|
||||
userAction,
|
||||
comment,
|
||||
externalReferenceAttachmentTypeRegistry,
|
||||
caseData,
|
||||
});
|
||||
return externalReferenceBuilder.build();
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
|
@ -103,6 +114,7 @@ const getCreateCommentUserAction = ({
|
|||
|
||||
export const createCommentUserActionBuilder: UserActionBuilder = ({
|
||||
caseData,
|
||||
externalReferenceAttachmentTypeRegistry,
|
||||
userAction,
|
||||
userCanCrud,
|
||||
commentRefs,
|
||||
|
@ -129,13 +141,16 @@ export const createCommentUserActionBuilder: UserActionBuilder = ({
|
|||
}
|
||||
|
||||
const comment = caseData.comments.find((c) => c.id === commentUserAction.commentId);
|
||||
|
||||
if (comment == null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (commentUserAction.action === Actions.create) {
|
||||
const commentAction = getCreateCommentUserAction({
|
||||
caseData,
|
||||
userAction: commentUserAction,
|
||||
externalReferenceAttachmentTypeRegistry,
|
||||
comment,
|
||||
userCanCrud,
|
||||
commentRefs,
|
||||
|
|
|
@ -0,0 +1,102 @@
|
|||
/*
|
||||
* 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, { Suspense } from 'react';
|
||||
|
||||
import { EuiCallOut, EuiCode, EuiLoadingSpinner } from '@elastic/eui';
|
||||
import { CommentResponseExternalReferenceType } from '../../../../common/api';
|
||||
import { UserActionBuilder, UserActionBuilderArgs } from '../types';
|
||||
import { UserActionTimestamp } from '../timestamp';
|
||||
import { SnakeToCamelCase } from '../../../../common/types';
|
||||
import { UserActionUsernameWithAvatar } from '../avatar_username';
|
||||
import { UserActionCopyLink } from '../copy_link';
|
||||
import { ATTACHMENT_NOT_REGISTERED_ERROR, DEFAULT_EVENT_ATTACHMENT_TITLE } from './translations';
|
||||
|
||||
type BuilderArgs = Pick<
|
||||
UserActionBuilderArgs,
|
||||
'userAction' | 'externalReferenceAttachmentTypeRegistry' | 'caseData'
|
||||
> & {
|
||||
comment: SnakeToCamelCase<CommentResponseExternalReferenceType>;
|
||||
};
|
||||
|
||||
export const createExternalReferenceAttachmentUserActionBuilder = ({
|
||||
userAction,
|
||||
comment,
|
||||
externalReferenceAttachmentTypeRegistry,
|
||||
caseData,
|
||||
}: BuilderArgs): ReturnType<UserActionBuilder> => ({
|
||||
// TODO: Fix this manually. Issue #123375
|
||||
// eslint-disable-next-line react/display-name
|
||||
build: () => {
|
||||
const isTypeRegistered = externalReferenceAttachmentTypeRegistry.has(
|
||||
comment.externalReferenceAttachmentTypeId
|
||||
);
|
||||
|
||||
if (!isTypeRegistered) {
|
||||
return [
|
||||
{
|
||||
username: (
|
||||
<UserActionUsernameWithAvatar
|
||||
username={comment.createdBy.username}
|
||||
fullName={comment.createdBy.fullName}
|
||||
/>
|
||||
),
|
||||
event: (
|
||||
<>
|
||||
{`${DEFAULT_EVENT_ATTACHMENT_TITLE} `}
|
||||
<EuiCode>{comment.externalReferenceAttachmentTypeId}</EuiCode>
|
||||
</>
|
||||
),
|
||||
className: 'comment-external-reference-not-found',
|
||||
'data-test-subj': 'comment-external-reference-not-found',
|
||||
timestamp: <UserActionTimestamp createdAt={userAction.createdAt} />,
|
||||
children: (
|
||||
<EuiCallOut title={ATTACHMENT_NOT_REGISTERED_ERROR} color="danger" iconType="alert" />
|
||||
),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
const externalReferenceType = externalReferenceAttachmentTypeRegistry.get(
|
||||
comment.externalReferenceAttachmentTypeId
|
||||
);
|
||||
|
||||
const externalReferenceViewObject = externalReferenceType.getAttachmentViewObject({
|
||||
externalReferenceId: comment.externalReferenceId,
|
||||
externalReferenceMetadata: comment.externalReferenceMetadata,
|
||||
caseData: { id: caseData.id, title: caseData.title },
|
||||
});
|
||||
|
||||
return [
|
||||
{
|
||||
username: (
|
||||
<UserActionUsernameWithAvatar
|
||||
username={comment.createdBy.username}
|
||||
fullName={comment.createdBy.fullName}
|
||||
/>
|
||||
),
|
||||
type: externalReferenceViewObject.type,
|
||||
className: `comment-external-reference${comment.externalReferenceAttachmentTypeId}`,
|
||||
event: externalReferenceViewObject.event,
|
||||
'data-test-subj': `comment-external-reference-${comment.externalReferenceAttachmentTypeId}`,
|
||||
timestamp: <UserActionTimestamp createdAt={userAction.createdAt} />,
|
||||
timelineIcon: externalReferenceViewObject.timelineIcon,
|
||||
actions: (
|
||||
<>
|
||||
<UserActionCopyLink id={comment.id} />
|
||||
{externalReferenceViewObject.actions}
|
||||
</>
|
||||
),
|
||||
children: externalReferenceViewObject.children ? (
|
||||
<Suspense fallback={<EuiLoadingSpinner />}>
|
||||
{React.createElement(externalReferenceViewObject.children)}
|
||||
</Suspense>
|
||||
) : undefined,
|
||||
},
|
||||
];
|
||||
},
|
||||
});
|
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
export const ATTACHMENT_NOT_REGISTERED_ERROR = i18n.translate(
|
||||
'xpack.cases.userActions.attachmentNotRegisteredErrorMsg',
|
||||
{
|
||||
defaultMessage: 'Attachment type is not registered',
|
||||
}
|
||||
);
|
||||
|
||||
export const DEFAULT_EVENT_ATTACHMENT_TITLE = i18n.translate(
|
||||
'xpack.cases.userActions.defaultEventAttachmentTitle',
|
||||
{
|
||||
defaultMessage: 'added an attachment of type',
|
||||
}
|
||||
);
|
|
@ -27,6 +27,7 @@ import type { UserActionTreeProps } from './types';
|
|||
import { getDescriptionUserAction } from './description';
|
||||
import { useUserActionsHandler } from './use_user_actions_handler';
|
||||
import { NEW_COMMENT_ID } from './constants';
|
||||
import { useCasesContext } from '../cases_context/use_cases_context';
|
||||
|
||||
const MyEuiFlexGroup = styled(EuiFlexGroup)`
|
||||
margin-bottom: 8px;
|
||||
|
@ -95,6 +96,7 @@ export const UserActions = React.memo(
|
|||
const { detailName: caseId, commentId } = useCaseViewParams();
|
||||
const [initLoading, setInitLoading] = useState(true);
|
||||
const currentUser = useCurrentUser();
|
||||
const { externalReferenceAttachmentTypeRegistry } = useCasesContext();
|
||||
|
||||
const alertIdsWithoutRuleInfo = useMemo(
|
||||
() => getManualAlertIdsWithNoRuleId(caseData.comments),
|
||||
|
@ -188,6 +190,7 @@ export const UserActions = React.memo(
|
|||
|
||||
const userActionBuilder = builder({
|
||||
caseData,
|
||||
externalReferenceAttachmentTypeRegistry,
|
||||
userAction,
|
||||
caseServices,
|
||||
comments: caseData.comments,
|
||||
|
@ -215,6 +218,7 @@ export const UserActions = React.memo(
|
|||
),
|
||||
[
|
||||
caseUserActions,
|
||||
externalReferenceAttachmentTypeRegistry,
|
||||
descriptionCommentListObj,
|
||||
caseData,
|
||||
caseServices,
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
import { Actions } from '../../../common/api';
|
||||
import { SECURITY_SOLUTION_OWNER } from '../../../common/constants';
|
||||
import { ExternalReferenceAttachmentTypeRegistry } from '../../client/attachment_framework/external_reference_registry';
|
||||
import { basicCase, basicPush, getUserAction } from '../../containers/mock';
|
||||
import { UserActionBuilderArgs } from './types';
|
||||
|
||||
|
@ -56,9 +57,11 @@ export const getMockBuilderArgs = (): UserActionBuilderArgs => {
|
|||
const handleDeleteComment = jest.fn();
|
||||
const handleManageQuote = jest.fn();
|
||||
const handleOutlineComment = jest.fn();
|
||||
const externalReferenceAttachmentTypeRegistry = new ExternalReferenceAttachmentTypeRegistry();
|
||||
|
||||
return {
|
||||
userAction,
|
||||
externalReferenceAttachmentTypeRegistry,
|
||||
caseData: basicCase,
|
||||
comments: basicCase.comments,
|
||||
caseServices,
|
||||
|
|
|
@ -15,6 +15,7 @@ import { UserActionMarkdownRefObject } from './markdown_form';
|
|||
import { CasesNavigation } from '../links';
|
||||
import { UNSUPPORTED_ACTION_TYPES } from './constants';
|
||||
import type { OnUpdateFields } from '../case_view/types';
|
||||
import { ExternalReferenceAttachmentTypeRegistry } from '../../client/attachment_framework/external_reference_registry';
|
||||
|
||||
export interface UserActionTreeProps {
|
||||
caseServices: CaseServices;
|
||||
|
@ -37,6 +38,7 @@ export type SupportedUserActionTypes = keyof Omit<typeof ActionTypes, Unsupporte
|
|||
|
||||
export interface UserActionBuilderArgs {
|
||||
caseData: Case;
|
||||
externalReferenceAttachmentTypeRegistry: ExternalReferenceAttachmentTypeRegistry;
|
||||
userAction: CaseUserActions;
|
||||
caseServices: CaseServices;
|
||||
comments: Comment[];
|
||||
|
|
|
@ -13,6 +13,7 @@ import type {
|
|||
SingleCaseMetricsFeature,
|
||||
AlertComment,
|
||||
CasesMetrics,
|
||||
ExternalReferenceComment,
|
||||
} from '../../common/ui/types';
|
||||
import {
|
||||
Actions,
|
||||
|
@ -33,10 +34,15 @@ import {
|
|||
UserActionWithResponse,
|
||||
CommentUserAction,
|
||||
CaseSeverity,
|
||||
ExternalReferenceStorageType,
|
||||
} from '../../common/api';
|
||||
import { SECURITY_SOLUTION_OWNER } from '../../common/constants';
|
||||
import { SnakeToCamelCase } from '../../common/types';
|
||||
import { covertToSnakeCase } from './utils';
|
||||
import {
|
||||
ExternalReferenceAttachmentType,
|
||||
ExternalReferenceAttachmentViewObject,
|
||||
} from '../client/attachment_framework/types';
|
||||
|
||||
export { connectorsMock } from '../common/mock/connectors';
|
||||
export const basicCaseId = 'basic-case-id';
|
||||
|
@ -159,6 +165,23 @@ export const hostReleaseComment: () => Comment = () => {
|
|||
};
|
||||
};
|
||||
|
||||
export const externalReferenceAttachment: ExternalReferenceComment = {
|
||||
type: CommentType.externalReference,
|
||||
id: 'external-reference-comment-id',
|
||||
externalReferenceId: 'my-id',
|
||||
externalReferenceStorage: { type: ExternalReferenceStorageType.elasticSearchDoc },
|
||||
externalReferenceAttachmentTypeId: '.test',
|
||||
externalReferenceMetadata: null,
|
||||
createdAt: basicCreatedAt,
|
||||
createdBy: elasticUser,
|
||||
owner: SECURITY_SOLUTION_OWNER,
|
||||
pushedAt: null,
|
||||
pushedBy: null,
|
||||
updatedAt: null,
|
||||
updatedBy: null,
|
||||
version: 'WzQ3LDFc',
|
||||
};
|
||||
|
||||
export const basicCase: Case = {
|
||||
owner: SECURITY_SOLUTION_OWNER,
|
||||
closedAt: null,
|
||||
|
@ -691,3 +714,36 @@ export const basicCaseClosed: Case = {
|
|||
closedBy: elasticUser,
|
||||
status: CaseStatuses.closed,
|
||||
};
|
||||
|
||||
export const getExternalReferenceUserAction = (): SnakeToCamelCase<
|
||||
UserActionWithResponse<CommentUserAction>
|
||||
> => ({
|
||||
...getUserAction(ActionTypes.comment, Actions.create),
|
||||
actionId: 'external-reference-action-id',
|
||||
type: ActionTypes.comment,
|
||||
commentId: 'external-reference-comment-id',
|
||||
payload: {
|
||||
comment: {
|
||||
type: CommentType.externalReference,
|
||||
externalReferenceId: 'my-id',
|
||||
externalReferenceStorage: { type: ExternalReferenceStorageType.elasticSearchDoc },
|
||||
externalReferenceAttachmentTypeId: '.test',
|
||||
externalReferenceMetadata: null,
|
||||
owner: SECURITY_SOLUTION_OWNER,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const getExternalReferenceAttachment = (
|
||||
viewObject: ExternalReferenceAttachmentViewObject = {}
|
||||
): ExternalReferenceAttachmentType => ({
|
||||
id: '.test',
|
||||
icon: 'casesApp',
|
||||
displayName: 'Test',
|
||||
getAttachmentViewObject: () => ({
|
||||
type: 'update',
|
||||
event: 'added a chart',
|
||||
timelineIcon: 'casesApp',
|
||||
...viewObject,
|
||||
}),
|
||||
});
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
import { CoreSetup, CoreStart, Plugin, PluginInitializerContext } from '@kbn/core/public';
|
||||
import { ManagementAppMountParams } from '@kbn/management-plugin/public';
|
||||
import { Storage } from '@kbn/kibana-utils-plugin/public';
|
||||
import { CasesUiStart, CasesPluginSetup, CasesPluginStart } from './types';
|
||||
import { CasesUiStart, CasesPluginSetup, CasesPluginStart, CasesUiSetup } from './types';
|
||||
import { KibanaServices } from './common/lib/kibana';
|
||||
import { CasesUiConfigType } from '../common/ui/types';
|
||||
import { APP_ID, APP_PATH } from '../common/constants';
|
||||
|
@ -24,6 +24,7 @@ import { getCasesContextLazy } from './client/ui/get_cases_context';
|
|||
import { getCreateCaseFlyoutLazy } from './client/ui/get_create_case_flyout';
|
||||
import { getRecentCasesLazy } from './client/ui/get_recent_cases';
|
||||
import { groupAlertsByRule } from './client/helpers/group_alerts_by_rule';
|
||||
import { ExternalReferenceAttachmentTypeRegistry } from './client/attachment_framework/external_reference_registry';
|
||||
|
||||
/**
|
||||
* @public
|
||||
|
@ -34,14 +35,17 @@ export class CasesUiPlugin
|
|||
{
|
||||
private readonly kibanaVersion: string;
|
||||
private readonly storage = new Storage(localStorage);
|
||||
private externalReferenceAttachmentTypeRegistry: ExternalReferenceAttachmentTypeRegistry;
|
||||
|
||||
constructor(private readonly initializerContext: PluginInitializerContext) {
|
||||
this.kibanaVersion = initializerContext.env.packageInfo.version;
|
||||
this.externalReferenceAttachmentTypeRegistry = new ExternalReferenceAttachmentTypeRegistry();
|
||||
}
|
||||
|
||||
public setup(core: CoreSetup, plugins: CasesPluginSetup) {
|
||||
public setup(core: CoreSetup, plugins: CasesPluginSetup): CasesUiSetup {
|
||||
const kibanaVersion = this.kibanaVersion;
|
||||
const storage = this.storage;
|
||||
const externalReferenceAttachmentTypeRegistry = this.externalReferenceAttachmentTypeRegistry;
|
||||
|
||||
if (plugins.home) {
|
||||
plugins.home.featureCatalogue.register({
|
||||
|
@ -74,27 +78,58 @@ export class CasesUiPlugin
|
|||
pluginsStart,
|
||||
storage,
|
||||
kibanaVersion,
|
||||
externalReferenceAttachmentTypeRegistry,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
// Return methods that should be available to other plugins
|
||||
return {};
|
||||
return {
|
||||
attachmentFramework: {
|
||||
registerExternalReference: (externalReferenceAttachmentType) => {
|
||||
this.externalReferenceAttachmentTypeRegistry.register(externalReferenceAttachmentType);
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
public start(core: CoreStart, plugins: CasesPluginStart): CasesUiStart {
|
||||
const config = this.initializerContext.config.get<CasesUiConfigType>();
|
||||
KibanaServices.init({ ...core, ...plugins, kibanaVersion: this.kibanaVersion, config });
|
||||
|
||||
/**
|
||||
* getCasesContextLazy returns a new component each time is being called. To avoid re-renders
|
||||
* we get the component on start and provide the same component to all consumers.
|
||||
*/
|
||||
const getCasesContext = getCasesContextLazy({
|
||||
externalReferenceAttachmentTypeRegistry: this.externalReferenceAttachmentTypeRegistry,
|
||||
});
|
||||
|
||||
return {
|
||||
api: createClientAPI({ http: core.http }),
|
||||
ui: {
|
||||
getCases: getCasesLazy,
|
||||
getCasesContext: getCasesContextLazy,
|
||||
getRecentCases: getRecentCasesLazy,
|
||||
getCases: (props) =>
|
||||
getCasesLazy({
|
||||
...props,
|
||||
externalReferenceAttachmentTypeRegistry: this.externalReferenceAttachmentTypeRegistry,
|
||||
}),
|
||||
getCasesContext,
|
||||
getRecentCases: (props) =>
|
||||
getRecentCasesLazy({
|
||||
...props,
|
||||
externalReferenceAttachmentTypeRegistry: this.externalReferenceAttachmentTypeRegistry,
|
||||
}),
|
||||
// @deprecated Please use the hook getUseCasesAddToNewCaseFlyout
|
||||
getCreateCaseFlyout: getCreateCaseFlyoutLazy,
|
||||
getCreateCaseFlyout: (props) =>
|
||||
getCreateCaseFlyoutLazy({
|
||||
...props,
|
||||
externalReferenceAttachmentTypeRegistry: this.externalReferenceAttachmentTypeRegistry,
|
||||
}),
|
||||
// @deprecated Please use the hook getUseCasesAddToExistingCaseModal
|
||||
getAllCasesSelectorModal: getAllCasesSelectorModalLazy,
|
||||
getAllCasesSelectorModal: (props) =>
|
||||
getAllCasesSelectorModalLazy({
|
||||
...props,
|
||||
externalReferenceAttachmentTypeRegistry: this.externalReferenceAttachmentTypeRegistry,
|
||||
}),
|
||||
},
|
||||
hooks: {
|
||||
getUseCasesAddToNewCaseFlyout: useCasesAddToNewCaseFlyout,
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
*/
|
||||
|
||||
import { CoreStart, IHttpFetchError, ResponseErrorBody } from '@kbn/core/public';
|
||||
import { ReactElement, ReactNode } from 'react';
|
||||
import React, { ReactElement } from 'react';
|
||||
import type { DataPublicPluginStart } from '@kbn/data-plugin/public';
|
||||
import type { EmbeddableStart } from '@kbn/embeddable-plugin/public';
|
||||
import type { Storage } from '@kbn/kibana-utils-plugin/public';
|
||||
|
@ -37,6 +37,8 @@ import { GetCreateCaseFlyoutProps } from './client/ui/get_create_case_flyout';
|
|||
import { GetRecentCasesProps } from './client/ui/get_recent_cases';
|
||||
import { Cases, CasesStatus, CasesMetrics } from '../common/ui';
|
||||
import { groupAlertsByRule } from './client/helpers/group_alerts_by_rule';
|
||||
import { AttachmentFramework } from './client/attachment_framework/types';
|
||||
import { ExternalReferenceAttachmentTypeRegistry } from './client/attachment_framework/external_reference_registry';
|
||||
|
||||
export interface CasesPluginSetup {
|
||||
security: SecurityPluginSetup;
|
||||
|
@ -71,6 +73,11 @@ export interface RenderAppProps {
|
|||
pluginsStart: CasesPluginStart;
|
||||
storage: Storage;
|
||||
kibanaVersion: string;
|
||||
externalReferenceAttachmentTypeRegistry: ExternalReferenceAttachmentTypeRegistry;
|
||||
}
|
||||
|
||||
export interface CasesUiSetup {
|
||||
attachmentFramework: AttachmentFramework;
|
||||
}
|
||||
|
||||
export interface CasesUiStart {
|
||||
|
@ -89,9 +96,8 @@ export interface CasesUiStart {
|
|||
* @return {ReactElement<GetCasesProps>}
|
||||
*/
|
||||
getCases: (props: GetCasesProps) => ReactElement<GetCasesProps>;
|
||||
getCasesContext: () => (
|
||||
props: GetCasesContextProps & { children: ReactNode }
|
||||
) => ReactElement<GetCasesContextProps>;
|
||||
getCasesContext: () => React.FC<GetCasesContextProps>;
|
||||
|
||||
/**
|
||||
* Modal to select a case in a list of all owner cases
|
||||
* @param props GetAllCasesSelectorModalProps
|
||||
|
|
|
@ -6,10 +6,11 @@
|
|||
*/
|
||||
|
||||
import pMap from 'p-map';
|
||||
import { CasePostRequest } from '@kbn/cases-plugin/common/api';
|
||||
import { CasePostRequest, CaseResponse } from '@kbn/cases-plugin/common/api';
|
||||
import {
|
||||
createCase as createCaseAPI,
|
||||
deleteAllCaseItems,
|
||||
createComment,
|
||||
} from '../../../cases_api_integration/common/lib/utils';
|
||||
import { FtrProviderContext } from '../../ftr_provider_context';
|
||||
import { generateRandomCaseWithoutConnector } from './helpers';
|
||||
|
@ -19,12 +20,13 @@ export function CasesAPIServiceProvider({ getService }: FtrProviderContext) {
|
|||
const es = getService('es');
|
||||
|
||||
return {
|
||||
async createCase(overwrites: Partial<CasePostRequest> = {}) {
|
||||
async createCase(overwrites: Partial<CasePostRequest> = {}): Promise<CaseResponse> {
|
||||
const caseData = {
|
||||
...generateRandomCaseWithoutConnector(),
|
||||
...overwrites,
|
||||
} as CasePostRequest;
|
||||
await createCaseAPI(kbnSupertest, caseData);
|
||||
const res = await createCaseAPI(kbnSupertest, caseData);
|
||||
return res;
|
||||
},
|
||||
|
||||
async createNthRandomCases(amount: number = 3) {
|
||||
|
@ -44,5 +46,15 @@ export function CasesAPIServiceProvider({ getService }: FtrProviderContext) {
|
|||
async deleteAllCases() {
|
||||
deleteAllCaseItems(es);
|
||||
},
|
||||
|
||||
async createAttachment({
|
||||
caseId,
|
||||
params,
|
||||
}: {
|
||||
caseId: Parameters<typeof createComment>[0]['caseId'];
|
||||
params: Parameters<typeof createComment>[0]['params'];
|
||||
}): Promise<CaseResponse> {
|
||||
return createComment({ supertest: kbnSupertest, params, caseId });
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -0,0 +1,61 @@
|
|||
/*
|
||||
* 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 {
|
||||
ExternalReferenceStorageType,
|
||||
CommentType,
|
||||
CaseResponse,
|
||||
} from '@kbn/cases-plugin/common/api';
|
||||
import { FtrProviderContext } from '../../ftr_provider_context';
|
||||
|
||||
export default ({ getPageObject, getService }: FtrProviderContext) => {
|
||||
const header = getPageObject('header');
|
||||
const testSubjects = getService('testSubjects');
|
||||
const cases = getService('cases');
|
||||
|
||||
/**
|
||||
* Attachment types are being registered in
|
||||
* x-pack/test/functional_with_es_ssl/fixtures/plugins/cases/public/plugin.ts
|
||||
*/
|
||||
describe('Attachment framework', () => {
|
||||
describe('External reference attachments', () => {
|
||||
let caseWithAttachment: CaseResponse;
|
||||
|
||||
before(async () => {
|
||||
const caseData = await cases.api.createCase({ title: 'External references' });
|
||||
caseWithAttachment = await cases.api.createAttachment({
|
||||
caseId: caseData.id,
|
||||
params: {
|
||||
type: CommentType.externalReference,
|
||||
externalReferenceId: 'my-id',
|
||||
externalReferenceStorage: { type: ExternalReferenceStorageType.elasticSearchDoc },
|
||||
externalReferenceAttachmentTypeId: '.test',
|
||||
externalReferenceMetadata: null,
|
||||
owner: 'cases',
|
||||
},
|
||||
});
|
||||
|
||||
await cases.navigation.navigateToApp();
|
||||
await cases.casesTable.waitForCasesToBeListed();
|
||||
await cases.casesTable.goToFirstListedCase();
|
||||
await header.waitUntilLoadingHasFinished();
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await cases.api.deleteAllCases();
|
||||
});
|
||||
|
||||
it('renders an external reference attachment type correctly', async () => {
|
||||
const attachmentId = caseWithAttachment?.comments?.[0].id;
|
||||
await testSubjects.existOrFail('comment-external-reference-.test');
|
||||
await testSubjects.existOrFail(`copy-link-${attachmentId}`);
|
||||
await testSubjects.existOrFail('test-attachment-action');
|
||||
await testSubjects.existOrFail('test-attachment-content');
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
|
@ -13,5 +13,6 @@ export default ({ loadTestFile }: FtrProviderContext) => {
|
|||
loadTestFile(require.resolve('./view_case'));
|
||||
loadTestFile(require.resolve('./list_view'));
|
||||
loadTestFile(require.resolve('./configure'));
|
||||
loadTestFile(require.resolve('./attachment_framework'));
|
||||
});
|
||||
};
|
||||
|
|
|
@ -73,6 +73,7 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) {
|
|||
`--elasticsearch.hosts=https://${servers.elasticsearch.hostname}:${servers.elasticsearch.port}`,
|
||||
`--elasticsearch.ssl.certificateAuthorities=${CA_CERT_PATH}`,
|
||||
`--plugin-path=${join(__dirname, 'fixtures', 'plugins', 'alerts')}`,
|
||||
`--plugin-path=${join(__dirname, 'fixtures', 'plugins', 'cases')}`,
|
||||
`--xpack.trigger_actions_ui.enableExperimental=${JSON.stringify([
|
||||
'internalAlertsTable',
|
||||
'internalShareableComponentsSandbox',
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"id": "casesFixture",
|
||||
"owner": { "name": "Response Ops", "githubTeam": "response-ops" },
|
||||
"version": "1.0.0",
|
||||
"kibanaVersion": "kibana",
|
||||
"requiredPlugins": ["cases"],
|
||||
"server": true,
|
||||
"ui": true
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"name": "cases-fixture",
|
||||
"version": "0.0.0",
|
||||
"kibana": {
|
||||
"version": "kibana"
|
||||
},
|
||||
"main": "target/test/functional_with_es_ssl/fixtures/plugins/cases",
|
||||
"scripts": {
|
||||
"kbn": "node ../../../../../../scripts/kbn.js",
|
||||
"build": "rm -rf './target' && ../../../../../../node_modules/.bin/tsc"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
/*
|
||||
* 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, { lazy } from 'react';
|
||||
import { EuiButtonIcon } from '@elastic/eui';
|
||||
import { ExternalReferenceAttachmentType } from '@kbn/cases-plugin/public/client/attachment_framework/types';
|
||||
|
||||
const AttachmentContentLazy = lazy(() => import('./external_references_content'));
|
||||
|
||||
const AttachmentActions: React.FC = () => {
|
||||
return (
|
||||
<EuiButtonIcon
|
||||
data-test-subj="test-attachment-action"
|
||||
onClick={() => {}}
|
||||
iconType="arrowRight"
|
||||
aria-label="See attachment"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const getExternalReferenceAttachmentRegular = (): ExternalReferenceAttachmentType => ({
|
||||
id: '.test',
|
||||
icon: 'casesApp',
|
||||
displayName: 'Test',
|
||||
getAttachmentViewObject: () => ({
|
||||
type: 'regular',
|
||||
event: 'added a chart',
|
||||
timelineIcon: 'casesApp',
|
||||
actions: <AttachmentActions />,
|
||||
children: AttachmentContentLazy,
|
||||
}),
|
||||
});
|
|
@ -0,0 +1,53 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { Chart, Settings, BarSeries, LineSeries, Axis, DataGenerator } from '@elastic/charts';
|
||||
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
|
||||
const dg = new DataGenerator();
|
||||
const data1 = dg.generateGroupedSeries(20, 1);
|
||||
const data2 = dg.generateGroupedSeries(20, 5);
|
||||
|
||||
const AttachmentContent: React.FC = () => {
|
||||
return (
|
||||
<EuiFlexGroup data-test-subj="test-attachment-content">
|
||||
<EuiFlexItem>
|
||||
<Chart size={{ height: 200 }}>
|
||||
<Settings showLegend={false} />
|
||||
<BarSeries
|
||||
id="status"
|
||||
name="Status"
|
||||
data={data2}
|
||||
xAccessor={'x'}
|
||||
yAccessors={['y']}
|
||||
splitSeriesAccessors={['g']}
|
||||
stackAccessors={['g']}
|
||||
/>
|
||||
<LineSeries
|
||||
id="control"
|
||||
name="Control"
|
||||
data={data1}
|
||||
xAccessor={'x'}
|
||||
yAccessors={['y']}
|
||||
color={['black']}
|
||||
/>
|
||||
<Axis id="bottom-axis" position="bottom" showGridLines />
|
||||
<Axis
|
||||
id="left-axis"
|
||||
position="left"
|
||||
showGridLines
|
||||
tickFormat={(d) => Number(d).toFixed(2)}
|
||||
/>
|
||||
</Chart>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
};
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export { AttachmentContent as default };
|
|
@ -0,0 +1,10 @@
|
|||
/*
|
||||
* 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 { CasesFixturePlugin } from './plugin';
|
||||
|
||||
export const plugin = () => new CasesFixturePlugin();
|
|
@ -0,0 +1,26 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { Plugin, CoreSetup } from '@kbn/core/public';
|
||||
import { CasesUiSetup } from '@kbn/cases-plugin/public/types';
|
||||
import { getExternalReferenceAttachmentRegular } from './attachments/external_reference';
|
||||
|
||||
export type Setup = void;
|
||||
export type Start = void;
|
||||
|
||||
export interface CasesExamplePublicSetupDeps {
|
||||
cases: CasesUiSetup;
|
||||
}
|
||||
|
||||
export class CasesFixturePlugin implements Plugin<Setup, Start, CasesExamplePublicSetupDeps> {
|
||||
public setup(core: CoreSetup, { cases }: CasesExamplePublicSetupDeps) {
|
||||
cases.attachmentFramework.registerExternalReference(getExternalReferenceAttachmentRegular());
|
||||
}
|
||||
|
||||
public start() {}
|
||||
public stop() {}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
/*
|
||||
* 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 { PluginInitializer } from '@kbn/core/server';
|
||||
import { CasesFixturePlugin } from './plugin';
|
||||
|
||||
export const plugin: PluginInitializer<void, void> = () => new CasesFixturePlugin();
|
|
@ -0,0 +1,14 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { Plugin, CoreSetup } from '@kbn/core/server';
|
||||
|
||||
export class CasesFixturePlugin implements Plugin<void, void> {
|
||||
public setup(core: CoreSetup) {}
|
||||
public start() {}
|
||||
public stop() {}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue