diff --git a/x-pack/plugins/cases/common/api/helpers.ts b/x-pack/plugins/cases/common/api/helpers.ts index 9e33eb3173d0e..da16dc028b854 100644 --- a/x-pack/plugins/cases/common/api/helpers.ts +++ b/x-pack/plugins/cases/common/api/helpers.ts @@ -7,7 +7,7 @@ import { CASE_DETAILS_URL, - CASE_METRICS_DETAILS_URL, + INTERNAL_CASE_METRICS_DETAILS_URL, CASE_COMMENTS_URL, CASE_USER_ACTIONS_URL, CASE_PUSH_URL, @@ -28,7 +28,7 @@ export const getCaseDetailsUrl = (id: string): string => { }; export const getCaseDetailsMetricsUrl = (id: string): string => { - return CASE_METRICS_DETAILS_URL.replace('{case_id}', id); + return INTERNAL_CASE_METRICS_DETAILS_URL.replace('{case_id}', id); }; export const getCaseCommentsUrl = (id: string): string => { diff --git a/x-pack/plugins/cases/common/api/metrics/case.test.ts b/x-pack/plugins/cases/common/api/metrics/case.test.ts index 073aac4899fb9..aa6c9bfd11eeb 100644 --- a/x-pack/plugins/cases/common/api/metrics/case.test.ts +++ b/x-pack/plugins/cases/common/api/metrics/case.test.ts @@ -5,17 +5,19 @@ * 2.0. */ +import { PathReporter } from 'io-ts/lib/PathReporter'; import { SingleCaseMetricsRequestRt, CasesMetricsRequestRt, SingleCaseMetricsResponseRt, CasesMetricsResponseRt, + CaseMetricsFeature, } from './case'; describe('Metrics case', () => { describe('SingleCaseMetricsRequestRt', () => { const defaultRequest = { - features: ['alerts.count', 'lifespan'], + features: [CaseMetricsFeature.ALERTS_COUNT, CaseMetricsFeature.LIFESPAN], }; it('has expected attributes in request', () => { @@ -38,10 +40,27 @@ describe('Metrics case', () => { right: defaultRequest, }); }); + + describe('errors', () => { + it('has invalid feature in request', () => { + expect( + PathReporter.report( + SingleCaseMetricsRequestRt.decode({ + features: [CaseMetricsFeature.MTTR], + }) + )[0] + ).toContain('Invalid value "mttr" supplied'); + }); + }); }); describe('CasesMetricsRequestRt', () => { - const defaultRequest = { features: ['mttr'], to: 'now-1d', from: 'now-1d', owner: ['cases'] }; + const defaultRequest = { + features: [CaseMetricsFeature.MTTR], + to: 'now-1d', + from: 'now-1d', + owner: ['cases'], + }; it('has expected attributes in request', () => { const query = CasesMetricsRequestRt.decode(defaultRequest); @@ -65,16 +84,31 @@ describe('Metrics case', () => { }); it('removes foo:bar attributes from when partial fields', () => { - const query = CasesMetricsRequestRt.decode({ features: ['mttr'], to: 'now-1d', foo: 'bar' }); + const query = CasesMetricsRequestRt.decode({ + features: [CaseMetricsFeature.MTTR], + to: 'now-1d', + foo: 'bar', + }); expect(query).toStrictEqual({ _tag: 'Right', right: { - features: ['mttr'], + features: [CaseMetricsFeature.MTTR], to: 'now-1d', }, }); }); + describe('errors', () => { + it('has invalid feature in request', () => { + expect( + PathReporter.report( + CasesMetricsRequestRt.decode({ + features: ['foobar'], + }) + )[0] + ).toContain('Invalid value "foobar" supplied'); + }); + }); }); describe('SingleCaseMetricsResponseRt', () => { diff --git a/x-pack/plugins/cases/common/api/metrics/case.ts b/x-pack/plugins/cases/common/api/metrics/case.ts index 0e470a74bc4fa..895c0fe99b3bf 100644 --- a/x-pack/plugins/cases/common/api/metrics/case.ts +++ b/x-pack/plugins/cases/common/api/metrics/case.ts @@ -15,6 +15,30 @@ export type AlertHostsMetrics = rt.TypeOf; export type AlertUsersMetrics = rt.TypeOf; export type StatusInfo = rt.TypeOf; +export enum CaseMetricsFeature { + ALERTS_COUNT = 'alerts.count', + ALERTS_USERS = 'alerts.users', + ALERTS_HOSTS = 'alerts.hosts', + ACTIONS_ISOLATE_HOST = 'actions.isolateHost', + CONNECTORS = 'connectors', + LIFESPAN = 'lifespan', + MTTR = 'mttr', +} + +export const SingleCaseMetricsFeatureFieldRt = rt.union([ + rt.literal(CaseMetricsFeature.ALERTS_COUNT), + rt.literal(CaseMetricsFeature.ALERTS_USERS), + rt.literal(CaseMetricsFeature.ALERTS_HOSTS), + rt.literal(CaseMetricsFeature.ACTIONS_ISOLATE_HOST), + rt.literal(CaseMetricsFeature.CONNECTORS), + rt.literal(CaseMetricsFeature.LIFESPAN), +]); + +export const CasesMetricsFeatureFieldRt = rt.union([ + SingleCaseMetricsFeatureFieldRt, + rt.literal(CaseMetricsFeature.MTTR), +]); + const StatusInfoRt = rt.strict({ /** * Duration the case was in the open status in milliseconds @@ -76,7 +100,7 @@ export const SingleCaseMetricsRequestRt = rt.strict({ /** * The metrics to retrieve. */ - features: rt.array(rt.string), + features: rt.array(SingleCaseMetricsFeatureFieldRt), }); export const CasesMetricsRequestRt = rt.intersection([ @@ -84,7 +108,7 @@ export const CasesMetricsRequestRt = rt.intersection([ /** * The metrics to retrieve. */ - features: rt.array(rt.string), + features: rt.array(CasesMetricsFeatureFieldRt), }), rt.exact( rt.partial({ @@ -188,3 +212,6 @@ export const CasesMetricsResponseRt = rt.exact( mttr: rt.union([rt.number, rt.null]), }) ); + +export type CasesMetricsFeatureField = rt.TypeOf; +export type SingleCaseMetricsFeatureField = rt.TypeOf; diff --git a/x-pack/plugins/cases/common/constants/index.ts b/x-pack/plugins/cases/common/constants/index.ts index 02a20b014aa8a..ac689ad5b29d4 100644 --- a/x-pack/plugins/cases/common/constants/index.ts +++ b/x-pack/plugins/cases/common/constants/index.ts @@ -61,9 +61,6 @@ export const CASE_FIND_USER_ACTIONS_URL = `${CASE_USER_ACTIONS_URL}/_find` as co 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_URL = `${CASES_URL}/metrics` as const; -export const CASE_METRICS_DETAILS_URL = `${CASES_URL}/metrics/{case_id}` as const; - /** * Internal routes */ @@ -83,6 +80,8 @@ export const INTERNAL_CASE_USERS_URL = `${CASES_INTERNAL_URL}/{case_id}/_users` export const INTERNAL_DELETE_FILE_ATTACHMENTS_URL = `${CASES_INTERNAL_URL}/{case_id}/attachments/files/_bulk_delete` as const; export const INTERNAL_GET_CASE_CATEGORIES_URL = `${CASES_INTERNAL_URL}/categories` as const; +export const INTERNAL_CASE_METRICS_URL = `${CASES_INTERNAL_URL}/metrics` as const; +export const INTERNAL_CASE_METRICS_DETAILS_URL = `${CASES_INTERNAL_URL}/metrics/{case_id}` as const; /** * Action routes diff --git a/x-pack/plugins/cases/common/index.ts b/x-pack/plugins/cases/common/index.ts index 070945d322039..4b170fa9289a7 100644 --- a/x-pack/plugins/cases/common/index.ts +++ b/x-pack/plugins/cases/common/index.ts @@ -58,3 +58,4 @@ export { getCasesFromAlertsUrl, getCaseFindUserActionsUrl, throwErrors } from '. export { StatusAll } from './ui/types'; export { createUICapabilities } from './utils/capabilities'; export { getApiTags } from './utils/api_tags'; +export { CaseMetricsFeature } from './api/metrics/case'; diff --git a/x-pack/plugins/cases/common/ui/types.ts b/x-pack/plugins/cases/common/ui/types.ts index c9a5335f6388c..0a8a0acbb9899 100644 --- a/x-pack/plugins/cases/common/ui/types.ts +++ b/x-pack/plugins/cases/common/ui/types.ts @@ -12,7 +12,6 @@ import type { READ_CASES_CAPABILITY, UPDATE_CASES_CAPABILITY, } from '..'; -import type { SingleCaseMetricsResponse, CasesMetricsResponse } from '../api'; import type { PUSH_CASES_CAPABILITY } from '../constants'; import type { SnakeToCamelCase } from '../types'; import type { @@ -37,6 +36,7 @@ import type { UserActionFindRequestTypes, UserActionFindResponse, } from '../types/api'; +import type { CaseMetricsFeature, CasesMetricsResponse, SingleCaseMetricsResponse } from '../api'; type DeepRequired = { [K in keyof T]: DeepRequired } & Required; @@ -153,13 +153,7 @@ export interface FilterOptions { export type PartialFilterOptions = Partial; export type SingleCaseMetrics = SingleCaseMetricsResponse; -export type SingleCaseMetricsFeature = - | 'alerts.count' - | 'alerts.users' - | 'alerts.hosts' - | 'actions.isolateHost' - | 'connectors' - | 'lifespan'; +export type SingleCaseMetricsFeature = Exclude; export enum SortFieldCase { closedAt = 'closedAt', diff --git a/x-pack/plugins/cases/public/api/index.test.ts b/x-pack/plugins/cases/public/api/index.test.ts index c3a88c04cf8c7..0e887c185b313 100644 --- a/x-pack/plugins/cases/public/api/index.test.ts +++ b/x-pack/plugins/cases/public/api/index.test.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { CaseMetricsFeature } from '../../common/api/metrics/case'; import { httpServiceMock } from '@kbn/core/public/mocks'; import { bulkGetCases, getCases, getCasesMetrics } from '.'; import { allCases, allCasesSnake, casesSnake } from '../containers/mock'; @@ -36,14 +37,17 @@ describe('api', () => { it('should return the correct response', async () => { expect( - await getCasesMetrics({ http, query: { features: ['mttr'], from: 'now-1d' } }) + await getCasesMetrics({ + http, + query: { features: [CaseMetricsFeature.MTTR], from: 'now-1d' }, + }) ).toEqual({ mttr: 0 }); }); it('should have been called with the correct path', async () => { - await getCasesMetrics({ http, query: { features: ['mttr'], to: 'now-1d' } }); - expect(http.get).toHaveBeenCalledWith('/api/cases/metrics', { - query: { features: ['mttr'], to: 'now-1d' }, + await getCasesMetrics({ http, query: { features: [CaseMetricsFeature.MTTR], to: 'now-1d' } }); + expect(http.get).toHaveBeenCalledWith('/internal/cases/metrics', { + query: { features: [CaseMetricsFeature.MTTR], to: 'now-1d' }, }); }); }); diff --git a/x-pack/plugins/cases/public/api/index.ts b/x-pack/plugins/cases/public/api/index.ts index 743bb2f2346ba..deb72d4c4beaf 100644 --- a/x-pack/plugins/cases/public/api/index.ts +++ b/x-pack/plugins/cases/public/api/index.ts @@ -17,7 +17,7 @@ import type { import type { CasesStatus, CasesMetrics, CasesFindResponseUI } from '../../common/ui'; import { CASE_FIND_URL, - CASE_METRICS_URL, + INTERNAL_CASE_METRICS_URL, CASE_STATUS_URL, INTERNAL_BULK_GET_CASES_URL, } from '../../common/constants'; @@ -62,7 +62,7 @@ export const getCasesMetrics = async ({ signal, query, }: HTTPService & { query: CasesMetricsRequest }): Promise => { - const res = await http.get(CASE_METRICS_URL, { signal, query }); + const res = await http.get(INTERNAL_CASE_METRICS_URL, { signal, query }); return convertToCamelCase(decodeCasesMetricsResponse(res)); }; diff --git a/x-pack/plugins/cases/public/client/api/index.test.ts b/x-pack/plugins/cases/public/client/api/index.test.ts index 0a07cd5454b02..792fa861b4241 100644 --- a/x-pack/plugins/cases/public/client/api/index.test.ts +++ b/x-pack/plugins/cases/public/client/api/index.test.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { CaseMetricsFeature } from '../../../common/api/metrics/case'; import { httpServiceMock } from '@kbn/core/public/mocks'; import { createClientAPI } from '.'; import { allCases, allCasesSnake, casesSnake } from '../../containers/mock'; @@ -68,15 +69,17 @@ describe('createClientAPI', () => { http.get.mockResolvedValue({ mttr: 0 }); it('should return the correct response', async () => { - expect(await api.cases.getCasesMetrics({ features: ['mttr'], from: 'now-1d' })).toEqual({ + expect( + await api.cases.getCasesMetrics({ features: [CaseMetricsFeature.MTTR], from: 'now-1d' }) + ).toEqual({ mttr: 0, }); }); it('should have been called with the correct path', async () => { - await api.cases.getCasesMetrics({ features: ['mttr'], from: 'now-1d' }); - expect(http.get).toHaveBeenCalledWith('/api/cases/metrics', { - query: { features: ['mttr'], from: 'now-1d' }, + await api.cases.getCasesMetrics({ features: [CaseMetricsFeature.MTTR], from: 'now-1d' }); + expect(http.get).toHaveBeenCalledWith('/internal/cases/metrics', { + query: { features: [CaseMetricsFeature.MTTR], from: 'now-1d' }, }); }); }); diff --git a/x-pack/plugins/cases/public/common/use_cases_features.test.tsx b/x-pack/plugins/cases/public/common/use_cases_features.test.tsx index 973ed1326baa8..3f6ba6676d63e 100644 --- a/x-pack/plugins/cases/public/common/use_cases_features.test.tsx +++ b/x-pack/plugins/cases/public/common/use_cases_features.test.tsx @@ -14,6 +14,7 @@ import { useCasesFeatures } from './use_cases_features'; import { TestProviders } from './mock/test_providers'; import type { LicenseType } from '@kbn/licensing-plugin/common/types'; import { LICENSE_TYPE } from '@kbn/licensing-plugin/common/types'; +import { CaseMetricsFeature } from '../../common/api/metrics/case'; describe('useCasesFeatures', () => { // isAlertsEnabled, isSyncAlertsEnabled, alerts @@ -53,14 +54,16 @@ describe('useCasesFeatures', () => { it('returns the metrics correctly', async () => { const { result } = renderHook<{}, UseCasesFeatures>(() => useCasesFeatures(), { wrapper: ({ children }) => ( - {children} + + {children} + ), }); expect(result.current).toEqual({ isAlertsEnabled: true, isSyncAlertsEnabled: true, - metricsFeatures: ['connectors'], + metricsFeatures: [CaseMetricsFeature.CONNECTORS], caseAssignmentAuthorized: false, pushToServiceAuthorized: false, }); diff --git a/x-pack/plugins/cases/public/components/case_action_bar/index.test.tsx b/x-pack/plugins/cases/public/components/case_action_bar/index.test.tsx index 909bcc317b505..26af77bc77982 100644 --- a/x-pack/plugins/cases/public/components/case_action_bar/index.test.tsx +++ b/x-pack/plugins/cases/public/components/case_action_bar/index.test.tsx @@ -22,6 +22,7 @@ import { import { useGetCaseConnectors } from '../../containers/use_get_case_connectors'; import { useRefreshCaseViewPage } from '../case_view/use_on_refresh_case_view_page'; import { getCaseConnectorsMockResponse } from '../../common/mock/connectors'; +import { CaseMetricsFeature } from '../../../common/api/metrics/case'; jest.mock('../../containers/use_get_case_connectors'); jest.mock('../case_view/use_on_refresh_case_view_page'); @@ -152,7 +153,7 @@ describe('CaseActionBar', () => { it('should not show the Case open text when the lifespan feature is enabled', () => { const props: CaseActionBarProps = { ...defaultProps }; const { queryByText } = render( - + ); diff --git a/x-pack/plugins/cases/public/components/case_action_bar/index.tsx b/x-pack/plugins/cases/public/components/case_action_bar/index.tsx index eb4ff93aac99d..95ca7e407191f 100644 --- a/x-pack/plugins/cases/public/components/case_action_bar/index.tsx +++ b/x-pack/plugins/cases/public/components/case_action_bar/index.tsx @@ -16,8 +16,9 @@ import { EuiFlexItem, EuiIconTip, } from '@elastic/eui'; -import type { CaseUI } from '../../../common/ui/types'; import type { CaseStatuses } from '../../../common/types/domain'; +import type { CaseUI } from '../../../common/ui/types'; +import { CaseMetricsFeature } from '../../../common/api'; import * as i18n from '../case_view/translations'; import { Actions } from './actions'; import { StatusContextMenu } from './status_context_menu'; @@ -100,7 +101,7 @@ const CaseActionBarComponent: React.FC = ({ /> - {!metricsFeatures.includes('lifespan') ? ( + {!metricsFeatures.includes(CaseMetricsFeature.LIFESPAN) ? ( {title} diff --git a/x-pack/plugins/cases/public/components/case_view/case_view_page.test.tsx b/x-pack/plugins/cases/public/components/case_view/case_view_page.test.tsx index 49af1aed3a075..cba862d06fab7 100644 --- a/x-pack/plugins/cases/public/components/case_view/case_view_page.test.tsx +++ b/x-pack/plugins/cases/public/components/case_view/case_view_page.test.tsx @@ -41,6 +41,7 @@ import { useInfiniteFindCaseUserActions } from '../../containers/use_infinite_fi import { useGetCaseUserActionsStats } from '../../containers/use_get_case_user_actions_stats'; import { createQueryWithMarkup } from '../../common/test_utils'; import { useCasesFeatures } from '../../common/use_cases_features'; +import { CaseMetricsFeature } from '../../../common/api/metrics/case'; jest.mock('../../containers/use_get_action_license'); jest.mock('../../containers/use_update_case'); @@ -176,7 +177,7 @@ describe('CaseViewPage', () => { useGetTagsMock.mockReturnValue({ data: [], isLoading: false }); useGetCaseUsersMock.mockReturnValue({ isLoading: false, data: caseUsers }); useCasesFeaturesMock.mockReturnValue({ - metricsFeatures: ['alerts.count'], + metricsFeatures: [CaseMetricsFeature.ALERTS_COUNT], pushToServiceAuthorized: true, caseAssignmentAuthorized: true, isAlertsEnabled: true, diff --git a/x-pack/plugins/cases/public/components/case_view/components/case_view_activity.test.tsx b/x-pack/plugins/cases/public/components/case_view/components/case_view_activity.test.tsx index 76db6783bb037..5f2288a93191b 100644 --- a/x-pack/plugins/cases/public/components/case_view/components/case_view_activity.test.tsx +++ b/x-pack/plugins/cases/public/components/case_view/components/case_view_activity.test.tsx @@ -37,6 +37,7 @@ import { useInfiniteFindCaseUserActions } from '../../../containers/use_infinite import { useOnUpdateField } from '../use_on_update_field'; import { useCasesFeatures } from '../../../common/use_cases_features'; import { ConnectorTypes, UserActionTypes } from '../../../../common/types/domain'; +import { CaseMetricsFeature } from '../../../../common/api/metrics/case'; jest.mock('../../../containers/use_infinite_find_case_user_actions'); jest.mock('../../../containers/use_find_case_user_actions'); @@ -116,7 +117,7 @@ const caseProps = { const caseUsers = getCaseUsersMockResponse(); const useGetCasesFeaturesRes = { - metricsFeatures: ['alerts.count'], + metricsFeatures: [CaseMetricsFeature.ALERTS_COUNT], pushToServiceAuthorized: true, caseAssignmentAuthorized: true, isAlertsEnabled: true, diff --git a/x-pack/plugins/cases/public/components/case_view/metrics/index.test.tsx b/x-pack/plugins/cases/public/components/case_view/metrics/index.test.tsx index 166ec92d5798c..6f28f35845429 100644 --- a/x-pack/plugins/cases/public/components/case_view/metrics/index.test.tsx +++ b/x-pack/plugins/cases/public/components/case_view/metrics/index.test.tsx @@ -17,6 +17,7 @@ import type { SingleCaseMetrics, SingleCaseMetricsFeature } from '../../../../co import { TestProviders } from '../../../common/mock'; import { useGetCaseMetrics } from '../../../containers/use_get_case_metrics'; import { useCasesFeatures } from '../../../common/use_cases_features'; +import { CaseMetricsFeature } from '../../../../common/api/metrics/case'; jest.mock('../../../containers/use_get_case_metrics'); jest.mock('../../../common/use_cases_features'); @@ -56,19 +57,19 @@ interface FeatureTest { const metricsFeaturesTests: FeatureTest[] = [ { - feature: 'alerts.count', + feature: CaseMetricsFeature.ALERTS_COUNT, items: [{ title: 'Total alerts', value: basicCaseMetrics.alerts!.count! }], }, { - feature: 'alerts.users', + feature: CaseMetricsFeature.ALERTS_USERS, items: [{ title: 'Associated users', value: basicCaseMetrics.alerts!.users!.total! }], }, { - feature: 'alerts.hosts', + feature: CaseMetricsFeature.ALERTS_HOSTS, items: [{ title: 'Associated hosts', value: basicCaseMetrics.alerts!.hosts!.total! }], }, { - feature: 'actions.isolateHost', + feature: CaseMetricsFeature.ACTIONS_ISOLATE_HOST, items: [ { title: 'Isolated hosts', @@ -79,11 +80,11 @@ const metricsFeaturesTests: FeatureTest[] = [ ], }, { - feature: 'connectors', + feature: CaseMetricsFeature.CONNECTORS, items: [{ title: 'Total connectors', value: basicCaseMetrics.connectors!.total! }], }, { - feature: 'lifespan', + feature: CaseMetricsFeature.LIFESPAN, items: [ { title: 'Case created', @@ -187,7 +188,7 @@ describe('CaseViewMetrics', () => { }; const { getByText } = renderCaseMetrics({ metrics: incosistentMetrics, - features: ['actions.isolateHost'], + features: [CaseMetricsFeature.ACTIONS_ISOLATE_HOST], }); expect(getByText('Isolated hosts')).toBeInTheDocument(); expect(getByText('0')).toBeInTheDocument(); diff --git a/x-pack/plugins/cases/public/components/case_view/metrics/status.tsx b/x-pack/plugins/cases/public/components/case_view/metrics/status.tsx index e7364f029fc2d..761eea20fe74c 100644 --- a/x-pack/plugins/cases/public/components/case_view/metrics/status.tsx +++ b/x-pack/plugins/cases/public/components/case_view/metrics/status.tsx @@ -9,6 +9,7 @@ import React, { useMemo } from 'react'; import prettyMilliseconds from 'pretty-ms'; import { EuiFlexGrid, EuiFlexGroup, EuiFlexItem, EuiIconTip, EuiSpacer } from '@elastic/eui'; import { euiStyled } from '@kbn/kibana-react-plugin/common'; +import { CaseMetricsFeature } from '../../../../common/api/metrics/case'; import type { SingleCaseMetrics, SingleCaseMetricsFeature } from '../../../../common/ui'; import { CASE_CREATED, @@ -100,7 +101,7 @@ const useGetLifespanMetrics = ( statusInfo: { inProgressDuration: 0, reopenDates: [], openDuration: 0 }, }; - if (!features.includes('lifespan')) { + if (!features.includes(CaseMetricsFeature.LIFESPAN)) { return; } diff --git a/x-pack/plugins/cases/public/components/case_view/metrics/totals.tsx b/x-pack/plugins/cases/public/components/case_view/metrics/totals.tsx index dd81a2328a97a..46dc6c5bd6d92 100644 --- a/x-pack/plugins/cases/public/components/case_view/metrics/totals.tsx +++ b/x-pack/plugins/cases/public/components/case_view/metrics/totals.tsx @@ -8,6 +8,7 @@ import React, { useMemo } from 'react'; import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { euiStyled } from '@kbn/kibana-react-plugin/common'; +import { CaseMetricsFeature } from '../../../../common/api/metrics/case'; import type { SingleCaseMetrics, SingleCaseMetricsFeature } from '../../../../common/ui'; import { ASSOCIATED_HOSTS_METRIC, @@ -62,11 +63,14 @@ const useGetTitleValueMetricItems = ( const metricItems = useMemo(() => { const items: Array<[SingleCaseMetricsFeature, Omit]> = [ - ['alerts.count', { title: TOTAL_ALERTS_METRIC, value: alertsCount }], - ['alerts.users', { title: ASSOCIATED_USERS_METRIC, value: totalAlertUsers }], - ['alerts.hosts', { title: ASSOCIATED_HOSTS_METRIC, value: totalAlertHosts }], - ['actions.isolateHost', { title: ISOLATED_HOSTS_METRIC, value: totalIsolatedHosts }], - ['connectors', { title: TOTAL_CONNECTORS_METRIC, value: totalConnectors }], + [CaseMetricsFeature.ALERTS_COUNT, { title: TOTAL_ALERTS_METRIC, value: alertsCount }], + [CaseMetricsFeature.ALERTS_USERS, { title: ASSOCIATED_USERS_METRIC, value: totalAlertUsers }], + [CaseMetricsFeature.ALERTS_HOSTS, { title: ASSOCIATED_HOSTS_METRIC, value: totalAlertHosts }], + [ + CaseMetricsFeature.ACTIONS_ISOLATE_HOST, + { title: ISOLATED_HOSTS_METRIC, value: totalIsolatedHosts }, + ], + [CaseMetricsFeature.CONNECTORS, { title: TOTAL_CONNECTORS_METRIC, value: totalConnectors }], ]; return items.reduce( diff --git a/x-pack/plugins/cases/public/containers/mock.ts b/x-pack/plugins/cases/public/containers/mock.ts index 3ce9b44997148..107dff18abbe6 100644 --- a/x-pack/plugins/cases/public/containers/mock.ts +++ b/x-pack/plugins/cases/public/containers/mock.ts @@ -43,6 +43,7 @@ import type { CasesUI, AttachmentUI, } from '../../common/ui/types'; +import { CaseMetricsFeature } from '../../common/api'; import { SECURITY_SOLUTION_OWNER } from '../../common/constants'; import type { SnakeToCamelCase } from '../../common/types'; import { covertToSnakeCase } from './utils'; @@ -290,14 +291,14 @@ export const basicResolvedCase: ResolvedCase = { }; export const basicCaseNumericValueFeatures: SingleCaseMetricsFeature[] = [ - 'alerts.count', - 'alerts.users', - 'alerts.hosts', - 'actions.isolateHost', - 'connectors', + CaseMetricsFeature.ALERTS_COUNT, + CaseMetricsFeature.ALERTS_USERS, + CaseMetricsFeature.ALERTS_HOSTS, + CaseMetricsFeature.ACTIONS_ISOLATE_HOST, + CaseMetricsFeature.CONNECTORS, ]; -export const basicCaseStatusFeatures: SingleCaseMetricsFeature[] = ['lifespan']; +export const basicCaseStatusFeatures: SingleCaseMetricsFeature[] = [CaseMetricsFeature.LIFESPAN]; export const basicCaseMetrics: SingleCaseMetrics = { alerts: { diff --git a/x-pack/plugins/cases/public/containers/use_get_case_metrics.test.tsx b/x-pack/plugins/cases/public/containers/use_get_case_metrics.test.tsx index 6d8d155a6818a..f6537bbb9b4e1 100644 --- a/x-pack/plugins/cases/public/containers/use_get_case_metrics.test.tsx +++ b/x-pack/plugins/cases/public/containers/use_get_case_metrics.test.tsx @@ -13,6 +13,7 @@ import { basicCase } from './mock'; import * as api from './api'; import { TestProviders } from '../common/mock'; import { useToasts } from '../common/lib/kibana'; +import { CaseMetricsFeature } from '../../common/api/metrics/case'; jest.mock('./api'); jest.mock('../common/lib/kibana'); @@ -21,7 +22,7 @@ const wrapper: React.FC = ({ children }) => {children} { const abortCtrl = new AbortController(); - const features: SingleCaseMetricsFeature[] = ['alerts.count']; + const features: SingleCaseMetricsFeature[] = [CaseMetricsFeature.ALERTS_COUNT]; beforeEach(() => { jest.clearAllMocks(); diff --git a/x-pack/plugins/cases/public/containers/use_get_cases_metrics.test.tsx b/x-pack/plugins/cases/public/containers/use_get_cases_metrics.test.tsx index 31bb539fbea17..3f474f45c3fcb 100644 --- a/x-pack/plugins/cases/public/containers/use_get_cases_metrics.test.tsx +++ b/x-pack/plugins/cases/public/containers/use_get_cases_metrics.test.tsx @@ -12,6 +12,7 @@ import { createAppMockRenderer } from '../common/mock'; import { useGetCasesMetrics } from './use_get_cases_metrics'; import { SECURITY_SOLUTION_OWNER } from '../../common/constants'; import { useToasts } from '../common/lib/kibana'; +import { CaseMetricsFeature } from '../../common/api/metrics/case'; jest.mock('../api'); jest.mock('../common/lib/kibana'); @@ -41,7 +42,7 @@ describe('useGetCasesMetrics', () => { expect(spy).toHaveBeenCalledWith({ http: expect.anything(), signal: abortCtrl.signal, - query: { owner: [SECURITY_SOLUTION_OWNER], features: ['mttr'] }, + query: { owner: [SECURITY_SOLUTION_OWNER], features: [CaseMetricsFeature.MTTR] }, }); }); diff --git a/x-pack/plugins/cases/public/containers/use_get_cases_metrics.tsx b/x-pack/plugins/cases/public/containers/use_get_cases_metrics.tsx index 3e3f8421eac00..edd440cbd3241 100644 --- a/x-pack/plugins/cases/public/containers/use_get_cases_metrics.tsx +++ b/x-pack/plugins/cases/public/containers/use_get_cases_metrics.tsx @@ -6,6 +6,7 @@ */ import { useQuery } from '@tanstack/react-query'; +import { CaseMetricsFeature } from '../../common/api/metrics/case'; import { useCasesContext } from '../components/cases_context/use_cases_context'; import * as i18n from './translations'; import { useHttp } from '../common/lib/kibana'; @@ -25,7 +26,7 @@ export const useGetCasesMetrics = () => { ({ signal }) => getCasesMetrics({ http, - query: { owner, features: ['mttr'] }, + query: { owner, features: [CaseMetricsFeature.MTTR] }, signal, }), { diff --git a/x-pack/plugins/cases/server/client/metrics/actions/actions.test.ts b/x-pack/plugins/cases/server/client/metrics/actions/actions.test.ts index ab25e206c05cf..a496437b7f1ce 100644 --- a/x-pack/plugins/cases/server/client/metrics/actions/actions.test.ts +++ b/x-pack/plugins/cases/server/client/metrics/actions/actions.test.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { CaseMetricsFeature } from '../../../../common/api'; import { createCasesClientMock } from '../../mocks'; import type { CasesClientArgs } from '../../types'; import { loggingSystemMock } from '@kbn/core/server/mocks'; @@ -47,7 +48,7 @@ describe('Actions', () => { attachmentService.executeCaseActionsAggregations.mockResolvedValue(undefined); const handler = new Actions(constructorOptions); - handler.setupFeature('actions.isolateHost'); + handler.setupFeature(CaseMetricsFeature.ACTIONS_ISOLATE_HOST); expect(await handler.compute()).toEqual({ actions: { @@ -63,7 +64,7 @@ describe('Actions', () => { attachmentService.executeCaseActionsAggregations.mockResolvedValue({}); const handler = new Actions(constructorOptions); - handler.setupFeature('actions.isolateHost'); + handler.setupFeature(CaseMetricsFeature.ACTIONS_ISOLATE_HOST); expect(await handler.compute()).toEqual({ actions: { @@ -81,7 +82,7 @@ describe('Actions', () => { }); const handler = new Actions(constructorOptions); - handler.setupFeature('actions.isolateHost'); + handler.setupFeature(CaseMetricsFeature.ACTIONS_ISOLATE_HOST); expect(await handler.compute()).toEqual({ actions: { @@ -99,7 +100,7 @@ describe('Actions', () => { }); const handler = new Actions(constructorOptions); - handler.setupFeature('actions.isolateHost'); + handler.setupFeature(CaseMetricsFeature.ACTIONS_ISOLATE_HOST); expect(await handler.compute()).toEqual({ actions: { @@ -120,7 +121,7 @@ describe('Actions', () => { }); const handler = new Actions(constructorOptions); - handler.setupFeature('actions.isolateHost'); + handler.setupFeature(CaseMetricsFeature.ACTIONS_ISOLATE_HOST); expect(await handler.compute()).toEqual({ actions: { diff --git a/x-pack/plugins/cases/server/client/metrics/actions/actions.ts b/x-pack/plugins/cases/server/client/metrics/actions/actions.ts index f452e163e70f3..139951b41897b 100644 --- a/x-pack/plugins/cases/server/client/metrics/actions/actions.ts +++ b/x-pack/plugins/cases/server/client/metrics/actions/actions.ts @@ -7,6 +7,7 @@ import { merge } from 'lodash'; import type { SingleCaseMetricsResponse } from '../../../../common/api'; +import { CaseMetricsFeature } from '../../../../common/api'; import { Operations } from '../../../authorization'; import { createCaseError } from '../../../common/error'; import { SingleCaseAggregationHandler } from '../single_case_aggregation_handler'; @@ -18,7 +19,7 @@ export class Actions extends SingleCaseAggregationHandler { super( options, new Map>([ - ['actions.isolateHost', new IsolateHostActions()], + [CaseMetricsFeature.ACTIONS_ISOLATE_HOST, new IsolateHostActions()], ]) ); } diff --git a/x-pack/plugins/cases/server/client/metrics/aggregation_handler.ts b/x-pack/plugins/cases/server/client/metrics/aggregation_handler.ts index d3ea78d7ccf2f..8e8f4ef2d773a 100644 --- a/x-pack/plugins/cases/server/client/metrics/aggregation_handler.ts +++ b/x-pack/plugins/cases/server/client/metrics/aggregation_handler.ts @@ -6,6 +6,7 @@ */ import { merge } from 'lodash'; +import type { CaseMetricsFeature } from '../../../common/api'; import { BaseHandler } from './base_handler'; import type { AggregationBuilder, AggregationResponse, BaseHandlerCommonOptions } from './types'; @@ -19,11 +20,11 @@ export abstract class AggregationHandler extends BaseHandler { super(options); } - getFeatures(): Set { - return new Set(this.aggregations.keys()); + getFeatures(): Set { + return new Set(this.aggregations.keys() as unknown as CaseMetricsFeature[]); } - public setupFeature(feature: string) { + public setupFeature(feature: CaseMetricsFeature) { const aggregation = this.aggregations.get(feature); if (aggregation) { this.aggregationBuilders.push(aggregation); diff --git a/x-pack/plugins/cases/server/client/metrics/alerts/count.ts b/x-pack/plugins/cases/server/client/metrics/alerts/count.ts index 70344016910b7..c41d11e365271 100644 --- a/x-pack/plugins/cases/server/client/metrics/alerts/count.ts +++ b/x-pack/plugins/cases/server/client/metrics/alerts/count.ts @@ -6,6 +6,7 @@ */ import type { SingleCaseMetricsResponse } from '../../../../common/api'; +import { CaseMetricsFeature } from '../../../../common/api'; import { Operations } from '../../../authorization'; import { createCaseError } from '../../../common/error'; import { SingleCaseBaseHandler } from '../single_case_base_handler'; @@ -13,7 +14,7 @@ import type { SingleCaseBaseHandlerCommonOptions } from '../types'; export class AlertsCount extends SingleCaseBaseHandler { constructor(options: SingleCaseBaseHandlerCommonOptions) { - super(options, ['alerts.count']); + super(options, [CaseMetricsFeature.ALERTS_COUNT]); } public async compute(): Promise { diff --git a/x-pack/plugins/cases/server/client/metrics/alerts/details.test.ts b/x-pack/plugins/cases/server/client/metrics/alerts/details.test.ts index 23797b75992e4..5e5ba652317a1 100644 --- a/x-pack/plugins/cases/server/client/metrics/alerts/details.test.ts +++ b/x-pack/plugins/cases/server/client/metrics/alerts/details.test.ts @@ -13,6 +13,7 @@ import { loggingSystemMock } from '@kbn/core/server/mocks'; import { AlertDetails } from './details'; import { mockAlertsService } from '../test_utils/alerts'; import type { SingleCaseBaseHandlerCommonOptions } from '../types'; +import { CaseMetricsFeature } from '../../../../common/api/metrics/case'; describe('AlertDetails', () => { let client: CasesClientMock; @@ -53,7 +54,7 @@ describe('AlertDetails', () => { casesClient: client, clientArgs: { services: {} } as CasesClientArgs, }); - handler.setupFeature('alerts.hosts'); + handler.setupFeature(CaseMetricsFeature.ALERTS_HOSTS); expect(await handler.compute()).toEqual({ alerts: { @@ -69,7 +70,7 @@ describe('AlertDetails', () => { mockServices.services.alertsService.executeAggregations.mockImplementation(async () => ({})); const handler = new AlertDetails(constructorOptions); - handler.setupFeature('alerts.hosts'); + handler.setupFeature(CaseMetricsFeature.ALERTS_HOSTS); expect(await handler.compute()).toEqual({ alerts: { @@ -85,7 +86,7 @@ describe('AlertDetails', () => { mockServices.services.alertsService.executeAggregations.mockImplementation(async () => ({})); const handler = new AlertDetails(constructorOptions); - handler.setupFeature('alerts.users'); + handler.setupFeature(CaseMetricsFeature.ALERTS_USERS); expect(await handler.compute()).toEqual({ alerts: { @@ -101,7 +102,7 @@ describe('AlertDetails', () => { mockServices.services.alertsService.executeAggregations.mockImplementation(async () => ({})); const handler = new AlertDetails(constructorOptions); - handler.setupFeature('alerts.hosts'); + handler.setupFeature(CaseMetricsFeature.ALERTS_HOSTS); expect(await handler.compute()).toEqual({ alerts: { @@ -117,7 +118,7 @@ describe('AlertDetails', () => { mockServices.services.alertsService.executeAggregations.mockImplementation(async () => ({})); const handler = new AlertDetails(constructorOptions); - handler.setupFeature('alerts.users'); + handler.setupFeature(CaseMetricsFeature.ALERTS_USERS); expect(await handler.compute()).toEqual({ alerts: { @@ -159,7 +160,7 @@ describe('AlertDetails', () => { it('returns host details when the host feature is setup', async () => { const handler = new AlertDetails(constructorOptions); - handler.setupFeature('alerts.hosts'); + handler.setupFeature(CaseMetricsFeature.ALERTS_HOSTS); expect(await handler.compute()).toEqual({ alerts: { @@ -174,7 +175,7 @@ describe('AlertDetails', () => { it('returns user details when the user feature is setup', async () => { const handler = new AlertDetails(constructorOptions); - handler.setupFeature('alerts.users'); + handler.setupFeature(CaseMetricsFeature.ALERTS_USERS); expect(await handler.compute()).toEqual({ alerts: { @@ -189,8 +190,8 @@ describe('AlertDetails', () => { it('returns user and host details when the user and host features are setup', async () => { const handler = new AlertDetails(constructorOptions); - handler.setupFeature('alerts.users'); - handler.setupFeature('alerts.hosts'); + handler.setupFeature(CaseMetricsFeature.ALERTS_USERS); + handler.setupFeature(CaseMetricsFeature.ALERTS_HOSTS); expect(await handler.compute()).toEqual({ alerts: { diff --git a/x-pack/plugins/cases/server/client/metrics/alerts/details.ts b/x-pack/plugins/cases/server/client/metrics/alerts/details.ts index 75e96482ae9c0..d74dc4a0748ff 100644 --- a/x-pack/plugins/cases/server/client/metrics/alerts/details.ts +++ b/x-pack/plugins/cases/server/client/metrics/alerts/details.ts @@ -6,6 +6,7 @@ */ import type { SingleCaseMetricsResponse } from '../../../../common/api'; +import { CaseMetricsFeature } from '../../../../common/api'; import { createCaseError } from '../../../common/error'; import { SingleCaseAggregationHandler } from '../single_case_aggregation_handler'; @@ -17,8 +18,8 @@ export class AlertDetails extends SingleCaseAggregationHandler { super( options, new Map>([ - ['alerts.hosts', new AlertHosts()], - ['alerts.users', new AlertUsers()], + [CaseMetricsFeature.ALERTS_HOSTS, new AlertHosts()], + [CaseMetricsFeature.ALERTS_USERS, new AlertUsers()], ]) ); } diff --git a/x-pack/plugins/cases/server/client/metrics/all_cases/mttr.test.ts b/x-pack/plugins/cases/server/client/metrics/all_cases/mttr.test.ts index 9b2bbbf3ff412..64b0dc5d1dc18 100644 --- a/x-pack/plugins/cases/server/client/metrics/all_cases/mttr.test.ts +++ b/x-pack/plugins/cases/server/client/metrics/all_cases/mttr.test.ts @@ -6,6 +6,7 @@ */ import type { Case } from '../../../../common/types/domain'; +import { CaseMetricsFeature } from '../../../../common/api'; import { createCasesClientMock } from '../../mocks'; import type { CasesClientArgs } from '../../types'; import { loggingSystemMock } from '@kbn/core/server/mocks'; @@ -48,7 +49,7 @@ describe('MTTR', () => { it('returns null when aggregation returns undefined', async () => { caseService.executeAggregations.mockResolvedValue(undefined); const handler = new MTTR(constructorOptions); - handler.setupFeature('mttr'); + handler.setupFeature(CaseMetricsFeature.MTTR); expect(await handler.compute()).toEqual({ mttr: null }); }); @@ -56,7 +57,7 @@ describe('MTTR', () => { it('returns null when aggregation returns empty object', async () => { caseService.executeAggregations.mockResolvedValue({}); const handler = new MTTR(constructorOptions); - handler.setupFeature('mttr'); + handler.setupFeature(CaseMetricsFeature.MTTR); expect(await handler.compute()).toEqual({ mttr: null }); }); @@ -64,7 +65,7 @@ describe('MTTR', () => { it('returns null when aggregation returns empty mttr object', async () => { caseService.executeAggregations.mockResolvedValue({ mttr: {} }); const handler = new MTTR(constructorOptions); - handler.setupFeature('mttr'); + handler.setupFeature(CaseMetricsFeature.MTTR); expect(await handler.compute()).toEqual({ mttr: null }); }); @@ -72,7 +73,7 @@ describe('MTTR', () => { it('returns values when there is a mttr value', async () => { caseService.executeAggregations.mockResolvedValue({ mttr: { value: 5 } }); const handler = new MTTR(constructorOptions); - handler.setupFeature('mttr'); + handler.setupFeature(CaseMetricsFeature.MTTR); expect(await handler.compute()).toEqual({ mttr: 5 }); }); @@ -86,7 +87,7 @@ describe('MTTR', () => { owner: 'cases', }); - handler.setupFeature('mttr'); + handler.setupFeature(CaseMetricsFeature.MTTR); await handler.compute(); expect(caseService.executeAggregations.mock.calls[0][0]).toMatchInlineSnapshot(` diff --git a/x-pack/plugins/cases/server/client/metrics/all_cases_base_handler.ts b/x-pack/plugins/cases/server/client/metrics/all_cases_base_handler.ts index d4303d8c41d8c..4c27f54abf86a 100644 --- a/x-pack/plugins/cases/server/client/metrics/all_cases_base_handler.ts +++ b/x-pack/plugins/cases/server/client/metrics/all_cases_base_handler.ts @@ -5,14 +5,14 @@ * 2.0. */ -import type { CasesMetricsResponse } from '../../../common/api'; +import type { CaseMetricsFeature, CasesMetricsResponse } from '../../../common/api'; import { BaseHandler } from './base_handler'; import type { AllCasesBaseHandlerCommonOptions } from './types'; export abstract class AllCasesBaseHandler extends BaseHandler { protected readonly owner?: string | string[]; - constructor(options: AllCasesBaseHandlerCommonOptions, features?: string[]) { + constructor(options: AllCasesBaseHandlerCommonOptions, features?: CaseMetricsFeature[]) { const { owner, ...restOptions } = options; super(restOptions, features); diff --git a/x-pack/plugins/cases/server/client/metrics/base_handler.ts b/x-pack/plugins/cases/server/client/metrics/base_handler.ts index 92117fb6f34cf..0ff8c5e7bde8b 100644 --- a/x-pack/plugins/cases/server/client/metrics/base_handler.ts +++ b/x-pack/plugins/cases/server/client/metrics/base_handler.ts @@ -5,15 +5,16 @@ * 2.0. */ +import type { CasesMetricsFeatureField } from '../../../common/api/metrics/case'; import type { BaseHandlerCommonOptions, MetricsHandler } from './types'; export abstract class BaseHandler implements MetricsHandler { constructor( protected readonly options: BaseHandlerCommonOptions, - private readonly features?: string[] + private readonly features?: CasesMetricsFeatureField[] ) {} - getFeatures(): Set { + getFeatures(): Set { return new Set(this.features); } diff --git a/x-pack/plugins/cases/server/client/metrics/connectors.ts b/x-pack/plugins/cases/server/client/metrics/connectors.ts index 5248efb30939e..39e974a9fe81d 100644 --- a/x-pack/plugins/cases/server/client/metrics/connectors.ts +++ b/x-pack/plugins/cases/server/client/metrics/connectors.ts @@ -6,6 +6,7 @@ */ import type { SingleCaseMetricsResponse } from '../../../common/api'; +import { CaseMetricsFeature } from '../../../common/api'; import { Operations } from '../../authorization'; import { createCaseError } from '../../common/error'; import { SingleCaseBaseHandler } from './single_case_base_handler'; @@ -13,7 +14,7 @@ import type { SingleCaseBaseHandlerCommonOptions } from './types'; export class Connectors extends SingleCaseBaseHandler { constructor(options: SingleCaseBaseHandlerCommonOptions) { - super(options, ['connectors']); + super(options, [CaseMetricsFeature.CONNECTORS]); } public async compute(): Promise { diff --git a/x-pack/plugins/cases/server/client/metrics/get_case_metrics.test.ts b/x-pack/plugins/cases/server/client/metrics/get_case_metrics.test.ts index 0148053d56595..7b317ceb86af2 100644 --- a/x-pack/plugins/cases/server/client/metrics/get_case_metrics.test.ts +++ b/x-pack/plugins/cases/server/client/metrics/get_case_metrics.test.ts @@ -8,8 +8,7 @@ import { loggingSystemMock, savedObjectsClientMock } from '@kbn/core/server/mocks'; import { getCaseMetrics } from './get_case_metrics'; -import type { Case } from '../../../common/types/domain'; -import { CaseStatuses } from '../../../common/types/domain'; +import { CaseMetricsFeature } from '../../../common/api'; import type { CasesClientMock } from '../mocks'; import { createCasesClientMock } from '../mocks'; import type { CasesClientArgs } from '../types'; @@ -22,6 +21,8 @@ import { import { mockAlertsService } from './test_utils/alerts'; import { createStatusChangeSavedObject } from './test_utils/lifespan'; import type { CaseSavedObjectTransformed } from '../../common/types/case'; +import { CaseStatuses } from '@kbn/cases-components'; +import type { Case } from '../../../common'; describe('getCaseMetrics', () => { const inProgressStatusChangeTimestamp = new Date('2021-11-23T20:00:43Z'); @@ -55,7 +56,7 @@ describe('getCaseMetrics', () => { it('returns the lifespan metrics', async () => { const metrics = await getCaseMetrics( - { caseId: '', features: ['lifespan'] }, + { caseId: '', features: [CaseMetricsFeature.LIFESPAN] }, client, clientArgs ); @@ -75,7 +76,7 @@ describe('getCaseMetrics', () => { it('populates the alerts.hosts and alerts.users sections', async () => { const metrics = await getCaseMetrics( - { caseId: '', features: ['alerts.hosts', 'alerts.users'] }, + { caseId: '', features: [CaseMetricsFeature.ALERTS_HOSTS, CaseMetricsFeature.ALERTS_USERS] }, client, clientArgs ); @@ -89,7 +90,7 @@ describe('getCaseMetrics', () => { it('populates multiple sections at a time', async () => { const metrics = await getCaseMetrics( - { caseId: '', features: ['alerts.count', 'lifespan'] }, + { caseId: '', features: [CaseMetricsFeature.ALERTS_COUNT, CaseMetricsFeature.LIFESPAN] }, client, clientArgs ); @@ -108,7 +109,7 @@ describe('getCaseMetrics', () => { it('populates multiple alerts sections at a time', async () => { const metrics = await getCaseMetrics( - { caseId: '', features: ['alerts.count', 'alerts.hosts'] }, + { caseId: '', features: [CaseMetricsFeature.ALERTS_COUNT, CaseMetricsFeature.ALERTS_HOSTS] }, client, clientArgs ); @@ -124,6 +125,7 @@ describe('getCaseMetrics', () => { expect.assertions(1); await expect( + // @ts-expect-error: testing invalid features getCaseMetrics({ caseId: '', features: ['bananas'] }, client, clientArgs) ).rejects.toThrow(); }); @@ -133,13 +135,17 @@ describe('getCaseMetrics', () => { try { await getCaseMetrics( - { caseId: '1', features: ['bananas', 'lifespan', 'alerts.count'] }, + { + caseId: '1', + // @ts-expect-error: testing invalid features + features: ['bananas', CaseMetricsFeature.LIFESPAN, CaseMetricsFeature.ALERTS_COUNT], + }, client, clientArgs ); } catch (error) { expect(error.message).toMatchInlineSnapshot( - `"Failed to retrieve metrics within client for case id: 1: Error: invalid features: [bananas], please only provide valid features: [actions.isolateHost, alerts.count, alerts.hosts, alerts.users, connectors, lifespan]"` + `"Failed to retrieve metrics within client for case id: 1: Error: Invalid value \\"bananas\\" supplied to \\"features\\""` ); } }); @@ -148,7 +154,7 @@ describe('getCaseMetrics', () => { expect.assertions(1); await getCaseMetrics( - { caseId: '', features: ['alerts.users', 'alerts.hosts'] }, + { caseId: '', features: [CaseMetricsFeature.ALERTS_USERS, CaseMetricsFeature.ALERTS_HOSTS] }, client, clientArgs ); diff --git a/x-pack/plugins/cases/server/client/metrics/get_cases_metrics.test.ts b/x-pack/plugins/cases/server/client/metrics/get_cases_metrics.test.ts index 0aca8ad914144..d52bfb9486e2a 100644 --- a/x-pack/plugins/cases/server/client/metrics/get_cases_metrics.test.ts +++ b/x-pack/plugins/cases/server/client/metrics/get_cases_metrics.test.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { CaseMetricsFeature } from '../../../common/api'; import type { CasesClientMock } from '../mocks'; import { getCasesMetrics } from './get_cases_metrics'; import { createMockClientArgs, createMockClient } from './test_utils/client'; @@ -35,19 +36,23 @@ describe('getCasesMetrics', () => { it('throws with excess fields', async () => { await expect( // @ts-expect-error: excess attribute - getCasesMetrics({ features: ['mttr'], foo: 'bar' }, client, clientArgs) + getCasesMetrics({ features: [CaseMetricsFeature.MTTR], foo: 'bar' }, client, clientArgs) ).rejects.toThrow('invalid keys "foo"'); }); it('returns the mttr metric', async () => { - const metrics = await getCasesMetrics({ features: ['mttr'] }, client, clientArgs); + const metrics = await getCasesMetrics( + { features: [CaseMetricsFeature.MTTR] }, + client, + clientArgs + ); expect(metrics).toEqual({ mttr: 5 }); }); it('calls the executeAggregations correctly', async () => { await getCasesMetrics( { - features: ['mttr'], + features: [CaseMetricsFeature.MTTR], from: '2022-04-28T15:18:00.000Z', to: '2022-04-28T15:22:00.000Z', owner: 'cases', @@ -129,4 +134,19 @@ describe('getCasesMetrics', () => { `); }); }); + + describe('validation', () => { + beforeEach(() => { + mockServices.services.caseService.executeAggregations.mockResolvedValue({ + mttr: { value: 5 }, + }); + }); + + it('throws with unknown feature value', async () => { + // @ts-expect-error: invalid feature value + await expect(getCasesMetrics({ features: ['foobar'] }, client, clientArgs)).rejects.toThrow( + 'Invalid value "foobar" supplied to "features"' + ); + }); + }); }); diff --git a/x-pack/plugins/cases/server/client/metrics/lifespan.ts b/x-pack/plugins/cases/server/client/metrics/lifespan.ts index 17f02cc089b6b..0167e12432aee 100644 --- a/x-pack/plugins/cases/server/client/metrics/lifespan.ts +++ b/x-pack/plugins/cases/server/client/metrics/lifespan.ts @@ -10,6 +10,7 @@ import type { StatusUserAction, UserActionAttributes } from '../../../common/typ import type { UserActionWithResponse } from '../../../common/types/api'; import { StatusUserActionRt, CaseStatuses } from '../../../common/types/domain'; import type { SingleCaseMetricsResponse, StatusInfo } from '../../../common/api'; +import { CaseMetricsFeature } from '../../../common/api'; import { Operations } from '../../authorization'; import { createCaseError } from '../../common/error'; import { SingleCaseBaseHandler } from './single_case_base_handler'; @@ -17,7 +18,7 @@ import type { SingleCaseBaseHandlerCommonOptions } from './types'; export class Lifespan extends SingleCaseBaseHandler { constructor(options: SingleCaseBaseHandlerCommonOptions) { - super(options, ['lifespan']); + super(options, [CaseMetricsFeature.LIFESPAN]); } public async compute(): Promise { diff --git a/x-pack/plugins/cases/server/client/metrics/single_case_base_handler.ts b/x-pack/plugins/cases/server/client/metrics/single_case_base_handler.ts index 054263739dd83..76b438c8874aa 100644 --- a/x-pack/plugins/cases/server/client/metrics/single_case_base_handler.ts +++ b/x-pack/plugins/cases/server/client/metrics/single_case_base_handler.ts @@ -5,14 +5,14 @@ * 2.0. */ -import type { SingleCaseMetricsResponse } from '../../../common/api'; +import type { CaseMetricsFeature, SingleCaseMetricsResponse } from '../../../common/api'; import { BaseHandler } from './base_handler'; import type { SingleCaseBaseHandlerCommonOptions } from './types'; export abstract class SingleCaseBaseHandler extends BaseHandler { protected readonly caseId: string; - constructor(options: SingleCaseBaseHandlerCommonOptions, features?: string[]) { + constructor(options: SingleCaseBaseHandlerCommonOptions, features?: CaseMetricsFeature[]) { const { caseId, ...restOptions } = options; super(restOptions, features); diff --git a/x-pack/plugins/cases/server/client/metrics/types.ts b/x-pack/plugins/cases/server/client/metrics/types.ts index a28b3294cccfb..d2fd89786ed72 100644 --- a/x-pack/plugins/cases/server/client/metrics/types.ts +++ b/x-pack/plugins/cases/server/client/metrics/types.ts @@ -6,13 +6,17 @@ */ import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import type { + CasesMetricsFeatureField, + SingleCaseMetricsFeatureField, +} from '../../../common/api/metrics/case'; import type { CasesClient } from '../client'; import type { CasesClientArgs } from '../types'; export interface MetricsHandler { - getFeatures(): Set; + getFeatures(): Set; compute(): Promise; - setupFeature?(feature: string): void; + setupFeature?(feature: CasesMetricsFeatureField): void; } export interface AggregationBuilder { @@ -40,5 +44,5 @@ export interface AllCasesBaseHandlerCommonOptions extends BaseHandlerCommonOptio export interface GetCaseMetricsParams { caseId: string; - features: string[]; + features: SingleCaseMetricsFeatureField[]; } diff --git a/x-pack/plugins/cases/server/client/metrics/utils.test.ts b/x-pack/plugins/cases/server/client/metrics/utils.test.ts index d376ed56dc232..168dcb6db1f8d 100644 --- a/x-pack/plugins/cases/server/client/metrics/utils.test.ts +++ b/x-pack/plugins/cases/server/client/metrics/utils.test.ts @@ -5,6 +5,8 @@ * 2.0. */ +import type { CasesMetricsFeatureField } from '../../../common/api/metrics/case'; +import { CaseMetricsFeature } from '../../../common/api/metrics/case'; import { createMockClient, createMockClientArgs } from './test_utils/client'; import { buildHandlers } from './utils'; @@ -12,16 +14,16 @@ describe('utils', () => { describe('buildHandlers', () => { const casesClient = createMockClient(); const clientArgs = createMockClientArgs(); - const SINGLE_CASE_FEATURES = [ - 'alerts.count', - 'alerts.users', - 'alerts.hosts', - 'actions.isolateHost', - 'connectors', - 'lifespan', + const SINGLE_CASE_FEATURES: CasesMetricsFeatureField[] = [ + CaseMetricsFeature.ALERTS_COUNT, + CaseMetricsFeature.ALERTS_HOSTS, + CaseMetricsFeature.ALERTS_USERS, + CaseMetricsFeature.ACTIONS_ISOLATE_HOST, + CaseMetricsFeature.CONNECTORS, + CaseMetricsFeature.LIFESPAN, ]; - const CASES_FEATURES = ['mttr']; + const CASES_FEATURES: CasesMetricsFeatureField[] = [CaseMetricsFeature.MTTR]; it('returns the correct single case handlers', async () => { const handlers = buildHandlers( @@ -86,6 +88,7 @@ describe('utils', () => { buildHandlers( { ...opts, + // @ts-expect-error features: ['not-exists'], }, casesClient, @@ -98,7 +101,7 @@ describe('utils', () => { const handlers = buildHandlers( { caseId: 'test-case-id', - features: ['alerts.count'], + features: [CaseMetricsFeature.ALERTS_COUNT], }, casesClient, clientArgs.clientArgs @@ -107,14 +110,14 @@ describe('utils', () => { const handler = Array.from(handlers)[0]; // @ts-expect-error expect(handler.caseId).toBe('test-case-id'); - expect(Array.from(handler.getFeatures().values())).toEqual(['alerts.count']); + expect(Array.from(handler.getFeatures().values())).toEqual([CaseMetricsFeature.ALERTS_COUNT]); }); it('set up the feature correctly', async () => { const handlers = buildHandlers( { caseId: 'test-case-id', - features: ['alerts.hosts'], + features: [CaseMetricsFeature.ALERTS_HOSTS], }, casesClient, clientArgs.clientArgs diff --git a/x-pack/plugins/cases/server/routes/api/get_external_routes.ts b/x-pack/plugins/cases/server/routes/api/get_external_routes.ts index 9644046db5799..bd990deefbdfa 100644 --- a/x-pack/plugins/cases/server/routes/api/get_external_routes.ts +++ b/x-pack/plugins/cases/server/routes/api/get_external_routes.ts @@ -29,8 +29,6 @@ import { getConnectorsRoute } from './configure/get_connectors'; import { patchCaseConfigureRoute } from './configure/patch_configure'; import { postCaseConfigureRoute } from './configure/post_configure'; import { getAllAlertsAttachedToCaseRoute } from './comments/get_alerts'; -import { getCaseMetricRoute } from './metrics/get_case_metrics'; -import { getCasesMetricRoute } from './metrics/get_cases_metrics'; import { findUserActionsRoute } from './user_actions/find_user_actions'; export const getExternalRoutes = () => @@ -60,6 +58,4 @@ export const getExternalRoutes = () => patchCaseConfigureRoute, postCaseConfigureRoute, getAllAlertsAttachedToCaseRoute, - getCaseMetricRoute, - getCasesMetricRoute, ] as CaseRoute[]; diff --git a/x-pack/plugins/cases/server/routes/api/get_internal_routes.ts b/x-pack/plugins/cases/server/routes/api/get_internal_routes.ts index a75836ffe2f72..a28d254161d07 100644 --- a/x-pack/plugins/cases/server/routes/api/get_internal_routes.ts +++ b/x-pack/plugins/cases/server/routes/api/get_internal_routes.ts @@ -16,6 +16,8 @@ import { bulkGetAttachmentsRoute } from './internal/bulk_get_attachments'; import { getCaseUsersRoute } from './internal/get_case_users'; import { bulkDeleteFileAttachments } from './internal/bulk_delete_file_attachments'; import { getCategoriesRoute } from './cases/categories/get_categories'; +import { getCaseMetricRoute } from './internal/get_case_metrics'; +import { getCasesMetricRoute } from './internal/get_cases_metrics'; export const getInternalRoutes = (userProfileService: UserProfileService) => [ @@ -28,4 +30,6 @@ export const getInternalRoutes = (userProfileService: UserProfileService) => getCaseUsersRoute, bulkDeleteFileAttachments, getCategoriesRoute, + getCaseMetricRoute, + getCasesMetricRoute, ] as CaseRoute[]; diff --git a/x-pack/plugins/cases/server/routes/api/metrics/get_case_metrics.ts b/x-pack/plugins/cases/server/routes/api/internal/get_case_metrics.ts similarity index 77% rename from x-pack/plugins/cases/server/routes/api/metrics/get_case_metrics.ts rename to x-pack/plugins/cases/server/routes/api/internal/get_case_metrics.ts index 394c0099b4991..95990554290c7 100644 --- a/x-pack/plugins/cases/server/routes/api/metrics/get_case_metrics.ts +++ b/x-pack/plugins/cases/server/routes/api/internal/get_case_metrics.ts @@ -6,14 +6,15 @@ */ import { schema } from '@kbn/config-schema'; +import type { SingleCaseMetricsFeatureField } from '../../../../common/api'; -import { CASE_METRICS_DETAILS_URL } from '../../../../common/constants'; +import { INTERNAL_CASE_METRICS_DETAILS_URL } from '../../../../common/constants'; import { createCaseError } from '../../../common/error'; import { createCasesRoute } from '../create_cases_route'; export const getCaseMetricRoute = createCasesRoute({ method: 'get', - path: CASE_METRICS_DETAILS_URL, + path: INTERNAL_CASE_METRICS_DETAILS_URL, params: { params: schema.object({ case_id: schema.string({ minLength: 1 }), @@ -34,7 +35,9 @@ export const getCaseMetricRoute = createCasesRoute({ return response.ok({ body: await client.metrics.getCaseMetrics({ caseId: request.params.case_id, - features: Array.isArray(features) ? features : [features], + features: Array.isArray(features) + ? (features as SingleCaseMetricsFeatureField[]) + : [features as SingleCaseMetricsFeatureField], }), }); } catch (error) { diff --git a/x-pack/plugins/cases/server/routes/api/metrics/get_cases_metrics.ts b/x-pack/plugins/cases/server/routes/api/internal/get_cases_metrics.ts similarity index 79% rename from x-pack/plugins/cases/server/routes/api/metrics/get_cases_metrics.ts rename to x-pack/plugins/cases/server/routes/api/internal/get_cases_metrics.ts index 44d351571edbb..ba08acaca7ce7 100644 --- a/x-pack/plugins/cases/server/routes/api/metrics/get_cases_metrics.ts +++ b/x-pack/plugins/cases/server/routes/api/internal/get_cases_metrics.ts @@ -7,13 +7,14 @@ import { schema } from '@kbn/config-schema'; -import { CASE_METRICS_URL } from '../../../../common/constants'; +import { INTERNAL_CASE_METRICS_URL } from '../../../../common/constants'; +import type { CasesMetricsFeatureField } from '../../../../common/api/metrics/case'; import { createCaseError } from '../../../common/error'; import { createCasesRoute } from '../create_cases_route'; export const getCasesMetricRoute = createCasesRoute({ method: 'get', - path: CASE_METRICS_URL, + path: INTERNAL_CASE_METRICS_URL, params: { query: schema.object({ features: schema.oneOf([ @@ -34,7 +35,9 @@ export const getCasesMetricRoute = createCasesRoute({ return response.ok({ body: await client.metrics.getCasesMetrics({ ...request.query, - features: Array.isArray(features) ? features : [features], + features: Array.isArray(features) + ? (features as CasesMetricsFeatureField[]) + : [features as CasesMetricsFeatureField], }), }); } catch (error) { diff --git a/x-pack/plugins/security_solution/cypress/screens/case_details.ts b/x-pack/plugins/security_solution/cypress/screens/case_details.ts index 1b4a6cab6524a..1cf4af85f401e 100644 --- a/x-pack/plugins/security_solution/cypress/screens/case_details.ts +++ b/x-pack/plugins/security_solution/cypress/screens/case_details.ts @@ -5,6 +5,8 @@ * 2.0. */ +import { CaseMetricsFeature } from '@kbn/cases-plugin/common/api'; + export const CASE_ACTIONS = '[data-test-subj="property-actions-ellipses"]'; export const CASE_CONNECTOR = '[data-test-subj="connector-fields"] .euiCard__title'; @@ -50,8 +52,13 @@ export const PARTICIPANTS = 1; export const REPORTER = 0; -export const EXPECTED_METRICS = ['alerts.count', 'alerts.users', 'alerts.hosts', 'connectors']; -export const UNEXPECTED_METRICS = ['actions.isolateHost']; +export const EXPECTED_METRICS = [ + CaseMetricsFeature.ALERTS_COUNT, + CaseMetricsFeature.ALERTS_USERS, + CaseMetricsFeature.ALERTS_HOSTS, + CaseMetricsFeature.CONNECTORS, +]; +export const UNEXPECTED_METRICS = [CaseMetricsFeature.ACTIONS_ISOLATE_HOST]; export const CASES_METRIC = (metric: string) => { return `[data-test-subj="case-metrics-totals-${metric}"]`; diff --git a/x-pack/plugins/security_solution/public/cases/pages/index.tsx b/x-pack/plugins/security_solution/public/cases/pages/index.tsx index 6485a395df95a..93b55e5a460c3 100644 --- a/x-pack/plugins/security_solution/public/cases/pages/index.tsx +++ b/x-pack/plugins/security_solution/public/cases/pages/index.tsx @@ -8,6 +8,7 @@ import React, { useCallback, useEffect, useMemo, useRef } from 'react'; import { useDispatch } from 'react-redux'; import type { CaseViewRefreshPropInterface } from '@kbn/cases-plugin/common'; +import { CaseMetricsFeature } from '@kbn/cases-plugin/common'; import { useTourContext } from '../../common/components/guided_onboarding_tour'; import { AlertsCasesTourSteps, @@ -114,7 +115,13 @@ const CaseContainerComponent: React.FC = () => { basePath: CASES_PATH, owner: [APP_ID], features: { - metrics: ['alerts.count', 'alerts.users', 'alerts.hosts', 'connectors', 'lifespan'], + metrics: [ + CaseMetricsFeature.ALERTS_COUNT, + CaseMetricsFeature.ALERTS_USERS, + CaseMetricsFeature.ALERTS_HOSTS, + CaseMetricsFeature.CONNECTORS, + CaseMetricsFeature.LIFESPAN, + ], alerts: { isExperimental: false }, }, refreshRef, diff --git a/x-pack/plugins/security_solution/public/overview/components/detection_response/soc_trends/hooks/use_cases_mttr.tsx b/x-pack/plugins/security_solution/public/overview/components/detection_response/soc_trends/hooks/use_cases_mttr.tsx index 7b52bc11e58c4..ace17a91b0f13 100644 --- a/x-pack/plugins/security_solution/public/overview/components/detection_response/soc_trends/hooks/use_cases_mttr.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/detection_response/soc_trends/hooks/use_cases_mttr.tsx @@ -7,6 +7,7 @@ import { useEffect, useMemo, useReducer } from 'react'; import { v4 as uuidv4 } from 'uuid'; +import { CaseMetricsFeature } from '@kbn/cases-plugin/common'; import { statReducer } from './stat_reducer'; import type { GlobalTimeArgs } from '../../../../../common/containers/use_global_time'; import { useKibana } from '../../../../../common/lib/kibana'; @@ -63,7 +64,7 @@ export const useCasesMttr = ({ from, to, owner: APP_ID, - features: ['mttr'], + features: [CaseMetricsFeature.MTTR], }, abortCtrl.signal ), @@ -73,7 +74,7 @@ export const useCasesMttr = ({ from: fromCompare, to: toCompare, owner: APP_ID, - features: ['mttr'], + features: [CaseMetricsFeature.MTTR], }, abortCtrl.signal ), diff --git a/x-pack/test/cases_api_integration/common/lib/api/index.ts b/x-pack/test/cases_api_integration/common/lib/api/index.ts index 007efff840ba1..f9bbe17a00924 100644 --- a/x-pack/test/cases_api_integration/common/lib/api/index.ts +++ b/x-pack/test/cases_api_integration/common/lib/api/index.ts @@ -24,9 +24,14 @@ import { CASE_STATUS_URL, CASE_TAGS_URL, CASE_USER_ACTION_SAVED_OBJECT, + INTERNAL_CASE_METRICS_URL, INTERNAL_GET_CASE_CATEGORIES_URL, } from '@kbn/cases-plugin/common/constants'; -import { SingleCaseMetricsResponse, CasesMetricsResponse } from '@kbn/cases-plugin/common/api'; +import { + SingleCaseMetricsResponse, + CasesMetricsResponse, + CaseMetricsFeature, +} from '@kbn/cases-plugin/common/api'; import { SignalHit } from '@kbn/security-solution-plugin/server/lib/detection_engine/rule_types/types'; import { ActionResult } from '@kbn/actions-plugin/server/types'; import { CasePersistedAttributes } from '@kbn/cases-plugin/server/common/types/case'; @@ -537,12 +542,12 @@ export const getCaseMetrics = async ({ }: { supertest: SuperTest.SuperTest; caseId: string; - features: string[] | string; + features: CaseMetricsFeature[] | CaseMetricsFeature; expectedHttpCode?: number; auth?: { user: User; space: string | null }; }): Promise => { const { body: metricsResponse } = await supertest - .get(`${getSpaceUrlPrefix(auth?.space)}${CASES_URL}/metrics/${caseId}`) + .get(`${getSpaceUrlPrefix(auth?.space)}${INTERNAL_CASE_METRICS_URL}/${caseId}`) .query({ features }) .auth(auth.user.username, auth.user.password) .expect(expectedHttpCode); @@ -779,7 +784,7 @@ export const getCasesMetrics = async ({ auth?: { user: User; space: string | null }; }): Promise => { const { body: metricsResponse } = await supertest - .get(`${getSpaceUrlPrefix(auth?.space)}${CASES_URL}/metrics`) + .get(`${getSpaceUrlPrefix(auth?.space)}${INTERNAL_CASE_METRICS_URL}`) .query({ features, ...query }) .auth(auth.user.username, auth.user.password) .expect(expectedHttpCode); diff --git a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/index.ts b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/index.ts index 02f8e8d57ba57..72d6c093f4bfb 100644 --- a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/index.ts +++ b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/index.ts @@ -37,11 +37,11 @@ export default ({ loadTestFile }: FtrProviderContext): void => { loadTestFile(require.resolve('./configure/get_configure')); loadTestFile(require.resolve('./configure/patch_configure')); loadTestFile(require.resolve('./configure/post_configure')); - loadTestFile(require.resolve('./metrics/get_case_metrics')); - loadTestFile(require.resolve('./metrics/get_case_metrics_alerts')); - loadTestFile(require.resolve('./metrics/get_case_metrics_actions')); - loadTestFile(require.resolve('./metrics/get_case_metrics_connectors')); - loadTestFile(require.resolve('./metrics/get_cases_metrics')); + loadTestFile(require.resolve('./internal/metrics/get_case_metrics')); + loadTestFile(require.resolve('./internal/metrics/get_case_metrics_alerts')); + loadTestFile(require.resolve('./internal/metrics/get_case_metrics_actions')); + loadTestFile(require.resolve('./internal/metrics/get_case_metrics_connectors')); + loadTestFile(require.resolve('./internal/metrics/get_cases_metrics')); /** * Internal routes diff --git a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/metrics/get_case_metrics.ts b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/internal/metrics/get_case_metrics.ts similarity index 81% rename from x-pack/test/cases_api_integration/security_and_spaces/tests/common/metrics/get_case_metrics.ts rename to x-pack/test/cases_api_integration/security_and_spaces/tests/common/internal/metrics/get_case_metrics.ts index 1bafcfc84eb7b..9b3a81c99e31f 100644 --- a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/metrics/get_case_metrics.ts +++ b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/internal/metrics/get_case_metrics.ts @@ -5,10 +5,11 @@ * 2.0. */ +import { CaseMetricsFeature } from '@kbn/cases-plugin/common/api/metrics/case'; import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../../../common/ftr_provider_context'; -import { createCase, deleteAllCaseItems, getCaseMetrics } from '../../../../common/lib/api'; +import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; +import { createCase, deleteAllCaseItems, getCaseMetrics } from '../../../../../common/lib/api'; import { secOnly, obsOnly, @@ -19,8 +20,8 @@ import { obsSecRead, noKibanaPrivileges, obsSec, -} from '../../../../common/lib/authentication/users'; -import { getPostCaseRequest } from '../../../../common/lib/mock'; +} from '../../../../../common/lib/authentication/users'; +import { getPostCaseRequest } from '../../../../../common/lib/mock'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { @@ -36,7 +37,7 @@ export default ({ getService }: FtrProviderContext): void => { const metrics = await getCaseMetrics({ supertest, caseId: newCase.id, - features: 'connectors', + features: CaseMetricsFeature.CONNECTORS, }); expect(metrics).to.eql({ @@ -68,7 +69,7 @@ export default ({ getService }: FtrProviderContext): void => { const metrics = await getCaseMetrics({ supertest, caseId: closedCaseId, - features: ['lifespan'], + features: [CaseMetricsFeature.LIFESPAN], }); expect(metrics.lifespan?.creationDate).to.be('2021-06-17T18:57:41.682Z'); @@ -79,12 +80,13 @@ export default ({ getService }: FtrProviderContext): void => { const errorResponse = (await getCaseMetrics({ supertest, caseId: closedCaseId, + // @ts-expect-error: testing invalid feature features: ['bananas'], expectedHttpCode: 400, // casting here because we're expecting an error with a message field })) as unknown as { message: string }; - expect(errorResponse.message).to.contain('invalid features'); + expect(errorResponse.message).to.contain('Invalid value "bananas" supplied to "features"'); }); }); @@ -110,7 +112,7 @@ export default ({ getService }: FtrProviderContext): void => { const metrics = await getCaseMetrics({ supertest, caseId, - features: ['lifespan'], + features: [CaseMetricsFeature.LIFESPAN], }); expect(metrics).to.eql({ @@ -148,12 +150,12 @@ export default ({ getService }: FtrProviderContext): void => { supertest: supertestWithoutAuth, caseId: newCase.id, features: [ - 'lifespan', - 'alerts.hosts', - 'alerts.users', - 'alerts.count', - 'connectors', - 'actions.isolateHost', + CaseMetricsFeature.LIFESPAN, + CaseMetricsFeature.ALERTS_COUNT, + CaseMetricsFeature.ALERTS_HOSTS, + CaseMetricsFeature.ALERTS_USERS, + CaseMetricsFeature.CONNECTORS, + CaseMetricsFeature.ACTIONS_ISOLATE_HOST, ], auth: { user, space: 'space1' }, }); @@ -189,12 +191,12 @@ export default ({ getService }: FtrProviderContext): void => { supertest: supertestWithoutAuth, caseId: newCase.id, features: [ - 'lifespan', - 'alerts.hosts', - 'alerts.users', - 'alerts.count', - 'connectors', - 'actions.isolateHost', + CaseMetricsFeature.LIFESPAN, + CaseMetricsFeature.ALERTS_COUNT, + CaseMetricsFeature.ALERTS_HOSTS, + CaseMetricsFeature.ALERTS_USERS, + CaseMetricsFeature.CONNECTORS, + CaseMetricsFeature.ACTIONS_ISOLATE_HOST, ], expectedHttpCode: 403, auth: { user, space: 'space1' }, @@ -217,12 +219,12 @@ export default ({ getService }: FtrProviderContext): void => { supertest: supertestWithoutAuth, caseId: newCase.id, features: [ - 'lifespan', - 'alerts.hosts', - 'alerts.users', - 'alerts.count', - 'connectors', - 'actions.isolateHost', + CaseMetricsFeature.LIFESPAN, + CaseMetricsFeature.ALERTS_COUNT, + CaseMetricsFeature.ALERTS_HOSTS, + CaseMetricsFeature.ALERTS_USERS, + CaseMetricsFeature.CONNECTORS, + CaseMetricsFeature.ACTIONS_ISOLATE_HOST, ], expectedHttpCode: 403, auth: { user: secOnly, space: 'space2' }, diff --git a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/metrics/get_case_metrics_actions.ts b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/internal/metrics/get_case_metrics_actions.ts similarity index 88% rename from x-pack/test/cases_api_integration/security_and_spaces/tests/common/metrics/get_case_metrics_actions.ts rename to x-pack/test/cases_api_integration/security_and_spaces/tests/common/internal/metrics/get_case_metrics_actions.ts index f850ed1d472f1..8b4f0f5870f7d 100644 --- a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/metrics/get_case_metrics_actions.ts +++ b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/internal/metrics/get_case_metrics_actions.ts @@ -6,15 +6,16 @@ */ import expect from '@kbn/expect'; -import { getPostCaseRequest, postCommentActionsReq } from '../../../../common/lib/mock'; +import { CaseMetricsFeature } from '@kbn/cases-plugin/common/api/metrics/case'; +import { getPostCaseRequest, postCommentActionsReq } from '../../../../../common/lib/mock'; -import { FtrProviderContext } from '../../../../common/ftr_provider_context'; +import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; import { createCase, createComment, deleteAllCaseItems, getCaseMetrics, -} from '../../../../common/lib/api'; +} from '../../../../../common/lib/api'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { @@ -33,7 +34,7 @@ export default ({ getService }: FtrProviderContext): void => { const metrics = await getCaseMetrics({ supertest, caseId: theCase.id, - features: ['actions.isolateHost'], + features: [CaseMetricsFeature.ACTIONS_ISOLATE_HOST], }); expect(metrics).to.eql({ @@ -66,7 +67,7 @@ export default ({ getService }: FtrProviderContext): void => { const metrics = await getCaseMetrics({ supertest, caseId: theCase.id, - features: ['actions.isolateHost'], + features: [CaseMetricsFeature.ACTIONS_ISOLATE_HOST], }); expect(metrics).to.eql({ @@ -103,7 +104,7 @@ export default ({ getService }: FtrProviderContext): void => { const metrics = await getCaseMetrics({ supertest, caseId: theCase.id, - features: ['actions.isolateHost'], + features: [CaseMetricsFeature.ACTIONS_ISOLATE_HOST], }); expect(metrics).to.eql({ diff --git a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/metrics/get_case_metrics_alerts.ts b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/internal/metrics/get_case_metrics_alerts.ts similarity index 89% rename from x-pack/test/cases_api_integration/security_and_spaces/tests/common/metrics/get_case_metrics_alerts.ts rename to x-pack/test/cases_api_integration/security_and_spaces/tests/common/internal/metrics/get_case_metrics_alerts.ts index b270121ee947b..04a0c5ff514a3 100644 --- a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/metrics/get_case_metrics_alerts.ts +++ b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/internal/metrics/get_case_metrics_alerts.ts @@ -6,16 +6,17 @@ */ import expect from '@kbn/expect'; -import { getPostCaseRequest, postCommentAlertReq } from '../../../../common/lib/mock'; +import { CaseMetricsFeature } from '@kbn/cases-plugin/common/api/metrics/case'; +import { getPostCaseRequest, postCommentAlertReq } from '../../../../../common/lib/mock'; -import { FtrProviderContext } from '../../../../common/ftr_provider_context'; +import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; import { createCase, createComment, deleteAllCaseItems, getCaseMetrics, -} from '../../../../common/lib/api'; -import { arraysToEqual } from '../../../../common/lib/validation'; +} from '../../../../../common/lib/api'; +import { arraysToEqual } from '../../../../../common/lib/validation'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { @@ -41,7 +42,7 @@ export default ({ getService }: FtrProviderContext): void => { const metrics = await getCaseMetrics({ supertest, caseId, - features: ['alerts.hosts'], + features: [CaseMetricsFeature.ALERTS_HOSTS], }); expect(metrics.alerts?.hosts?.total).to.be(3); @@ -58,7 +59,7 @@ export default ({ getService }: FtrProviderContext): void => { const metrics = await getCaseMetrics({ supertest, caseId, - features: ['alerts.users'], + features: [CaseMetricsFeature.ALERTS_USERS], }); expect(metrics.alerts?.users?.total).to.be(4); @@ -76,7 +77,7 @@ export default ({ getService }: FtrProviderContext): void => { const metrics = await getCaseMetrics({ supertest, caseId, - features: ['alerts.users', 'alerts.hosts'], + features: [CaseMetricsFeature.ALERTS_USERS, CaseMetricsFeature.ALERTS_HOSTS], }); expect(metrics.alerts?.hosts?.total).to.be(3); @@ -122,7 +123,7 @@ export default ({ getService }: FtrProviderContext): void => { const metrics = await getCaseMetrics({ supertest, caseId: theCase.id, - features: ['alerts.users', 'alerts.hosts'], + features: [CaseMetricsFeature.ALERTS_USERS, CaseMetricsFeature.ALERTS_HOSTS], }); expect(metrics.alerts?.hosts).to.eql({ @@ -141,7 +142,11 @@ export default ({ getService }: FtrProviderContext): void => { const metrics = await getCaseMetrics({ supertest, caseId, - features: ['alerts.users', 'alerts.hosts', 'alerts.count'], + features: [ + CaseMetricsFeature.ALERTS_USERS, + CaseMetricsFeature.ALERTS_HOSTS, + CaseMetricsFeature.ALERTS_COUNT, + ], }); expect(metrics.alerts?.hosts?.total).to.be(3); @@ -172,7 +177,7 @@ export default ({ getService }: FtrProviderContext): void => { const metrics = await getCaseMetrics({ supertest, caseId: theCase.id, - features: ['alerts.count'], + features: [CaseMetricsFeature.ALERTS_COUNT], }); expect(metrics).to.eql({ @@ -199,7 +204,7 @@ export default ({ getService }: FtrProviderContext): void => { const metrics = await getCaseMetrics({ supertest, caseId: theCase.id, - features: ['alerts.count'], + features: [CaseMetricsFeature.ALERTS_COUNT], }); expect(metrics).to.eql({ diff --git a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/metrics/get_case_metrics_connectors.ts b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/internal/metrics/get_case_metrics_connectors.ts similarity index 91% rename from x-pack/test/cases_api_integration/security_and_spaces/tests/common/metrics/get_case_metrics_connectors.ts rename to x-pack/test/cases_api_integration/security_and_spaces/tests/common/internal/metrics/get_case_metrics_connectors.ts index 19564ade7b02a..8a28e64b48c49 100644 --- a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/metrics/get_case_metrics_connectors.ts +++ b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/internal/metrics/get_case_metrics_connectors.ts @@ -7,16 +7,17 @@ import expect from '@kbn/expect'; import { ConnectorTypes } from '@kbn/cases-plugin/common/types/domain'; -import { getPostCaseRequest } from '../../../../common/lib/mock'; -import { ObjectRemover as ActionsRemover } from '../../../../../alerting_api_integration/common/lib'; +import { CaseMetricsFeature } from '@kbn/cases-plugin/common/api/metrics/case'; +import { getPostCaseRequest } from '../../../../../common/lib/mock'; +import { ObjectRemover as ActionsRemover } from '../../../../../../alerting_api_integration/common/lib'; -import { FtrProviderContext } from '../../../../common/ftr_provider_context'; +import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; import { createCase, deleteAllCaseItems, getCaseMetrics, updateCase, -} from '../../../../common/lib/api'; +} from '../../../../../common/lib/api'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { @@ -55,7 +56,7 @@ export default ({ getService }: FtrProviderContext): void => { const metrics = await getCaseMetrics({ supertest, caseId, - features: ['connectors'], + features: [CaseMetricsFeature.CONNECTORS], }); expect(metrics).to.eql({ diff --git a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/metrics/get_cases_metrics.ts b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/internal/metrics/get_cases_metrics.ts similarity index 89% rename from x-pack/test/cases_api_integration/security_and_spaces/tests/common/metrics/get_cases_metrics.ts rename to x-pack/test/cases_api_integration/security_and_spaces/tests/common/internal/metrics/get_cases_metrics.ts index 24b3defbc5d88..f7089db84985b 100644 --- a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/metrics/get_cases_metrics.ts +++ b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/internal/metrics/get_cases_metrics.ts @@ -7,6 +7,7 @@ import expect from '@kbn/expect'; import { CaseStatuses } from '@kbn/cases-plugin/common/types/domain'; +import { CaseMetricsFeature } from '@kbn/cases-plugin/common'; import { secOnly, obsOnlyRead, @@ -16,15 +17,15 @@ import { globalRead, obsSecRead, obsSec, -} from '../../../../common/lib/authentication/users'; -import { FtrProviderContext } from '../../../../common/ftr_provider_context'; +} from '../../../../../common/lib/authentication/users'; +import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; import { createCase, deleteAllCaseItems, getCasesMetrics, updateCase, -} from '../../../../common/lib/api'; -import { getPostCaseRequest } from '../../../../common/lib/mock'; +} from '../../../../../common/lib/api'; +import { getPostCaseRequest } from '../../../../../common/lib/mock'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { @@ -37,7 +38,7 @@ export default ({ getService }: FtrProviderContext): void => { it('accepts the features as string', async () => { const metrics = await getCasesMetrics({ supertest, - features: 'mttr', + features: CaseMetricsFeature.MTTR, }); expect(metrics).to.eql({ mttr: null }); @@ -45,11 +46,11 @@ export default ({ getService }: FtrProviderContext): void => { await deleteAllCaseItems(es); }); - describe('MTTR', () => { + describe(CaseMetricsFeature.MTTR, () => { it('responses with null if there are no cases', async () => { const metrics = await getCasesMetrics({ supertest, - features: ['mttr'], + features: [CaseMetricsFeature.MTTR], }); expect(metrics).to.eql({ mttr: null }); @@ -74,7 +75,7 @@ export default ({ getService }: FtrProviderContext): void => { const metrics = await getCasesMetrics({ supertest, - features: ['mttr'], + features: [CaseMetricsFeature.MTTR], }); expect(metrics).to.eql({ mttr: null }); @@ -97,7 +98,7 @@ export default ({ getService }: FtrProviderContext): void => { it('should calculate the mttr correctly across all cases', async () => { const metrics = await getCasesMetrics({ supertest, - features: ['mttr'], + features: [CaseMetricsFeature.MTTR], }); expect(metrics).to.eql({ mttr: 220 }); @@ -106,7 +107,7 @@ export default ({ getService }: FtrProviderContext): void => { it('should respects the range parameters', async () => { const metrics = await getCasesMetrics({ supertest, - features: ['mttr'], + features: [CaseMetricsFeature.MTTR], query: { from: '2022-04-28', to: '2022-04-29', @@ -160,7 +161,7 @@ export default ({ getService }: FtrProviderContext): void => { ]) { const metrics = await getCasesMetrics({ supertest: supertestWithoutAuth, - features: ['mttr'], + features: [CaseMetricsFeature.MTTR], auth: { user: scenario.user, space: 'space1', @@ -181,7 +182,7 @@ export default ({ getService }: FtrProviderContext): void => { // user should not be able to read cases at the appropriate space await getCasesMetrics({ supertest: supertestWithoutAuth, - features: ['mttr'], + features: [CaseMetricsFeature.MTTR], auth: { user: scenario.user, space: scenario.space, @@ -194,7 +195,7 @@ export default ({ getService }: FtrProviderContext): void => { it('should respect the owner filter when having permissions', async () => { const metrics = await getCasesMetrics({ supertest: supertestWithoutAuth, - features: ['mttr'], + features: [CaseMetricsFeature.MTTR], query: { owner: 'securitySolutionFixture', }, @@ -210,7 +211,7 @@ export default ({ getService }: FtrProviderContext): void => { it('should return the correct cases when trying to exploit RBAC through the owner query parameter', async () => { const metrics = await getCasesMetrics({ supertest: supertestWithoutAuth, - features: ['mttr'], + features: [CaseMetricsFeature.MTTR], query: { owner: ['securitySolutionFixture', 'observabilityFixture'], }, @@ -226,7 +227,7 @@ export default ({ getService }: FtrProviderContext): void => { it('should respect the owner filter when using range queries', async () => { const metrics = await getCasesMetrics({ supertest: supertestWithoutAuth, - features: ['mttr'], + features: [CaseMetricsFeature.MTTR], query: { from: '2022-04-20', to: '2022-04-30', diff --git a/x-pack/test/cases_api_integration/spaces_only/tests/common/metrics/get_cases_metrics.ts b/x-pack/test/cases_api_integration/spaces_only/tests/common/metrics/get_cases_metrics.ts index 574fdcaa2be9c..cd9cb877ce5a1 100644 --- a/x-pack/test/cases_api_integration/spaces_only/tests/common/metrics/get_cases_metrics.ts +++ b/x-pack/test/cases_api_integration/spaces_only/tests/common/metrics/get_cases_metrics.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { CaseMetricsFeature } from '@kbn/cases-plugin/common/api/metrics/case'; import expect from '@kbn/expect'; import { FtrProviderContext } from '../../../../common/ftr_provider_context'; @@ -51,7 +52,7 @@ export default ({ getService }: FtrProviderContext): void => { it('should calculate the mttr correctly on space 1', async () => { const metrics = await getCasesMetrics({ supertest: supertestWithoutAuth, - features: ['mttr'], + features: [CaseMetricsFeature.MTTR], auth: authSpace1, }); @@ -62,7 +63,7 @@ export default ({ getService }: FtrProviderContext): void => { const authSpace2 = getAuthWithSuperUser('space2'); const metrics = await getCasesMetrics({ supertest: supertestWithoutAuth, - features: ['mttr'], + features: [CaseMetricsFeature.MTTR], auth: authSpace2, });