kibana/x-pack/plugins/cases/server/common/utils.ts
Jonathan Buttner aa7d8e8ab7
[ResponseOps][Cases] Add assignee field and filtering (#137532)
* Refactoring services, auth

* Adding suggest api and tests

* Working integration tests

* Switching suggest api tags

* Adding assignees field

* Adding tests for size and owner

* Adding assignee integration tests

* Starting user actions changes

* Using lodash for array comparison logic and tests

* Fixing type error

* Adding assignees user action

* [ResponseOps][Cases] Refactoring client args and authentication (#137345)

* Refactoring services, auth

* Fixing type errors

* Adding assignees migration and tests

* Fixing types and added more tests

* Fixing cypress test

* Fixing test

* Adding migration for assignees field and tests

* Adding comments and a few more tests

* Updating comments and spelling

* Addressing feedback

* Removing optional owners

* Forgot rest of files

* Adding default empty array for user actions

* Fixing test error

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
2022-08-04 15:25:36 -04:00

393 lines
10 KiB
TypeScript

/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import {
SavedObjectsFindResult,
SavedObjectsFindResponse,
SavedObject,
SavedObjectReference,
} from '@kbn/core/server';
import { flatMap, uniqWith, xorWith } from 'lodash';
import { LensServerPluginSetup } from '@kbn/lens-plugin/server';
import { AlertInfo } from './types';
import {
CaseAttributes,
CasePostRequest,
CaseResponse,
CaseSeverity,
CasesFindResponse,
CaseStatuses,
CommentAttributes,
CommentRequest,
CommentRequestActionsType,
CommentRequestAlertType,
CommentRequestExternalReferenceSOType,
CommentRequestUserType,
CommentResponse,
CommentsResponse,
CommentType,
ConnectorTypes,
ExternalReferenceStorageType,
User,
} from '../../common/api';
import { UpdateAlertRequest } from '../client/alerts/types';
import {
parseCommentString,
getLensVisualizations,
} from '../../common/utils/markdown_plugins/utils';
/**
* Default sort field for querying saved objects.
*/
export const defaultSortField = 'created_at';
/**
* Default unknown user
*/
export const nullUser: User = { username: null, full_name: null, email: null };
export const transformNewCase = ({
user,
newCase,
}: {
user: User;
newCase: CasePostRequest;
}): CaseAttributes => ({
...newCase,
duration: null,
severity: newCase.severity ?? CaseSeverity.LOW,
closed_at: null,
closed_by: null,
created_at: new Date().toISOString(),
created_by: user,
external_service: null,
status: CaseStatuses.open,
updated_at: null,
updated_by: null,
assignees: newCase.assignees ?? [],
});
export const transformCases = ({
casesMap,
countOpenCases,
countInProgressCases,
countClosedCases,
page,
perPage,
total,
}: {
casesMap: Map<string, CaseResponse>;
countOpenCases: number;
countInProgressCases: number;
countClosedCases: number;
page: number;
perPage: number;
total: number;
}): CasesFindResponse => ({
page,
per_page: perPage,
total,
cases: Array.from(casesMap.values()),
count_open_cases: countOpenCases,
count_in_progress_cases: countInProgressCases,
count_closed_cases: countClosedCases,
});
export const flattenCaseSavedObject = ({
savedObject,
comments = [],
totalComment = comments.length,
totalAlerts = 0,
}: {
savedObject: SavedObject<CaseAttributes>;
comments?: Array<SavedObject<CommentAttributes>>;
totalComment?: number;
totalAlerts?: number;
}): CaseResponse => ({
id: savedObject.id,
version: savedObject.version ?? '0',
comments: flattenCommentSavedObjects(comments),
totalComment,
totalAlerts,
...savedObject.attributes,
});
export const transformComments = (
comments: SavedObjectsFindResponse<CommentAttributes>
): CommentsResponse => ({
page: comments.page,
per_page: comments.per_page,
total: comments.total,
comments: flattenCommentSavedObjects(comments.saved_objects),
});
export const flattenCommentSavedObjects = (
savedObjects: Array<SavedObject<CommentAttributes>>
): CommentResponse[] =>
savedObjects.reduce((acc: CommentResponse[], savedObject: SavedObject<CommentAttributes>) => {
return [...acc, flattenCommentSavedObject(savedObject)];
}, []);
export const flattenCommentSavedObject = (
savedObject: SavedObject<CommentAttributes>
): CommentResponse => ({
id: savedObject.id,
version: savedObject.version ?? '0',
...savedObject.attributes,
});
export const getIDsAndIndicesAsArrays = (
comment: CommentRequestAlertType
): { ids: string[]; indices: string[] } => {
return {
ids: Array.isArray(comment.alertId) ? comment.alertId : [comment.alertId],
indices: Array.isArray(comment.index) ? comment.index : [comment.index],
};
};
/**
* This functions extracts the ids and indices from an alert comment. It enforces that the alertId and index are either
* both strings or string arrays that are the same length. If they are arrays they represent a 1-to-1 mapping of
* id existing in an index at each position in the array. This is not ideal. Ideally an alert comment request would
* accept an array of objects like this: Array<{id: string; index: string; ruleName: string ruleID: string}> instead.
*
* To reformat the alert comment request requires a migration and a breaking API change.
*/
const getAndValidateAlertInfoFromComment = (comment: CommentRequest): AlertInfo[] => {
if (!isCommentRequestTypeAlert(comment)) {
return [];
}
const { ids, indices } = getIDsAndIndicesAsArrays(comment);
if (ids.length !== indices.length) {
return [];
}
return ids.map((id, index) => ({ id, index: indices[index] }));
};
/**
* Builds an AlertInfo object accumulating the alert IDs and indices for the passed in alerts.
*/
export const getAlertInfoFromComments = (comments: CommentRequest[] = []): AlertInfo[] =>
comments.reduce((acc: AlertInfo[], comment) => {
const alertInfo = getAndValidateAlertInfoFromComment(comment);
acc.push(...alertInfo);
return acc;
}, []);
type NewCommentArgs = CommentRequest & {
createdDate: string;
owner: string;
email?: string | null;
full_name?: string | null;
username?: string | null;
};
export const transformNewComment = ({
createdDate,
email,
// eslint-disable-next-line @typescript-eslint/naming-convention
full_name,
username,
...comment
}: NewCommentArgs): CommentAttributes => {
return {
...comment,
created_at: createdDate,
created_by: { email, full_name, username },
pushed_at: null,
pushed_by: null,
updated_at: null,
updated_by: null,
};
};
/**
* A type narrowing function for user comments.
*/
export const isCommentRequestTypeUser = (
context: CommentRequest
): context is CommentRequestUserType => {
return context.type === CommentType.user;
};
/**
* A type narrowing function for actions comments.
*/
export const isCommentRequestTypeActions = (
context: CommentRequest
): context is CommentRequestActionsType => {
return context.type === CommentType.actions;
};
/**
* A type narrowing function for alert comments.
*/
export const isCommentRequestTypeAlert = (
context: CommentRequest
): context is CommentRequestAlertType => {
return context.type === CommentType.alert;
};
/**
* A type narrowing function for external reference so attachments.
*/
export const isCommentRequestTypeExternalReferenceSO = (
context: Partial<CommentRequest>
): context is CommentRequestExternalReferenceSOType => {
return (
context.type === CommentType.externalReference &&
context.externalReferenceStorage?.type === ExternalReferenceStorageType.savedObject
);
};
/**
* Adds the ids and indices to a map of statuses
*/
export function createAlertUpdateRequest({
comment,
status,
}: {
comment: CommentRequest;
status: CaseStatuses;
}): UpdateAlertRequest[] {
return getAlertInfoFromComments([comment]).map((alert) => ({ ...alert, status }));
}
/**
* Counts the total alert IDs within a single comment.
*/
export const countAlerts = (comment: SavedObjectsFindResult<CommentAttributes>) => {
let totalAlerts = 0;
if (comment.attributes.type === CommentType.alert) {
if (Array.isArray(comment.attributes.alertId)) {
totalAlerts += comment.attributes.alertId.length;
} else {
totalAlerts++;
}
}
return totalAlerts;
};
/**
* Count the number of alerts for each id in the alert's references.
*/
export const groupTotalAlertsByID = ({
comments,
}: {
comments: SavedObjectsFindResponse<CommentAttributes>;
}): Map<string, number> => {
return comments.saved_objects.reduce((acc, alertsInfo) => {
const alertTotalForComment = countAlerts(alertsInfo);
for (const alert of alertsInfo.references) {
if (alert.id) {
const totalAlerts = acc.get(alert.id);
if (totalAlerts !== undefined) {
acc.set(alert.id, totalAlerts + alertTotalForComment);
} else {
acc.set(alert.id, alertTotalForComment);
}
}
}
return acc;
}, new Map<string, number>());
};
/**
* Counts the total alert IDs for a single case.
*/
export const countAlertsForID = ({
comments,
id,
}: {
comments: SavedObjectsFindResponse<CommentAttributes>;
id: string;
}): number | undefined => {
return groupTotalAlertsByID({ comments }).get(id);
};
/**
* Returns a connector that indicates that no connector was set.
*
* @returns the 'none' connector
*/
export const getNoneCaseConnector = () => ({
id: 'none',
name: 'none',
type: ConnectorTypes.none,
fields: null,
});
export const extractLensReferencesFromCommentString = (
lensEmbeddableFactory: LensServerPluginSetup['lensEmbeddableFactory'],
comment: string
): SavedObjectReference[] => {
const extract = lensEmbeddableFactory()?.extract;
if (extract) {
const parsedComment = parseCommentString(comment);
const lensVisualizations = getLensVisualizations(parsedComment.children);
const flattenRefs = flatMap(
lensVisualizations,
(lensObject) => extract(lensObject)?.references ?? []
);
const uniqRefs = uniqWith(
flattenRefs,
(refA, refB) => refA.type === refB.type && refA.id === refB.id && refA.name === refB.name
);
return uniqRefs;
}
return [];
};
export const getOrUpdateLensReferences = (
lensEmbeddableFactory: LensServerPluginSetup['lensEmbeddableFactory'],
newComment: string,
currentComment?: SavedObject<CommentRequestUserType>
) => {
if (!currentComment) {
return extractLensReferencesFromCommentString(lensEmbeddableFactory, newComment);
}
const savedObjectReferences = currentComment.references;
const savedObjectLensReferences = extractLensReferencesFromCommentString(
lensEmbeddableFactory,
currentComment.attributes.comment
);
const currentNonLensReferences = xorWith(
savedObjectReferences,
savedObjectLensReferences,
(refA, refB) => refA.type === refB.type && refA.id === refB.id
);
const newCommentLensReferences = extractLensReferencesFromCommentString(
lensEmbeddableFactory,
newComment
);
return currentNonLensReferences.concat(newCommentLensReferences);
};
export const asArray = <T>(field?: T | T[] | null): T[] => {
if (field === undefined || field === null) {
return [];
}
return Array.isArray(field) ? field : [field];
};
export const assertUnreachable = (x: never): never => {
throw new Error('You should not reach this part of code');
};