, i: number) => {
+ if (!control.indexPattern) {
+ return;
+ }
+ control.indexPatternRefName = `control_${i}_index_pattern`;
+ references.push({
+ name: control.indexPatternRefName,
+ type: 'index-pattern',
+ id: control.indexPattern,
+ });
+ });
+ }
+
+ return { state: _state, references };
+ }
}
diff --git a/x-pack/plugins/case/server/client/cases/mock.ts b/x-pack/plugins/case/server/client/cases/mock.ts
index 809c4ad1ea1bd..490519187f49e 100644
--- a/x-pack/plugins/case/server/client/cases/mock.ts
+++ b/x-pack/plugins/case/server/client/cases/mock.ts
@@ -11,6 +11,7 @@ import {
ConnectorMappingsAttributes,
CaseUserActionsResponse,
AssociationType,
+ CommentResponseAlertsType,
} from '../../../common/api';
import { BasicParams } from './types';
@@ -76,6 +77,20 @@ export const commentAlert: CommentResponse = {
version: 'WzEsMV0=',
};
+export const commentAlertMultipleIds: CommentResponseAlertsType = {
+ ...commentAlert,
+ id: 'mock-comment-2',
+ alertId: ['alert-id-1', 'alert-id-2'],
+ index: 'alert-index-1',
+ type: CommentType.alert as const,
+};
+
+export const commentGeneratedAlert: CommentResponseAlertsType = {
+ ...commentAlertMultipleIds,
+ id: 'mock-comment-3',
+ type: CommentType.generatedAlert as const,
+};
+
export const defaultPipes = ['informationCreated'];
export const basicParams: BasicParams = {
description: 'a description',
diff --git a/x-pack/plugins/case/server/client/cases/types.ts b/x-pack/plugins/case/server/client/cases/types.ts
index f1d56e7132bd1..2dd2caf9fe73a 100644
--- a/x-pack/plugins/case/server/client/cases/types.ts
+++ b/x-pack/plugins/case/server/client/cases/types.ts
@@ -72,7 +72,7 @@ export interface TransformFieldsArgs {
export interface ExternalServiceComment {
comment: string;
- commentId: string;
+ commentId?: string;
}
export interface MapIncident {
diff --git a/x-pack/plugins/case/server/client/cases/utils.test.ts b/x-pack/plugins/case/server/client/cases/utils.test.ts
index 361d0fb561afd..44e7a682aa7ed 100644
--- a/x-pack/plugins/case/server/client/cases/utils.test.ts
+++ b/x-pack/plugins/case/server/client/cases/utils.test.ts
@@ -17,6 +17,8 @@ import {
basicParams,
userActions,
commentAlert,
+ commentAlertMultipleIds,
+ commentGeneratedAlert,
} from './mock';
import {
@@ -48,7 +50,7 @@ describe('utils', () => {
{
actionType: 'overwrite',
key: 'short_description',
- pipes: ['informationCreated'],
+ pipes: [],
value: 'a title',
},
{
@@ -71,7 +73,7 @@ describe('utils', () => {
{
actionType: 'overwrite',
key: 'short_description',
- pipes: ['myTestPipe'],
+ pipes: [],
value: 'a title',
},
{
@@ -98,7 +100,7 @@ describe('utils', () => {
});
expect(res).toEqual({
- short_description: 'a title (created at 2020-03-13T08:34:53.450Z by Elastic User)',
+ short_description: 'a title',
description: 'a description (created at 2020-03-13T08:34:53.450Z by Elastic User)',
});
});
@@ -122,13 +124,13 @@ describe('utils', () => {
},
fields,
currentIncident: {
- short_description: 'first title (created at 2020-03-13T08:34:53.450Z by Elastic User)',
+ short_description: 'first title',
description: 'first description (created at 2020-03-13T08:34:53.450Z by Elastic User)',
},
});
expect(res).toEqual({
- short_description: 'a title (updated at 2020-03-15T08:34:53.450Z by Another User)',
+ short_description: 'a title',
description:
'first description (created at 2020-03-13T08:34:53.450Z by Elastic User) \r\na description (updated at 2020-03-15T08:34:53.450Z by Another User)',
});
@@ -168,7 +170,7 @@ describe('utils', () => {
});
expect(res).toEqual({
- short_description: 'a title (created at 2020-03-13T08:34:53.450Z by elastic)',
+ short_description: 'a title',
description: 'a description (created at 2020-03-13T08:34:53.450Z by elastic)',
});
});
@@ -190,7 +192,7 @@ describe('utils', () => {
});
expect(res).toEqual({
- short_description: 'a title (updated at 2020-03-15T08:34:53.450Z by anotherUser)',
+ short_description: 'a title',
description: 'a description (updated at 2020-03-15T08:34:53.450Z by anotherUser)',
});
});
@@ -448,8 +450,7 @@ describe('utils', () => {
labels: ['defacement'],
issueType: null,
parent: null,
- short_description:
- 'Super Bad Security Issue (created at 2019-11-25T21:54:48.952Z by elastic)',
+ short_description: 'Super Bad Security Issue',
description:
'This is a brand new case of a bad meanie defacing data (created at 2019-11-25T21:54:48.952Z by elastic)',
externalId: null,
@@ -504,7 +505,7 @@ describe('utils', () => {
expect(res.comments).toEqual([]);
});
- it('it creates comments of type alert correctly', async () => {
+ it('it adds the total alert comments correctly', async () => {
const res = await createIncident({
actionsClient: actionsMock,
theCase: {
@@ -512,7 +513,9 @@ describe('utils', () => {
comments: [
{ ...commentObj, id: 'comment-user-1' },
{ ...commentAlert, id: 'comment-alert-1' },
- { ...commentAlert, id: 'comment-alert-2' },
+ {
+ ...commentAlertMultipleIds,
+ },
],
},
// Remove second push
@@ -536,14 +539,36 @@ describe('utils', () => {
commentId: 'comment-user-1',
},
{
- comment:
- 'Alert with ids alert-id-1 added to case (added at 2019-11-25T21:55:00.177Z by elastic)',
- commentId: 'comment-alert-1',
+ comment: 'Elastic Security Alerts attached to the case: 3',
},
+ ]);
+ });
+
+ it('it removes alerts correctly', async () => {
+ const res = await createIncident({
+ actionsClient: actionsMock,
+ theCase: {
+ ...theCase,
+ comments: [
+ { ...commentObj, id: 'comment-user-1' },
+ commentAlertMultipleIds,
+ commentGeneratedAlert,
+ ],
+ },
+ userActions,
+ connector,
+ mappings,
+ alerts: [],
+ });
+
+ expect(res.comments).toEqual([
{
comment:
- 'Alert with ids alert-id-1 added to case (added at 2019-11-25T21:55:00.177Z by elastic)',
- commentId: 'comment-alert-2',
+ 'Wow, good luck catching that bad meanie! (added at 2019-11-25T21:55:00.177Z by elastic)',
+ commentId: 'comment-user-1',
+ },
+ {
+ comment: 'Elastic Security Alerts attached to the case: 4',
},
]);
});
@@ -578,8 +603,7 @@ describe('utils', () => {
description:
'fun description \r\nThis is a brand new case of a bad meanie defacing data (updated at 2019-11-25T21:54:48.952Z by elastic)',
externalId: 'external-id',
- short_description:
- 'Super Bad Security Issue (updated at 2019-11-25T21:54:48.952Z by elastic)',
+ short_description: 'Super Bad Security Issue',
},
comments: [],
});
diff --git a/x-pack/plugins/case/server/client/cases/utils.ts b/x-pack/plugins/case/server/client/cases/utils.ts
index fda4142bf77c7..a5013d9b93982 100644
--- a/x-pack/plugins/case/server/client/cases/utils.ts
+++ b/x-pack/plugins/case/server/client/cases/utils.ts
@@ -40,6 +40,15 @@ import {
} from './types';
import { getAlertIds } from '../../routes/api/utils';
+interface CreateIncidentArgs {
+ actionsClient: ActionsClient;
+ theCase: CaseResponse;
+ userActions: CaseUserActionsResponse;
+ connector: ActionConnector;
+ mappings: ConnectorMappingsAttributes[];
+ alerts: CaseClientGetAlertsResponse;
+}
+
export const getLatestPushInfo = (
connectorId: string,
userActions: CaseUserActionsResponse
@@ -75,14 +84,13 @@ const getCommentContent = (comment: CommentResponse): string => {
return '';
};
-interface CreateIncidentArgs {
- actionsClient: ActionsClient;
- theCase: CaseResponse;
- userActions: CaseUserActionsResponse;
- connector: ActionConnector;
- mappings: ConnectorMappingsAttributes[];
- alerts: CaseClientGetAlertsResponse;
-}
+const countAlerts = (comments: CaseResponse['comments']): number =>
+ comments?.reduce((total, comment) => {
+ if (comment.type === CommentType.alert || comment.type === CommentType.generatedAlert) {
+ return total + (Array.isArray(comment.alertId) ? comment.alertId.length : 1);
+ }
+ return total;
+ }, 0) ?? 0;
export const createIncident = async ({
actionsClient,
@@ -152,22 +160,34 @@ export const createIncident = async ({
userActions
.slice(latestPushInfo?.index ?? 0)
.filter(
- (action, index) =>
- Array.isArray(action.action_field) && action.action_field[0] === 'comment'
+ (action) => Array.isArray(action.action_field) && action.action_field[0] === 'comment'
)
.map((action) => action.comment_id)
);
- const commentsToBeUpdated = caseComments?.filter((comment) =>
- commentsIdsToBeUpdated.has(comment.id)
+
+ const commentsToBeUpdated = caseComments?.filter(
+ (comment) =>
+ // We push only user's comments
+ comment.type === CommentType.user && commentsIdsToBeUpdated.has(comment.id)
);
+ const totalAlerts = countAlerts(caseComments);
+
let comments: ExternalServiceComment[] = [];
+
if (commentsToBeUpdated && Array.isArray(commentsToBeUpdated) && commentsToBeUpdated.length > 0) {
const commentsMapping = mappings.find((m) => m.source === 'comments');
if (commentsMapping?.action_type !== 'nothing') {
comments = transformComments(commentsToBeUpdated, ['informationAdded']);
}
}
+
+ if (totalAlerts > 0) {
+ comments.push({
+ comment: `Elastic Security Alerts attached to the case: ${totalAlerts}`,
+ });
+ }
+
return { incident, comments };
};
@@ -247,7 +267,13 @@ export const prepareFieldsForTransformation = ({
key: mapping.target,
value: params[mapping.source] ?? '',
actionType: mapping.action_type,
- pipes: mapping.action_type === 'append' ? [...defaultPipes, 'append'] : defaultPipes,
+ pipes:
+ // Do not transform titles
+ mapping.source !== 'title'
+ ? mapping.action_type === 'append'
+ ? [...defaultPipes, 'append']
+ : defaultPipes
+ : [],
},
]
: acc,
diff --git a/x-pack/plugins/case/server/routes/api/cases/push_case.test.ts b/x-pack/plugins/case/server/routes/api/cases/push_case.test.ts
index bf398d1ffcf40..c8501130493ba 100644
--- a/x-pack/plugins/case/server/routes/api/cases/push_case.test.ts
+++ b/x-pack/plugins/case/server/routes/api/cases/push_case.test.ts
@@ -170,7 +170,7 @@ describe('Push case', () => {
parent: null,
priority: 'High',
labels: ['LOLBins'],
- summary: 'Another bad one (created at 2019-11-25T22:32:17.947Z by elastic)',
+ summary: 'Another bad one',
description:
'Oh no, a bad meanie going LOLBins all over the place! (created at 2019-11-25T22:32:17.947Z by elastic)',
externalId: null,
diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx
index 59b64de369745..1d75e873f9b18 100644
--- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx
+++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx
@@ -187,272 +187,275 @@ export function LayerPanel(
]);
return (
-
-
-
-
-
-
-
- {layerDatasource && (
-
- {
- const newState =
- typeof updater === 'function' ? updater(layerDatasourceState) : updater;
- // Look for removed columns
- const nextPublicAPI = layerDatasource.getPublicAPI({
- state: newState,
- layerId,
- });
- const nextTable = new Set(
- nextPublicAPI.getTableSpec().map(({ columnId }) => columnId)
- );
- const removed = datasourcePublicAPI
- .getTableSpec()
- .map(({ columnId }) => columnId)
- .filter((columnId) => !nextTable.has(columnId));
- let nextVisState = props.visualizationState;
- removed.forEach((columnId) => {
- nextVisState = activeVisualization.removeDimension({
- layerId,
- columnId,
- prevState: nextVisState,
- });
- });
-
- props.updateAll(datasourceId, newState, nextVisState);
- },
+ <>
+
+
+
+
+
- )}
-
-
-
-
- {groups.map((group, groupIndex) => {
- const isMissing = !isEmptyLayer && group.required && group.accessors.length === 0;
- return (
- {group.groupLabel}}
- labelType="legend"
- key={group.groupId}
- isInvalid={isMissing}
- error={
- isMissing ? (
-
- {i18n.translate('xpack.lens.editorFrame.requiredDimensionWarningLabel', {
- defaultMessage: 'Required dimension',
- })}
-
- ) : (
- []
- )
- }
- >
- <>
-
- {group.accessors.map((accessorConfig, accessorIndex) => {
- const { columnId } = accessorConfig;
- return (
-
-
- {
- setActiveDimension({
- isNew: false,
- activeGroup: group,
- activeId: id,
- });
- }}
- onRemoveClick={(id: string) => {
- trackUiEvent('indexpattern_dimension_removed');
- props.updateAll(
- datasourceId,
- layerDatasource.removeColumn({
- layerId,
- columnId: id,
- prevState: layerDatasourceState,
- }),
- activeVisualization.removeDimension({
- layerId,
- columnId: id,
- prevState: props.visualizationState,
- })
- );
- removeButtonRef(id);
- }}
- >
-
-
-
-
- );
- })}
-
- {group.supportsMoreColumns ? (
- {
- setActiveDimension({
- activeGroup: group,
- activeId: id,
- isNew: true,
- });
- }}
- onDrop={onDrop}
- />
- ) : null}
- >
-
- );
- })}
- {
- if (layerDatasource.updateStateOnCloseDimension) {
- const newState = layerDatasource.updateStateOnCloseDimension({
- state: layerDatasourceState,
- layerId,
- columnId: activeId!,
- });
- if (newState) {
- props.updateDatasource(datasourceId, newState);
- }
- }
- setActiveDimension(initialActiveDimensionState);
- }}
- panel={
- <>
- {activeGroup && activeId && (
+ {layerDatasource && (
+
{
- if (shouldReplaceDimension || shouldRemoveDimension) {
- props.updateAll(
- datasourceId,
- newState,
- shouldRemoveDimension
- ? activeVisualization.removeDimension({
- layerId,
- columnId: activeId,
- prevState: props.visualizationState,
- })
- : activeVisualization.setDimension({
- layerId,
- groupId: activeGroup.groupId,
- columnId: activeId,
- prevState: props.visualizationState,
- })
- );
- } else {
- props.updateDatasource(datasourceId, newState);
- }
- setActiveDimension({
- ...activeDimension,
- isNew: false,
+ layerId,
+ state: layerDatasourceState,
+ activeData: props.framePublicAPI.activeData,
+ setState: (updater: unknown) => {
+ const newState =
+ typeof updater === 'function' ? updater(layerDatasourceState) : updater;
+ // Look for removed columns
+ const nextPublicAPI = layerDatasource.getPublicAPI({
+ state: newState,
+ layerId,
+ });
+ const nextTable = new Set(
+ nextPublicAPI.getTableSpec().map(({ columnId }) => columnId)
+ );
+ const removed = datasourcePublicAPI
+ .getTableSpec()
+ .map(({ columnId }) => columnId)
+ .filter((columnId) => !nextTable.has(columnId));
+ let nextVisState = props.visualizationState;
+ removed.forEach((columnId) => {
+ nextVisState = activeVisualization.removeDimension({
+ layerId,
+ columnId,
+ prevState: nextVisState,
+ });
});
+
+ props.updateAll(datasourceId, newState, nextVisState);
},
}}
/>
- )}
- {activeGroup &&
- activeId &&
- !activeDimension.isNew &&
- activeVisualization.renderDimensionEditor &&
- activeGroup?.enableDimensionEditor && (
-
-
+ )}
+
+
+
+
+ {groups.map((group, groupIndex) => {
+ const isMissing = !isEmptyLayer && group.required && group.accessors.length === 0;
+ return (
+ {group.groupLabel}
}
+ labelType="legend"
+ key={group.groupId}
+ isInvalid={isMissing}
+ error={
+ isMissing ? (
+
+ {i18n.translate('xpack.lens.editorFrame.requiredDimensionWarningLabel', {
+ defaultMessage: 'Required dimension',
+ })}
+
+ ) : (
+ []
+ )
+ }
+ >
+ <>
+
+ {group.accessors.map((accessorConfig, accessorIndex) => {
+ const { columnId } = accessorConfig;
+
+ return (
+
+
+ {
+ setActiveDimension({
+ isNew: false,
+ activeGroup: group,
+ activeId: id,
+ });
+ }}
+ onRemoveClick={(id: string) => {
+ trackUiEvent('indexpattern_dimension_removed');
+ props.updateAll(
+ datasourceId,
+ layerDatasource.removeColumn({
+ layerId,
+ columnId: id,
+ prevState: layerDatasourceState,
+ }),
+ activeVisualization.removeDimension({
+ layerId,
+ columnId: id,
+ prevState: props.visualizationState,
+ })
+ );
+ removeButtonRef(id);
+ }}
+ >
+
+
+
+
+ );
+ })}
+
+ {group.supportsMoreColumns ? (
+ {
+ setActiveDimension({
+ activeGroup: group,
+ activeId: id,
+ isNew: true,
+ });
}}
+ onDrop={onDrop}
/>
-
- )}
- >
- }
- />
+ ) : null}
+ >
+
+ );
+ })}
-
+
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+ {
+ if (layerDatasource.updateStateOnCloseDimension) {
+ const newState = layerDatasource.updateStateOnCloseDimension({
+ state: layerDatasourceState,
+ layerId,
+ columnId: activeId!,
+ });
+ if (newState) {
+ props.updateDatasource(datasourceId, newState);
+ }
+ }
+ setActiveDimension(initialActiveDimensionState);
+ }}
+ panel={
+ <>
+ {activeGroup && activeId && (
+ {
+ if (shouldReplaceDimension || shouldRemoveDimension) {
+ props.updateAll(
+ datasourceId,
+ newState,
+ shouldRemoveDimension
+ ? activeVisualization.removeDimension({
+ layerId,
+ columnId: activeId,
+ prevState: props.visualizationState,
+ })
+ : activeVisualization.setDimension({
+ layerId,
+ groupId: activeGroup.groupId,
+ columnId: activeId,
+ prevState: props.visualizationState,
+ })
+ );
+ } else {
+ props.updateDatasource(datasourceId, newState);
+ }
+ setActiveDimension({
+ ...activeDimension,
+ isNew: false,
+ });
+ },
+ }}
+ />
+ )}
+ {activeGroup &&
+ activeId &&
+ !activeDimension.isNew &&
+ activeVisualization.renderDimensionEditor &&
+ activeGroup?.enableDimensionEditor && (
+
+
+
+ )}
+ >
+ }
+ />
+ >
);
}
diff --git a/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable_factory.ts b/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable_factory.ts
index 4c40282012d6d..a676b7283671c 100644
--- a/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable_factory.ts
+++ b/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable_factory.ts
@@ -5,10 +5,11 @@
* 2.0.
*/
-import { Capabilities, HttpSetup } from 'kibana/public';
+import { Capabilities, HttpSetup, SavedObjectReference } from 'kibana/public';
import { i18n } from '@kbn/i18n';
import { RecursiveReadonly } from '@kbn/utility-types';
import { Ast } from '@kbn/interpreter/target/common';
+import { EmbeddableStateWithType } from 'src/plugins/embeddable/common';
import {
IndexPatternsContract,
TimefilterContract,
@@ -105,4 +106,15 @@ export class EmbeddableFactory implements EmbeddableFactoryDefinition {
parent
);
}
+
+ extract(state: EmbeddableStateWithType) {
+ let references: SavedObjectReference[] = [];
+ const typedState = (state as unknown) as LensEmbeddableInput;
+
+ if ('attributes' in typedState && typedState.attributes !== undefined) {
+ references = typedState.attributes.references;
+ }
+
+ return { state, references };
+ }
}
diff --git a/x-pack/plugins/maps/public/embeddable/map_embeddable_factory.ts b/x-pack/plugins/maps/public/embeddable/map_embeddable_factory.ts
index b039076305498..7e15bfa9a340e 100644
--- a/x-pack/plugins/maps/public/embeddable/map_embeddable_factory.ts
+++ b/x-pack/plugins/maps/public/embeddable/map_embeddable_factory.ts
@@ -6,6 +6,7 @@
*/
import { i18n } from '@kbn/i18n';
+import { EmbeddableStateWithType } from 'src/plugins/embeddable/common';
import {
EmbeddableFactoryDefinition,
IContainer,
@@ -13,8 +14,10 @@ import {
import '../index.scss';
import { MAP_SAVED_OBJECT_TYPE, APP_ICON } from '../../common/constants';
import { getMapEmbeddableDisplayName } from '../../common/i18n_getters';
-import { MapByReferenceInput, MapEmbeddableInput } from './types';
+import { MapByReferenceInput, MapEmbeddableInput, MapByValueInput } from './types';
import { lazyLoadMapModules } from '../lazy_load_bundle';
+// @ts-expect-error
+import { extractReferences } from '../../common/migrations/references';
export class MapEmbeddableFactory implements EmbeddableFactoryDefinition {
type = MAP_SAVED_OBJECT_TYPE;
@@ -61,4 +64,16 @@ export class MapEmbeddableFactory implements EmbeddableFactoryDefinition {
parent
);
};
+
+ extract(state: EmbeddableStateWithType) {
+ const maybeMapByValueInput = state as EmbeddableStateWithType | MapByValueInput;
+
+ if ((maybeMapByValueInput as MapByValueInput).attributes !== undefined) {
+ const { references } = extractReferences(maybeMapByValueInput);
+
+ return { state, references };
+ }
+
+ return { state, references: [] };
+ }
}
diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/common/schemas.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/common/schemas.ts
index d97820f010a80..bfe450d240b08 100644
--- a/x-pack/plugins/security_solution/common/detection_engine/schemas/common/schemas.ts
+++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/common/schemas.ts
@@ -444,13 +444,19 @@ export const threat_technique = t.intersection([
]);
export type ThreatTechnique = t.TypeOf;
export const threat_techniques = t.array(threat_technique);
-export const threat = t.exact(
- t.type({
- framework: threat_framework,
- tactic: threat_tactic,
- technique: threat_techniques,
- })
-);
+export const threat = t.intersection([
+ t.exact(
+ t.type({
+ framework: threat_framework,
+ tactic: threat_tactic,
+ })
+ ),
+ t.exact(
+ t.partial({
+ technique: threat_techniques,
+ })
+ ),
+]);
export type Threat = t.TypeOf;
export const threats = t.array(threat);
diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/add_prepackged_rules_schema.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/add_prepackged_rules_schema.test.ts
index 93094e3445488..f3bef5ad7445f 100644
--- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/add_prepackged_rules_schema.test.ts
+++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/add_prepackged_rules_schema.test.ts
@@ -924,7 +924,7 @@ describe('add prepackaged rules schema', () => {
expect(message.schema).toEqual({});
});
- test('You cannot send in an array of threat that are missing "technique"', () => {
+ test('You can send in an array of threat that are missing "technique"', () => {
const payload: Omit & {
threat: Array>>;
} = {
@@ -944,10 +944,21 @@ describe('add prepackaged rules schema', () => {
const decoded = addPrepackagedRulesSchema.decode(payload);
const checked = exactCheck(payload, decoded);
const message = pipe(checked, foldLeftRight);
- expect(getPaths(left(message.errors))).toEqual([
- 'Invalid value "undefined" supplied to "threat,technique"',
- ]);
- expect(message.schema).toEqual({});
+ expect(getPaths(left(message.errors))).toEqual([]);
+ const expected: AddPrepackagedRulesSchemaDecoded = {
+ ...getAddPrepackagedRulesSchemaDecodedMock(),
+ threat: [
+ {
+ framework: 'fake',
+ tactic: {
+ id: 'fakeId',
+ name: 'fakeName',
+ reference: 'fakeRef',
+ },
+ },
+ ],
+ };
+ expect(message.schema).toEqual(expected);
});
test('You can optionally send in an array of false positives', () => {
diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/import_rules_schema.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/import_rules_schema.test.ts
index a59c873658411..2caedd2e01193 100644
--- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/import_rules_schema.test.ts
+++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/import_rules_schema.test.ts
@@ -926,7 +926,7 @@ describe('import rules schema', () => {
expect(message.schema).toEqual({});
});
- test('You cannot send in an array of threat that are missing "technique"', () => {
+ test('You can send in an array of threat that are missing "technique"', () => {
const payload: Omit & {
threat: Array>>;
} = {
@@ -946,10 +946,21 @@ describe('import rules schema', () => {
const decoded = importRulesSchema.decode(payload);
const checked = exactCheck(payload, decoded);
const message = pipe(checked, foldLeftRight);
- expect(getPaths(left(message.errors))).toEqual([
- 'Invalid value "undefined" supplied to "threat,technique"',
- ]);
- expect(message.schema).toEqual({});
+ expect(getPaths(left(message.errors))).toEqual([]);
+ const expected: ImportRulesSchemaDecoded = {
+ ...getImportRulesSchemaDecodedMock(),
+ threat: [
+ {
+ framework: 'fake',
+ tactic: {
+ id: 'fakeId',
+ name: 'fakeName',
+ reference: 'fakeRef',
+ },
+ },
+ ],
+ };
+ expect(message.schema).toEqual(expected);
});
test('You can optionally send in an array of false positives', () => {
diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/patch_rules_schema.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/patch_rules_schema.test.ts
index 8cdb85a555451..3dfa12acc29d5 100644
--- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/patch_rules_schema.test.ts
+++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/patch_rules_schema.test.ts
@@ -973,7 +973,7 @@ describe('patch_rules_schema', () => {
expect(message.schema).toEqual({});
});
- test('threat is invalid when updated with missing technique', () => {
+ test('threat is valid when updated with missing technique', () => {
const threat: Omit = [
{
framework: 'fake',
@@ -993,10 +993,8 @@ describe('patch_rules_schema', () => {
const decoded = patchRulesSchema.decode(payload);
const checked = exactCheck(payload, decoded);
const message = pipe(checked, foldLeftRight);
- expect(getPaths(left(message.errors))).toEqual([
- 'Invalid value "undefined" supplied to "threat,technique"',
- ]);
- expect(message.schema).toEqual({});
+ expect(getPaths(left(message.errors))).toEqual([]);
+ expect(message.schema).toEqual(payload);
});
test('validates with timeline_id and timeline_title', () => {
diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.test.ts
index 6b8211b23088c..70ff921d3b334 100644
--- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.test.ts
+++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.test.ts
@@ -618,7 +618,7 @@ describe('create rules schema', () => {
expect(message.schema).toEqual({});
});
- test('You cannot send in an array of threat that are missing "technique"', () => {
+ test('You can send in an array of threat that are missing "technique"', () => {
const payload = {
...getCreateRulesSchemaMock(),
threat: [
@@ -636,10 +636,8 @@ describe('create rules schema', () => {
const decoded = createRulesSchema.decode(payload);
const checked = exactCheck(payload, decoded);
const message = pipe(checked, foldLeftRight);
- expect(getPaths(left(message.errors))).toEqual([
- 'Invalid value "undefined" supplied to "threat,technique"',
- ]);
- expect(message.schema).toEqual({});
+ expect(getPaths(left(message.errors))).toEqual([]);
+ expect(message.schema).toEqual(payload);
});
test('You can optionally send in an array of false positives', () => {
diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_rules/indicator_match_rule.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_rules/indicator_match_rule.spec.ts
index 966ce3098d6a7..ef9c7f49cb371 100644
--- a/x-pack/plugins/security_solution/cypress/integration/detection_rules/indicator_match_rule.spec.ts
+++ b/x-pack/plugins/security_solution/cypress/integration/detection_rules/indicator_match_rule.spec.ts
@@ -60,12 +60,15 @@ import {
} from '../../tasks/alerts';
import {
changeRowsPerPageTo300,
+ duplicateFirstRule,
+ duplicateRuleFromMenu,
filterByCustomRules,
goToCreateNewRule,
goToRuleDetails,
waitForRulesTableToBeLoaded,
} from '../../tasks/alerts_detection_rules';
-import { cleanKibana } from '../../tasks/common';
+import { createCustomIndicatorRule } from '../../tasks/api_calls/rules';
+import { cleanKibana, reload } from '../../tasks/common';
import {
createAndActivateRule,
fillAboutRuleAndContinue,
@@ -92,8 +95,10 @@ import {
waitForAlertsToPopulate,
waitForTheRuleToBeExecuted,
} from '../../tasks/create_new_rule';
+import { waitForKibana } from '../../tasks/edit_rule';
import { esArchiverLoad, esArchiverUnload } from '../../tasks/es_archiver';
import { loginAndWaitForPageWithoutDateRange } from '../../tasks/login';
+import { goBackToAllRulesTable } from '../../tasks/rule_details';
import { DETECTIONS_URL, RULE_CREATION } from '../../urls/navigation';
@@ -465,5 +470,30 @@ describe('indicator match', () => {
cy.get(ALERT_RULE_RISK_SCORE).first().should('have.text', newThreatIndicatorRule.riskScore);
});
});
+
+ describe('Duplicates the indicator rule', () => {
+ beforeEach(() => {
+ cleanKibana();
+ loginAndWaitForPageWithoutDateRange(DETECTIONS_URL);
+ goToManageAlertsDetectionRules();
+ createCustomIndicatorRule(newThreatIndicatorRule);
+ reload();
+ });
+
+ it('Allows the rule to be duplicated from the table', () => {
+ waitForKibana();
+ duplicateFirstRule();
+ cy.contains(RULE_NAME, `${newThreatIndicatorRule.name} [Duplicate]`);
+ });
+
+ it('Allows the rule to be duplicated from the edit screen', () => {
+ waitForKibana();
+ goToRuleDetails();
+ duplicateRuleFromMenu();
+ goBackToAllRulesTable();
+ reload();
+ cy.contains(RULE_NAME, `${newThreatIndicatorRule.name} [Duplicate]`);
+ });
+ });
});
});
diff --git a/x-pack/plugins/security_solution/cypress/screens/alerts_detection_rules.ts b/x-pack/plugins/security_solution/cypress/screens/alerts_detection_rules.ts
index 68baad7d3d259..30365c9bd4c70 100644
--- a/x-pack/plugins/security_solution/cypress/screens/alerts_detection_rules.ts
+++ b/x-pack/plugins/security_solution/cypress/screens/alerts_detection_rules.ts
@@ -17,6 +17,12 @@ export const DELETE_RULE_ACTION_BTN = '[data-test-subj="deleteRuleAction"]';
export const EDIT_RULE_ACTION_BTN = '[data-test-subj="editRuleAction"]';
+export const DUPLICATE_RULE_ACTION_BTN = '[data-test-subj="duplicateRuleAction"]';
+
+export const DUPLICATE_RULE_MENU_PANEL_BTN = '[data-test-subj="rules-details-duplicate-rule"]';
+
+export const REFRESH_BTN = '[data-test-subj="refreshRulesAction"] button';
+
export const DELETE_RULE_BULK_BTN = '[data-test-subj="deleteRuleBulk"]';
export const ELASTIC_RULES_BTN = '[data-test-subj="showElasticRulesFilterButton"]';
diff --git a/x-pack/plugins/security_solution/cypress/tasks/alerts_detection_rules.ts b/x-pack/plugins/security_solution/cypress/tasks/alerts_detection_rules.ts
index 3553889449e6d..529ef4afdfa63 100644
--- a/x-pack/plugins/security_solution/cypress/tasks/alerts_detection_rules.ts
+++ b/x-pack/plugins/security_solution/cypress/tasks/alerts_detection_rules.ts
@@ -31,6 +31,8 @@ import {
RULE_AUTO_REFRESH_IDLE_MODAL_CONTINUE,
rowsPerPageSelector,
pageSelector,
+ DUPLICATE_RULE_ACTION_BTN,
+ DUPLICATE_RULE_MENU_PANEL_BTN,
} from '../screens/alerts_detection_rules';
import { ALL_ACTIONS, DELETE_RULE } from '../screens/rule_details';
@@ -45,6 +47,33 @@ export const editFirstRule = () => {
cy.get(EDIT_RULE_ACTION_BTN).click();
};
+export const duplicateFirstRule = () => {
+ cy.get(COLLAPSED_ACTION_BTN).should('be.visible');
+ cy.get(COLLAPSED_ACTION_BTN).first().click({ force: true });
+ cy.get(DUPLICATE_RULE_ACTION_BTN).should('be.visible');
+ cy.get(DUPLICATE_RULE_ACTION_BTN).click();
+};
+
+/**
+ * Duplicates the rule from the menu and does additional
+ * pipes and checking that the elements are present on the
+ * page as well as removed when doing the clicks to help reduce
+ * flake.
+ */
+export const duplicateRuleFromMenu = () => {
+ cy.get(ALL_ACTIONS).should('be.visible');
+ cy.root()
+ .pipe(($el) => {
+ $el.find(ALL_ACTIONS).trigger('click');
+ return $el.find(DUPLICATE_RULE_MENU_PANEL_BTN);
+ })
+ .should(($el) => expect($el).to.be.visible);
+ // Because of a fade effect and fast clicking this can produce more than one click
+ cy.get(DUPLICATE_RULE_MENU_PANEL_BTN)
+ .pipe(($el) => $el.trigger('click'))
+ .should('not.be.visible');
+};
+
export const deleteFirstRule = () => {
cy.get(COLLAPSED_ACTION_BTN).first().click({ force: true });
cy.get(DELETE_RULE_ACTION_BTN).click();
diff --git a/x-pack/plugins/security_solution/cypress/tasks/api_calls/rules.ts b/x-pack/plugins/security_solution/cypress/tasks/api_calls/rules.ts
index ab6063f5809c4..99f5bd9c20230 100644
--- a/x-pack/plugins/security_solution/cypress/tasks/api_calls/rules.ts
+++ b/x-pack/plugins/security_solution/cypress/tasks/api_calls/rules.ts
@@ -5,7 +5,7 @@
* 2.0.
*/
-import { CustomRule } from '../../objects/rule';
+import { CustomRule, ThreatIndicatorRule } from '../../objects/rule';
export const createCustomRule = (rule: CustomRule, ruleId = 'rule_testing') =>
cy.request({
@@ -29,6 +29,44 @@ export const createCustomRule = (rule: CustomRule, ruleId = 'rule_testing') =>
failOnStatusCode: false,
});
+export const createCustomIndicatorRule = (rule: ThreatIndicatorRule, ruleId = 'rule_testing') =>
+ cy.request({
+ method: 'POST',
+ url: 'api/detection_engine/rules',
+ body: {
+ rule_id: ruleId,
+ risk_score: parseInt(rule.riskScore, 10),
+ description: rule.description,
+ interval: '10s',
+ name: rule.name,
+ severity: rule.severity.toLocaleLowerCase(),
+ type: 'threat_match',
+ threat_mapping: [
+ {
+ entries: [
+ {
+ field: rule.indicatorMapping,
+ type: 'mapping',
+ value: rule.indicatorMapping,
+ },
+ ],
+ },
+ ],
+ threat_query: '*:*',
+ threat_language: 'kuery',
+ threat_filters: [],
+ threat_index: ['mock*'],
+ threat_indicator_path: '',
+ from: 'now-17520h',
+ index: ['exceptions-*'],
+ query: rule.customQuery || '*:*',
+ language: 'kuery',
+ enabled: false,
+ },
+ headers: { 'kbn-xsrf': 'cypress-creds' },
+ failOnStatusCode: false,
+ });
+
export const createCustomRuleActivated = (rule: CustomRule, ruleId = '1') =>
cy.request({
method: 'POST',
diff --git a/x-pack/plugins/security_solution/public/cases/components/connectors/servicenow/translations.ts b/x-pack/plugins/security_solution/public/cases/components/connectors/servicenow/translations.ts
index 0867dc41eeb78..77c263385df0a 100644
--- a/x-pack/plugins/security_solution/public/cases/components/connectors/servicenow/translations.ts
+++ b/x-pack/plugins/security_solution/public/cases/components/connectors/servicenow/translations.ts
@@ -87,7 +87,7 @@ export const PRIORITY = i18n.translate(
export const ALERT_FIELDS_LABEL = i18n.translate(
'xpack.securitySolution.components.connectors.serviceNow.alertFieldsTitle',
{
- defaultMessage: 'Fields associated with alerts',
+ defaultMessage: 'Select Observables to push',
}
);
diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/helpers.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/helpers.tsx
index 7e2da88a58f18..af3e427056867 100644
--- a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/helpers.tsx
+++ b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/helpers.tsx
@@ -157,49 +157,54 @@ export const buildThreatDescription = ({ label, threat }: BuildThreatDescription
: `${singleThreat.tactic.name} (${singleThreat.tactic.id})`}
- {singleThreat.technique.map((technique, techniqueIndex) => {
- const myTechnique = techniquesOptions.find((t) => t.id === technique.id);
- return (
-
-
- {myTechnique != null
- ? myTechnique.label
- : `${technique.name} (${technique.id})`}
-
-
- {technique.subtechnique != null &&
- technique.subtechnique.map((subtechnique, subtechniqueIndex) => {
- const mySubtechnique = subtechniquesOptions.find(
- (t) => t.id === subtechnique.id
- );
- return (
-
- {
+ const myTechnique = techniquesOptions.find((t) => t.id === technique.id);
+ return (
+
+
+ {myTechnique != null
+ ? myTechnique.label
+ : `${technique.name} (${technique.id})`}
+
+
+ {technique.subtechnique != null &&
+ technique.subtechnique.map((subtechnique, subtechniqueIndex) => {
+ const mySubtechnique = subtechniquesOptions.find(
+ (t) => t.id === subtechnique.id
+ );
+ return (
+
- {mySubtechnique != null
- ? mySubtechnique.label
- : `${subtechnique.name} (${subtechnique.id})`}
-
-
- );
- })}
-
-
- );
- })}
+
+ {mySubtechnique != null
+ ? mySubtechnique.label
+ : `${subtechnique.name} (${subtechnique.id})`}
+
+
+ );
+ })}
+
+
+ );
+ })}
);
diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/mitre/helpers.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/mitre/helpers.test.tsx
index da18f28257452..2a083ef89ab19 100644
--- a/x-pack/plugins/security_solution/public/detections/components/rules/mitre/helpers.test.tsx
+++ b/x-pack/plugins/security_solution/public/detections/components/rules/mitre/helpers.test.tsx
@@ -8,7 +8,7 @@
import { getValidThreat } from '../../../mitre/valid_threat_mock';
import { hasSubtechniqueOptions } from './helpers';
-const mockTechniques = getValidThreat()[0].technique;
+const mockTechniques = getValidThreat()[0].technique ?? [];
describe('helpers', () => {
describe('hasSubtechniqueOptions', () => {
diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/mitre/subtechnique_fields.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/mitre/subtechnique_fields.tsx
index e3c771534beda..d283c19bd13da 100644
--- a/x-pack/plugins/security_solution/public/detections/components/rules/mitre/subtechnique_fields.tsx
+++ b/x-pack/plugins/security_solution/public/detections/components/rules/mitre/subtechnique_fields.tsx
@@ -51,45 +51,46 @@ export const MitreAttackSubtechniqueFields: React.FC = ({
const values = field.value as Threats;
const technique = useMemo(() => {
- return values[threatIndex].technique[techniqueIndex];
- }, [values, threatIndex, techniqueIndex]);
+ return [...(values[threatIndex].technique ?? [])];
+ }, [values, threatIndex]);
const removeSubtechnique = useCallback(
(index: number) => {
const threats = [...(field.value as Threats)];
- const subtechniques = threats[threatIndex].technique[techniqueIndex].subtechnique;
+ const subtechniques = technique[techniqueIndex].subtechnique ?? [];
if (subtechniques != null) {
subtechniques.splice(index, 1);
- threats[threatIndex].technique[techniqueIndex] = {
- ...threats[threatIndex].technique[techniqueIndex],
+ technique[techniqueIndex] = {
+ ...technique[techniqueIndex],
subtechnique: subtechniques,
};
+ threats[threatIndex].technique = technique;
onFieldChange(threats);
}
},
- [field, threatIndex, onFieldChange, techniqueIndex]
+ [field, onFieldChange, techniqueIndex, technique, threatIndex]
);
const addMitreAttackSubtechnique = useCallback(() => {
const threats = [...(field.value as Threats)];
- const subtechniques = threats[threatIndex].technique[techniqueIndex].subtechnique;
+ const subtechniques = technique[techniqueIndex].subtechnique;
if (subtechniques != null) {
- threats[threatIndex].technique[techniqueIndex] = {
- ...threats[threatIndex].technique[techniqueIndex],
+ technique[techniqueIndex] = {
+ ...technique[techniqueIndex],
subtechnique: [...subtechniques, { id: 'none', name: 'none', reference: 'none' }],
};
} else {
- threats[threatIndex].technique[techniqueIndex] = {
- ...threats[threatIndex].technique[techniqueIndex],
+ technique[techniqueIndex] = {
+ ...technique[techniqueIndex],
subtechnique: [{ id: 'none', name: 'none', reference: 'none' }],
};
}
-
+ threats[threatIndex].technique = technique;
onFieldChange(threats);
- }, [field, threatIndex, onFieldChange, techniqueIndex]);
+ }, [field, onFieldChange, techniqueIndex, technique, threatIndex]);
const updateSubtechnique = useCallback(
(index: number, value: string) => {
@@ -99,7 +100,7 @@ export const MitreAttackSubtechniqueFields: React.FC = ({
name: '',
reference: '',
};
- const subtechniques = threats[threatIndex].technique[techniqueIndex].subtechnique;
+ const subtechniques = technique[techniqueIndex].subtechnique;
if (subtechniques != null) {
onFieldChange([
@@ -107,9 +108,9 @@ export const MitreAttackSubtechniqueFields: React.FC = ({
{
...threats[threatIndex],
technique: [
- ...threats[threatIndex].technique.slice(0, techniqueIndex),
+ ...technique.slice(0, techniqueIndex),
{
- ...threats[threatIndex].technique[techniqueIndex],
+ ...technique[techniqueIndex],
subtechnique: [
...subtechniques.slice(0, index),
{
@@ -120,19 +121,21 @@ export const MitreAttackSubtechniqueFields: React.FC = ({
...subtechniques.slice(index + 1),
],
},
- ...threats[threatIndex].technique.slice(techniqueIndex + 1),
+ ...technique.slice(techniqueIndex + 1),
],
},
...threats.slice(threatIndex + 1),
]);
}
},
- [threatIndex, techniqueIndex, onFieldChange, field]
+ [threatIndex, techniqueIndex, onFieldChange, field, technique]
);
const getSelectSubtechnique = useCallback(
(index: number, disabled: boolean, subtechnique: ThreatSubtechnique) => {
- const options = subtechniquesOptions.filter((t) => t.techniqueId === technique.id);
+ const options = subtechniquesOptions.filter(
+ (t) => t.techniqueId === technique[techniqueIndex].id
+ );
return (
<>
@@ -166,13 +169,17 @@ export const MitreAttackSubtechniqueFields: React.FC = ({
>
);
},
- [field, updateSubtechnique, technique]
+ [field, updateSubtechnique, technique, techniqueIndex]
);
+ const subtechniques = useMemo(() => {
+ return technique[techniqueIndex].subtechnique;
+ }, [technique, techniqueIndex]);
+
return (
- {technique.subtechnique != null &&
- technique.subtechnique.map((subtechnique, index) => (
+ {subtechniques != null &&
+ subtechniques.map((subtechnique, index) => (
= ({
const removeTechnique = useCallback(
(index: number) => {
const threats = [...(field.value as Threats)];
- const techniques = threats[threatIndex].technique;
+ const techniques = threats[threatIndex].technique ?? [];
techniques.splice(index, 1);
threats[threatIndex] = {
...threats[threatIndex],
@@ -73,7 +73,7 @@ export const MitreAttackTechniqueFields: React.FC = ({
threats[threatIndex] = {
...threats[threatIndex],
technique: [
- ...threats[threatIndex].technique,
+ ...(threats[threatIndex].technique ?? []),
{ id: 'none', name: 'none', reference: 'none', subtechnique: [] },
],
};
@@ -88,19 +88,20 @@ export const MitreAttackTechniqueFields: React.FC = ({
name: '',
reference: '',
};
+ const technique = threats[threatIndex].technique ?? [];
onFieldChange([
...threats.slice(0, threatIndex),
{
...threats[threatIndex],
technique: [
- ...threats[threatIndex].technique.slice(0, index),
+ ...technique.slice(0, index),
{
id,
reference,
name,
subtechnique: [],
},
- ...threats[threatIndex].technique.slice(index + 1),
+ ...technique.slice(index + 1),
],
},
...threats.slice(threatIndex + 1),
@@ -147,9 +148,11 @@ export const MitreAttackTechniqueFields: React.FC = ({
[field, updateTechnique]
);
+ const techniques = values[threatIndex].technique ?? [];
+
return (
- {values[threatIndex].technique.map((technique, index) => (
+ {techniques.map((technique, index) => (
{
history.push(getEditRuleUrl(rule.id));
@@ -41,7 +43,11 @@ export const duplicateRulesAction = async (
) => {
try {
dispatch({ type: 'loadingRuleIds', ids: ruleIds, actionType: 'duplicate' });
- const response = await duplicateRules({ rules });
+ const response = await duplicateRules({
+ // We cast this back and forth here as the front end types are not really the right io-ts ones
+ // and the two types conflict with each other.
+ rules: rules.map((rule) => transformOutput(rule as CreateRulesSchema) as Rule),
+ });
const { errors } = bucketRulesResponse(response);
if (errors.length > 0) {
displayErrorToast(
diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/columns.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/columns.tsx
index d2488bd3d043c..d2eadef48d9c7 100644
--- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/columns.tsx
+++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/columns.tsx
@@ -67,6 +67,7 @@ export const getActions = (
enabled: (rowItem: Rule) => canEditRuleWithActions(rowItem, actionsPrivileges),
},
{
+ 'data-test-subj': 'duplicateRuleAction',
description: i18n.DUPLICATE_RULE,
icon: 'copy',
name: !actionsPrivileges ? (
diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.ts
index 12e6d276c18d8..b8824d2b8798e 100644
--- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.ts
+++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.ts
@@ -182,7 +182,7 @@ export const filterEmptyThreats = (threats: Threats): Threats => {
.map((threat) => {
return {
...threat,
- technique: trimThreatsWithNoName(threat.technique).map((technique) => {
+ technique: trimThreatsWithNoName(threat.technique ?? []).map((technique) => {
return {
...technique,
subtechnique:
diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_delete/delete_action_modal.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_delete/delete_action_modal.tsx
index d82f0769c8b74..fb846d041bd17 100644
--- a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_delete/delete_action_modal.tsx
+++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_delete/delete_action_modal.tsx
@@ -123,6 +123,7 @@ export const DeleteActionModal: FC = ({
return (
= ({ closeModal, items, startAndC
return (
8.0.0)
+ * The release branch should match the release version (e.g., 7.x --> 7.0.0)
+ */
+export const mockKibanaVersion = '8.0.0';
+export const mockKibanaSemverVersion = new SemVer(mockKibanaVersion);
diff --git a/x-pack/plugins/upgrade_assistant/common/types.ts b/x-pack/plugins/upgrade_assistant/common/types.ts
index 91a19bfec3e81..6d83bdc5f36e9 100644
--- a/x-pack/plugins/upgrade_assistant/common/types.ts
+++ b/x-pack/plugins/upgrade_assistant/common/types.ts
@@ -94,7 +94,7 @@ export type ReindexSavedObject = SavedObject;
export enum ReindexWarning {
// 7.0 -> 8.0 warnings
- apmReindex,
+ customTypeName,
// 8.0 -> 9.0 warnings
}
diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/tabs.test.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/tabs.test.tsx
index ee722a3937216..b732f6806a388 100644
--- a/x-pack/plugins/upgrade_assistant/public/application/components/tabs.test.tsx
+++ b/x-pack/plugins/upgrade_assistant/public/application/components/tabs.test.tsx
@@ -6,9 +6,9 @@
*/
import React from 'react';
-import SemVer from 'semver/classes/semver';
import { mountWithIntl } from '@kbn/test/jest';
import { httpServiceMock } from 'src/core/public/mocks';
+import { mockKibanaSemverVersion } from '../../../common/constants';
import { UpgradeAssistantTabs } from './tabs';
import { LoadingState } from './types';
@@ -18,7 +18,6 @@ import { OverviewTab } from './tabs/overview';
const promisesToResolve = () => new Promise((resolve) => setTimeout(resolve, 0));
const mockHttp = httpServiceMock.createSetupContract();
-const mockKibanaVersion = new SemVer('8.0.0');
jest.mock('../app_context', () => {
return {
@@ -29,9 +28,9 @@ jest.mock('../app_context', () => {
ELASTIC_WEBSITE_URL: 'https://www.elastic.co/',
},
kibanaVersionInfo: {
- currentMajor: mockKibanaVersion.major,
- prevMajor: mockKibanaVersion.major - 1,
- nextMajor: mockKibanaVersion.major + 1,
+ currentMajor: mockKibanaSemverVersion.major,
+ prevMajor: mockKibanaSemverVersion.major - 1,
+ nextMajor: mockKibanaSemverVersion.major + 1,
},
};
},
diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/checkup_tab.test.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/checkup_tab.test.tsx
index 1ed1e0b01f65b..bf890c856239e 100644
--- a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/checkup_tab.test.tsx
+++ b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/checkup_tab.test.tsx
@@ -7,7 +7,7 @@
import { shallow } from 'enzyme';
import React from 'react';
-import SemVer from 'semver/classes/semver';
+import { mockKibanaSemverVersion } from '../../../../../common/constants';
import { LoadingState } from '../../types';
import AssistanceData from '../__fixtures__/checkup_api_response.json';
@@ -22,8 +22,6 @@ const defaultProps = {
setSelectedTabIndex: jest.fn(),
};
-const mockKibanaVersion = new SemVer('8.0.0');
-
jest.mock('../../../app_context', () => {
return {
useAppContext: () => {
@@ -33,9 +31,9 @@ jest.mock('../../../app_context', () => {
ELASTIC_WEBSITE_URL: 'https://www.elastic.co/',
},
kibanaVersionInfo: {
- currentMajor: mockKibanaVersion.major,
- prevMajor: mockKibanaVersion.major - 1,
- nextMajor: mockKibanaVersion.major + 1,
+ currentMajor: mockKibanaSemverVersion.major,
+ prevMajor: mockKibanaSemverVersion.major - 1,
+ nextMajor: mockKibanaSemverVersion.major + 1,
},
};
},
diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/index_table.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/index_table.tsx
index 67aa5d8b9d7de..292887853e4b3 100644
--- a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/index_table.tsx
+++ b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/index_table.tsx
@@ -143,9 +143,11 @@ export class IndexDeprecationTable extends React.Component<
private generateActionsColumn() {
// NOTE: this naive implementation assumes all indices in the table are
- // should show the reindex button. This should work for known usecases.
+ // should show the reindex button. This should work for known use cases.
const { indices } = this.props;
- if (!indices.find((i) => i.reindex === true)) {
+ const hasActionsColumn = Boolean(indices.find((i) => i.reindex === true));
+
+ if (hasActionsColumn === false) {
return null;
}
diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/reindex/flyout/__snapshots__/warning_step.test.tsx.snap b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/reindex/flyout/__snapshots__/warning_step.test.tsx.snap
index d92db98ae40cb..dba019550f2a1 100644
--- a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/reindex/flyout/__snapshots__/warning_step.test.tsx.snap
+++ b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/reindex/flyout/__snapshots__/warning_step.test.tsx.snap
@@ -23,30 +23,6 @@ exports[`WarningsFlyoutStep renders 1`] = `
-
- }
- documentationUrl="https://www.elastic.co/guide/en/observability/master/whats-new.html"
- label={
-
- }
- onChange={[Function]}
- warning={0}
- />
{
status: undefined,
reindexTaskPercComplete: null,
errorMessage: null,
- reindexWarnings: [ReindexWarning.apmReindex],
+ reindexWarnings: [ReindexWarning.customTypeName],
hasRequiredPrivileges: true,
} as ReindexState,
};
diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/reindex/flyout/warning_step.test.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/reindex/flyout/warning_step.test.tsx
index 9f76ef0aa78ba..d365cd82ba86c 100644
--- a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/reindex/flyout/warning_step.test.tsx
+++ b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/reindex/flyout/warning_step.test.tsx
@@ -8,6 +8,7 @@
import { I18nProvider } from '@kbn/i18n/react';
import { mount, shallow } from 'enzyme';
import React from 'react';
+import { mockKibanaSemverVersion } from '../../../../../../../../common/constants';
import { ReindexWarning } from '../../../../../../../../common/types';
import { idForWarning, WarningsFlyoutStep } from './warnings_step';
@@ -20,6 +21,11 @@ jest.mock('../../../../../../app_context', () => {
DOC_LINK_VERSION: 'current',
ELASTIC_WEBSITE_URL: 'https://www.elastic.co/',
},
+ kibanaVersionInfo: {
+ currentMajor: mockKibanaSemverVersion.major,
+ prevMajor: mockKibanaSemverVersion.major - 1,
+ nextMajor: mockKibanaSemverVersion.major + 1,
+ },
};
},
};
@@ -28,7 +34,7 @@ jest.mock('../../../../../../app_context', () => {
describe('WarningsFlyoutStep', () => {
const defaultProps = {
advanceNextStep: jest.fn(),
- warnings: [ReindexWarning.apmReindex],
+ warnings: [ReindexWarning.customTypeName],
closeFlyout: jest.fn(),
renderGlobalCallouts: jest.fn(),
};
@@ -37,19 +43,21 @@ describe('WarningsFlyoutStep', () => {
expect(shallow()).toMatchSnapshot();
});
- it('does not allow proceeding until all are checked', () => {
- const wrapper = mount(
-
-
-
- );
- const button = wrapper.find('EuiButton');
-
- button.simulate('click');
- expect(defaultProps.advanceNextStep).not.toHaveBeenCalled();
-
- wrapper.find(`input#${idForWarning(ReindexWarning.apmReindex)}`).simulate('change');
- button.simulate('click');
- expect(defaultProps.advanceNextStep).toHaveBeenCalled();
- });
+ if (mockKibanaSemverVersion.major === 7) {
+ it('does not allow proceeding until all are checked', () => {
+ const wrapper = mount(
+
+
+
+ );
+ const button = wrapper.find('EuiButton');
+
+ button.simulate('click');
+ expect(defaultProps.advanceNextStep).not.toHaveBeenCalled();
+
+ wrapper.find(`input#${idForWarning(ReindexWarning.customTypeName)}`).simulate('change');
+ button.simulate('click');
+ expect(defaultProps.advanceNextStep).toHaveBeenCalled();
+ });
+ }
});
diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/reindex/flyout/warnings_step.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/reindex/flyout/warnings_step.tsx
index 2e6b039a2fe76..f6620e4125c9a 100644
--- a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/reindex/flyout/warnings_step.tsx
+++ b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/reindex/flyout/warnings_step.tsx
@@ -10,6 +10,7 @@ import React, { useState } from 'react';
import {
EuiButton,
EuiButtonEmpty,
+ EuiCode,
EuiCallOut,
EuiCheckbox,
EuiFlexGroup,
@@ -100,9 +101,9 @@ export const WarningsFlyoutStep: React.FunctionComponent
@@ -128,25 +129,31 @@ export const WarningsFlyoutStep: React.FunctionComponent
- {warnings.includes(ReindexWarning.apmReindex) && (
+ {kibanaVersionInfo.currentMajor === 7 && warnings.includes(ReindexWarning.customTypeName) && (
_doc,
+ }}
/>
}
description={
_doc,
+ }}
/>
}
- documentationUrl={`${observabilityDocBasePath}/master/whats-new.html`}
+ documentationUrl={`${esDocBasePath}/${DOC_LINK_VERSION}/removal-of-types.html`}
/>
)}
diff --git a/x-pack/plugins/upgrade_assistant/server/lib/__fixtures__/version.ts b/x-pack/plugins/upgrade_assistant/server/lib/__fixtures__/version.ts
index 6caad4f5050fc..d93fe7920f1d7 100644
--- a/x-pack/plugins/upgrade_assistant/server/lib/__fixtures__/version.ts
+++ b/x-pack/plugins/upgrade_assistant/server/lib/__fixtures__/version.ts
@@ -5,16 +5,13 @@
* 2.0.
*/
-import { SemVer } from 'semver';
+import { mockKibanaSemverVersion } from '../../../common/constants';
-export const MOCK_VERSION_STRING = '8.0.0';
-
-export const getMockVersionInfo = (versionString = MOCK_VERSION_STRING) => {
- const currentVersion = new SemVer(versionString);
- const currentMajor = currentVersion.major;
+export const getMockVersionInfo = () => {
+ const currentMajor = mockKibanaSemverVersion.major;
return {
- currentVersion,
+ currentVersion: mockKibanaSemverVersion,
currentMajor,
prevMajor: currentMajor - 1,
nextMajor: currentMajor + 1,
diff --git a/x-pack/plugins/upgrade_assistant/server/lib/es_migration_apis.test.ts b/x-pack/plugins/upgrade_assistant/server/lib/es_migration_apis.test.ts
index 479a7475efd68..9ab8d0aa7cffb 100644
--- a/x-pack/plugins/upgrade_assistant/server/lib/es_migration_apis.test.ts
+++ b/x-pack/plugins/upgrade_assistant/server/lib/es_migration_apis.test.ts
@@ -24,6 +24,7 @@ describe('getUpgradeAssistantStatus', () => {
const resolvedIndices = {
indices: fakeIndexNames.map((f) => ({ name: f, attributes: ['open'] })),
};
+
// @ts-expect-error mock data is too loosely typed
const deprecationsResponse: DeprecationAPIResponse = _.cloneDeep(fakeDeprecations);
diff --git a/x-pack/plugins/upgrade_assistant/server/lib/es_version_precheck.test.ts b/x-pack/plugins/upgrade_assistant/server/lib/es_version_precheck.test.ts
index 25dcd2521525d..f4631f3ba459d 100644
--- a/x-pack/plugins/upgrade_assistant/server/lib/es_version_precheck.test.ts
+++ b/x-pack/plugins/upgrade_assistant/server/lib/es_version_precheck.test.ts
@@ -9,7 +9,8 @@ import { SemVer } from 'semver';
import { IScopedClusterClient, kibanaResponseFactory } from 'src/core/server';
import { coreMock } from 'src/core/server/mocks';
import { licensingMock } from '../../../../plugins/licensing/server/mocks';
-import { MOCK_VERSION_STRING, getMockVersionInfo } from './__fixtures__/version';
+import { mockKibanaVersion } from '../../common/constants';
+import { getMockVersionInfo } from './__fixtures__/version';
import {
esVersionCheck,
@@ -97,7 +98,7 @@ describe('verifyAllMatchKibanaVersion', () => {
describe('EsVersionPrecheck', () => {
beforeEach(() => {
- versionService.setup(MOCK_VERSION_STRING);
+ versionService.setup(mockKibanaVersion);
});
it('returns a 403 when callCluster fails with a 403', async () => {
diff --git a/x-pack/plugins/upgrade_assistant/server/lib/reindexing/index_settings.test.ts b/x-pack/plugins/upgrade_assistant/server/lib/reindexing/index_settings.test.ts
index 609f36c25619e..f778981b95054 100644
--- a/x-pack/plugins/upgrade_assistant/server/lib/reindexing/index_settings.test.ts
+++ b/x-pack/plugins/upgrade_assistant/server/lib/reindexing/index_settings.test.ts
@@ -5,8 +5,10 @@
* 2.0.
*/
+import { mockKibanaSemverVersion, mockKibanaVersion } from '../../../common/constants';
+import { ReindexWarning } from '../../../common/types';
import { versionService } from '../version';
-import { MOCK_VERSION_STRING, getMockVersionInfo } from '../__fixtures__/version';
+import { getMockVersionInfo } from '../__fixtures__/version';
import {
generateNewIndexName,
@@ -123,7 +125,7 @@ describe('transformFlatSettings', () => {
describe('sourceNameForIndex', () => {
beforeEach(() => {
- versionService.setup(MOCK_VERSION_STRING);
+ versionService.setup(mockKibanaVersion);
});
it('parses internal indices', () => {
@@ -144,7 +146,7 @@ describe('sourceNameForIndex', () => {
describe('generateNewIndexName', () => {
beforeEach(() => {
- versionService.setup(MOCK_VERSION_STRING);
+ versionService.setup(mockKibanaVersion);
});
it('parses internal indices', () => {
@@ -177,4 +179,26 @@ describe('getReindexWarnings', () => {
})
).toEqual([]);
});
+
+ if (mockKibanaSemverVersion.major === 7) {
+ describe('customTypeName warning', () => {
+ it('returns customTypeName for non-_doc mapping types', () => {
+ expect(
+ getReindexWarnings({
+ settings: {},
+ mappings: { doc: {} },
+ })
+ ).toEqual([ReindexWarning.customTypeName]);
+ });
+
+ it('does not return customTypeName for _doc mapping types', () => {
+ expect(
+ getReindexWarnings({
+ settings: {},
+ mappings: { _doc: {} },
+ })
+ ).toEqual([]);
+ });
+ });
+ }
});
diff --git a/x-pack/plugins/upgrade_assistant/server/lib/reindexing/index_settings.ts b/x-pack/plugins/upgrade_assistant/server/lib/reindexing/index_settings.ts
index 11cc01b69d3a5..70e1992d5b3e9 100644
--- a/x-pack/plugins/upgrade_assistant/server/lib/reindexing/index_settings.ts
+++ b/x-pack/plugins/upgrade_assistant/server/lib/reindexing/index_settings.ts
@@ -8,8 +8,7 @@
import { flow, omit } from 'lodash';
import { ReindexWarning } from '../../../common/types';
import { versionService } from '../version';
-import { FlatSettings } from './types';
-
+import { FlatSettings, FlatSettingsWithTypeName } from './types';
export interface ParsedIndexName {
cleanIndexName: string;
baseName: string;
@@ -69,11 +68,24 @@ export const generateNewIndexName = (indexName: string): string => {
* Returns an array of warnings that should be displayed to user before reindexing begins.
* @param flatSettings
*/
-export const getReindexWarnings = (flatSettings: FlatSettings): ReindexWarning[] => {
+export const getReindexWarnings = (
+ flatSettings: FlatSettingsWithTypeName | FlatSettings
+): ReindexWarning[] => {
const warnings = [
// No warnings yet for 8.0 -> 9.0
] as Array<[ReindexWarning, boolean]>;
+ if (versionService.getMajorVersion() === 7) {
+ const DEFAULT_TYPE_NAME = '_doc';
+ // In 7+ it's not possible to have more than one type anyways, so always grab the first
+ // (and only) key.
+ const typeName = Object.getOwnPropertyNames(flatSettings.mappings)[0];
+
+ const typeNameWarning = Boolean(typeName && typeName !== DEFAULT_TYPE_NAME);
+
+ warnings.push([ReindexWarning.customTypeName, typeNameWarning]);
+ }
+
return warnings.filter(([_, applies]) => applies).map(([warning, _]) => warning);
};
diff --git a/x-pack/plugins/upgrade_assistant/server/lib/reindexing/reindex_actions.test.ts b/x-pack/plugins/upgrade_assistant/server/lib/reindexing/reindex_actions.test.ts
index 59c83a05aa551..592c2d15b9c0c 100644
--- a/x-pack/plugins/upgrade_assistant/server/lib/reindexing/reindex_actions.test.ts
+++ b/x-pack/plugins/upgrade_assistant/server/lib/reindexing/reindex_actions.test.ts
@@ -19,9 +19,10 @@ import {
ReindexStatus,
ReindexStep,
} from '../../../common/types';
+import { mockKibanaVersion } from '../../../common/constants';
import { versionService } from '../version';
import { LOCK_WINDOW, ReindexActions, reindexActionsFactory } from './reindex_actions';
-import { MOCK_VERSION_STRING, getMockVersionInfo } from '../__fixtures__/version';
+import { getMockVersionInfo } from '../__fixtures__/version';
const { currentMajor, prevMajor } = getMockVersionInfo();
@@ -53,7 +54,7 @@ describe('ReindexActions', () => {
describe('createReindexOp', () => {
beforeEach(() => {
- versionService.setup(MOCK_VERSION_STRING);
+ versionService.setup(mockKibanaVersion);
client.create.mockResolvedValue();
});
diff --git a/x-pack/plugins/upgrade_assistant/server/lib/reindexing/reindex_actions.ts b/x-pack/plugins/upgrade_assistant/server/lib/reindexing/reindex_actions.ts
index 738d54c6f6d4f..fe8844b28e37a 100644
--- a/x-pack/plugins/upgrade_assistant/server/lib/reindexing/reindex_actions.ts
+++ b/x-pack/plugins/upgrade_assistant/server/lib/reindexing/reindex_actions.ts
@@ -21,8 +21,9 @@ import {
ReindexStatus,
ReindexStep,
} from '../../../common/types';
+import { versionService } from '../version';
import { generateNewIndexName } from './index_settings';
-import { FlatSettings } from './types';
+import { FlatSettings, FlatSettingsWithTypeName } from './types';
// TODO: base on elasticsearch.requestTimeout?
export const LOCK_WINDOW = moment.duration(90, 'seconds');
@@ -85,7 +86,7 @@ export interface ReindexActions {
* Retrieve index settings (in flat, dot-notation style) and mappings.
* @param indexName
*/
- getFlatSettings(indexName: string): Promise;
+ getFlatSettings(indexName: string): Promise;
// ----- Functions below are for enforcing locks around groups of indices like ML or Watcher
@@ -237,18 +238,33 @@ export const reindexActionsFactory = (
},
async getFlatSettings(indexName: string) {
- const { body: flatSettings } = await esClient.indices.get<{
- [indexName: string]: FlatSettings;
- }>({
- index: indexName,
- flat_settings: true,
- });
+ let flatSettings;
+
+ if (versionService.getMajorVersion() === 7) {
+ // On 7.x, we need to get index settings with mapping type
+ flatSettings = await esClient.indices.get<{
+ [indexName: string]: FlatSettingsWithTypeName;
+ }>({
+ index: indexName,
+ flat_settings: true,
+ // This @ts-ignore is needed on master since the flag is deprecated on >7.x
+ // @ts-ignore
+ include_type_name: true,
+ });
+ } else {
+ flatSettings = await esClient.indices.get<{
+ [indexName: string]: FlatSettings;
+ }>({
+ index: indexName,
+ flat_settings: true,
+ });
+ }
- if (!flatSettings[indexName]) {
+ if (!flatSettings.body[indexName]) {
return null;
}
- return flatSettings[indexName];
+ return flatSettings.body[indexName];
},
async _fetchAndLockIndexGroupDoc(indexGroup) {
diff --git a/x-pack/plugins/upgrade_assistant/server/lib/reindexing/reindex_service.test.ts b/x-pack/plugins/upgrade_assistant/server/lib/reindexing/reindex_service.test.ts
index 69105465a04f0..a91cf8ddeada9 100644
--- a/x-pack/plugins/upgrade_assistant/server/lib/reindexing/reindex_service.test.ts
+++ b/x-pack/plugins/upgrade_assistant/server/lib/reindexing/reindex_service.test.ts
@@ -20,10 +20,11 @@ import {
ReindexStatus,
ReindexStep,
} from '../../../common/types';
+import { mockKibanaVersion } from '../../../common/constants';
import { licensingMock } from '../../../../licensing/server/mocks';
import { LicensingPluginSetup } from '../../../../licensing/server';
-import { MOCK_VERSION_STRING, getMockVersionInfo } from '../__fixtures__/version';
+import { getMockVersionInfo } from '../__fixtures__/version';
import { esIndicesStateCheck } from '../es_indices_state_check';
import { versionService } from '../version';
@@ -88,7 +89,7 @@ describe('reindexService', () => {
licensingPluginSetup
);
- versionService.setup(MOCK_VERSION_STRING);
+ versionService.setup(mockKibanaVersion);
});
describe('hasRequiredPrivileges', () => {
@@ -215,7 +216,7 @@ describe('reindexService', () => {
'index.provided_name': indexName,
},
mappings: {
- properties: { https: { type: 'boolean' } },
+ _doc: { properties: { https: { type: 'boolean' } } },
},
});
@@ -571,7 +572,10 @@ describe('reindexService', () => {
const mlReindexedOp = {
id: '2',
- attributes: { ...reindexOp.attributes, indexName: '.reindexed-v7-ml-anomalies' },
+ attributes: {
+ ...reindexOp.attributes,
+ indexName: `.reindexed-v${prevMajor}-ml-anomalies`,
+ },
} as ReindexSavedObject;
const updatedOp = await service.processNextStep(mlReindexedOp);
diff --git a/x-pack/plugins/upgrade_assistant/server/lib/reindexing/reindex_service.ts b/x-pack/plugins/upgrade_assistant/server/lib/reindexing/reindex_service.ts
index 72bcb5330f819..1b5f91e0c53b8 100644
--- a/x-pack/plugins/upgrade_assistant/server/lib/reindexing/reindex_service.ts
+++ b/x-pack/plugins/upgrade_assistant/server/lib/reindexing/reindex_service.ts
@@ -219,7 +219,7 @@ export const reindexServiceFactory = (
.cancel({
task_id: reindexOp.attributes.reindexTaskId ?? undefined,
})
- .catch((e) => undefined); // Ignore any exceptions trying to cancel (it may have already completed).
+ .catch(() => undefined); // Ignore any exceptions trying to cancel (it may have already completed).
}
// Set index back to writable if we ever got past this point.
@@ -347,6 +347,11 @@ export const reindexServiceFactory = (
await esClient.indices.open({ index: indexName });
}
+ const flatSettings = await actions.getFlatSettings(indexName);
+ if (!flatSettings) {
+ throw error.indexNotFound(`Index ${indexName} does not exist.`);
+ }
+
const { body: startReindexResponse } = await esClient.reindex({
refresh: true,
wait_for_completion: false,
diff --git a/x-pack/plugins/upgrade_assistant/server/lib/reindexing/types.ts b/x-pack/plugins/upgrade_assistant/server/lib/reindexing/types.ts
index b24625a8c2a9d..569316e276e43 100644
--- a/x-pack/plugins/upgrade_assistant/server/lib/reindexing/types.ts
+++ b/x-pack/plugins/upgrade_assistant/server/lib/reindexing/types.ts
@@ -27,3 +27,16 @@ export interface FlatSettings {
_meta?: MetaProperties;
};
}
+
+// Specific to 7.x-8 upgrade
+export interface FlatSettingsWithTypeName {
+ settings: {
+ [key: string]: string;
+ };
+ mappings: {
+ [typeName: string]: {
+ properties?: MappingProperties;
+ _meta?: MetaProperties;
+ };
+ };
+}
diff --git a/x-pack/plugins/upgrade_assistant/server/routes/reindex_indices/reindex_indices.test.ts b/x-pack/plugins/upgrade_assistant/server/routes/reindex_indices/reindex_indices.test.ts
index 82d039ab9413a..21dded346bbd3 100644
--- a/x-pack/plugins/upgrade_assistant/server/routes/reindex_indices/reindex_indices.test.ts
+++ b/x-pack/plugins/upgrade_assistant/server/routes/reindex_indices/reindex_indices.test.ts
@@ -89,7 +89,9 @@ describe('reindex API', () => {
mockReindexService.findReindexOperation.mockResolvedValueOnce({
attributes: { indexName: 'wowIndex', status: ReindexStatus.inProgress },
});
- mockReindexService.detectReindexWarnings.mockResolvedValueOnce([ReindexWarning.apmReindex]);
+ mockReindexService.detectReindexWarnings.mockResolvedValueOnce([
+ ReindexWarning.customTypeName,
+ ]);
const resp = await routeDependencies.router.getHandler({
method: 'get',
diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/step_detail.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/step_detail.tsx
index 9fcd946df2f84..befe53219a449 100644
--- a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/step_detail.tsx
+++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/step_detail.tsx
@@ -68,16 +68,16 @@ export const StepDetail: React.FC = ({
}) => {
return (
<>
-
+
-
+
{stepName}
-
+
= ({
-
+
= (item) => {
- return {item.name};
+ return (
+
+ {item.name}
+
+ );
};
interface Props {
diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/legend.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/legend.tsx
index c746a5cc63a9b..9a66b586d1d56 100644
--- a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/legend.tsx
+++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/legend.tsx
@@ -9,18 +9,25 @@ import React from 'react';
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { IWaterfallContext } from '../context/waterfall_chart';
import { WaterfallChartProps } from './waterfall_chart';
+import { euiStyled } from '../../../../../../../../../src/plugins/kibana_react/common';
interface LegendProps {
items: Required['legendItems'];
render: Required['renderLegendItem'];
}
+const StyledFlexItem = euiStyled(EuiFlexItem)`
+ margin-right: ${(props) => props.theme.eui.paddingSizes.m};
+ max-width: 7%;
+ min-width: 160px;
+`;
+
export const Legend: React.FC = ({ items, render }) => {
return (
-
- {items.map((item, index) => {
- return {render(item, index)};
- })}
+
+ {items.map((item, index) => (
+ {render(item, index)}
+ ))}
);
};
diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/waterfall_chart.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/waterfall_chart.tsx
index 59990b29db5db..119c907f76ca1 100644
--- a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/waterfall_chart.tsx
+++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/waterfall_chart.tsx
@@ -120,8 +120,12 @@ export const WaterfallChart = ({
-
-
+
+
{shouldRenderSidebar && }
) {
@@ -188,6 +191,40 @@ export function defineRoutes(core: CoreSetup) {
}
);
+ router.put(
+ {
+ path: '/api/alerts_fixture/{id}/reset_task_status',
+ validate: {
+ params: schema.object({
+ id: schema.string(),
+ }),
+ body: schema.object({
+ status: schema.string(),
+ }),
+ },
+ },
+ async (
+ context: RequestHandlerContext,
+ req: KibanaRequest,
+ res: KibanaResponseFactory
+ ): Promise> => {
+ const { id } = req.params;
+ const { status } = req.body;
+
+ const [{ savedObjects }] = await core.getStartServices();
+ const savedObjectsWithTasksAndAlerts = await savedObjects.getScopedClient(req, {
+ includedHiddenTypes: ['task', 'alert'],
+ });
+ const alert = await savedObjectsWithTasksAndAlerts.get('alert', id);
+ const result = await savedObjectsWithTasksAndAlerts.update(
+ 'task',
+ alert.attributes.scheduledTaskId!,
+ { status }
+ );
+ return res.ok({ body: result });
+ }
+ );
+
router.get(
{
path: '/api/alerts_fixture/api_keys_pending_invalidation',
diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/index.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/index.ts
index c1f65fab3669e..e8cc8ea699e17 100644
--- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/index.ts
+++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/index.ts
@@ -11,8 +11,7 @@ import { setupSpacesAndUsers, tearDown } from '..';
// eslint-disable-next-line import/no-default-export
export default function alertingTests({ loadTestFile, getService }: FtrProviderContext) {
describe('Alerts', () => {
- // FLAKY: https://github.com/elastic/kibana/issues/86952
- describe.skip('legacy alerts', () => {
+ describe('legacy alerts', () => {
before(async () => {
await setupSpacesAndUsers(getService);
});
diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/rbac_legacy.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/rbac_legacy.ts
index ef5914965ddce..3db3565374740 100644
--- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/rbac_legacy.ts
+++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/rbac_legacy.ts
@@ -77,6 +77,7 @@ export default function alertTests({ getService }: FtrProviderContext) {
case 'space_1_all at space1':
case 'superuser at space1':
case 'space_1_all_with_restricted_fixture at space1':
+ await resetTaskStatus(migratedAlertId);
await ensureLegacyAlertHasBeenMigrated(migratedAlertId);
await updateMigratedAlertToUseApiKeyOfCurrentUser(migratedAlertId);
@@ -92,6 +93,7 @@ export default function alertTests({ getService }: FtrProviderContext) {
await ensureAlertIsRunning();
break;
case 'global_read at space1':
+ await resetTaskStatus(migratedAlertId);
await ensureLegacyAlertHasBeenMigrated(migratedAlertId);
await updateMigratedAlertToUseApiKeyOfCurrentUser(migratedAlertId);
@@ -115,6 +117,7 @@ export default function alertTests({ getService }: FtrProviderContext) {
});
break;
case 'space_1_all_alerts_none_actions at space1':
+ await resetTaskStatus(migratedAlertId);
await ensureLegacyAlertHasBeenMigrated(migratedAlertId);
await updateMigratedAlertToUseApiKeyOfCurrentUser(migratedAlertId);
@@ -140,6 +143,21 @@ export default function alertTests({ getService }: FtrProviderContext) {
throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`);
}
+ async function resetTaskStatus(alertId: string) {
+ // occasionally when the task manager starts running while the alert saved objects
+ // are mid-migration, the task will fail and set its status to "failed". this prevents
+ // the alert from running ever again and downstream tasks that depend on successful alert
+ // execution will fail. this ensures the task status is set to "idle" so the
+ // task manager will continue claiming and executing it.
+ await supertest
+ .put(`${getUrlPrefix(space.id)}/api/alerts_fixture/${alertId}/reset_task_status`)
+ .set('kbn-xsrf', 'foo')
+ .send({
+ status: 'idle',
+ })
+ .expect(200);
+ }
+
async function ensureLegacyAlertHasBeenMigrated(alertId: string) {
const getResponse = await supertestWithoutAuth
.get(`${getUrlPrefix(space.id)}/api/alerts/alert/${alertId}`)
diff --git a/x-pack/test/functional/apps/transform/cloning.ts b/x-pack/test/functional/apps/transform/cloning.ts
index d804f0ef14cf8..665c126e00a01 100644
--- a/x-pack/test/functional/apps/transform/cloning.ts
+++ b/x-pack/test/functional/apps/transform/cloning.ts
@@ -159,7 +159,7 @@ export default function ({ getService }: FtrProviderContext) {
await transform.table.filterWithSearchString(testData.originalConfig.id, 1);
await transform.testExecution.logTestStep('should show the actions popover');
- await transform.table.assertTransformRowActions(false);
+ await transform.table.assertTransformRowActions(testData.originalConfig.id, false);
await transform.testExecution.logTestStep('should display the define pivot step');
await transform.table.clickTransformRowAction('Clone');
diff --git a/x-pack/test/functional/apps/transform/deleting.ts b/x-pack/test/functional/apps/transform/deleting.ts
new file mode 100644
index 0000000000000..bdba06454c5c2
--- /dev/null
+++ b/x-pack/test/functional/apps/transform/deleting.ts
@@ -0,0 +1,136 @@
+/*
+ * 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 { TRANSFORM_STATE } from '../../../../plugins/transform/common/constants';
+
+import { FtrProviderContext } from '../../ftr_provider_context';
+import { getLatestTransformConfig, getPivotTransformConfig } from './index';
+
+export default function ({ getService }: FtrProviderContext) {
+ const esArchiver = getService('esArchiver');
+ const transform = getService('transform');
+
+ describe('deleting', function () {
+ const PREFIX = 'deleting';
+
+ const testDataList = [
+ {
+ suiteTitle: 'batch transform with pivot configuration',
+ originalConfig: getPivotTransformConfig(PREFIX, false),
+ expected: {
+ row: {
+ status: TRANSFORM_STATE.STOPPED,
+ mode: 'batch',
+ progress: 100,
+ },
+ },
+ },
+ {
+ suiteTitle: 'continuous transform with pivot configuration',
+ originalConfig: getPivotTransformConfig(PREFIX, true),
+ expected: {
+ row: {
+ status: TRANSFORM_STATE.STOPPED,
+ mode: 'continuous',
+ progress: undefined,
+ },
+ },
+ },
+ {
+ suiteTitle: 'batch transform with latest configuration',
+ originalConfig: getLatestTransformConfig(PREFIX),
+ transformDescription: 'updated description',
+ transformDocsPerSecond: '1000',
+ transformFrequency: '10m',
+ expected: {
+ messageText: 'updated transform.',
+ row: {
+ status: TRANSFORM_STATE.STOPPED,
+ mode: 'batch',
+ progress: 100,
+ },
+ },
+ },
+ ];
+
+ before(async () => {
+ await esArchiver.loadIfNeeded('ml/ecommerce');
+ await transform.testResources.createIndexPatternIfNeeded('ft_ecommerce', 'order_date');
+
+ for (const testData of testDataList) {
+ await transform.api.createAndRunTransform(
+ testData.originalConfig.id,
+ testData.originalConfig
+ );
+ }
+
+ await transform.testResources.setKibanaTimeZoneToUTC();
+ await transform.securityUI.loginAsTransformPowerUser();
+ });
+
+ after(async () => {
+ for (const testData of testDataList) {
+ await transform.testResources.deleteIndexPatternByTitle(testData.originalConfig.dest.index);
+ await transform.api.deleteIndices(testData.originalConfig.dest.index);
+ }
+ await transform.api.cleanTransformIndices();
+ });
+
+ for (const testData of testDataList) {
+ describe(`${testData.suiteTitle}`, function () {
+ it('delete transform', async () => {
+ await transform.testExecution.logTestStep('should load the home page');
+ await transform.navigation.navigateTo();
+ await transform.management.assertTransformListPageExists();
+
+ await transform.testExecution.logTestStep('should display the transforms table');
+ await transform.management.assertTransformsTableExists();
+
+ if (testData.expected.row.mode === 'continuous') {
+ await transform.testExecution.logTestStep('should have the delete action disabled');
+ await transform.table.assertTransformRowActionEnabled(
+ testData.originalConfig.id,
+ 'Delete',
+ false
+ );
+
+ await transform.testExecution.logTestStep('should stop the transform');
+ await transform.table.clickTransformRowActionWithRetry(
+ testData.originalConfig.id,
+ 'Stop'
+ );
+ }
+
+ await transform.testExecution.logTestStep('should display the stopped transform');
+ await transform.table.assertTransformRowFields(testData.originalConfig.id, {
+ id: testData.originalConfig.id,
+ description: testData.originalConfig.description,
+ status: testData.expected.row.status,
+ mode: testData.expected.row.mode,
+ progress: testData.expected.row.progress,
+ });
+
+ await transform.testExecution.logTestStep('should show the delete modal');
+ await transform.table.assertTransformRowActionEnabled(
+ testData.originalConfig.id,
+ 'Delete',
+ true
+ );
+ await transform.table.clickTransformRowActionWithRetry(
+ testData.originalConfig.id,
+ 'Delete'
+ );
+ await transform.table.assertTransformDeleteModalExists();
+
+ await transform.testExecution.logTestStep('should delete the transform');
+ await transform.table.confirmDeleteTransform();
+ await transform.table.assertTransformRowNotExists(testData.originalConfig.id);
+ });
+ });
+ }
+ });
+}
diff --git a/x-pack/test/functional/apps/transform/editing.ts b/x-pack/test/functional/apps/transform/editing.ts
index 71a7cf02df1fd..1f0bb058bdc38 100644
--- a/x-pack/test/functional/apps/transform/editing.ts
+++ b/x-pack/test/functional/apps/transform/editing.ts
@@ -109,7 +109,7 @@ export default function ({ getService }: FtrProviderContext) {
await transform.table.filterWithSearchString(testData.originalConfig.id, 1);
await transform.testExecution.logTestStep('should show the actions popover');
- await transform.table.assertTransformRowActions(false);
+ await transform.table.assertTransformRowActions(testData.originalConfig.id, false);
await transform.testExecution.logTestStep('should show the edit flyout');
await transform.table.clickTransformRowAction('Edit');
diff --git a/x-pack/test/functional/apps/transform/index.ts b/x-pack/test/functional/apps/transform/index.ts
index 63d8d0b51bc8c..1440f0a3f9a09 100644
--- a/x-pack/test/functional/apps/transform/index.ts
+++ b/x-pack/test/functional/apps/transform/index.ts
@@ -6,7 +6,10 @@
*/
import { FtrProviderContext } from '../../ftr_provider_context';
-import { TransformLatestConfig } from '../../../../plugins/transform/common/types/transform';
+import {
+ TransformLatestConfig,
+ TransformPivotConfig,
+} from '../../../../plugins/transform/common/types/transform';
export default function ({ getService, loadTestFile }: FtrProviderContext) {
const esArchiver = getService('esArchiver');
@@ -41,6 +44,8 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) {
loadTestFile(require.resolve('./cloning'));
loadTestFile(require.resolve('./editing'));
loadTestFile(require.resolve('./feature_controls'));
+ loadTestFile(require.resolve('./deleting'));
+ loadTestFile(require.resolve('./starting'));
});
}
export interface ComboboxOption {
@@ -80,20 +85,46 @@ export function isLatestTransformTestData(arg: any): arg is LatestTransformTestD
return arg.type === 'latest';
}
-export function getLatestTransformConfig(): TransformLatestConfig {
+export function getPivotTransformConfig(
+ prefix: string,
+ continuous?: boolean
+): TransformPivotConfig {
const timestamp = Date.now();
return {
- id: `ec_cloning_2_${timestamp}`,
+ id: `ec_${prefix}_pivot_${timestamp}_${continuous ? 'cont' : 'batch'}`,
+ source: { index: ['ft_ecommerce'] },
+ pivot: {
+ group_by: { category: { terms: { field: 'category.keyword' } } },
+ aggregations: { 'products.base_price.avg': { avg: { field: 'products.base_price' } } },
+ },
+ description: `ecommerce ${
+ continuous ? 'continuous' : 'batch'
+ } transform with avg(products.base_price) grouped by terms(category.keyword)`,
+ dest: { index: `user-ec_2_${timestamp}` },
+ ...(continuous ? { sync: { time: { field: 'order_date', delay: '60s' } } } : {}),
+ };
+}
+
+export function getLatestTransformConfig(
+ prefix: string,
+ continuous?: boolean
+): TransformLatestConfig {
+ const timestamp = Date.now();
+ return {
+ id: `ec_${prefix}_latest_${timestamp}_${continuous ? 'cont' : 'batch'}`,
source: { index: ['ft_ecommerce'] },
latest: {
unique_key: ['category.keyword'],
sort: 'order_date',
},
- description: 'ecommerce batch transform with category unique key and sorted by order date',
+ description: `ecommerce ${
+ continuous ? 'continuous' : 'batch'
+ } transform with category unique key and sorted by order date`,
frequency: '3s',
settings: {
max_page_search_size: 250,
},
dest: { index: `user-ec_3_${timestamp}` },
+ ...(continuous ? { sync: { time: { field: 'order_date', delay: '60s' } } } : {}),
};
}
diff --git a/x-pack/test/functional/apps/transform/starting.ts b/x-pack/test/functional/apps/transform/starting.ts
new file mode 100644
index 0000000000000..4b0b6f8dade66
--- /dev/null
+++ b/x-pack/test/functional/apps/transform/starting.ts
@@ -0,0 +1,106 @@
+/*
+ * 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 { FtrProviderContext } from '../../ftr_provider_context';
+import { TRANSFORM_STATE } from '../../../../plugins/transform/common/constants';
+import { getLatestTransformConfig, getPivotTransformConfig } from './index';
+
+export default function ({ getService }: FtrProviderContext) {
+ const esArchiver = getService('esArchiver');
+ const transform = getService('transform');
+
+ describe('starting', function () {
+ const PREFIX = 'starting';
+ const testDataList = [
+ {
+ suiteTitle: 'batch transform with pivot configuration',
+ originalConfig: getPivotTransformConfig(PREFIX, false),
+ mode: 'batch',
+ },
+ {
+ suiteTitle: 'continuous transform with pivot configuration',
+ originalConfig: getPivotTransformConfig(PREFIX, true),
+ mode: 'continuous',
+ },
+ {
+ suiteTitle: 'batch transform with latest configuration',
+ originalConfig: getLatestTransformConfig(PREFIX, false),
+ mode: 'batch',
+ },
+ {
+ suiteTitle: 'continuous transform with latest configuration',
+ originalConfig: getLatestTransformConfig(PREFIX, true),
+ mode: 'continuous',
+ },
+ ];
+
+ before(async () => {
+ await esArchiver.loadIfNeeded('ml/ecommerce');
+ await transform.testResources.createIndexPatternIfNeeded('ft_ecommerce', 'order_date');
+
+ for (const testData of testDataList) {
+ await transform.api.createTransform(testData.originalConfig.id, testData.originalConfig);
+ }
+ await transform.testResources.setKibanaTimeZoneToUTC();
+ await transform.securityUI.loginAsTransformPowerUser();
+ });
+
+ after(async () => {
+ for (const testData of testDataList) {
+ await transform.testResources.deleteIndexPatternByTitle(testData.originalConfig.dest.index);
+ await transform.api.deleteIndices(testData.originalConfig.dest.index);
+ }
+
+ await transform.api.cleanTransformIndices();
+ });
+
+ for (const testData of testDataList) {
+ const transformId = testData.originalConfig.id;
+
+ describe(`${testData.suiteTitle}`, function () {
+ it('start transform', async () => {
+ await transform.testExecution.logTestStep('should load the home page');
+ await transform.navigation.navigateTo();
+ await transform.management.assertTransformListPageExists();
+
+ await transform.testExecution.logTestStep('should display the transforms table');
+ await transform.management.assertTransformsTableExists();
+
+ await transform.testExecution.logTestStep(
+ 'should display the original transform in the transform list'
+ );
+ await transform.table.filterWithSearchString(transformId, 1);
+
+ await transform.testExecution.logTestStep('should start the transform');
+ await transform.table.assertTransformRowActionEnabled(transformId, 'Start', true);
+ await transform.table.clickTransformRowActionWithRetry(transformId, 'Start');
+ await transform.table.confirmStartTransform();
+ await transform.table.clearSearchString(testDataList.length);
+
+ if (testData.mode === 'continuous') {
+ await transform.testExecution.logTestStep('should display the started transform');
+ await transform.table.assertTransformRowStatusNotEql(
+ testData.originalConfig.id,
+ TRANSFORM_STATE.STOPPED
+ );
+ } else {
+ await transform.table.assertTransformRowProgressGreaterThan(transformId, 0);
+ }
+
+ await transform.table.assertTransformRowStatusNotEql(
+ testData.originalConfig.id,
+ TRANSFORM_STATE.FAILED
+ );
+ await transform.table.assertTransformRowStatusNotEql(
+ testData.originalConfig.id,
+ TRANSFORM_STATE.ABORTING
+ );
+ });
+ });
+ }
+ });
+}
diff --git a/x-pack/test/functional/services/transform/management.ts b/x-pack/test/functional/services/transform/management.ts
index fdfd1d1d9b40f..807c3d49e344c 100644
--- a/x-pack/test/functional/services/transform/management.ts
+++ b/x-pack/test/functional/services/transform/management.ts
@@ -5,8 +5,11 @@
* 2.0.
*/
+import { ProvidedType } from '@kbn/test/types/ftr';
import { FtrProviderContext } from '../../ftr_provider_context';
+export type TransformManagement = ProvidedType;
+
export function TransformManagementProvider({ getService }: FtrProviderContext) {
const testSubjects = getService('testSubjects');
diff --git a/x-pack/test/functional/services/transform/transform_table.ts b/x-pack/test/functional/services/transform/transform_table.ts
index 72626580e9461..ce2625677e479 100644
--- a/x-pack/test/functional/services/transform/transform_table.ts
+++ b/x-pack/test/functional/services/transform/transform_table.ts
@@ -12,6 +12,7 @@ import { FtrProviderContext } from '../../ftr_provider_context';
export function TransformTableProvider({ getService }: FtrProviderContext) {
const retry = getService('retry');
const testSubjects = getService('testSubjects');
+ const browser = getService('browser');
return new (class TransformTable {
public async parseTransformTable() {
@@ -129,21 +130,63 @@ export function TransformTableProvider({ getService }: FtrProviderContext) {
const filteredRows = rows.filter((row) => row.id === filter);
expect(filteredRows).to.have.length(
expectedRowCount,
- `Filtered DFA job table should have ${expectedRowCount} row(s) for filter '${filter}' (got matching items '${filteredRows}')`
+ `Filtered Transform table should have ${expectedRowCount} row(s) for filter '${filter}' (got matching items '${filteredRows}')`
);
}
- public async assertTransformRowFields(transformId: string, expectedRow: object) {
+ public async clearSearchString(expectedRowCount: number = 1) {
+ await this.waitForTransformsToLoad();
+ const tableListContainer = await testSubjects.find('transformListTableContainer');
+ const searchBarInput = await tableListContainer.findByClassName('euiFieldSearch');
+ await searchBarInput.clearValueWithKeyboard();
const rows = await this.parseTransformTable();
- const transformRow = rows.filter((row) => row.id === transformId)[0];
- expect(transformRow).to.eql(
- expectedRow,
- `Expected transform row to be '${JSON.stringify(expectedRow)}' (got '${JSON.stringify(
- transformRow
- )}')`
+ expect(rows).to.have.length(
+ expectedRowCount,
+ `Transform table should have ${expectedRowCount} row(s) after clearing search' (got '${rows.length}')`
);
}
+ public async assertTransformRowFields(transformId: string, expectedRow: object) {
+ await retry.tryForTime(30 * 1000, async () => {
+ await this.refreshTransformList();
+ const rows = await this.parseTransformTable();
+ const transformRow = rows.filter((row) => row.id === transformId)[0];
+ expect(transformRow).to.eql(
+ expectedRow,
+ `Expected transform row to be '${JSON.stringify(expectedRow)}' (got '${JSON.stringify(
+ transformRow
+ )}')`
+ );
+ });
+ }
+
+ public async assertTransformRowProgressGreaterThan(
+ transformId: string,
+ expectedProgress: number
+ ) {
+ await retry.tryForTime(30 * 1000, async () => {
+ await this.refreshTransformList();
+ const rows = await this.parseTransformTable();
+ const transformRow = rows.filter((row) => row.id === transformId)[0];
+ expect(transformRow.progress).to.greaterThan(
+ 0,
+ `Expected transform row progress to be greater than '${expectedProgress}' (got '${transformRow.progress}')`
+ );
+ });
+ }
+
+ public async assertTransformRowStatusNotEql(transformId: string, status: string) {
+ await retry.tryForTime(30 * 1000, async () => {
+ await this.refreshTransformList();
+ const rows = await this.parseTransformTable();
+ const transformRow = rows.filter((row) => row.id === transformId)[0];
+ expect(transformRow.status).to.not.eql(
+ status,
+ `Expected transform row status to not be '${status}' (got '${transformRow.status}')`
+ );
+ });
+ }
+
public async assertTransformExpandedRow() {
await testSubjects.click('transformListRowDetailsToggle');
@@ -185,8 +228,13 @@ export function TransformTableProvider({ getService }: FtrProviderContext) {
});
}
- public async assertTransformRowActions(isTransformRunning = false) {
- await testSubjects.click('euiCollapsedItemActionsButton');
+ public rowSelector(transformId: string, subSelector?: string) {
+ const row = `~transformListTable > ~row-${transformId}`;
+ return !subSelector ? row : `${row} > ${subSelector}`;
+ }
+
+ public async assertTransformRowActions(transformId: string, isTransformRunning = false) {
+ await testSubjects.click(this.rowSelector(transformId, 'euiCollapsedItemActionsButton'));
await testSubjects.existOrFail('transformActionClone');
await testSubjects.existOrFail('transformActionDelete');
@@ -201,6 +249,42 @@ export function TransformTableProvider({ getService }: FtrProviderContext) {
}
}
+ public async assertTransformRowActionEnabled(
+ transformId: string,
+ action: 'Delete' | 'Start' | 'Stop' | 'Clone' | 'Edit',
+ expectedValue: boolean
+ ) {
+ const selector = `transformAction${action}`;
+ await retry.tryForTime(60 * 1000, async () => {
+ await this.refreshTransformList();
+
+ await browser.pressKeys(browser.keys.ESCAPE);
+ await testSubjects.click(this.rowSelector(transformId, 'euiCollapsedItemActionsButton'));
+
+ await testSubjects.existOrFail(selector);
+ const isEnabled = await testSubjects.isEnabled(selector);
+ expect(isEnabled).to.eql(
+ expectedValue,
+ `Expected '${action}' button to be '${expectedValue ? 'enabled' : 'disabled'}' (got '${
+ isEnabled ? 'enabled' : 'disabled'
+ }')`
+ );
+ });
+ }
+
+ public async clickTransformRowActionWithRetry(
+ transformId: string,
+ action: 'Delete' | 'Start' | 'Stop' | 'Clone' | 'Edit'
+ ) {
+ await retry.tryForTime(30 * 1000, async () => {
+ await browser.pressKeys(browser.keys.ESCAPE);
+ await testSubjects.click(this.rowSelector(transformId, 'euiCollapsedItemActionsButton'));
+ await testSubjects.existOrFail(`transformAction${action}`);
+ await testSubjects.click(`transformAction${action}`);
+ await testSubjects.missingOrFail(`transformAction${action}`);
+ });
+ }
+
public async clickTransformRowAction(action: string) {
await testSubjects.click(`transformAction${action}`);
}
@@ -214,5 +298,53 @@ export function TransformTableProvider({ getService }: FtrProviderContext) {
await this.waitForTransformsExpandedRowPreviewTabToLoad();
await this.assertEuiDataGridColumnValues('transformPivotPreview', column, values);
}
+
+ public async assertTransformDeleteModalExists() {
+ await testSubjects.existOrFail('transformDeleteModal', { timeout: 60 * 1000 });
+ }
+
+ public async assertTransformDeleteModalNotExists() {
+ await testSubjects.missingOrFail('transformDeleteModal', { timeout: 60 * 1000 });
+ }
+
+ public async assertTransformStartModalExists() {
+ await testSubjects.existOrFail('transformStartModal', { timeout: 60 * 1000 });
+ }
+
+ public async assertTransformStartModalNotExists() {
+ await testSubjects.missingOrFail('transformStartModal', { timeout: 60 * 1000 });
+ }
+
+ public async confirmDeleteTransform() {
+ await retry.tryForTime(30 * 1000, async () => {
+ await this.assertTransformDeleteModalExists();
+ await testSubjects.click('transformDeleteModal > confirmModalConfirmButton');
+ await this.assertTransformDeleteModalNotExists();
+ });
+ }
+
+ public async assertTransformRowNotExists(transformId: string) {
+ await retry.tryForTime(30 * 1000, async () => {
+ // If after deletion, and there's no transform left
+ const noTransformsFoundMessageExists = await testSubjects.exists(
+ 'transformNoTransformsFound'
+ );
+
+ if (noTransformsFoundMessageExists) {
+ return true;
+ } else {
+ // Checks that the tranform was deleted
+ await this.filterWithSearchString(transformId, 0);
+ }
+ });
+ }
+
+ public async confirmStartTransform() {
+ await retry.tryForTime(30 * 1000, async () => {
+ await this.assertTransformStartModalExists();
+ await testSubjects.click('transformStartModal > confirmModalConfirmButton');
+ await this.assertTransformStartModalNotExists();
+ });
+ }
})();
}
diff --git a/yarn.lock b/yarn.lock
index 4a3399ece1fd0..7c93a37dffe6f 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -19991,10 +19991,10 @@ lodash.uniq@4.5.0, lodash.uniq@^4.5.0:
resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773"
integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=
-lodash@4.17.11, lodash@4.17.15, lodash@>4.17.4, lodash@^4, lodash@^4.0.0, lodash@^4.0.1, lodash@^4.10.0, lodash@^4.15.0, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.4, lodash@^4.2.0, lodash@~4.17.10, lodash@~4.17.15, lodash@~4.17.19, lodash@~4.17.20:
- version "4.17.20"
- resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52"
- integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==
+lodash@4.17.11, lodash@4.17.15, lodash@>4.17.4, lodash@^4, lodash@^4.0.0, lodash@^4.0.1, lodash@^4.10.0, lodash@^4.15.0, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.21, lodash@^4.17.4, lodash@^4.2.0, lodash@~4.17.10, lodash@~4.17.15, lodash@~4.17.19, lodash@~4.17.20:
+ version "4.17.21"
+ resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
+ integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
log-ok@^0.1.1:
version "0.1.1"