[Cases] Enable Cases on the stack management page (#125224)

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Christos Nasikas 2022-03-04 13:52:01 +02:00 committed by GitHub
parent de4f3e204c
commit 494047a2c0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
27 changed files with 531 additions and 108 deletions

View file

@ -13,6 +13,12 @@ import {
} from './constants/saved_objects';
import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/server';
/**
* The order of appearance in the feature privilege page
* under the management section.
*/
const FEATURE_ORDER = 3000;
export const ACTIONS_FEATURE = {
id: 'actions',
name: i18n.translate('xpack.actions.featureRegistry.actionsFeatureName', {
@ -20,6 +26,7 @@ export const ACTIONS_FEATURE = {
}),
category: DEFAULT_APP_CATEGORIES.management,
app: [],
order: FEATURE_ORDER,
management: {
insightsAndAlerting: ['triggersActions'],
},

View file

@ -7,16 +7,33 @@
import { ConnectorTypes } from './api';
import { CasesContextFeatures } from './ui/types';
export const DEFAULT_DATE_FORMAT = 'dateFormat';
export const DEFAULT_DATE_FORMAT_TZ = 'dateFormat:tz';
export const DEFAULT_DATE_FORMAT = 'dateFormat' as const;
export const DEFAULT_DATE_FORMAT_TZ = 'dateFormat:tz' as const;
export const APP_ID = 'cases';
/**
* Application
*/
export const CASE_SAVED_OBJECT = 'cases';
export const CASE_CONNECTOR_MAPPINGS_SAVED_OBJECT = 'cases-connector-mappings';
export const CASE_USER_ACTION_SAVED_OBJECT = 'cases-user-actions';
export const CASE_COMMENT_SAVED_OBJECT = 'cases-comments';
export const CASE_CONFIGURE_SAVED_OBJECT = 'cases-configure';
export const APP_ID = 'cases' as const;
export const FEATURE_ID = 'generalCases' as const;
export const APP_OWNER = 'cases' as const;
export const APP_PATH = '/app/management/insightsAndAlerting/cases' as const;
/**
* The main Cases application is in the stack management under the
* Alerts and Insights section. To do that, Cases registers to the management
* application. This constant holds the application ID of the management plugin
*/
export const STACK_APP_ID = 'management' as const;
/**
* Saved objects
*/
export const CASE_SAVED_OBJECT = 'cases' as const;
export const CASE_CONNECTOR_MAPPINGS_SAVED_OBJECT = 'cases-connector-mappings' as const;
export const CASE_USER_ACTION_SAVED_OBJECT = 'cases-user-actions' as const;
export const CASE_COMMENT_SAVED_OBJECT = 'cases-comments' as const;
export const CASE_CONFIGURE_SAVED_OBJECT = 'cases-configure' as const;
/**
* If more values are added here please also add them here: x-pack/test/cases_api_integration/common/fixtures/plugins
@ -33,32 +50,32 @@ export const SAVED_OBJECT_TYPES = [
* Case routes
*/
export const CASES_URL = '/api/cases';
export const CASE_DETAILS_URL = `${CASES_URL}/{case_id}`;
export const CASE_CONFIGURE_URL = `${CASES_URL}/configure`;
export const CASE_CONFIGURE_DETAILS_URL = `${CASES_URL}/configure/{configuration_id}`;
export const CASE_CONFIGURE_CONNECTORS_URL = `${CASE_CONFIGURE_URL}/connectors`;
export const CASES_URL = '/api/cases' as const;
export const CASE_DETAILS_URL = `${CASES_URL}/{case_id}` as const;
export const CASE_CONFIGURE_URL = `${CASES_URL}/configure` as const;
export const CASE_CONFIGURE_DETAILS_URL = `${CASES_URL}/configure/{configuration_id}` as const;
export const CASE_CONFIGURE_CONNECTORS_URL = `${CASE_CONFIGURE_URL}/connectors` as const;
export const CASE_COMMENTS_URL = `${CASE_DETAILS_URL}/comments`;
export const CASE_COMMENT_DETAILS_URL = `${CASE_DETAILS_URL}/comments/{comment_id}`;
export const CASE_PUSH_URL = `${CASE_DETAILS_URL}/connector/{connector_id}/_push`;
export const CASE_REPORTERS_URL = `${CASES_URL}/reporters`;
export const CASE_STATUS_URL = `${CASES_URL}/status`;
export const CASE_TAGS_URL = `${CASES_URL}/tags`;
export const CASE_USER_ACTIONS_URL = `${CASE_DETAILS_URL}/user_actions`;
export const CASE_COMMENTS_URL = `${CASE_DETAILS_URL}/comments` as const;
export const CASE_COMMENT_DETAILS_URL = `${CASE_DETAILS_URL}/comments/{comment_id}` as const;
export const CASE_PUSH_URL = `${CASE_DETAILS_URL}/connector/{connector_id}/_push` as const;
export const CASE_REPORTERS_URL = `${CASES_URL}/reporters` as const;
export const CASE_STATUS_URL = `${CASES_URL}/status` as const;
export const CASE_TAGS_URL = `${CASES_URL}/tags` as const;
export const CASE_USER_ACTIONS_URL = `${CASE_DETAILS_URL}/user_actions` as const;
export const CASE_ALERTS_URL = `${CASES_URL}/alerts/{alert_id}`;
export const CASE_DETAILS_ALERTS_URL = `${CASE_DETAILS_URL}/alerts`;
export const CASE_ALERTS_URL = `${CASES_URL}/alerts/{alert_id}` as const;
export const CASE_DETAILS_ALERTS_URL = `${CASE_DETAILS_URL}/alerts` as const;
export const CASE_METRICS_DETAILS_URL = `${CASES_URL}/metrics/{case_id}`;
export const CASE_METRICS_DETAILS_URL = `${CASES_URL}/metrics/{case_id}` as const;
/**
* Action routes
*/
export const ACTION_URL = '/api/actions';
export const ACTION_TYPES_URL = `${ACTION_URL}/connector_types`;
export const CONNECTORS_URL = `${ACTION_URL}/connectors`;
export const ACTION_URL = '/api/actions' as const;
export const ACTION_TYPES_URL = `${ACTION_URL}/connector_types` as const;
export const CONNECTORS_URL = `${ACTION_URL}/connectors` as const;
export const SUPPORTED_CONNECTORS = [
`${ConnectorTypes.serviceNowITSM}`,
@ -71,10 +88,10 @@ export const SUPPORTED_CONNECTORS = [
/**
* Alerts
*/
export const MAX_ALERTS_PER_CASE = 5000;
export const MAX_ALERTS_PER_CASE = 5000 as const;
export const SECURITY_SOLUTION_OWNER = 'securitySolution';
export const OBSERVABILITY_OWNER = 'observability';
export const SECURITY_SOLUTION_OWNER = 'securitySolution' as const;
export const OBSERVABILITY_OWNER = 'observability' as const;
export const OWNER_INFO = {
[SECURITY_SOLUTION_OWNER]: {
@ -85,16 +102,16 @@ export const OWNER_INFO = {
label: 'Observability',
iconType: 'logoObservability',
},
};
} as const;
export const MAX_DOCS_PER_PAGE = 10000;
export const MAX_CONCURRENT_SEARCHES = 10;
export const MAX_DOCS_PER_PAGE = 10000 as const;
export const MAX_CONCURRENT_SEARCHES = 10 as const;
/**
* Validation
*/
export const MAX_TITLE_LENGTH = 64;
export const MAX_TITLE_LENGTH = 64 as const;
/**
* Cases features

View file

@ -10,8 +10,10 @@
"id":"cases",
"kibanaVersion":"kibana",
"optionalPlugins":[
"home",
"security",
"spaces",
"features",
"usageCollection"
],
"owner":{
@ -20,14 +22,18 @@
},
"requiredPlugins":[
"actions",
"data",
"embeddable",
"esUiShared",
"lens",
"features",
"kibanaReact",
"kibanaUtils",
"triggersActionsUi"
"triggersActionsUi",
"management"
],
"requiredBundles": [
"home",
"savedObjects"
],
"server":true,

View file

@ -0,0 +1,72 @@
/*
* 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 ReactDOM from 'react-dom';
import { Router } from 'react-router-dom';
import { I18nProvider } from '@kbn/i18n-react';
import { EuiErrorBoundary } from '@elastic/eui';
import {
KibanaContextProvider,
KibanaThemeProvider,
useUiSetting$,
} from '../../../../src/plugins/kibana_react/public';
import { EuiThemeProvider as StyledComponentsThemeProvider } from '../../../../src/plugins/kibana_react/common';
import { RenderAppProps } from './types';
import { CasesApp } from './components/app';
export const renderApp = (deps: RenderAppProps) => {
const { mountParams } = deps;
const { element } = mountParams;
ReactDOM.render(<App deps={deps} />, element);
return () => {
ReactDOM.unmountComponentAtNode(element);
};
};
const CasesAppWithContext = () => {
const [darkMode] = useUiSetting$<boolean>('theme:darkMode');
return (
<StyledComponentsThemeProvider darkMode={darkMode}>
<CasesApp />
</StyledComponentsThemeProvider>
);
};
CasesAppWithContext.displayName = 'CasesAppWithContext';
export const App: React.FC<{ deps: RenderAppProps }> = ({ deps }) => {
const { mountParams, coreStart, pluginsStart, storage, kibanaVersion } = deps;
const { history, theme$ } = mountParams;
return (
<EuiErrorBoundary>
<I18nProvider>
<KibanaThemeProvider theme$={theme$}>
<KibanaContextProvider
services={{
kibanaVersion,
...coreStart,
...pluginsStart,
storage,
}}
>
<Router history={history}>
<CasesAppWithContext />
</Router>
</KibanaContextProvider>
</KibanaThemeProvider>
</I18nProvider>
</EuiErrorBoundary>
);
};
App.displayName = 'App';

View file

@ -0,0 +1,43 @@
/*
* 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 { renderHook } from '@testing-library/react-hooks';
import { TestProviders } from '../common/mock';
import { useIsMainApplication } from './hooks';
import { useApplication } from '../components/cases_context/use_application';
jest.mock('../components/cases_context/use_application');
const useApplicationMock = useApplication as jest.Mock;
describe('hooks', () => {
beforeEach(() => {
jest.clearAllMocks();
useApplicationMock.mockReturnValue({ appId: 'management', appTitle: 'Management' });
});
describe('useIsMainApplication', () => {
it('returns true if it is the main application', () => {
const { result } = renderHook(() => useIsMainApplication(), {
wrapper: ({ children }) => <TestProviders>{children}</TestProviders>,
});
expect(result.current).toBe(true);
});
it('returns false if it is not the main application', () => {
useApplicationMock.mockReturnValue({ appId: 'testAppId', appTitle: 'Test app' });
const { result } = renderHook(() => useIsMainApplication(), {
wrapper: ({ children }) => <TestProviders>{children}</TestProviders>,
});
expect(result.current).toBe(false);
});
});
});

View file

@ -0,0 +1,15 @@
/*
* 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 { STACK_APP_ID } from '../../common/constants';
import { useCasesContext } from '../components/cases_context/use_cases_context';
export const useIsMainApplication = () => {
const { appId } = useCasesContext();
return appId === STACK_APP_ID;
};

View file

@ -10,7 +10,11 @@ import moment from 'moment-timezone';
import { useCallback, useEffect, useState } from 'react';
import { i18n } from '@kbn/i18n';
import { DEFAULT_DATE_FORMAT, DEFAULT_DATE_FORMAT_TZ } from '../../../../common/constants';
import {
FEATURE_ID,
DEFAULT_DATE_FORMAT,
DEFAULT_DATE_FORMAT_TZ,
} from '../../../../common/constants';
import { AuthenticatedUser } from '../../../../../security/common/model';
import { convertToCamelCase } from '../../../containers/utils';
import { StartServices } from '../../../types';
@ -155,3 +159,17 @@ export const useNavigation = (appId: string) => {
const { getAppUrl } = useAppUrl(appId);
return { navigateTo, getAppUrl };
};
/**
* Returns the capabilities of the main cases application
*
*/
export const useApplicationCapabilities = (): { crud: boolean; read: boolean } => {
const capabilities = useKibana().services.application.capabilities;
const casesCapabilities = capabilities[FEATURE_ID];
return {
crud: !!casesCapabilities?.crud_cases,
read: !!casesCapabilities?.read_cases,
};
};

View file

@ -8,6 +8,7 @@
import React from 'react';
import { act, renderHook } from '@testing-library/react-hooks';
import { APP_ID } from '../../../common/constants';
import { useNavigation } from '../../common/lib/kibana';
import { TestProviders } from '../../common/mock';
import {
@ -33,9 +34,12 @@ describe('hooks', () => {
describe('useCasesNavigation', () => {
it('it calls getAppUrl with correct arguments', () => {
const { result } = renderHook(() => useCasesNavigation(CasesDeepLinkId.cases), {
wrapper: ({ children }) => <TestProviders>{children}</TestProviders>,
});
const { result } = renderHook(
() => useCasesNavigation({ deepLinkId: CasesDeepLinkId.cases }),
{
wrapper: ({ children }) => <TestProviders>{children}</TestProviders>,
}
);
const [getCasesUrl] = result.current;
@ -43,20 +47,23 @@ describe('hooks', () => {
getCasesUrl(false);
});
expect(getAppUrl).toHaveBeenCalledWith({ absolute: false, deepLinkId: 'cases' });
expect(getAppUrl).toHaveBeenCalledWith({ absolute: false, deepLinkId: APP_ID });
});
it('it calls navigateToAllCases with correct arguments', () => {
const { result } = renderHook(() => useCasesNavigation(CasesDeepLinkId.cases), {
wrapper: ({ children }) => <TestProviders>{children}</TestProviders>,
});
const { result } = renderHook(
() => useCasesNavigation({ deepLinkId: CasesDeepLinkId.cases }),
{
wrapper: ({ children }) => <TestProviders>{children}</TestProviders>,
}
);
const [, navigateToCases] = result.current;
act(() => {
navigateToCases();
});
expect(navigateTo).toHaveBeenCalledWith({ deepLinkId: 'cases' });
expect(navigateTo).toHaveBeenCalledWith({ deepLinkId: APP_ID });
});
});
@ -70,7 +77,7 @@ describe('hooks', () => {
result.current.getAllCasesUrl(false);
});
expect(getAppUrl).toHaveBeenCalledWith({ absolute: false, deepLinkId: 'cases' });
expect(getAppUrl).toHaveBeenCalledWith({ absolute: false, path: '/', deepLinkId: APP_ID });
});
it('it calls navigateToAllCases with correct arguments', () => {
@ -82,7 +89,7 @@ describe('hooks', () => {
result.current.navigateToAllCases();
});
expect(navigateTo).toHaveBeenCalledWith({ deepLinkId: 'cases' });
expect(navigateTo).toHaveBeenCalledWith({ path: '/', deepLinkId: APP_ID });
});
});
@ -96,7 +103,11 @@ describe('hooks', () => {
result.current.getCreateCaseUrl(false);
});
expect(getAppUrl).toHaveBeenCalledWith({ absolute: false, deepLinkId: 'cases_create' });
expect(getAppUrl).toHaveBeenCalledWith({
absolute: false,
path: '/create',
deepLinkId: APP_ID,
});
});
it('it calls navigateToAllCases with correct arguments', () => {
@ -108,7 +119,7 @@ describe('hooks', () => {
result.current.navigateToCreateCase();
});
expect(navigateTo).toHaveBeenCalledWith({ deepLinkId: 'cases_create' });
expect(navigateTo).toHaveBeenCalledWith({ deepLinkId: APP_ID, path: '/create' });
});
});
@ -122,7 +133,11 @@ describe('hooks', () => {
result.current.getConfigureCasesUrl(false);
});
expect(getAppUrl).toHaveBeenCalledWith({ absolute: false, deepLinkId: 'cases_configure' });
expect(getAppUrl).toHaveBeenCalledWith({
absolute: false,
path: '/configure',
deepLinkId: APP_ID,
});
});
it('it calls navigateToAllCases with correct arguments', () => {
@ -134,7 +149,7 @@ describe('hooks', () => {
result.current.navigateToConfigureCases();
});
expect(navigateTo).toHaveBeenCalledWith({ deepLinkId: 'cases_configure' });
expect(navigateTo).toHaveBeenCalledWith({ path: '/configure', deepLinkId: APP_ID });
});
});
@ -150,7 +165,7 @@ describe('hooks', () => {
expect(getAppUrl).toHaveBeenCalledWith({
absolute: false,
deepLinkId: 'cases',
deepLinkId: APP_ID,
path: '/test',
});
});
@ -164,7 +179,7 @@ describe('hooks', () => {
result.current.navigateToCaseView({ detailName: 'test' });
});
expect(navigateTo).toHaveBeenCalledWith({ deepLinkId: 'cases', path: '/test' });
expect(navigateTo).toHaveBeenCalledWith({ deepLinkId: APP_ID, path: '/test' });
});
});
});

View file

@ -7,10 +7,17 @@
import { useCallback } from 'react';
import { useParams } from 'react-router-dom';
import { APP_ID } from '../../../common/constants';
import { useNavigation } from '../lib/kibana';
import { useCasesContext } from '../../components/cases_context/use_cases_context';
import { CasesDeepLinkId, ICasesDeepLinkId } from './deep_links';
import { CaseViewPathParams, generateCaseViewPath } from './paths';
import { ICasesDeepLinkId } from './deep_links';
import {
CASES_CONFIGURE_PATH,
CASES_CREATE_PATH,
CaseViewPathParams,
generateCaseViewPath,
} from './paths';
export const useCaseViewParams = () => useParams<CaseViewPathParams>();
@ -18,34 +25,60 @@ type GetCasesUrl = (absolute?: boolean) => string;
type NavigateToCases = () => void;
type UseCasesNavigation = [GetCasesUrl, NavigateToCases];
export const useCasesNavigation = (deepLinkId: ICasesDeepLinkId): UseCasesNavigation => {
export const useCasesNavigation = ({
path,
deepLinkId,
}: {
path?: string;
deepLinkId?: ICasesDeepLinkId;
}): UseCasesNavigation => {
const { appId } = useCasesContext();
const { navigateTo, getAppUrl } = useNavigation(appId);
const getCasesUrl = useCallback<GetCasesUrl>(
(absolute) => getAppUrl({ deepLinkId, absolute }),
[getAppUrl, deepLinkId]
(absolute) => getAppUrl({ path, deepLinkId, absolute }),
[getAppUrl, deepLinkId, path]
);
const navigateToCases = useCallback<NavigateToCases>(
() => navigateTo({ deepLinkId }),
[navigateTo, deepLinkId]
() => navigateTo({ path, deepLinkId }),
[navigateTo, deepLinkId, path]
);
return [getCasesUrl, navigateToCases];
};
/**
* Cases can be either be part of a solution or a standalone application
* The standalone application is registered from the cases plugin and is called
* the main application. The main application uses paths and the solutions
* deep links.
*/
const navigationMapping = {
all: { path: '/' },
create: { path: CASES_CREATE_PATH },
configure: { path: CASES_CONFIGURE_PATH },
};
export const useAllCasesNavigation = () => {
const [getAllCasesUrl, navigateToAllCases] = useCasesNavigation(CasesDeepLinkId.cases);
const [getAllCasesUrl, navigateToAllCases] = useCasesNavigation({
path: navigationMapping.all.path,
deepLinkId: APP_ID,
});
return { getAllCasesUrl, navigateToAllCases };
};
export const useCreateCaseNavigation = () => {
const [getCreateCaseUrl, navigateToCreateCase] = useCasesNavigation(CasesDeepLinkId.casesCreate);
const [getCreateCaseUrl, navigateToCreateCase] = useCasesNavigation({
path: navigationMapping.create.path,
deepLinkId: APP_ID,
});
return { getCreateCaseUrl, navigateToCreateCase };
};
export const useConfigureCasesNavigation = () => {
const [getConfigureCasesUrl, navigateToConfigureCases] = useCasesNavigation(
CasesDeepLinkId.casesConfigure
);
const [getConfigureCasesUrl, navigateToConfigureCases] = useCasesNavigation({
path: navigationMapping.configure.path,
deepLinkId: APP_ID,
});
return { getConfigureCasesUrl, navigateToConfigureCases };
};
@ -55,19 +88,25 @@ type NavigateToCaseView = (pathParams: CaseViewPathParams) => void;
export const useCaseViewNavigation = () => {
const { appId } = useCasesContext();
const { navigateTo, getAppUrl } = useNavigation(appId);
const deepLinkId = APP_ID;
const getCaseViewUrl = useCallback<GetCaseViewUrl>(
(pathParams, absolute) =>
getAppUrl({
deepLinkId: CasesDeepLinkId.cases,
deepLinkId,
absolute,
path: generateCaseViewPath(pathParams),
}),
[getAppUrl]
[deepLinkId, getAppUrl]
);
const navigateToCaseView = useCallback<NavigateToCaseView>(
(pathParams) =>
navigateTo({ deepLinkId: CasesDeepLinkId.cases, path: generateCaseViewPath(pathParams) }),
[navigateTo]
navigateTo({
deepLinkId,
path: generateCaseViewPath(pathParams),
}),
[navigateTo, deepLinkId]
);
return { getCaseViewUrl, navigateToCaseView };
};

View file

@ -18,24 +18,40 @@ describe('Paths', () => {
it('returns the correct path', () => {
expect(getCreateCasePath('test')).toBe('test/create');
});
it('normalize the path correctly', () => {
expect(getCreateCasePath('//test//page')).toBe('/test/page/create');
});
});
describe('getCasesConfigurePath', () => {
it('returns the correct path', () => {
expect(getCasesConfigurePath('test')).toBe('test/configure');
});
it('normalize the path correctly', () => {
expect(getCasesConfigurePath('//test//page')).toBe('/test/page/configure');
});
});
describe('getCaseViewPath', () => {
it('returns the correct path', () => {
expect(getCaseViewPath('test')).toBe('test/:detailName');
});
it('normalize the path correctly', () => {
expect(getCaseViewPath('//test//page')).toBe('/test/page/:detailName');
});
});
describe('getCaseViewWithCommentPath', () => {
it('returns the correct path', () => {
expect(getCaseViewWithCommentPath('test')).toBe('test/:detailName/:commentId');
});
it('normalize the path correctly', () => {
expect(getCaseViewWithCommentPath('//test//page')).toBe('/test/page/:detailName/:commentId');
});
});
describe('generateCaseViewPath', () => {

View file

@ -18,12 +18,16 @@ export const CASES_CONFIGURE_PATH = '/configure' as const;
export const CASE_VIEW_PATH = '/:detailName' as const;
export const CASE_VIEW_COMMENT_PATH = `${CASE_VIEW_PATH}/:commentId` as const;
export const getCreateCasePath = (casesBasePath: string) => `${casesBasePath}${CASES_CREATE_PATH}`;
const normalizePath = (path: string): string => path.replaceAll('//', '/');
export const getCreateCasePath = (casesBasePath: string) =>
normalizePath(`${casesBasePath}${CASES_CREATE_PATH}`);
export const getCasesConfigurePath = (casesBasePath: string) =>
`${casesBasePath}${CASES_CONFIGURE_PATH}`;
export const getCaseViewPath = (casesBasePath: string) => `${casesBasePath}${CASE_VIEW_PATH}`;
normalizePath(`${casesBasePath}${CASES_CONFIGURE_PATH}`);
export const getCaseViewPath = (casesBasePath: string) =>
normalizePath(`${casesBasePath}${CASE_VIEW_PATH}`);
export const getCaseViewWithCommentPath = (casesBasePath: string) =>
`${casesBasePath}${CASE_VIEW_COMMENT_PATH}`;
normalizePath(`${casesBasePath}${CASE_VIEW_COMMENT_PATH}`);
export const generateCaseViewPath = (params: CaseViewPathParams): string => {
const { commentId } = params;
@ -31,7 +35,7 @@ export const generateCaseViewPath = (params: CaseViewPathParams): string => {
const pathParams = params as unknown as { [paramName: string]: string };
if (commentId) {
return generatePath(CASE_VIEW_COMMENT_PATH, pathParams);
return normalizePath(generatePath(CASE_VIEW_COMMENT_PATH, pathParams));
}
return generatePath(CASE_VIEW_PATH, pathParams);
return normalizePath(generatePath(CASE_VIEW_PATH, pathParams));
};

View file

@ -268,3 +268,11 @@ export const CASE_SUCCESS_SYNC_TEXT = i18n.translate('xpack.cases.actions.caseSu
export const VIEW_CASE = i18n.translate('xpack.cases.actions.viewCase', {
defaultMessage: 'View Case',
});
export const APP_TITLE = i18n.translate('xpack.cases.common.appTitle', {
defaultMessage: 'Cases',
});
export const APP_DESC = i18n.translate('xpack.cases.common.appDescription', {
defaultMessage: 'Open and track issues, push information to third party systems.',
});

View file

@ -5,9 +5,33 @@
* 2.0.
*/
import { CasesRoutes } from './routes';
import React from 'react';
import { APP_OWNER } from '../../../common/constants';
import { useApplicationCapabilities } from '../../common/lib/kibana';
import { getCasesLazy } from '../../methods';
import { Wrapper } from '../wrappers';
import { CasesRoutesProps } from './types';
export type CasesProps = CasesRoutesProps;
// eslint-disable-next-line import/no-default-export
export { CasesRoutes as default };
const CasesAppComponent: React.FC = () => {
const userCapabilities = useApplicationCapabilities();
return (
<Wrapper>
{getCasesLazy({
owner: [APP_OWNER],
useFetchAlertData: () => [false, {}],
userCanCrud: userCapabilities.crud,
basePath: '/',
features: { alerts: { sync: false } },
releasePhase: 'experimental',
})}
</Wrapper>
);
};
CasesAppComponent.displayName = 'CasesApp';
export const CasesApp = React.memo(CasesAppComponent);

View file

@ -91,3 +91,5 @@ const CasesRoutesComponent: React.FC<CasesRoutesProps> = ({
CasesRoutesComponent.displayName = 'CasesRoutes';
export const CasesRoutes = React.memo(CasesRoutesComponent);
// eslint-disable-next-line import/no-default-export
export { CasesRoutes as default };

View file

@ -7,21 +7,6 @@
import { i18n } from '@kbn/i18n';
export const NO_PRIVILEGES_MSG = (pageName: string) =>
i18n.translate('xpack.cases.noPrivileges.message', {
values: { pageName },
defaultMessage:
'To view {pageName} page, you must update privileges. For more information, contact your Kibana administrator.',
});
export const NO_PRIVILEGES_TITLE = i18n.translate('xpack.cases.noPrivileges.title', {
defaultMessage: 'Privileges required',
});
export const NO_PRIVILEGES_BUTTON = i18n.translate('xpack.cases.noPrivileges.button', {
defaultMessage: 'Back to Cases',
});
export const CREATE_CASE_PAGE_NAME = i18n.translate('xpack.cases.createCase', {
defaultMessage: 'Create Case',
});

View file

@ -36,6 +36,7 @@ import type { EmbeddablePackageState } from '../../../../../../../../src/plugins
import { SavedObjectFinderUi } from './saved_objects_finder';
import { useLensDraftComment } from './use_lens_draft_comment';
import { VISUALIZATION } from './translations';
import { useIsMainApplication } from '../../../../common/hooks';
const BetaBadgeWrapper = styled.span`
display: inline-flex;
@ -84,6 +85,7 @@ const LensEditorComponent: LensEuiMarkdownEditorUiPlugin['editor'] = ({
const { draftComment, clearDraftComment } = useLensDraftComment();
const commentEditorContext = useContext(CommentEditorContext);
const markdownContext = useContext(EuiMarkdownContext);
const isMainApplication = useIsMainApplication();
const handleClose = useCallback(() => {
if (currentAppId) {
@ -126,8 +128,11 @@ const LensEditorComponent: LensEuiMarkdownEditorUiPlugin['editor'] = ({
);
const originatingPath = useMemo(
() => `${location.pathname}${location.search}`,
[location.pathname, location.search]
() =>
isMainApplication
? `/insightsAndAlerting/cases${location.pathname}${location.search}`
: `${location.pathname}${location.search}`,
[isMainApplication, location.pathname, location.search]
);
const handleCreateInLensClick = useCallback(() => {

View file

@ -8,7 +8,7 @@
import { IconType } from '@elastic/eui';
import { ConnectorTypes } from '../../common/api';
import { FieldConfig, ValidationConfig } from '../common/shared_imports';
import { StartPlugins } from '../types';
import { CasesPluginStart } from '../types';
import { connectorValidator as swimlaneConnectorValidator } from './connectors/swimlane/validator';
import { connectorValidator as servicenowConnectorValidator } from './connectors/servicenow/validator';
import { CaseActionConnector } from './types';
@ -48,7 +48,7 @@ export const getConnectorsFormValidators = ({
});
export const getConnectorIcon = (
triggersActionsUi: StartPlugins['triggersActionsUi'],
triggersActionsUi: CasesPluginStart['triggersActionsUi'],
type?: string
): IconType => {
/**

View file

@ -27,3 +27,8 @@ export const ContentWrapper = styled.div`
padding: ${theme.eui.paddingSizes.l} 0 ${gutterTimeline} 0;
`};
`;
export const Wrapper = styled.div`
display: flex;
flex-direction: column;
`;

View file

@ -12,7 +12,8 @@ import { CasesProvider, CasesContextProps } from '../components/cases_context';
export type GetCasesProps = CasesProps & CasesContextProps;
const CasesLazy: React.FC<CasesProps> = lazy(() => import('../components/app'));
const CasesRoutesLazy: React.FC<CasesProps> = lazy(() => import('../components/app/routes'));
export const getCasesLazy = ({
owner,
userCanCrud,
@ -29,7 +30,7 @@ export const getCasesLazy = ({
}: GetCasesProps) => (
<CasesProvider value={{ owner, userCanCrud, basePath, features, releasePhase }}>
<Suspense fallback={<EuiLoadingSpinner />}>
<CasesLazy
<CasesRoutesLazy
onComponentInitialized={onComponentInitialized}
actionsNavigation={actionsNavigation}
ruleDetailsNavigation={ruleDetailsNavigation}

View file

@ -6,7 +6,7 @@
*/
import { CoreSetup, CoreStart, Plugin, PluginInitializerContext } from 'src/core/public';
import { CasesUiStart, SetupPlugins, StartPlugins } from './types';
import { CasesUiStart, CasesPluginSetup, CasesPluginStart } from './types';
import { KibanaServices } from './common/lib/kibana';
import {
getCasesLazy,
@ -18,24 +18,74 @@ import {
getAllCasesSelectorModalNoProviderLazy,
} from './methods';
import { CasesUiConfigType } from '../common/ui/types';
import { APP_ID, APP_PATH } from '../common/constants';
import { APP_TITLE, APP_DESC } from './common/translations';
import { FeatureCatalogueCategory } from '../../../../src/plugins/home/public';
import { ManagementAppMountParams } from '../../../../src/plugins/management/public';
import { Storage } from '../../../../src/plugins/kibana_utils/public';
import { getCasesContextLazy } from './methods/get_cases_context';
import { useCasesAddToNewCaseFlyout } from './components/create/flyout/use_cases_add_to_new_case_flyout';
import { useCasesAddToExistingCaseModal } from './components/all_cases/selector_modal/use_cases_add_to_existing_case_modal';
import { useCasesAddToNewCaseFlyout } from './components/create/flyout/use_cases_add_to_new_case_flyout';
import { getRuleIdFromEvent } from './methods/get_rule_id_from_event';
/**
* @public
* A plugin for retrieving Cases UI components
*/
export class CasesUiPlugin implements Plugin<void, CasesUiStart, SetupPlugins, StartPlugins> {
private kibanaVersion: string;
export class CasesUiPlugin
implements Plugin<void, CasesUiStart, CasesPluginSetup, CasesPluginStart>
{
private readonly kibanaVersion: string;
private readonly storage = new Storage(localStorage);
constructor(private readonly initializerContext: PluginInitializerContext) {
this.kibanaVersion = initializerContext.env.packageInfo.version;
}
public setup(core: CoreSetup, plugins: SetupPlugins) {}
public start(core: CoreStart, plugins: StartPlugins): CasesUiStart {
public setup(core: CoreSetup, plugins: CasesPluginSetup) {
const kibanaVersion = this.kibanaVersion;
const storage = this.storage;
if (plugins.home) {
plugins.home.featureCatalogue.register({
id: APP_ID,
title: APP_TITLE,
description: APP_DESC,
icon: 'watchesApp',
path: APP_PATH,
showOnHomePage: false,
category: FeatureCatalogueCategory.ADMIN,
});
}
plugins.management.sections.section.insightsAndAlerting.registerApp({
id: APP_ID,
title: APP_TITLE,
order: 0,
async mount(params: ManagementAppMountParams) {
const [coreStart, pluginsStart] = (await core.getStartServices()) as [
CoreStart,
CasesPluginStart,
unknown
];
const { renderApp } = await import('./application');
return renderApp({
mountParams: params,
coreStart,
pluginsStart,
storage,
kibanaVersion,
});
},
});
// Return methods that should be available to other plugins
return {};
}
public start(core: CoreStart, plugins: CasesPluginStart): CasesUiStart {
const config = this.initializerContext.config.get<CasesUiConfigType>();
KibanaServices.init({ ...core, ...plugins, kibanaVersion: this.kibanaVersion, config });
return {

View file

@ -10,6 +10,12 @@ import { ReactElement, ReactNode } from 'react';
import type { DataPublicPluginStart } from '../../../../src/plugins/data/public';
import type { EmbeddableStart } from '../../../../src/plugins/embeddable/public';
import type { Storage } from '../../../../src/plugins/kibana_utils/public';
import { HomePublicPluginSetup } from '../../../../src/plugins/home/public';
import {
ManagementSetup,
ManagementAppMountParams,
} from '../../../../src/plugins/management/public';
import { FeaturesPluginStart } from '../..//features/public';
import type { LensPublicStart } from '../../lens/public';
import type { SecurityPluginSetup } from '../../security/public';
import type { SpacesPluginStart } from '../../spaces/public';
@ -18,6 +24,7 @@ import { CommentRequestAlertType, CommentRequestUserType } from '../common/api';
import { UseCasesAddToExistingCaseModal } from './components/all_cases/selector_modal/use_cases_add_to_existing_case_modal';
import { CreateCaseFlyoutProps } from './components/create/flyout';
import { UseCasesAddToNewCaseFlyout } from './components/create/flyout/use_cases_add_to_new_case_flyout';
import type {
CasesOwners,
GetAllCasesSelectorModalProps,
@ -28,16 +35,19 @@ import type {
import { GetCasesContextProps } from './methods/get_cases_context';
import { getRuleIdFromEvent } from './methods/get_rule_id_from_event';
export interface SetupPlugins {
export interface CasesPluginSetup {
security: SecurityPluginSetup;
management: ManagementSetup;
home?: HomePublicPluginSetup;
}
export interface StartPlugins {
export interface CasesPluginStart {
data: DataPublicPluginStart;
embeddable: EmbeddableStart;
lens: LensPublicStart;
storage: Storage;
triggersActionsUi: TriggersActionsStart;
features: FeaturesPluginStart;
spaces?: SpacesPluginStart;
}
@ -48,10 +58,18 @@ export interface StartPlugins {
*/
export type StartServices = CoreStart &
StartPlugins & {
CasesPluginStart & {
security: SecurityPluginSetup;
};
export interface RenderAppProps {
mountParams: ManagementAppMountParams;
coreStart: CoreStart;
pluginsStart: CasesPluginStart;
storage: Storage;
kibanaVersion: string;
}
export interface CasesUiStart {
/**
* Returns an object denoting the current user's ability to read and crud cases.

View file

@ -0,0 +1,63 @@
/*
* 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';
import { KibanaFeatureConfig } from '../../features/common';
import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/server';
import { APP_ID, FEATURE_ID } from '../common/constants';
/**
* The order of appearance in the feature privilege page
* under the management section. Cases should be under
* the Actions and Connectors feature
*/
const FEATURE_ORDER = 3100;
export const getCasesKibanaFeature = (): KibanaFeatureConfig => ({
id: FEATURE_ID,
name: i18n.translate('xpack.cases.features.casesFeatureName', {
defaultMessage: 'Cases',
}),
category: DEFAULT_APP_CATEGORIES.management,
app: [],
order: FEATURE_ORDER,
management: {
insightsAndAlerting: [APP_ID],
},
cases: [APP_ID],
privileges: {
all: {
cases: {
all: [APP_ID],
},
management: {
insightsAndAlerting: [APP_ID],
},
savedObject: {
all: [],
read: [],
},
ui: ['crud_cases', 'read_cases'],
},
read: {
cases: {
read: [APP_ID],
},
management: {
insightsAndAlerting: [APP_ID],
},
savedObject: {
all: [],
read: [],
},
ui: ['read_cases'],
},
},
});

View file

@ -28,14 +28,19 @@ import { CasesClient } from './client';
import type { CasesRequestHandlerContext } from './types';
import { CasesClientFactory } from './client/factory';
import { SpacesPluginStart } from '../../spaces/server';
import { PluginStartContract as FeaturesPluginStart } from '../../features/server';
import {
PluginStartContract as FeaturesPluginStart,
PluginSetupContract as FeaturesPluginSetup,
} from '../../features/server';
import { LensServerPluginSetup } from '../../lens/server';
import { getCasesKibanaFeature } from './features';
import { registerRoutes } from './routes/api/register_routes';
import { getExternalRoutes } from './routes/api/get_external_routes';
export interface PluginsSetup {
actions: ActionsPluginSetup;
lens: LensServerPluginSetup;
features: FeaturesPluginSetup;
usageCollection?: UsageCollectionSetup;
security?: SecurityPluginSetup;
}
@ -77,6 +82,8 @@ export class CasePlugin {
this.securityPluginSetup = plugins.security;
this.lensEmbeddableFactory = plugins.lens.lensEmbeddableFactory;
plugins.features.registerKibanaFeature(getCasesKibanaFeature());
core.savedObjects.registerType(
createCaseCommentSavedObjectType({
migrationDeps: {

View file

@ -111,6 +111,7 @@ export default function ({ getService }: FtrProviderContext) {
'apm',
'stackAlerts',
'canvas',
'generalCases',
'infrastructure',
'logs',
'maps',

View file

@ -28,6 +28,7 @@ export default function ({ getService }: FtrProviderContext) {
savedObjectsTagging: ['all', 'read', 'minimal_all', 'minimal_read'],
canvas: ['all', 'read', 'minimal_all', 'minimal_read'],
maps: ['all', 'read', 'minimal_all', 'minimal_read'],
generalCases: ['all', 'read', 'minimal_all', 'minimal_read'],
observabilityCases: ['all', 'read', 'minimal_all', 'minimal_read'],
fleetv2: ['all', 'read', 'minimal_all', 'minimal_read'],
fleet: ['all', 'read', 'minimal_all', 'minimal_read'],

View file

@ -30,6 +30,7 @@ export default function ({ getService }: FtrProviderContext) {
savedObjectsTagging: ['all', 'read', 'minimal_all', 'minimal_read'],
graph: ['all', 'read', 'minimal_all', 'minimal_read'],
maps: ['all', 'read', 'minimal_all', 'minimal_read'],
generalCases: ['all', 'read', 'minimal_all', 'minimal_read'],
observabilityCases: ['all', 'read', 'minimal_all', 'minimal_read'],
canvas: ['all', 'read', 'minimal_all', 'minimal_read'],
infrastructure: ['all', 'read', 'minimal_all', 'minimal_read'],

View file

@ -64,7 +64,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
expect(sections).to.have.length(2);
expect(sections[0]).to.eql({
sectionId: 'insightsAndAlerting',
sectionLinks: ['triggersActions', 'jobsListLink'],
sectionLinks: ['triggersActions', 'cases', 'jobsListLink'],
});
expect(sections[1]).to.eql({
sectionId: 'kibana',