Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Cases] Attach framework registry #134744

Merged
merged 16 commits into from
Jun 30, 2022
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 @@
/*
cnasikas marked this conversation as resolved.
Show resolved Hide resolved
* 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);
}
}
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