Skip to content

Commit

Permalink
[Cases] Add MTTR metric (elastic#130459)
Browse files Browse the repository at this point in the history
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
  • Loading branch information
2 people authored and Esteban Beltran committed May 4, 2022
1 parent 39da49d commit d90a625
Show file tree
Hide file tree
Showing 62 changed files with 2,032 additions and 253 deletions.
49 changes: 47 additions & 2 deletions x-pack/plugins/cases/common/api/metrics/case.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@

import * as rt from 'io-ts';

export type CaseMetricsResponse = rt.TypeOf<typeof CaseMetricsResponseRt>;
export type SingleCaseMetricsRequest = rt.TypeOf<typeof SingleCaseMetricsRequestRt>;
export type SingleCaseMetricsResponse = rt.TypeOf<typeof SingleCaseMetricsResponseRt>;
export type CasesMetricsRequest = rt.TypeOf<typeof CasesMetricsRequestRt>;
export type CasesMetricsResponse = rt.TypeOf<typeof CasesMetricsResponseRt>;
export type AlertHostsMetrics = rt.TypeOf<typeof AlertHostsMetricsRt>;
export type AlertUsersMetrics = rt.TypeOf<typeof AlertUsersMetricsRt>;
export type StatusInfo = rt.TypeOf<typeof StatusInfoRt>;
Expand Down Expand Up @@ -69,7 +72,43 @@ const AlertUsersMetricsRt = rt.type({
),
});

export const CaseMetricsResponseRt = rt.partial(
export const SingleCaseMetricsRequestRt = rt.type({
/**
* The ID of the case.
*/
caseId: rt.string,
/**
* The metrics to retrieve.
*/
features: rt.array(rt.string),
});

export const CasesMetricsRequestRt = rt.intersection([
rt.type({
/**
* The metrics to retrieve.
*/
features: rt.array(rt.string),
}),
rt.partial({
/**
* A KQL date. If used all cases created after (gte) the from date will be returned
*/
from: rt.string,
/**
* A KQL date. If used all cases created before (lte) the to date will be returned.
*/
to: rt.string,
/**
* The owner(s) to filter by. The user making the request must have privileges to retrieve cases of that
* ownership or they will be ignored. If no owner is included, then all ownership types will be included in the response
* that the user has access to.
*/
owner: rt.union([rt.array(rt.string), rt.string]),
}),
]);

export const SingleCaseMetricsResponseRt = rt.partial(
rt.type({
alerts: rt.partial(
rt.type({
Expand Down Expand Up @@ -142,3 +181,9 @@ export const CaseMetricsResponseRt = rt.partial(
}),
}).props
);

export const CasesMetricsResponseRt = rt.partial(
rt.type({
mttr: rt.number,
}).props
);
1 change: 1 addition & 0 deletions x-pack/plugins/cases/common/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ export const CASE_USER_ACTIONS_URL = `${CASE_DETAILS_URL}/user_actions` as const
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;

/**
Expand Down
8 changes: 4 additions & 4 deletions x-pack/plugins/cases/common/ui/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
ActionConnector,
CaseExternalServiceBasic,
CaseUserActionResponse,
CaseMetricsResponse,
SingleCaseMetricsResponse,
CommentResponse,
CaseResponse,
CommentResponseAlertsType,
Expand All @@ -24,7 +24,7 @@ type DeepRequired<T> = { [K in keyof T]: DeepRequired<T[K]> } & Required<T>;

export interface CasesContextFeatures {
alerts: { sync?: boolean; enabled?: boolean };
metrics: CaseMetricsFeature[];
metrics: SingleCaseMetricsFeature[];
}

export type CasesFeaturesAllRequired = DeepRequired<CasesContextFeatures>;
Expand Down Expand Up @@ -97,8 +97,8 @@ export interface AllCases extends CasesStatus {
total: number;
}

export type CaseMetrics = CaseMetricsResponse;
export type CaseMetricsFeature =
export type SingleCaseMetrics = SingleCaseMetricsResponse;
export type SingleCaseMetricsFeature =
| 'alerts.count'
| 'alerts.users'
| 'alerts.hosts'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,16 @@ import {
basicCaseStatusFeatures,
} from '../../../containers/mock';
import { CaseViewMetrics } from '.';
import { CaseMetrics, CaseMetricsFeature } from '../../../../common/ui';
import { SingleCaseMetrics, SingleCaseMetricsFeature } from '../../../../common/ui';
import { TestProviders } from '../../../common/mock';

const renderCaseMetrics = ({
metrics = basicCaseMetrics,
features = [...basicCaseNumericValueFeatures, ...basicCaseStatusFeatures],
isLoading = false,
}: {
metrics?: CaseMetrics;
features?: CaseMetricsFeature[];
metrics?: SingleCaseMetrics;
features?: SingleCaseMetricsFeature[];
isLoading?: boolean;
} = {}) => {
return render(
Expand All @@ -33,7 +33,7 @@ const renderCaseMetrics = ({
};

interface FeatureTest {
feature: CaseMetricsFeature;
feature: SingleCaseMetricsFeature;
items: Array<{
title: string;
value: string | number;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +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 { CaseMetrics, CaseMetricsFeature } from '../../../../common/ui';
import { SingleCaseMetrics, SingleCaseMetricsFeature } from '../../../../common/ui';
import {
CASE_CREATED,
CASE_IN_PROGRESS_DURATION,
Expand Down Expand Up @@ -90,10 +90,10 @@ export const CaseStatusMetrics: React.FC<Pick<CaseViewMetricsProps, 'metrics' |
CaseStatusMetrics.displayName = 'CaseStatusMetrics';

const useGetLifespanMetrics = (
metrics: CaseMetrics | null,
features: CaseMetricsFeature[]
): CaseMetrics['lifespan'] | undefined => {
return useMemo<CaseMetrics['lifespan']>(() => {
metrics: SingleCaseMetrics | null,
features: SingleCaseMetricsFeature[]
): SingleCaseMetrics['lifespan'] | undefined => {
return useMemo<SingleCaseMetrics['lifespan']>(() => {
const lifespan = metrics?.lifespan ?? {
closeDate: '',
creationDate: '',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import React, { useMemo } from 'react';
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { euiStyled } from '@kbn/kibana-react-plugin/common';
import { CaseMetrics, CaseMetricsFeature } from '../../../../common/ui';
import { SingleCaseMetrics, SingleCaseMetricsFeature } from '../../../../common/ui';
import {
ASSOCIATED_HOSTS_METRIC,
ASSOCIATED_USERS_METRIC,
Expand Down Expand Up @@ -50,8 +50,8 @@ interface MetricItem {
type MetricItems = MetricItem[];

const useGetTitleValueMetricItems = (
metrics: CaseMetrics | null,
features: CaseMetricsFeature[]
metrics: SingleCaseMetrics | null,
features: SingleCaseMetricsFeature[]
): MetricItems => {
const { alerts, actions, connectors } = metrics ?? {};
const totalConnectors = connectors?.total ?? 0;
Expand All @@ -61,7 +61,7 @@ const useGetTitleValueMetricItems = (
const totalIsolatedHosts = calculateTotalIsolatedHosts(actions);

const metricItems = useMemo<MetricItems>(() => {
const items: Array<[CaseMetricsFeature, Omit<MetricItem, 'id'>]> = [
const items: Array<[SingleCaseMetricsFeature, Omit<MetricItem, 'id'>]> = [
['alerts.count', { title: TOTAL_ALERTS_METRIC, value: alertsCount }],
['alerts.users', { title: ASSOCIATED_USERS_METRIC, value: totalAlertUsers }],
['alerts.hosts', { title: ASSOCIATED_HOSTS_METRIC, value: totalAlertHosts }],
Expand All @@ -88,7 +88,7 @@ const useGetTitleValueMetricItems = (
return metricItems;
};

const calculateTotalIsolatedHosts = (actions: CaseMetrics['actions']) => {
const calculateTotalIsolatedHosts = (actions: SingleCaseMetrics['actions']) => {
if (!actions?.isolateHost) {
return 0;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@
* 2.0.
*/

import { CaseMetrics, CaseMetricsFeature } from '../../../../common/ui';
import { SingleCaseMetrics, SingleCaseMetricsFeature } from '../../../../common/ui';

export interface CaseViewMetricsProps {
metrics: CaseMetrics | null;
features: CaseMetricsFeature[];
metrics: SingleCaseMetrics | null;
features: SingleCaseMetricsFeature[];
isLoading: boolean;
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@
*/

import { useMemo } from 'react';
import { CaseMetricsFeature } from '../../containers/types';
import { SingleCaseMetricsFeature } from '../../containers/types';
import { useCasesContext } from './use_cases_context';

export interface UseCasesFeatures {
isAlertsEnabled: boolean;
isSyncAlertsEnabled: boolean;
metricsFeatures: CaseMetricsFeature[];
metricsFeatures: SingleCaseMetricsFeature[];
}

export const useCasesFeatures = (): UseCasesFeatures => {
Expand Down
6 changes: 3 additions & 3 deletions x-pack/plugins/cases/public/containers/__mocks__/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ import {
CommentRequest,
User,
CaseStatuses,
CaseMetricsResponse,
SingleCaseMetricsResponse,
} from '../../../common/api';

export const getCase = async (
Expand All @@ -51,10 +51,10 @@ export const resolveCase = async (
signal: AbortSignal
): Promise<ResolvedCase> => Promise.resolve(basicResolvedCase);

export const getCaseMetrics = async (
export const getSingleCaseMetrics = async (
caseId: string,
signal: AbortSignal
): Promise<CaseMetricsResponse> => Promise.resolve(basicCaseMetrics);
): Promise<SingleCaseMetricsResponse> => Promise.resolve(basicCaseMetrics);

export const getCasesStatus = async (signal: AbortSignal): Promise<CasesStatus> =>
Promise.resolve(casesStatus);
Expand Down
20 changes: 11 additions & 9 deletions x-pack/plugins/cases/public/containers/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@ import {
getCasePushUrl,
getCaseUserActionUrl,
User,
CaseMetricsResponse,
getCaseCommentDeleteUrl,
SingleCaseMetricsResponse,
} from '../../common/api';
import {
CASE_REPORTERS_URL,
Expand All @@ -45,8 +45,8 @@ import {
AllCases,
BulkUpdateStatus,
Case,
CaseMetrics,
CaseMetricsFeature,
SingleCaseMetrics,
SingleCaseMetricsFeature,
CasesStatus,
FetchCasesProps,
SortFieldCase,
Expand All @@ -63,7 +63,7 @@ import {
decodeCasesStatusResponse,
decodeCaseUserActionsResponse,
decodeCaseResolveResponse,
decodeCaseMetricsResponse,
decodeSingleCaseMetricsResponse,
} from './utils';

export const getCase = async (
Expand Down Expand Up @@ -129,20 +129,22 @@ export const getReporters = async (signal: AbortSignal, owner: string[]): Promis
return response ?? [];
};

export const getCaseMetrics = async (
export const getSingleCaseMetrics = async (
caseId: string,
features: CaseMetricsFeature[],
features: SingleCaseMetricsFeature[],
signal: AbortSignal
): Promise<CaseMetrics> => {
const response = await KibanaServices.get().http.fetch<CaseMetricsResponse>(
): Promise<SingleCaseMetrics> => {
const response = await KibanaServices.get().http.fetch<SingleCaseMetricsResponse>(
getCaseDetailsMetricsUrl(caseId),
{
method: 'GET',
signal,
query: { features: JSON.stringify(features) },
}
);
return convertToCamelCase<CaseMetricsResponse, CaseMetrics>(decodeCaseMetricsResponse(response));
return convertToCamelCase<SingleCaseMetricsResponse, SingleCaseMetrics>(
decodeSingleCaseMetricsResponse(response)
);
};

export const getCaseUserActions = async (
Expand Down
10 changes: 5 additions & 5 deletions x-pack/plugins/cases/public/containers/mock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ import { ActionLicense, AllCases, Case, CasesStatus, CaseUserActions, Comment }

import type {
ResolvedCase,
CaseMetrics,
CaseMetricsFeature,
SingleCaseMetrics,
SingleCaseMetricsFeature,
AlertComment,
} from '../../common/ui/types';
import {
Expand Down Expand Up @@ -189,17 +189,17 @@ export const basicResolvedCase: ResolvedCase = {
aliasTargetId: `${basicCase.id}_2`,
};

export const basicCaseNumericValueFeatures: CaseMetricsFeature[] = [
export const basicCaseNumericValueFeatures: SingleCaseMetricsFeature[] = [
'alerts.count',
'alerts.users',
'alerts.hosts',
'actions.isolateHost',
'connectors',
];

export const basicCaseStatusFeatures: CaseMetricsFeature[] = ['lifespan'];
export const basicCaseStatusFeatures: SingleCaseMetricsFeature[] = ['lifespan'];

export const basicCaseMetrics: CaseMetrics = {
export const basicCaseMetrics: SingleCaseMetrics = {
alerts: {
count: 12,
hosts: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
*/

import { renderHook, act } from '@testing-library/react-hooks';
import { CaseMetricsFeature } from '../../common/ui';
import { SingleCaseMetricsFeature } from '../../common/ui';
import { useGetCaseMetrics, UseGetCaseMetrics } from './use_get_case_metrics';
import { basicCase, basicCaseMetrics } from './mock';
import * as api from './api';
Expand All @@ -16,7 +16,7 @@ jest.mock('../common/lib/kibana');

describe('useGetCaseMetrics', () => {
const abortCtrl = new AbortController();
const features: CaseMetricsFeature[] = ['alerts.count'];
const features: SingleCaseMetricsFeature[] = ['alerts.count'];

beforeEach(() => {
jest.clearAllMocks();
Expand All @@ -38,8 +38,8 @@ describe('useGetCaseMetrics', () => {
});
});

it('calls getCaseMetrics with correct arguments', async () => {
const spyOnGetCaseMetrics = jest.spyOn(api, 'getCaseMetrics');
it('calls getSingleCaseMetrics with correct arguments', async () => {
const spyOnGetCaseMetrics = jest.spyOn(api, 'getSingleCaseMetrics');
await act(async () => {
const { waitForNextUpdate } = renderHook<string, UseGetCaseMetrics>(() =>
useGetCaseMetrics(basicCase.id, features)
Expand All @@ -50,8 +50,8 @@ describe('useGetCaseMetrics', () => {
});
});

it('does not call getCaseMetrics if empty feature parameter passed', async () => {
const spyOnGetCaseMetrics = jest.spyOn(api, 'getCaseMetrics');
it('does not call getSingleCaseMetrics if empty feature parameter passed', async () => {
const spyOnGetCaseMetrics = jest.spyOn(api, 'getSingleCaseMetrics');
await act(async () => {
const { waitForNextUpdate } = renderHook<string, UseGetCaseMetrics>(() =>
useGetCaseMetrics(basicCase.id, [])
Expand All @@ -78,7 +78,7 @@ describe('useGetCaseMetrics', () => {
});

it('refetch case metrics', async () => {
const spyOnGetCaseMetrics = jest.spyOn(api, 'getCaseMetrics');
const spyOnGetCaseMetrics = jest.spyOn(api, 'getSingleCaseMetrics');
await act(async () => {
const { result, waitForNextUpdate } = renderHook<string, UseGetCaseMetrics>(() =>
useGetCaseMetrics(basicCase.id, features)
Expand Down Expand Up @@ -116,8 +116,8 @@ describe('useGetCaseMetrics', () => {
});
});

it('returns an error when getCaseMetrics throws', async () => {
const spyOnGetCaseMetrics = jest.spyOn(api, 'getCaseMetrics');
it('returns an error when getSingleCaseMetrics throws', async () => {
const spyOnGetCaseMetrics = jest.spyOn(api, 'getSingleCaseMetrics');
spyOnGetCaseMetrics.mockImplementation(() => {
throw new Error('Something went wrong');
});
Expand Down
Loading

0 comments on commit d90a625

Please sign in to comment.