mirror of
https://github.com/elastic/kibana.git
synced 2025-06-28 03:01:21 -04:00
* 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>
393 lines
10 KiB
TypeScript
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');
|
|
};
|