Skip to content

Commit

Permalink
[Cases] Attach framework registry (#134744)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
cnasikas committed Jun 30, 2022
1 parent 465e6f0 commit 6583433
Show file tree
Hide file tree
Showing 41 changed files with 974 additions and 50 deletions.
2 changes: 2 additions & 0 deletions tsconfig.base.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"],
Expand Down
2 changes: 2 additions & 0 deletions x-pack/plugins/cases/common/ui/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
CasesStatusResponse,
CasesMetricsResponse,
CaseSeverity,
CommentResponseExternalReferenceType,
} from '../api';
import { SnakeToCamelCase } from '../types';

Expand Down Expand Up @@ -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[] };
Expand Down
31 changes: 22 additions & 9 deletions x-pack/plugins/cases/public/application.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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';

Expand All @@ -60,7 +69,11 @@ export const App: React.FC<{ deps: RenderAppProps }> = ({ deps }) => {
}}
>
<Router history={history}>
<CasesAppWithContext />
<CasesAppWithContext
externalReferenceAttachmentTypeRegistry={
deps.externalReferenceAttachmentTypeRegistry
}
/>
</Router>
</KibanaContextProvider>
</KibanaThemeProvider>
Expand Down
Original file line number Diff line number Diff line change
@@ -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> {}
Original file line number Diff line number Diff line change
@@ -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',
},
]);
});
});
});
Original file line number Diff line number Diff line change
@@ -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);
}
}
40 changes: 40 additions & 0 deletions x-pack/plugins/cases/public/client/attachment_framework/types.ts
Original file line number Diff line number Diff line change
@@ -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;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down
17 changes: 14 additions & 3 deletions x-pack/plugins/cases/public/client/ui/get_cases.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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}
Expand Down

0 comments on commit 6583433

Please sign in to comment.