From 7afae17948b92a4955f6d258262db1465bd88514 Mon Sep 17 00:00:00 2001
From: Stratoula Kalafateli
Date: Mon, 31 Jan 2022 11:52:51 +0200
Subject: [PATCH 01/65] [Lens] displays custom bounds error for right axis when
lower bound is above 0 (#124037)
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
---
.../lens/public/xy_visualization/xy_config_panel/index.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/index.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/index.tsx
index 3a757c539f08e..099ea258f0902 100644
--- a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/index.tsx
+++ b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/index.tsx
@@ -314,7 +314,7 @@ export const XyToolbar = memo(function XyToolbar(
);
const hasBarOrAreaOnRightAxis = Boolean(
axisGroups
- .find((group) => group.groupId === 'left')
+ .find((group) => group.groupId === 'right')
?.series?.some((series) => {
const seriesType = state.layers.find((l) => l.layerId === series.layer)?.seriesType;
return seriesType?.includes('bar') || seriesType?.includes('area');
From 17134697a0012ff1d2c96fb149b73cbfffe47ad7 Mon Sep 17 00:00:00 2001
From: Muhammad Ibragimov <53621505+mibragimov@users.noreply.github.com>
Date: Mon, 31 Jan 2022 14:58:13 +0500
Subject: [PATCH 02/65] [Console] Fix autocomplete inserting comma in triple
quotes (#123572)
* Fix autocomplete inserting comma in triple quotes
* Fix inserting commas and flaky test
* Fixed problems on triple quotes and single quotes replacement.
* Fixed cursor position after adding a comma to the prefix.
* Final generic solution for multiple edge cases.
Co-authored-by: Muhammad Ibragimov
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: Yaroslav Kuznietsov
---
.../models/sense_editor/integration.test.js | 21 +---
.../public/lib/autocomplete/autocomplete.ts | 107 +++++++++++++++---
test/functional/apps/console/_console.ts | 18 +--
test/functional/page_objects/console_page.ts | 12 +-
4 files changed, 112 insertions(+), 46 deletions(-)
diff --git a/src/plugins/console/public/application/models/sense_editor/integration.test.js b/src/plugins/console/public/application/models/sense_editor/integration.test.js
index a342c3429d03d..e60b4175f668f 100644
--- a/src/plugins/console/public/application/models/sense_editor/integration.test.js
+++ b/src/plugins/console/public/application/models/sense_editor/integration.test.js
@@ -324,7 +324,7 @@ describe('Integration', () => {
' "field": "something"\n' +
' },\n' +
' "facets": {},\n' +
- ' "size": 20 \n' +
+ ' "size": 20\n' +
'}',
MAPPING,
SEARCH_KB,
@@ -357,31 +357,18 @@ describe('Integration', () => {
autoCompleteSet: ['facets', 'query', 'size'],
},
{
- name: 'prefix comma, beginning of line',
+ name: 'prefix comma, end of line',
cursor: { lineNumber: 7, column: 1 },
initialValue: '',
addTemplate: true,
- prefixToAdd: '',
+ prefixToAdd: ',\n',
suffixToAdd: '',
rangeToReplace: {
- start: { lineNumber: 7, column: 1 },
+ start: { lineNumber: 6, column: 14 },
end: { lineNumber: 7, column: 1 },
},
autoCompleteSet: ['facets', 'query', 'size'],
},
- {
- name: 'prefix comma, end of line',
- cursor: { lineNumber: 6, column: 15 },
- initialValue: '',
- addTemplate: true,
- prefixToAdd: '',
- suffixToAdd: '',
- rangeToReplace: {
- start: { lineNumber: 6, column: 15 },
- end: { lineNumber: 6, column: 15 },
- },
- autoCompleteSet: ['facets', 'query', 'size'],
- },
]
);
diff --git a/src/plugins/console/public/lib/autocomplete/autocomplete.ts b/src/plugins/console/public/lib/autocomplete/autocomplete.ts
index 43fdde3f02349..63d43a849a681 100644
--- a/src/plugins/console/public/lib/autocomplete/autocomplete.ts
+++ b/src/plugins/console/public/lib/autocomplete/autocomplete.ts
@@ -335,6 +335,20 @@ export default function ({
});
}
+ function replaceLinesWithPrefixPieces(prefixPieces: string[], startLineNumber: number) {
+ const middlePiecesCount = prefixPieces.length - 1;
+ prefixPieces.forEach((piece, index) => {
+ if (index >= middlePiecesCount) {
+ return;
+ }
+ const line = startLineNumber + index + 1;
+ const column = editor.getLineValue(line).length - 1;
+ const start = { lineNumber: line, column: 0 };
+ const end = { lineNumber: line, column };
+ editor.replace({ start, end }, piece);
+ });
+ }
+
function applyTerm(term: {
value?: string;
context?: AutoCompleteContext;
@@ -390,12 +404,36 @@ export default function ({
templateInserted = false;
}
}
+ const linesToMoveDown = (context.prefixToAdd ?? '').match(/\n|\r/g)?.length ?? 0;
- valueToInsert = context.prefixToAdd + valueToInsert + context.suffixToAdd;
+ let prefix = context.prefixToAdd ?? '';
// disable listening to the changes we are making.
editor.off('changeSelection', editorChangeListener);
+ // if should add chars on the previous not empty line
+ if (linesToMoveDown) {
+ const [firstPart = '', ...prefixPieces] = context.prefixToAdd?.split(/\n|\r/g) ?? [];
+ const lastPart = _.last(prefixPieces) ?? '';
+ const { start } = context.rangeToReplace!;
+ const end = { ...start, column: start.column + firstPart.length };
+
+ // adding only the content of prefix before newlines
+ editor.replace({ start, end }, firstPart);
+
+ // replacing prefix pieces without the last one, which is handled separately
+ if (prefixPieces.length - 1 > 0) {
+ replaceLinesWithPrefixPieces(prefixPieces, start.lineNumber);
+ }
+
+ // and the last prefix line, keeping the editor's own newlines.
+ prefix = lastPart;
+ context.rangeToReplace!.start.lineNumber = context.rangeToReplace!.end.lineNumber;
+ context.rangeToReplace!.start.column = 0;
+ }
+
+ valueToInsert = prefix + valueToInsert + context.suffixToAdd;
+
if (context.rangeToReplace!.start.column !== context.rangeToReplace!.end.column) {
editor.replace(context.rangeToReplace!, valueToInsert);
} else {
@@ -410,7 +448,7 @@ export default function ({
column:
context.rangeToReplace!.start.column +
termAsString.length +
- context.prefixToAdd!.length +
+ prefix.length +
(templateInserted ? 0 : context.suffixToAdd!.length),
};
@@ -671,6 +709,47 @@ export default function ({
}
}
+ function addCommaToPrefixOnAutocomplete(
+ nonEmptyToken: Token | null,
+ context: AutoCompleteContext,
+ charsToSkipOnSameLine: number = 1
+ ) {
+ if (nonEmptyToken && nonEmptyToken.type.indexOf('url') < 0) {
+ const { position } = nonEmptyToken;
+ // if not on the first line
+ if (context.rangeToReplace && context.rangeToReplace.start?.lineNumber > 1) {
+ const prevTokenLineNumber = position.lineNumber;
+ const line = context.editor?.getLineValue(prevTokenLineNumber) ?? '';
+ const prevLineLength = line.length;
+ const linesToEnter = context.rangeToReplace.end.lineNumber - prevTokenLineNumber;
+
+ const isTheSameLine = linesToEnter === 0;
+ let startColumn = prevLineLength + 1;
+ let spaces = context.rangeToReplace.start.column - 1;
+
+ if (isTheSameLine) {
+ // prevent last char line from replacing
+ startColumn = position.column + charsToSkipOnSameLine;
+ // one char for pasted " and one for ,
+ spaces = context.rangeToReplace.end.column - startColumn - 2;
+ }
+
+ // go back to the end of the previous line
+ context.rangeToReplace = {
+ start: { lineNumber: prevTokenLineNumber, column: startColumn },
+ end: { ...context.rangeToReplace.end },
+ };
+
+ spaces = spaces >= 0 ? spaces : 0;
+ const spacesToEnter = isTheSameLine ? (spaces === 0 ? 1 : spaces) : spaces;
+ const newLineChars = `\n`.repeat(linesToEnter >= 0 ? linesToEnter : 0);
+ const whitespaceChars = ' '.repeat(spacesToEnter);
+ // add a comma at the end of the previous line, a new line and indentation
+ context.prefixToAdd = `,${newLineChars}${whitespaceChars}`;
+ }
+ }
+ }
+
function addBodyPrefixSuffixToContext(context: AutoCompleteContext) {
// Figure out what happens next to the token to see whether it needs trailing commas etc.
@@ -758,23 +837,19 @@ export default function ({
case 'paren.lparen':
case 'punctuation.comma':
case 'punctuation.colon':
+ case 'punctuation.start_triple_quote':
case 'method':
break;
+ case 'text':
+ case 'string':
+ case 'constant.numeric':
+ case 'constant.language.boolean':
+ case 'punctuation.end_triple_quote':
+ addCommaToPrefixOnAutocomplete(nonEmptyToken, context, nonEmptyToken?.value.length);
+ break;
default:
- if (nonEmptyToken && nonEmptyToken.type.indexOf('url') < 0) {
- const { position, value } = nonEmptyToken;
-
- // We can not rely on prefixToAdd here, because it adds a comma at the beginning of the new token
- // Since we have access to the position of the previous token here, this could be a good place to insert a comma manually
- context.prefixToAdd = '';
- editor.insert(
- {
- column: position.column + value.length,
- lineNumber: position.lineNumber,
- },
- ', '
- );
- }
+ addCommaToPrefixOnAutocomplete(nonEmptyToken, context);
+ break;
}
return context;
diff --git a/test/functional/apps/console/_console.ts b/test/functional/apps/console/_console.ts
index 7a8a36bec56d7..e4ddf5d6bb602 100644
--- a/test/functional/apps/console/_console.ts
+++ b/test/functional/apps/console/_console.ts
@@ -92,24 +92,24 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
);
});
- // Flaky, see https://github.com/elastic/kibana/issues/123556
- it.skip('should add comma after previous non empty line on autocomplete', async () => {
- const LINE_NUMBER = 2;
+ it('should add comma after previous non empty line on autocomplete', async () => {
+ const LINE_NUMBER = 4;
await PageObjects.console.dismissTutorial();
await PageObjects.console.clearTextArea();
+ await PageObjects.console.enterRequest();
+
await PageObjects.console.enterText(`{\n\t"query": {\n\t\t"match": {}`);
await PageObjects.console.pressEnter();
await PageObjects.console.pressEnter();
await PageObjects.console.pressEnter();
await PageObjects.console.promptAutocomplete();
+ await PageObjects.console.pressEnter();
- await retry.try(async () => {
- const textOfPreviousNonEmptyLine = await PageObjects.console.getVisibleTextAt(LINE_NUMBER);
- log.debug(textOfPreviousNonEmptyLine);
- const lastChar = textOfPreviousNonEmptyLine.charAt(textOfPreviousNonEmptyLine.length - 1);
- expect(lastChar).to.be.equal(',');
- });
+ const textOfPreviousNonEmptyLine = await PageObjects.console.getVisibleTextAt(LINE_NUMBER);
+ log.debug(textOfPreviousNonEmptyLine);
+ const lastChar = textOfPreviousNonEmptyLine.charAt(textOfPreviousNonEmptyLine.length - 1);
+ expect(lastChar).to.be.equal(',');
});
describe('with a data URI in the load_from query', () => {
diff --git a/test/functional/page_objects/console_page.ts b/test/functional/page_objects/console_page.ts
index f8a64c0032bb2..4fdc47756e710 100644
--- a/test/functional/page_objects/console_page.ts
+++ b/test/functional/page_objects/console_page.ts
@@ -110,9 +110,11 @@ export class ConsolePageObject extends FtrService {
private async getEditorTextArea() {
// This focusses the cursor on the bottom of the text area
- const editor = await this.getEditor();
- const content = await editor.findByCssSelector('.ace_content');
- await content.click();
+ await this.retry.try(async () => {
+ const editor = await this.getEditor();
+ const content = await editor.findByCssSelector('.ace_content');
+ await content.click();
+ });
return await this.testSubjects.find('console-textarea');
}
@@ -137,6 +139,8 @@ export class ConsolePageObject extends FtrService {
public async clearTextArea() {
const textArea = await this.getEditorTextArea();
- await textArea.clearValueWithKeyboard();
+ await this.retry.try(async () => {
+ await textArea.clearValueWithKeyboard();
+ });
}
}
From 7c4eea4dfef6b0c9024fec07210a6ed81ff58b95 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Alejandro=20Fern=C3=A1ndez=20Haro?=
Date: Mon, 31 Jan 2022 11:04:49 +0100
Subject: [PATCH 03/65] [Usage Collection] Report succeeded/failed/notReady
collectors stats (#123956)
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
---
src/plugins/telemetry/schema/oss_root.json | 56 +++++++++++
.../server/collector/collector_set.test.ts | 93 ++++++++++++++++++-
.../server/collector/collector_set.ts | 46 +++++++--
3 files changed, 185 insertions(+), 10 deletions(-)
diff --git a/src/plugins/telemetry/schema/oss_root.json b/src/plugins/telemetry/schema/oss_root.json
index e526dc6413916..cf9b881facef2 100644
--- a/src/plugins/telemetry/schema/oss_root.json
+++ b/src/plugins/telemetry/schema/oss_root.json
@@ -194,6 +194,62 @@
"properties": {
"kibana_config_usage": {
"type": "pass_through"
+ },
+ "usage_collector_stats": {
+ "properties": {
+ "not_ready": {
+ "properties": {
+ "count": {
+ "type": "short"
+ },
+ "names": {
+ "type": "array",
+ "items": {
+ "type": "keyword"
+ }
+ }
+ }
+ },
+ "not_ready_timeout": {
+ "properties": {
+ "count": {
+ "type": "short"
+ },
+ "names": {
+ "type": "array",
+ "items": {
+ "type": "keyword"
+ }
+ }
+ }
+ },
+ "succeeded": {
+ "properties": {
+ "count": {
+ "type": "short"
+ },
+ "names": {
+ "type": "array",
+ "items": {
+ "type": "keyword"
+ }
+ }
+ }
+ },
+ "failed": {
+ "properties": {
+ "count": {
+ "type": "short"
+ },
+ "names": {
+ "type": "array",
+ "items": {
+ "type": "keyword"
+ }
+ }
+ }
+ }
+ }
}
}
}
diff --git a/src/plugins/usage_collection/server/collector/collector_set.test.ts b/src/plugins/usage_collection/server/collector/collector_set.test.ts
index 491937bc29fcb..f6d667f2959d9 100644
--- a/src/plugins/usage_collection/server/collector/collector_set.test.ts
+++ b/src/plugins/usage_collection/server/collector/collector_set.test.ts
@@ -95,6 +95,15 @@ describe('CollectorSet', () => {
type: 'MY_TEST_COLLECTOR',
result: { passTest: 1000 },
},
+ {
+ type: 'usage_collector_stats',
+ result: {
+ not_ready: { count: 0, names: [] },
+ not_ready_timeout: { count: 0, names: [] },
+ succeeded: { count: 1, names: ['MY_TEST_COLLECTOR'] },
+ failed: { count: 0, names: [] },
+ },
+ },
]);
});
@@ -115,7 +124,17 @@ describe('CollectorSet', () => {
// Do nothing
}
// This must return an empty object instead of null/undefined
- expect(result).toStrictEqual([]);
+ expect(result).toStrictEqual([
+ {
+ type: 'usage_collector_stats',
+ result: {
+ not_ready: { count: 0, names: [] },
+ not_ready_timeout: { count: 0, names: [] },
+ succeeded: { count: 0, names: [] },
+ failed: { count: 1, names: ['MY_TEST_COLLECTOR'] },
+ },
+ },
+ ]);
});
it('should not break if isReady is not a function', async () => {
@@ -135,6 +154,15 @@ describe('CollectorSet', () => {
type: 'MY_TEST_COLLECTOR',
result: { test: 1 },
},
+ {
+ type: 'usage_collector_stats',
+ result: {
+ not_ready: { count: 0, names: [] },
+ not_ready_timeout: { count: 0, names: [] },
+ succeeded: { count: 1, names: ['MY_TEST_COLLECTOR'] },
+ failed: { count: 0, names: [] },
+ },
+ },
]);
});
@@ -154,6 +182,15 @@ describe('CollectorSet', () => {
type: 'MY_TEST_COLLECTOR',
result: { test: 1 },
},
+ {
+ type: 'usage_collector_stats',
+ result: {
+ not_ready: { count: 0, names: [] },
+ not_ready_timeout: { count: 0, names: [] },
+ succeeded: { count: 1, names: ['MY_TEST_COLLECTOR'] },
+ failed: { count: 0, names: [] },
+ },
+ },
]);
});
});
@@ -535,6 +572,31 @@ describe('CollectorSet', () => {
"result": Object {},
"type": "ready_col",
},
+ Object {
+ "result": Object {
+ "failed": Object {
+ "count": 0,
+ "names": Array [],
+ },
+ "not_ready": Object {
+ "count": 1,
+ "names": Array [
+ "not_ready_col",
+ ],
+ },
+ "not_ready_timeout": Object {
+ "count": 0,
+ "names": Array [],
+ },
+ "succeeded": Object {
+ "count": 1,
+ "names": Array [
+ "ready_col",
+ ],
+ },
+ },
+ "type": "usage_collector_stats",
+ },
]
`);
});
@@ -584,6 +646,31 @@ describe('CollectorSet', () => {
"result": Object {},
"type": "ready_col",
},
+ Object {
+ "result": Object {
+ "failed": Object {
+ "count": 0,
+ "names": Array [],
+ },
+ "not_ready": Object {
+ "count": 0,
+ "names": Array [],
+ },
+ "not_ready_timeout": Object {
+ "count": 1,
+ "names": Array [
+ "timeout_col",
+ ],
+ },
+ "succeeded": Object {
+ "count": 1,
+ "names": Array [
+ "ready_col",
+ ],
+ },
+ },
+ "type": "usage_collector_stats",
+ },
]
`);
});
@@ -608,7 +695,7 @@ describe('CollectorSet', () => {
esClient: mockEsClient,
soClient: mockSoClient,
});
- expect(results).toHaveLength(1);
+ expect(results).toHaveLength(2);
});
it('adds extra context to collectors with extendFetchContext config', async () => {
@@ -634,7 +721,7 @@ describe('CollectorSet', () => {
soClient: mockSoClient,
kibanaRequest: request,
});
- expect(results).toHaveLength(1);
+ expect(results).toHaveLength(2);
});
});
});
diff --git a/src/plugins/usage_collection/server/collector/collector_set.ts b/src/plugins/usage_collection/server/collector/collector_set.ts
index 05ebb2a34202c..79fb0a39c7801 100644
--- a/src/plugins/usage_collection/server/collector/collector_set.ts
+++ b/src/plugins/usage_collection/server/collector/collector_set.ts
@@ -31,6 +31,15 @@ interface CollectorSetConfig {
maximumWaitTimeForAllCollectorsInS?: number;
collectors?: AnyCollector[];
}
+
+// Schema manually added in src/plugins/telemetry/schema/oss_root.json under `stack_stats.kibana.plugins.usage_collector_stats`
+interface CollectorStats {
+ not_ready: { count: number; names: string[] };
+ not_ready_timeout: { count: number; names: string[] };
+ succeeded: { count: number; names: string[] };
+ failed: { count: number; names: string[] };
+}
+
export class CollectorSet {
private readonly logger: Logger;
private readonly maximumWaitTimeForAllCollectorsInS: number;
@@ -101,7 +110,11 @@ export class CollectorSet {
private getReadyCollectors = async (
collectors: Map = this.collectors
- ): Promise => {
+ ): Promise<{
+ readyCollectors: AnyCollector[];
+ nonReadyCollectorTypes: string[];
+ timedOutCollectorsTypes: string[];
+ }> => {
if (!(collectors instanceof Map)) {
throw new Error(
`getReadyCollectors method given bad Map of collectors: ` + typeof collectors
@@ -162,7 +175,11 @@ export class CollectorSet {
.filter(({ isReadyWithTimeout }) => isReadyWithTimeout.value === true)
.map(({ collector }) => collector);
- return readyCollectors;
+ return {
+ readyCollectors,
+ nonReadyCollectorTypes: collectorsTypesNotReady,
+ timedOutCollectorsTypes,
+ };
};
public bulkFetch = async (
@@ -172,7 +189,16 @@ export class CollectorSet {
collectors: Map = this.collectors
) => {
this.logger.debug(`Getting ready collectors`);
- const readyCollectors = await this.getReadyCollectors(collectors);
+ const { readyCollectors, nonReadyCollectorTypes, timedOutCollectorsTypes } =
+ await this.getReadyCollectors(collectors);
+
+ const collectorStats: CollectorStats = {
+ not_ready: { count: nonReadyCollectorTypes.length, names: nonReadyCollectorTypes },
+ not_ready_timeout: { count: timedOutCollectorsTypes.length, names: timedOutCollectorsTypes },
+ succeeded: { count: 0, names: [] },
+ failed: { count: 0, names: [] },
+ };
+
const responses = await Promise.all(
readyCollectors.map(async (collector) => {
this.logger.debug(`Fetching data from ${collector.type} collector`);
@@ -182,17 +208,23 @@ export class CollectorSet {
soClient,
...(collector.extendFetchContext.kibanaRequest && { kibanaRequest }),
};
- return {
- type: collector.type,
- result: await collector.fetch(context),
- };
+ const result = await collector.fetch(context);
+ collectorStats.succeeded.names.push(collector.type);
+ return { type: collector.type, result };
} catch (err) {
this.logger.warn(err);
this.logger.warn(`Unable to fetch data from ${collector.type} collector`);
+ collectorStats.failed.names.push(collector.type);
}
})
);
+ collectorStats.succeeded.count = collectorStats.succeeded.names.length;
+ collectorStats.failed.count = collectorStats.failed.names.length;
+
+ // Treat it as just another "collector"
+ responses.push({ type: 'usage_collector_stats', result: collectorStats });
+
return responses.filter(
(response): response is { type: string; result: unknown } => typeof response !== 'undefined'
);
From 860a2074c848238597b8b9e54ee32c684dd2779d Mon Sep 17 00:00:00 2001
From: Julia Bardi <90178898+juliaElastic@users.noreply.github.com>
Date: Mon, 31 Jan 2022 11:24:02 +0100
Subject: [PATCH 04/65] fix policy validation (#124040)
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
---
.../agent_policy/create_package_policy_page/index.tsx | 8 +++++---
1 file changed, 5 insertions(+), 3 deletions(-)
diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/index.tsx
index 5d71ef6c7a703..732231e178e8d 100644
--- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/index.tsx
+++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/index.tsx
@@ -174,17 +174,17 @@ export const CreatePackagePolicyPage: React.FunctionComponent = () => {
if (updatedAgentPolicy) {
setAgentPolicy(updatedAgentPolicy);
if (packageInfo) {
- setFormState('VALID');
+ setHasAgentPolicyError(false);
}
} else {
- setFormState('INVALID');
+ setHasAgentPolicyError(true);
setAgentPolicy(undefined);
}
// eslint-disable-next-line no-console
console.debug('Agent policy updated', updatedAgentPolicy);
},
- [packageInfo, setAgentPolicy, setFormState]
+ [packageInfo, setAgentPolicy]
);
const hasErrors = validationResults ? validationHasErrors(validationResults) : false;
@@ -227,6 +227,8 @@ export const CreatePackagePolicyPage: React.FunctionComponent = () => {
const hasAgentPolicy = newPackagePolicy.policy_id && newPackagePolicy.policy_id !== '';
if (hasPackage && hasAgentPolicy && !hasValidationErrors) {
setFormState('VALID');
+ } else {
+ setFormState('INVALID');
}
},
[packagePolicy, updatePackagePolicyValidation]
From 3d9a0826764e4e56d5c3298d8bda6a8027a582bc Mon Sep 17 00:00:00 2001
From: Stratoula Kalafateli
Date: Mon, 31 Jan 2022 12:55:18 +0200
Subject: [PATCH 05/65] [XY] Normal mode percentages computation with EC
(#123941)
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
---
.../utils/compute_percentage_data.test.ts | 22 +++----
.../public/utils/compute_percentage_data.ts | 61 ++++---------------
2 files changed, 24 insertions(+), 59 deletions(-)
diff --git a/src/plugins/vis_types/xy/public/utils/compute_percentage_data.test.ts b/src/plugins/vis_types/xy/public/utils/compute_percentage_data.test.ts
index 835b6918f196f..0429bb5253b6f 100644
--- a/src/plugins/vis_types/xy/public/utils/compute_percentage_data.test.ts
+++ b/src/plugins/vis_types/xy/public/utils/compute_percentage_data.test.ts
@@ -112,7 +112,7 @@ describe('computePercentageData', () => {
},
{
'col-0-4': 'ES-Air',
- 'col-1-5': 0.08653846153846152,
+ 'col-1-5': 0.08653846153846154,
'col-2-1': 0.1346153846153846,
'col-3-1': 1,
},
@@ -130,7 +130,7 @@ describe('computePercentageData', () => {
},
{
'col-0-4': 'JetBeats',
- 'col-1-5': 0.10576923076923078,
+ 'col-1-5': 0.10576923076923077,
'col-2-1': 0.6923076923076923,
'col-3-1': 0,
},
@@ -142,14 +142,14 @@ describe('computePercentageData', () => {
},
{
'col-0-4': 'Logstash Airways',
- 'col-1-5': 0.05376344086021506,
- 'col-2-1': 0.7634408602150539,
+ 'col-1-5': 0.053763440860215055,
+ 'col-2-1': 0.7634408602150538,
'col-3-1': 1,
},
{
'col-0-4': 'Logstash Airways',
'col-1-5': 0.07526881720430108,
- 'col-2-1': 0.10752688172043012,
+ 'col-2-1': 0.10752688172043011,
'col-3-1': 0,
},
]);
@@ -171,7 +171,7 @@ describe('computePercentageData', () => {
},
{
'col-0-4': 'ES-Air',
- 'col-1-5': 0.08653846153846152,
+ 'col-1-5': 0.08653846153846154,
'col-2-1': 0.1346153846153846,
'col-3-1': 1,
},
@@ -184,12 +184,12 @@ describe('computePercentageData', () => {
{
'col-0-4': 'Kibana Airlines',
'col-1-5': 0.38095238095238093,
- 'col-2-1': 0.619047619047619,
+ 'col-2-1': 0.6190476190476191,
'col-3-1': 1,
},
{
'col-0-4': 'JetBeats',
- 'col-1-5': 0.10576923076923078,
+ 'col-1-5': 0.10576923076923077,
'col-2-1': 0.6923076923076923,
'col-3-1': 0,
},
@@ -202,13 +202,13 @@ describe('computePercentageData', () => {
{
'col-0-4': 'Logstash Airways',
'col-1-5': 0.06578947368421052,
- 'col-2-1': 0.9342105263157894,
+ 'col-2-1': 0.9342105263157895,
'col-3-1': 1,
},
{
'col-0-4': 'Logstash Airways',
- 'col-1-5': 0.411764705882353,
- 'col-2-1': 0.5882352941176472,
+ 'col-1-5': 0.4117647058823529,
+ 'col-2-1': 0.5882352941176471,
'col-3-1': 0,
},
]);
diff --git a/src/plugins/vis_types/xy/public/utils/compute_percentage_data.ts b/src/plugins/vis_types/xy/public/utils/compute_percentage_data.ts
index 67e04a980d3c9..78457c2fd56cd 100644
--- a/src/plugins/vis_types/xy/public/utils/compute_percentage_data.ts
+++ b/src/plugins/vis_types/xy/public/utils/compute_percentage_data.ts
@@ -5,9 +5,9 @@
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
-import { groupBy } from 'lodash';
-import { Accessor, AccessorFn } from '@elastic/charts';
-import { DatatableRow } from '../../../../expressions/public';
+import type { Accessor, AccessorFn } from '@elastic/charts';
+import { computeRatioByGroups } from '@elastic/charts';
+import type { DatatableRow } from '../../../../expressions/public';
export const computePercentageData = (
rows: DatatableRow[],
@@ -15,52 +15,17 @@ export const computePercentageData = (
yAccessors: string[],
splitChartAccessor?: string | null
) => {
- // Group by xAccessor
- const groupedData = groupBy(rows, function (row) {
- return row[String(xAccessor)];
- });
- // In case of small multiples, I need to group by xAccessor and splitChartAccessor
+ // compute percentage mode data
+ const groupAccessors = [String(xAccessor)];
if (splitChartAccessor) {
- for (const key in groupedData) {
- if (Object.prototype.hasOwnProperty.call(groupedData, key)) {
- const groupedBySplitData = groupBy(groupedData[key], splitChartAccessor);
- for (const newGroupKey in groupedBySplitData) {
- if (Object.prototype.hasOwnProperty.call(groupedBySplitData, newGroupKey)) {
- groupedData[`${key}-${newGroupKey}`] = groupedBySplitData[newGroupKey];
- }
- }
- }
- }
+ groupAccessors.push(splitChartAccessor);
}
- // sum up all the yAccessors per group
- const sums: Record = {};
- for (const key in groupedData) {
- if (Object.prototype.hasOwnProperty.call(groupedData, key)) {
- let sum = 0;
- const array = groupedData[key];
- array.forEach((row) => {
- for (const yAccessor of yAccessors) {
- sum += row[yAccessor];
- }
- });
- sums[key] = sum;
- }
- }
- // compute the ratio of each group
- rows.forEach((row) => {
- const groupValue = splitChartAccessor
- ? `${row[String(xAccessor)]}-${row[splitChartAccessor]}`
- : row[String(xAccessor)];
- const sum = sums[groupValue] ?? 0;
- let metricsSum = 0;
- for (const yAccessor of yAccessors) {
- metricsSum += row[yAccessor];
- }
- const computedMetric = metricsSum / sum;
- for (const yAccessor of yAccessors) {
- row[yAccessor] = (computedMetric / metricsSum) * row[yAccessor];
- }
- });
- return rows;
+ return computeRatioByGroups(
+ rows,
+ groupAccessors,
+ yAccessors.map((accessor) => {
+ return [(d) => d[accessor], (d, v) => ({ ...d, [accessor]: v })];
+ })
+ );
};
From bc74ed2c5bfe600b56343dfe9666fbc050e06175 Mon Sep 17 00:00:00 2001
From: Joe Reuter
Date: Mon, 31 Jan 2022 12:17:01 +0100
Subject: [PATCH 06/65] [Lens] Stabilize drag and drop test (#124007)
---
x-pack/test/functional/page_objects/lens_page.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/x-pack/test/functional/page_objects/lens_page.ts b/x-pack/test/functional/page_objects/lens_page.ts
index 7cbed96328727..d859a17ec9982 100644
--- a/x-pack/test/functional/page_objects/lens_page.ts
+++ b/x-pack/test/functional/page_objects/lens_page.ts
@@ -204,7 +204,7 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont
testSubjects.getCssSelector('lnsWorkspace')
);
await this.waitForLensDragDropToFinish();
- await PageObjects.header.waitUntilLoadingHasFinished();
+ await this.waitForVisualization();
},
/**
From a791ed6043a91227e1022343f5911584ee792214 Mon Sep 17 00:00:00 2001
From: Kuldeep M
Date: Mon, 31 Jan 2022 11:20:51 +0000
Subject: [PATCH 07/65] [Workplace search] make use of panels more consistent
(#122335)
* fix 1786 source connection panel vertical alignment
* Update x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configured_sources_list.tsx
Co-authored-by: Constance
* change panels and nested panels to better reflect EUI guidelines
* change source panels into split panels
* restore status icons
* make available for configuration sources a list
* make available for configuration sources a better list
* improve configuration sources list
* improve configuration sources list columns
* align private source card text
* fix to pass checks
* move styles to CSS file and use EUI variables
* fix to pass checks
* switch to using flex grid to make source list
* improve source list spacing
* add disabled state for available sources list items
* fix to pass checks
* improve source list spacing
* move platinum license tooltip to connect button
* switch out EuiText for EuiButtonEmpty for source cards
* increase flex grid gutter
* change panels and nested panels to better reflect EUI guidelines
* change source panels into split panels
* restore status icons
* make available for configuration sources a list
* make available for configuration sources a better list
* improve configuration sources list
* improve configuration sources list columns
* align private source card text
* fix to pass checks
* move styles to CSS file and use EUI variables
* fix to pass checks
* switch to using flex grid to make source list
* improve source list spacing
* add disabled state for available sources list items
* fix to pass checks
* improve source list spacing
* move platinum license tooltip to connect button
* switch out EuiText for EuiButtonEmpty for source cards
* increase flex grid gutter
* remove unused imports
* fix jest tests
* replace CSS value with with EUI variable
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: Constance
---
.../components/add_source/add_source.scss | 8 +-
.../available_sources_list.test.tsx | 4 +-
.../add_source/available_sources_list.tsx | 95 ++++++++++++-------
.../configured_sources_list.test.tsx | 4 +-
.../add_source/configured_sources_list.tsx | 86 +++++++++--------
.../components/add_source/constants.ts | 2 +-
.../views/overview/organization_stats.tsx | 70 +++++++-------
.../views/overview/statistic_card.tsx | 3 +-
.../components/private_sources_table.tsx | 68 ++++++-------
.../views/security/security.tsx | 3 +-
10 files changed, 189 insertions(+), 154 deletions(-)
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source.scss b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source.scss
index fe772000f78f7..63b2d8b21ee25 100644
--- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source.scss
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source.scss
@@ -4,7 +4,6 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
-
// --------------------------------------------------
// View: Adding a Source flow
// --------------------------------------------------
@@ -79,6 +78,7 @@
.euiFlexItem:first-child {
align-self: flex-end;
}
+
.euiFlexItem:last-child {
align-self: flex-start;
}
@@ -98,3 +98,9 @@
background: transparent;
}
}
+
+@media (min-width: 768px) {
+ .organizational-content-source-item {
+ max-width: calc(33.3% - #{$euiSize});
+ }
+}
\ No newline at end of file
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/available_sources_list.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/available_sources_list.test.tsx
index 26a3d38f247ea..f168dfbea91ce 100644
--- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/available_sources_list.test.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/available_sources_list.test.tsx
@@ -13,7 +13,7 @@ import React from 'react';
import { shallow } from 'enzyme';
-import { EuiCard, EuiToolTip, EuiTitle } from '@elastic/eui';
+import { EuiToolTip, EuiTitle } from '@elastic/eui';
import { AvailableSourcesList } from './available_sources_list';
@@ -25,8 +25,8 @@ describe('AvailableSourcesList', () => {
it('renders', () => {
const wrapper = shallow();
- expect(wrapper.find(EuiCard)).toHaveLength(11);
expect(wrapper.find(EuiTitle)).toHaveLength(1);
+ expect(wrapper.find('[data-test-subj="AvailableSourceListItem"]')).toHaveLength(11);
expect(wrapper.find('[data-test-subj="CustomAPISourceLink"]')).toHaveLength(1);
});
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/available_sources_list.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/available_sources_list.tsx
index 12dd0d2c574ec..13f0f41643e16 100644
--- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/available_sources_list.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/available_sources_list.tsx
@@ -10,9 +10,10 @@ import React from 'react';
import { useValues } from 'kea';
import {
- EuiCard,
EuiFlexGrid,
+ EuiFlexGroup,
EuiFlexItem,
+ EuiHorizontalRule,
EuiSpacer,
EuiTitle,
EuiText,
@@ -21,7 +22,7 @@ import {
import { i18n } from '@kbn/i18n';
import { LicensingLogic } from '../../../../../shared/licensing';
-import { EuiLinkTo } from '../../../../../shared/react_router_helpers';
+import { EuiButtonEmptyTo, EuiLinkTo } from '../../../../../shared/react_router_helpers';
import { SourceIcon } from '../../../../components/shared/source_icon';
import { ADD_CUSTOM_PATH, getSourcesPath } from '../../../../routes';
import { SourceDataItem } from '../../../../types';
@@ -42,44 +43,70 @@ export const AvailableSourcesList: React.FC = ({ sour
const getSourceCard = ({ name, serviceType, addPath, accountContextOnly }: SourceDataItem) => {
const disabled = !hasPlatinumLicense && accountContextOnly;
+
+ const connectButton = () => {
+ if (disabled) {
+ return (
+
+
+ Connect
+
+
+ );
+ } else {
+ return (
+
+ Connect
+
+ );
+ }
+ };
+
const card = (
- >}
- isDisabled={disabled}
- icon={}
- />
+ <>
+
+
+
+
+
+ {name}
+
+ {connectButton()}
+
+
+
+ >
);
- if (disabled) {
- return (
-
- {card}
-
- );
- }
- return {card};
+ return card;
};
const visibleSources = (
-
- {sources.map((source, i) => (
-
- {getSourceCard(source)}
-
- ))}
-
+ <>
+
+ {sources.map((source, i) => (
+
+
+ {getSourceCard(source)}
+
+
+ ))}
+
+ >
);
const emptyState = (
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configured_sources_list.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configured_sources_list.test.tsx
index 2e2e04556cdb7..a1169cd582cba 100644
--- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configured_sources_list.test.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configured_sources_list.test.tsx
@@ -11,8 +11,6 @@ import React from 'react';
import { shallow } from 'enzyme';
-import { EuiPanel } from '@elastic/eui';
-
import { ConfiguredSourcesList } from './configured_sources_list';
describe('ConfiguredSourcesList', () => {
@@ -26,7 +24,7 @@ describe('ConfiguredSourcesList', () => {
expect(wrapper.find('[data-test-subj="UnConnectedTooltip"]')).toHaveLength(5);
expect(wrapper.find('[data-test-subj="AccountOnlyTooltip"]')).toHaveLength(1);
- expect(wrapper.find(EuiPanel)).toHaveLength(6);
+ expect(wrapper.find('[data-test-subj="ConfiguredSourcesListItem"]')).toHaveLength(6);
});
it('handles empty state', () => {
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configured_sources_list.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configured_sources_list.tsx
index b5b22afec39b1..ac465c43643a4 100644
--- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configured_sources_list.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configured_sources_list.tsx
@@ -8,11 +8,12 @@
import React from 'react';
import {
+ EuiButtonEmpty,
EuiFlexGrid,
EuiFlexGroup,
EuiFlexItem,
- EuiPanel,
EuiSpacer,
+ EuiSplitPanel,
EuiText,
EuiTitle,
EuiToken,
@@ -31,6 +32,7 @@ import {
CONFIGURED_SOURCES_EMPTY_STATE,
CONFIGURED_SOURCES_TITLE,
CONFIGURED_SOURCES_EMPTY_BODY,
+ ADD_SOURCE_ORG_SOURCES_TITLE,
} from './constants';
interface ConfiguredSourcesProps {
@@ -65,50 +67,52 @@ export const ConfiguredSourcesList: React.FC = ({
);
const visibleSources = (
-
+
{sources.map(({ name, serviceType, addPath, connected, accountContextOnly }, i) => (
-
-
-
-
-
-
-
-
-
-
-
- {name}
- {!connected &&
- !accountContextOnly &&
- isOrganization &&
- unConnectedTooltip}
- {accountContextOnly && isOrganization && accountOnlyTooltip}
-
-
-
-
-
- {(!isOrganization || (isOrganization && !accountContextOnly)) && (
-
-
- {CONFIGURED_SOURCES_CONNECT_BUTTON}
-
+
+
+
+
+
+
+
+
+
+
+ {name}
+ {!connected && !accountContextOnly && isOrganization && unConnectedTooltip}
+ {accountContextOnly && isOrganization && accountOnlyTooltip}
+
+
+
+
+
+ {((!isOrganization || (isOrganization && !accountContextOnly)) && (
+
+ {CONFIGURED_SOURCES_CONNECT_BUTTON}
+
+ )) || (
+
+ {ADD_SOURCE_ORG_SOURCES_TITLE}
+
)}
-
-
+
+
))}
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/constants.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/constants.ts
index cbc18f6d7a19e..3ce4f930b7a38 100644
--- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/constants.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/constants.ts
@@ -226,7 +226,7 @@ export const CONFIGURED_SOURCES_LIST_ACCOUNT_ONLY_TOOLTIP = i18n.translate(
export const CONFIGURED_SOURCES_CONNECT_BUTTON = i18n.translate(
'xpack.enterpriseSearch.workplaceSearch.contentSource.configuredSources.connectButton',
{
- defaultMessage: 'Connect',
+ defaultMessage: 'Connect another',
}
);
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/organization_stats.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/organization_stats.tsx
index 3528f97e16ad1..d90305fa92014 100644
--- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/organization_stats.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/organization_stats.tsx
@@ -9,7 +9,7 @@ import React from 'react';
import { useValues } from 'kea';
-import { EuiFlexGrid, EuiPanel } from '@elastic/eui';
+import { EuiFlexGrid } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
@@ -32,41 +32,39 @@ export const OrganizationStats: React.FC = () => {
/>
}
>
-
-
-
-
-
-
-
-
+
+
+
+
+
+
);
};
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/statistic_card.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/statistic_card.tsx
index 9b134b511b34e..9d927099b13b7 100644
--- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/statistic_card.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/statistic_card.tsx
@@ -25,6 +25,7 @@ export const StatisticCard: React.FC = ({ title, count = 0,
title={title}
titleSize="xs"
display="plain"
+ hasBorder
description={
{count}
@@ -37,7 +38,7 @@ export const StatisticCard: React.FC = ({ title, count = 0,
layout="horizontal"
title={title}
titleSize="xs"
- display="plain"
+ display="subdued"
description={
{count}
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/security/components/private_sources_table.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/security/components/private_sources_table.tsx
index 94906ab9f302d..60cc08cd6c39c 100644
--- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/security/components/private_sources_table.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/security/components/private_sources_table.tsx
@@ -100,7 +100,7 @@ export const PrivateSourcesTable: React.FC = ({
const emptyState = (
<>
-
+
{isRemote ? REMOTE_SOURCES_EMPTY_TABLE_TITLE : STANDARD_SOURCES_EMPTY_TABLE_TITLE}
@@ -115,6 +115,38 @@ export const PrivateSourcesTable: React.FC = ({
>
);
+ const sourcesTable = (
+ <>
+
+
+
+
+ {SOURCE}
+
+
+
+ {contentSources.map((source, i) => (
+
+ {source.name}
+
+ updateSource(source.id, e.target.checked)}
+ showLabel={false}
+ label={`${source.name} Toggle`}
+ data-test-subj={`${sourceType}SourceToggle`}
+ compressed
+ />
+
+
+ ))}
+
+
+
+ >
+ );
+
const sectionHeading = (
@@ -137,49 +169,19 @@ export const PrivateSourcesTable: React.FC = ({
{isRemote ? REMOTE_SOURCES_TABLE_DESCRIPTION : STANDARD_SOURCES_TABLE_DESCRIPTION}
{!hasSources && emptyState}
+ {hasSources && sourcesTable}
);
- const sourcesTable = (
- <>
-
-
-
- {SOURCE}
-
-
-
- {contentSources.map((source, i) => (
-
- {source.name}
-
- updateSource(source.id, e.target.checked)}
- showLabel={false}
- label={`${source.name} Toggle`}
- data-test-subj={`${sourceType}SourceToggle`}
- compressed
- />
-
-
- ))}
-
-
- >
- );
-
return (
{sectionHeading}
- {hasSources && sourcesTable}
);
};
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/security/security.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/security/security.tsx
index 997d79f67cb13..be397b4acbc36 100644
--- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/security/security.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/security/security.tsx
@@ -92,7 +92,6 @@ export const Security: React.FC = () => {
{
hasUnsavedChanges={unsavedChanges}
messageText={SECURITY_UNSAVED_CHANGES_MESSAGE}
/>
-
+
{allSourcesToggle}
{!hasPlatinumLicense && platinumLicenseCallout}
{sourceTables}
From bc6e30d7406f7d77dd2ddbf3f8fef5b41377ab30 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Alejandro=20Fern=C3=A1ndez=20Haro?=
Date: Mon, 31 Jan 2022 12:47:20 +0100
Subject: [PATCH 08/65] [Telemetry] Only send from active window (#123909)
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
---
src/plugins/telemetry/public/plugin.ts | 4 +++
.../public/services/telemetry_sender.test.ts | 29 +++++++++++++--
.../public/services/telemetry_sender.ts | 35 ++++++++++++++++---
3 files changed, 61 insertions(+), 7 deletions(-)
diff --git a/src/plugins/telemetry/public/plugin.ts b/src/plugins/telemetry/public/plugin.ts
index 73dc07d7a4fb9..3072ff67703d7 100644
--- a/src/plugins/telemetry/public/plugin.ts
+++ b/src/plugins/telemetry/public/plugin.ts
@@ -186,6 +186,10 @@ export class TelemetryPlugin implements Plugin {
});
describe('shouldSendReport', () => {
+ let hasFocus: jest.SpyInstance;
+
+ beforeEach(() => {
+ hasFocus = jest.spyOn(document, 'hasFocus');
+ hasFocus.mockReturnValue(true); // Return true by default for all tests;
+ });
+
+ afterEach(() => {
+ hasFocus.mockRestore();
+ });
+
+ it('returns false if the page is not visible', async () => {
+ hasFocus.mockReturnValue(false);
+ const telemetryService = mockTelemetryService();
+ telemetryService.getIsOptedIn = jest.fn().mockReturnValue(true);
+ telemetryService.fetchLastReported = jest.fn().mockResolvedValue(Date.now());
+ const telemetrySender = new TelemetrySender(telemetryService);
+ const shouldSendReport = await telemetrySender['shouldSendReport']();
+ expect(shouldSendReport).toBe(false);
+ expect(telemetryService.getIsOptedIn).toBeCalledTimes(0);
+ expect(telemetryService.fetchLastReported).toBeCalledTimes(0);
+ });
+
it('returns false whenever optIn is false', async () => {
const telemetryService = mockTelemetryService();
telemetryService.getIsOptedIn = jest.fn().mockReturnValue(false);
@@ -372,9 +395,11 @@ describe('TelemetrySender', () => {
it('calls sendIfDue every 60000 ms', () => {
const telemetryService = mockTelemetryService();
const telemetrySender = new TelemetrySender(telemetryService);
+ telemetrySender['sendIfDue'] = jest.fn().mockResolvedValue(void 0);
telemetrySender.startChecking();
- expect(setInterval).toBeCalledTimes(1);
- expect(setInterval).toBeCalledWith(telemetrySender['sendIfDue'], 60000);
+ expect(telemetrySender['sendIfDue']).toHaveBeenCalledTimes(0);
+ jest.advanceTimersByTime(60000);
+ expect(telemetrySender['sendIfDue']).toHaveBeenCalledTimes(1);
});
});
});
diff --git a/src/plugins/telemetry/public/services/telemetry_sender.ts b/src/plugins/telemetry/public/services/telemetry_sender.ts
index d0eb9142e724a..8fa2a015ef119 100644
--- a/src/plugins/telemetry/public/services/telemetry_sender.ts
+++ b/src/plugins/telemetry/public/services/telemetry_sender.ts
@@ -6,6 +6,9 @@
* Side Public License, v 1.
*/
+import type { Subscription } from 'rxjs';
+import { fromEvent, interval, merge } from 'rxjs';
+import { exhaustMap } from 'rxjs/operators';
import { LOCALSTORAGE_KEY, PAYLOAD_CONTENT_ENCODING } from '../../common/constants';
import { TelemetryService } from './telemetry_service';
import { Storage } from '../../../kibana_utils/public';
@@ -16,7 +19,7 @@ export class TelemetrySender {
private readonly telemetryService: TelemetryService;
private lastReported?: number;
private readonly storage: Storage;
- private intervalId: number = 0; // setInterval returns a positive integer, 0 means no interval is set
+ private sendIfDue$?: Subscription;
private retryCount: number = 0;
static getRetryDelay(retryCount: number) {
@@ -62,11 +65,21 @@ export class TelemetrySender {
};
/**
- * Using configuration and the lastReported dates, it decides whether a new telemetry report should be sent.
+ * Returns `true` when the page is visible and active in the browser.
+ */
+ private isActiveWindow = () => {
+ // Using `document.hasFocus()` instead of `document.visibilityState` because the latter may return "visible"
+ // if 2 windows are open side-by-side because they are "technically" visible.
+ return document.hasFocus();
+ };
+
+ /**
+ * Using configuration, page visibility state and the lastReported dates,
+ * it decides whether a new telemetry report should be sent.
* @returns `true` if a new report should be sent. `false` otherwise.
*/
private shouldSendReport = async (): Promise => {
- if (this.telemetryService.canSendTelemetry()) {
+ if (this.isActiveWindow() && this.telemetryService.canSendTelemetry()) {
return await this.isReportDue();
}
@@ -122,8 +135,20 @@ export class TelemetrySender {
};
public startChecking = () => {
- if (this.intervalId === 0) {
- this.intervalId = window.setInterval(this.sendIfDue, 60000);
+ if (!this.sendIfDue$) {
+ // Trigger sendIfDue...
+ this.sendIfDue$ = merge(
+ // ... periodically
+ interval(60000),
+ // ... when it regains `focus`
+ fromEvent(window, 'focus') // Using `window` instead of `document` because Chrome only emits on the first one.
+ )
+ .pipe(exhaustMap(this.sendIfDue))
+ .subscribe();
}
};
+
+ public stop = () => {
+ this.sendIfDue$?.unsubscribe();
+ };
}
From ecba4787f550e7dbce2f642ceb71b823de7c9d28 Mon Sep 17 00:00:00 2001
From: Esteban Beltran
Date: Mon, 31 Jan 2022 12:49:59 +0100
Subject: [PATCH 09/65] [Cases] Fixes toast message showing for non-synced
cases (#124026)
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
---
.../components/recent_cases/index.test.tsx | 2 +-
.../cases/public/containers/api.test.tsx | 1 +
.../plugins/cases/public/containers/mock.ts | 34 +++++++++++++++
.../public/containers/use_get_cases.test.tsx | 43 ++++++++++++++++++-
.../cases/public/containers/use_get_cases.tsx | 2 +-
5 files changed, 79 insertions(+), 3 deletions(-)
diff --git a/x-pack/plugins/cases/public/components/recent_cases/index.test.tsx b/x-pack/plugins/cases/public/components/recent_cases/index.test.tsx
index 0a1e0447511db..4f99f23b9b208 100644
--- a/x-pack/plugins/cases/public/components/recent_cases/index.test.tsx
+++ b/x-pack/plugins/cases/public/components/recent_cases/index.test.tsx
@@ -62,7 +62,7 @@ describe('RecentCases', () => {
);
- expect(getAllByTestId('case-details-link')).toHaveLength(5);
+ expect(getAllByTestId('case-details-link')).toHaveLength(7);
});
it('is good at rendering max cases', () => {
diff --git a/x-pack/plugins/cases/public/containers/api.test.tsx b/x-pack/plugins/cases/public/containers/api.test.tsx
index c83f5601da64b..e656544084d09 100644
--- a/x-pack/plugins/cases/public/containers/api.test.tsx
+++ b/x-pack/plugins/cases/public/containers/api.test.tsx
@@ -229,6 +229,7 @@ describe('Case Configuration API', () => {
});
test('should return correct response', async () => {
+ fetchMock.mockResolvedValue(allCasesSnake);
const resp = await getCases({
filterOptions: { ...DEFAULT_FILTER_OPTIONS, owner: [SECURITY_SOLUTION_OWNER] },
queryParams: DEFAULT_QUERY_PARAMS,
diff --git a/x-pack/plugins/cases/public/containers/mock.ts b/x-pack/plugins/cases/public/containers/mock.ts
index d5e749c46a900..c0fb9fd9f0138 100644
--- a/x-pack/plugins/cases/public/containers/mock.ts
+++ b/x-pack/plugins/cases/public/containers/mock.ts
@@ -36,6 +36,8 @@ import { covertToSnakeCase } from './utils';
export { connectorsMock } from './configure/mock';
export const basicCaseId = 'basic-case-id';
+export const caseWithAlertsId = 'case-with-alerts-id';
+export const caseWithAlertsSyncOffId = 'case-with-alerts-syncoff-id';
export const basicSubCaseId = 'basic-sub-case-id';
const basicCommentId = 'basic-comment-id';
const basicCreatedAt = '2020-02-19T23:06:33.798Z';
@@ -169,6 +171,20 @@ export const basicCase: Case = {
subCaseIds: [],
};
+export const caseWithAlerts = {
+ ...basicCase,
+ totalAlerts: 2,
+ id: caseWithAlertsId,
+};
+export const caseWithAlertsSyncOff = {
+ ...basicCase,
+ totalAlerts: 2,
+ settings: {
+ syncAlerts: false,
+ },
+ id: caseWithAlertsSyncOffId,
+};
+
export const basicResolvedCase: ResolvedCase = {
case: basicCase,
outcome: 'aliasMatch',
@@ -314,6 +330,8 @@ export const cases: Case[] = [
{ ...pushedCase, updatedAt: laterTime, id: '2', totalComment: 0, comments: [] },
{ ...basicCase, id: '3', totalComment: 0, comments: [] },
{ ...basicCase, id: '4', totalComment: 0, comments: [] },
+ caseWithAlerts,
+ caseWithAlertsSyncOff,
];
export const allCases: AllCases = {
@@ -378,6 +396,20 @@ export const basicCaseSnake: CaseResponse = {
owner: SECURITY_SOLUTION_OWNER,
} as CaseResponse;
+export const caseWithAlertsSnake = {
+ ...basicCaseSnake,
+ totalAlerts: 2,
+ id: caseWithAlertsId,
+};
+export const caseWithAlertsSyncOffSnake = {
+ ...basicCaseSnake,
+ totalAlerts: 2,
+ settings: {
+ syncAlerts: false,
+ },
+ id: caseWithAlertsSyncOffId,
+};
+
export const casesStatusSnake: CasesStatusResponse = {
count_closed_cases: 130,
count_in_progress_cases: 40,
@@ -423,6 +455,8 @@ export const casesSnake: CasesResponse = [
{ ...pushedCaseSnake, updated_at: laterTime, id: '2', totalComment: 0, comments: [] },
{ ...basicCaseSnake, id: '3', totalComment: 0, comments: [] },
{ ...basicCaseSnake, id: '4', totalComment: 0, comments: [] },
+ caseWithAlertsSnake,
+ caseWithAlertsSyncOffSnake,
];
export const allCasesSnake: CasesFindResponse = {
diff --git a/x-pack/plugins/cases/public/containers/use_get_cases.test.tsx b/x-pack/plugins/cases/public/containers/use_get_cases.test.tsx
index 23b57004ca4d7..dee4d424c84de 100644
--- a/x-pack/plugins/cases/public/containers/use_get_cases.test.tsx
+++ b/x-pack/plugins/cases/public/containers/use_get_cases.test.tsx
@@ -17,7 +17,7 @@ import {
UseGetCases,
} from './use_get_cases';
import { UpdateKey } from './types';
-import { allCases, basicCase } from './mock';
+import { allCases, basicCase, caseWithAlerts, caseWithAlertsSyncOff } from './mock';
import * as api from './api';
import { TestProviders } from '../common/mock';
import { useToasts } from '../common/lib/kibana';
@@ -121,6 +121,47 @@ describe('useGetCases', () => {
});
});
+ it('shows a success toast notifying of synced alerts when sync is on', async () => {
+ await act(async () => {
+ const updateCase = {
+ updateKey: 'status' as UpdateKey,
+ updateValue: 'open',
+ caseId: caseWithAlerts.id,
+ refetchCasesStatus: jest.fn(),
+ version: '99999',
+ };
+ const { result, waitForNextUpdate } = renderHook(() => useGetCases(), {
+ wrapper: ({ children }) => {children},
+ });
+ await waitForNextUpdate();
+ result.current.dispatchUpdateCaseProperty(updateCase);
+ });
+ expect(addSuccess).toHaveBeenCalledWith({
+ text: 'Updated the statuses of attached alerts.',
+ title: 'Updated "Another horrible breach!!"',
+ });
+ });
+
+ it('shows a success toast without notifying of synced alerts when sync is off', async () => {
+ await act(async () => {
+ const updateCase = {
+ updateKey: 'status' as UpdateKey,
+ updateValue: 'open',
+ caseId: caseWithAlertsSyncOff.id,
+ refetchCasesStatus: jest.fn(),
+ version: '99999',
+ };
+ const { result, waitForNextUpdate } = renderHook(() => useGetCases(), {
+ wrapper: ({ children }) => {children},
+ });
+ await waitForNextUpdate();
+ result.current.dispatchUpdateCaseProperty(updateCase);
+ });
+ expect(addSuccess).toHaveBeenCalledWith({
+ title: 'Updated "Another horrible breach!!"',
+ });
+ });
+
it('refetch cases', async () => {
const spyOnGetCases = jest.spyOn(api, 'getCases');
await act(async () => {
diff --git a/x-pack/plugins/cases/public/containers/use_get_cases.tsx b/x-pack/plugins/cases/public/containers/use_get_cases.tsx
index de0f0f514da1b..3b144620b6352 100644
--- a/x-pack/plugins/cases/public/containers/use_get_cases.tsx
+++ b/x-pack/plugins/cases/public/containers/use_get_cases.tsx
@@ -235,7 +235,7 @@ export const useGetCases = (
toasts.addSuccess({
title: i18n.UPDATED_CASE(caseData.title),
text:
- updateKey === 'status' && caseData.totalAlerts > 0
+ updateKey === 'status' && caseData.totalAlerts > 0 && caseData.settings.syncAlerts
? i18n.STATUS_CHANGED_TOASTER_TEXT
: undefined,
});
From 78643f495c925154bb20839b3d12f241cb4ffe66 Mon Sep 17 00:00:00 2001
From: Giorgos Bamparopoulos
Date: Mon, 31 Jan 2022 11:54:34 +0000
Subject: [PATCH 10/65] APM UI changes for serverless services / AWS lambda
(#122775)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* Adapted service UI for AWS lambda / serverless services
* Add unit tests for isServerlessAgent function
* Add story for cold start badge
* Add unit tests for service icons and icon details
* Add aws_lambda checks to isMetricsTabHidden and isJVMsTabHidden unit tests
* Add API test for coldstart_rate chart
* Change service icon API tests to use synthrace and test for serverless property
* Change service details API tests to use synthrace and add test for serverless
* Add e2e tests for cold start rate chart
* Add cold start badge to transaction flyout
* Add beta badge to cloud details in a lambda context
* Add support for multiple lambda functions in a single service
Co-authored-by: Alexander Wert
Co-authored-by: Casper Hübertz
---
.../src/lib/apm/apm_fields.ts | 16 ++
.../lib/apm/utils/get_transaction_metrics.ts | 14 ++
.../elasticsearch_fieldnames.test.ts.snap | 24 ++
x-pack/plugins/apm/common/agent_name.test.ts | 33 ++-
x-pack/plugins/apm/common/agent_name.ts | 4 +
.../apm/common/elasticsearch_fieldnames.ts | 5 +
.../aws_lambda/aws_lamba.spec.ts | 53 +++++
.../aws_lambda/generate_data.ts | 41 ++++
.../components/app/service_overview/index.tsx | 44 +++-
.../app/transaction_details/index.tsx | 11 +-
.../badge/cold_start_badge.stories.tsx | 18 ++
.../waterfall/badge/cold_start_badge.tsx | 20 ++
.../{ => badge}/sync_badge.stories.tsx | 2 +-
.../waterfall/{ => badge}/sync_badge.test.tsx | 0
.../waterfall/{ => badge}/sync_badge.tsx | 2 +-
.../waterfall/span_flyout/index.tsx | 2 +-
.../waterfall/transaction_flyout/index.tsx | 1 +
.../waterfall/waterfall_item.tsx | 6 +-
.../waterfall_container.stories.data.ts | 3 +
.../waterfall_container.stories.tsx | 11 +-
.../app/transaction_overview/index.tsx | 10 +-
.../apm_service_template/index.test.tsx | 2 +
.../templates/apm_service_template/index.tsx | 9 +-
.../charts/transaction_charts/index.tsx | 32 ++-
.../index.tsx | 184 ++++++++++++++++
.../shared/service_icons/cloud_details.tsx | 72 +++++-
.../shared/service_icons/index.test.tsx | 116 ++++++++++
.../components/shared/service_icons/index.tsx | 21 +-
.../service_icons/serverless_details.tsx | 73 ++++++
.../shared/summary/transaction_summary.tsx | 10 +-
.../lib/helpers/transaction_coldstart_rate.ts | 67 ++++++
.../transaction_groups/get_coldstart_rate.ts | 181 +++++++++++++++
.../services/get_service_metadata_details.ts | 58 +++++
.../services/get_service_metadata_icons.ts | 17 +-
.../apm/server/routes/transactions/route.ts | 155 +++++++++++++
.../typings/es_schemas/raw/fields/cloud.ts | 3 +
.../apm/typings/es_schemas/raw/fields/faas.ts | 16 ++
.../typings/es_schemas/raw/transaction_raw.ts | 2 +
.../tests/cold_start/cold_start.spec.ts | 205 +++++++++++++++++
.../cold_start_by_transaction_name.spec.ts | 207 ++++++++++++++++++
.../generate_data.ts | 64 ++++++
.../tests/cold_start/generate_data.ts | 70 ++++++
.../tests/services/service_details.spec.ts | 135 ------------
.../services/service_details/generate_data.ts | 130 +++++++++++
.../service_details/service_details.spec.ts | 128 +++++++++++
.../tests/services/service_icons.spec.ts | 77 -------
.../services/service_icons/generate_data.ts | 56 +++++
.../service_icons/service_icons.spec.ts | 77 +++++++
48 files changed, 2240 insertions(+), 247 deletions(-)
create mode 100644 x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_overview/aws_lambda/aws_lamba.spec.ts
create mode 100644 x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_overview/aws_lambda/generate_data.ts
create mode 100644 x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/badge/cold_start_badge.stories.tsx
create mode 100644 x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/badge/cold_start_badge.tsx
rename x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/{ => badge}/sync_badge.stories.tsx (94%)
rename x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/{ => badge}/sync_badge.test.tsx (100%)
rename x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/{ => badge}/sync_badge.tsx (94%)
create mode 100644 x-pack/plugins/apm/public/components/shared/charts/transaction_coldstart_rate_chart/index.tsx
create mode 100644 x-pack/plugins/apm/public/components/shared/service_icons/serverless_details.tsx
create mode 100644 x-pack/plugins/apm/server/lib/helpers/transaction_coldstart_rate.ts
create mode 100644 x-pack/plugins/apm/server/lib/transaction_groups/get_coldstart_rate.ts
create mode 100644 x-pack/plugins/apm/typings/es_schemas/raw/fields/faas.ts
create mode 100644 x-pack/test/apm_api_integration/tests/cold_start/cold_start.spec.ts
create mode 100644 x-pack/test/apm_api_integration/tests/cold_start/cold_start_by_transaction_name/cold_start_by_transaction_name.spec.ts
create mode 100644 x-pack/test/apm_api_integration/tests/cold_start/cold_start_by_transaction_name/generate_data.ts
create mode 100644 x-pack/test/apm_api_integration/tests/cold_start/generate_data.ts
delete mode 100644 x-pack/test/apm_api_integration/tests/services/service_details.spec.ts
create mode 100644 x-pack/test/apm_api_integration/tests/services/service_details/generate_data.ts
create mode 100644 x-pack/test/apm_api_integration/tests/services/service_details/service_details.spec.ts
delete mode 100644 x-pack/test/apm_api_integration/tests/services/service_icons.spec.ts
create mode 100644 x-pack/test/apm_api_integration/tests/services/service_icons/generate_data.ts
create mode 100644 x-pack/test/apm_api_integration/tests/services/service_icons/service_icons.spec.ts
diff --git a/packages/elastic-apm-synthtrace/src/lib/apm/apm_fields.ts b/packages/elastic-apm-synthtrace/src/lib/apm/apm_fields.ts
index e0a48fdcf2b89..4afebf0352a6a 100644
--- a/packages/elastic-apm-synthtrace/src/lib/apm/apm_fields.ts
+++ b/packages/elastic-apm-synthtrace/src/lib/apm/apm_fields.ts
@@ -63,8 +63,12 @@ export type ApmFields = Fields &
};
'transaction.sampled': true;
'service.name': string;
+ 'service.version': string;
'service.environment': string;
'service.node.name': string;
+ 'service.runtime.name': string;
+ 'service.runtime.version': string;
+ 'service.framework.name': string;
'span.id': string;
'span.name': string;
'span.type': string;
@@ -77,5 +81,17 @@ export type ApmFields = Fields &
'span.destination.service.response_time.count': number;
'span.self_time.count': number;
'span.self_time.sum.us': number;
+ 'cloud.provider': string;
+ 'cloud.project.name': string;
+ 'cloud.service.name': string;
+ 'cloud.availability_zone': string;
+ 'cloud.machine.type': string;
+ 'cloud.region': string;
+ 'host.os.platform': string;
+ 'faas.id': string;
+ 'faas.coldstart': boolean;
+ 'faas.execution': string;
+ 'faas.trigger.type': string;
+ 'faas.trigger.request_id': string;
}> &
ApmApplicationMetricFields;
diff --git a/packages/elastic-apm-synthtrace/src/lib/apm/utils/get_transaction_metrics.ts b/packages/elastic-apm-synthtrace/src/lib/apm/utils/get_transaction_metrics.ts
index 8545ae65d8aa0..baa9f57a19a46 100644
--- a/packages/elastic-apm-synthtrace/src/lib/apm/utils/get_transaction_metrics.ts
+++ b/packages/elastic-apm-synthtrace/src/lib/apm/utils/get_transaction_metrics.ts
@@ -51,6 +51,20 @@ export function getTransactionMetrics(events: ApmFields[]) {
'host.name',
'container.id',
'kubernetes.pod.name',
+ 'cloud.account.id',
+ 'cloud.account.name',
+ 'cloud.machine.type',
+ 'cloud.project.id',
+ 'cloud.project.name',
+ 'cloud.service.name',
+ 'service.language.name',
+ 'service.language.version',
+ 'service.runtime.name',
+ 'service.runtime.version',
+ 'host.os.platform',
+ 'faas.id',
+ 'faas.coldstart',
+ 'faas.trigger.type',
]);
return metricsets.map((metricset) => {
diff --git a/x-pack/plugins/apm/common/__snapshots__/elasticsearch_fieldnames.test.ts.snap b/x-pack/plugins/apm/common/__snapshots__/elasticsearch_fieldnames.test.ts.snap
index 5dd3588674179..9b255a87df39e 100644
--- a/x-pack/plugins/apm/common/__snapshots__/elasticsearch_fieldnames.test.ts.snap
+++ b/x-pack/plugins/apm/common/__snapshots__/elasticsearch_fieldnames.test.ts.snap
@@ -37,6 +37,8 @@ exports[`Error CLOUD_PROVIDER 1`] = `"gcp"`;
exports[`Error CLOUD_REGION 1`] = `"europe-west1"`;
+exports[`Error CLOUD_SERVICE_NAME 1`] = `undefined`;
+
exports[`Error CLS_FIELD 1`] = `undefined`;
exports[`Error CONTAINER_ID 1`] = `undefined`;
@@ -63,6 +65,12 @@ exports[`Error ERROR_PAGE_URL 1`] = `undefined`;
exports[`Error EVENT_OUTCOME 1`] = `undefined`;
+exports[`Error FAAS_COLDSTART 1`] = `undefined`;
+
+exports[`Error FAAS_ID 1`] = `undefined`;
+
+exports[`Error FAAS_TRIGGER_TYPE 1`] = `undefined`;
+
exports[`Error FCP_FIELD 1`] = `undefined`;
exports[`Error FID_FIELD 1`] = `undefined`;
@@ -282,6 +290,8 @@ exports[`Span CLOUD_PROVIDER 1`] = `"gcp"`;
exports[`Span CLOUD_REGION 1`] = `"europe-west1"`;
+exports[`Span CLOUD_SERVICE_NAME 1`] = `undefined`;
+
exports[`Span CLS_FIELD 1`] = `undefined`;
exports[`Span CONTAINER_ID 1`] = `undefined`;
@@ -308,6 +318,12 @@ exports[`Span ERROR_PAGE_URL 1`] = `undefined`;
exports[`Span EVENT_OUTCOME 1`] = `"unknown"`;
+exports[`Span FAAS_COLDSTART 1`] = `undefined`;
+
+exports[`Span FAAS_ID 1`] = `undefined`;
+
+exports[`Span FAAS_TRIGGER_TYPE 1`] = `undefined`;
+
exports[`Span FCP_FIELD 1`] = `undefined`;
exports[`Span FID_FIELD 1`] = `undefined`;
@@ -519,6 +535,8 @@ exports[`Transaction CLOUD_PROVIDER 1`] = `"gcp"`;
exports[`Transaction CLOUD_REGION 1`] = `"europe-west1"`;
+exports[`Transaction CLOUD_SERVICE_NAME 1`] = `undefined`;
+
exports[`Transaction CLS_FIELD 1`] = `undefined`;
exports[`Transaction CONTAINER_ID 1`] = `"container1234567890abcdef"`;
@@ -545,6 +563,12 @@ exports[`Transaction ERROR_PAGE_URL 1`] = `undefined`;
exports[`Transaction EVENT_OUTCOME 1`] = `"unknown"`;
+exports[`Transaction FAAS_COLDSTART 1`] = `undefined`;
+
+exports[`Transaction FAAS_ID 1`] = `undefined`;
+
+exports[`Transaction FAAS_TRIGGER_TYPE 1`] = `undefined`;
+
exports[`Transaction FCP_FIELD 1`] = `undefined`;
exports[`Transaction FID_FIELD 1`] = `undefined`;
diff --git a/x-pack/plugins/apm/common/agent_name.test.ts b/x-pack/plugins/apm/common/agent_name.test.ts
index 162a5716d6c7b..e48fa502d33d1 100644
--- a/x-pack/plugins/apm/common/agent_name.test.ts
+++ b/x-pack/plugins/apm/common/agent_name.test.ts
@@ -5,7 +5,12 @@
* 2.0.
*/
-import { isJavaAgentName, isRumAgentName, isIosAgentName } from './agent_name';
+import {
+ isJavaAgentName,
+ isRumAgentName,
+ isIosAgentName,
+ isServerlessAgent,
+} from './agent_name';
describe('agent name helpers', () => {
describe('isJavaAgentName', () => {
@@ -79,4 +84,30 @@ describe('agent name helpers', () => {
});
});
});
+
+ describe('isServerlessAgent', () => {
+ describe('when the runtime name is AWS_LAMBDA', () => {
+ it('returns true', () => {
+ expect(isServerlessAgent('AWS_LAMBDA')).toEqual(true);
+ });
+ });
+
+ describe('when the runtime name is aws_lambda', () => {
+ it('returns true', () => {
+ expect(isServerlessAgent('aws_lambda')).toEqual(true);
+ });
+ });
+
+ describe('when the runtime name is aws_lambda_test', () => {
+ it('returns true', () => {
+ expect(isServerlessAgent('aws_lambda_test')).toEqual(true);
+ });
+ });
+
+ describe('when the runtime name is something else', () => {
+ it('returns false', () => {
+ expect(isServerlessAgent('not_aws_lambda')).toEqual(false);
+ });
+ });
+ });
});
diff --git a/x-pack/plugins/apm/common/agent_name.ts b/x-pack/plugins/apm/common/agent_name.ts
index b41ae949d5867..e8947d550a8fc 100644
--- a/x-pack/plugins/apm/common/agent_name.ts
+++ b/x-pack/plugins/apm/common/agent_name.ts
@@ -90,3 +90,7 @@ export function isIosAgentName(agentName?: string) {
export function isJRubyAgent(agentName?: string, runtimeName?: string) {
return agentName === 'ruby' && runtimeName?.toLowerCase() === 'jruby';
}
+
+export function isServerlessAgent(runtimeName?: string) {
+ return runtimeName?.toLowerCase().startsWith('aws_lambda');
+}
diff --git a/x-pack/plugins/apm/common/elasticsearch_fieldnames.ts b/x-pack/plugins/apm/common/elasticsearch_fieldnames.ts
index 5c7c953d8d900..f0ea5b6cb116e 100644
--- a/x-pack/plugins/apm/common/elasticsearch_fieldnames.ts
+++ b/x-pack/plugins/apm/common/elasticsearch_fieldnames.ts
@@ -13,6 +13,7 @@ export const CLOUD_MACHINE_TYPE = 'cloud.machine.type';
export const CLOUD_ACCOUNT_ID = 'cloud.account.id';
export const CLOUD_INSTANCE_ID = 'cloud.instance.id';
export const CLOUD_INSTANCE_NAME = 'cloud.instance.name';
+export const CLOUD_SERVICE_NAME = 'cloud.service.name';
export const SERVICE = 'service';
export const SERVICE_NAME = 'service.name';
@@ -152,3 +153,7 @@ export const PROFILE_ALLOC_OBJECTS = 'profile.alloc_objects.count';
export const PROFILE_ALLOC_SPACE = 'profile.alloc_space.bytes';
export const PROFILE_INUSE_OBJECTS = 'profile.inuse_objects.count';
export const PROFILE_INUSE_SPACE = 'profile.inuse_space.bytes';
+
+export const FAAS_ID = 'faas.id';
+export const FAAS_COLDSTART = 'faas.coldstart';
+export const FAAS_TRIGGER_TYPE = 'faas.trigger.type';
diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_overview/aws_lambda/aws_lamba.spec.ts b/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_overview/aws_lambda/aws_lamba.spec.ts
new file mode 100644
index 0000000000000..518cfefde2fb1
--- /dev/null
+++ b/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_overview/aws_lambda/aws_lamba.spec.ts
@@ -0,0 +1,53 @@
+/*
+ * 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 url from 'url';
+import { synthtrace } from '../../../../../synthtrace';
+import { generateData } from './generate_data';
+
+const start = '2021-10-10T00:00:00.000Z';
+const end = '2021-10-10T00:15:00.000Z';
+
+const serviceOverviewHref = url.format({
+ pathname: '/app/apm/services/synth-python/overview',
+ query: { rangeFrom: start, rangeTo: end },
+});
+
+const apiToIntercept = {
+ endpoint:
+ '/internal/apm/services/synth-python/transactions/charts/coldstart_rate?*',
+ name: 'coldStartRequest',
+};
+
+describe('Service overview - aws lambda', () => {
+ before(async () => {
+ await synthtrace.index(
+ generateData({
+ start: new Date(start).getTime(),
+ end: new Date(end).getTime(),
+ })
+ );
+ });
+
+ after(async () => {
+ await synthtrace.clean();
+ });
+
+ beforeEach(() => {
+ cy.loginAsReadOnlyUser();
+ });
+
+ it('displays a cold start rate chart and not a transaction breakdown chart', () => {
+ const { endpoint, name } = apiToIntercept;
+
+ cy.intercept('GET', endpoint).as(name);
+ cy.visit(serviceOverviewHref);
+ cy.wait(`@${name}`);
+
+ cy.contains('Cold start rate');
+ cy.contains('Time spent by span type').should('not.exist');
+ });
+});
diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_overview/aws_lambda/generate_data.ts b/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_overview/aws_lambda/generate_data.ts
new file mode 100644
index 0000000000000..2dba10e8e517e
--- /dev/null
+++ b/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_overview/aws_lambda/generate_data.ts
@@ -0,0 +1,41 @@
+/*
+ * 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 { apm, timerange } from '@elastic/apm-synthtrace';
+
+const dataConfig = {
+ serviceName: 'synth-python',
+ rate: 10,
+ transaction: {
+ name: 'GET /apple 🍎',
+ duration: 1000,
+ },
+};
+
+export function generateData({ start, end }: { start: number; end: number }) {
+ const { rate, transaction, serviceName } = dataConfig;
+ const instance = apm
+ .service(serviceName, 'production', 'python')
+ .instance('instance-a');
+
+ const traceEvents = timerange(start, end)
+ .interval('1m')
+ .rate(rate)
+ .flatMap((timestamp) => [
+ ...instance
+ .transaction(transaction.name)
+ .defaults({
+ 'service.runtime.name': 'AWS_Lambda_python3.8',
+ 'faas.coldstart': true,
+ })
+ .timestamp(timestamp)
+ .duration(transaction.duration)
+ .success()
+ .serialize(),
+ ]);
+
+ return traceEvents;
+}
diff --git a/x-pack/plugins/apm/public/components/app/service_overview/index.tsx b/x-pack/plugins/apm/public/components/app/service_overview/index.tsx
index 2c30027770f43..3c2db54088770 100644
--- a/x-pack/plugins/apm/public/components/app/service_overview/index.tsx
+++ b/x-pack/plugins/apm/public/components/app/service_overview/index.tsx
@@ -9,13 +9,18 @@ import { EuiFlexGroup, EuiFlexItem, EuiLink, EuiPanel } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import React from 'react';
import { useHistory } from 'react-router-dom';
-import { isRumAgentName, isIosAgentName } from '../../../../common/agent_name';
+import {
+ isRumAgentName,
+ isIosAgentName,
+ isServerlessAgent,
+} from '../../../../common/agent_name';
import { AnnotationsContextProvider } from '../../../context/annotations/annotations_context';
import { useApmServiceContext } from '../../../context/apm_service/use_apm_service_context';
import { ChartPointerEventContextProvider } from '../../../context/chart_pointer_event/chart_pointer_event_context';
import { useBreakpoints } from '../../../hooks/use_breakpoints';
import { LatencyChart } from '../../shared/charts/latency_chart';
import { TransactionBreakdownChart } from '../../shared/charts/transaction_breakdown_chart';
+import { TransactionColdstartRateChart } from '../../shared/charts/transaction_coldstart_rate_chart';
import { FailedTransactionRateChart } from '../../shared/charts/failed_transaction_rate_chart';
import { ServiceOverviewDependenciesTable } from './service_overview_dependencies_table';
import { ServiceOverviewErrorsTable } from './service_overview_errors_table';
@@ -35,8 +40,13 @@ import { replace } from '../../shared/links/url_helpers';
export const chartHeight = 288;
export function ServiceOverview() {
- const { agentName, serviceName, transactionType, fallbackToTransactions } =
- useApmServiceContext();
+ const {
+ agentName,
+ serviceName,
+ transactionType,
+ fallbackToTransactions,
+ runtimeName,
+ } = useApmServiceContext();
const {
query,
query: {
@@ -69,7 +79,7 @@ export function ServiceOverview() {
const rowDirection = isSingleColumn ? 'column' : 'row';
const isRumAgent = isRumAgentName(agentName);
const isIosAgent = isIosAgentName(agentName);
-
+ const isServerless = isServerlessAgent(runtimeName);
const router = useApmRouter();
const dependenciesLink = router.link('/services/{serviceName}/dependencies', {
path: {
@@ -152,13 +162,23 @@ export function ServiceOverview() {
gutterSize="s"
responsive={false}
>
-
-
-
+ {isServerless ? (
+
+
+
+ ) : (
+
+
+
+ )}
{!isRumAgent && (
@@ -180,7 +200,7 @@ export function ServiceOverview() {
)}
- {!isRumAgent && !isIosAgent && (
+ {!isRumAgent && !isIosAgent && !isServerless && (
{fallbackToTransactions && }
@@ -66,6 +72,9 @@ export function TransactionDetails() {
start={start}
end={end}
transactionName={transactionName}
+ isServerlessContext={isServerless}
+ comparisonEnabled={comparisonEnabled}
+ comparisonType={comparisonType}
/>
diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/badge/cold_start_badge.stories.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/badge/cold_start_badge.stories.tsx
new file mode 100644
index 0000000000000..50af7cc42435b
--- /dev/null
+++ b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/badge/cold_start_badge.stories.tsx
@@ -0,0 +1,18 @@
+/*
+ * 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 React from 'react';
+import { ColdStartBadge } from './cold_start_badge';
+
+export default {
+ title: 'app/TransactionDetails/Waterfall/Badge/ColdStartBadge',
+ component: ColdStartBadge,
+};
+
+export function Example() {
+ return ;
+}
diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/badge/cold_start_badge.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/badge/cold_start_badge.tsx
new file mode 100644
index 0000000000000..dfe38aab5021d
--- /dev/null
+++ b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/badge/cold_start_badge.tsx
@@ -0,0 +1,20 @@
+/*
+ * 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 { EuiBadge } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+import React from 'react';
+
+export function ColdStartBadge() {
+ return (
+
+ {i18n.translate('xpack.apm.transactionDetails.coldstartBadge', {
+ defaultMessage: 'cold start',
+ })}
+
+ );
+}
diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/sync_badge.stories.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/badge/sync_badge.stories.tsx
similarity index 94%
rename from x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/sync_badge.stories.tsx
rename to x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/badge/sync_badge.stories.tsx
index dea05961c4cef..7209203b54cc0 100644
--- a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/sync_badge.stories.tsx
+++ b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/badge/sync_badge.stories.tsx
@@ -9,7 +9,7 @@ import React from 'react';
import { SyncBadge, SyncBadgeProps } from './sync_badge';
export default {
- title: 'app/TransactionDetails/SyncBadge',
+ title: 'app/TransactionDetails/Waterfall/Badge/SyncBadge',
component: SyncBadge,
argTypes: {
sync: {
diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/sync_badge.test.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/badge/sync_badge.test.tsx
similarity index 100%
rename from x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/sync_badge.test.tsx
rename to x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/badge/sync_badge.test.tsx
diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/sync_badge.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/badge/sync_badge.tsx
similarity index 94%
rename from x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/sync_badge.tsx
rename to x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/badge/sync_badge.tsx
index a51d710bf3961..85571b9065c4d 100644
--- a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/sync_badge.tsx
+++ b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/badge/sync_badge.tsx
@@ -8,7 +8,7 @@
import { EuiBadge } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import React from 'react';
-import { AgentName } from '../../../../../../../typings/es_schemas/ui/fields/agent';
+import { AgentName } from '../../../../../../../../typings/es_schemas/ui/fields/agent';
export interface SyncBadgeProps {
/**
diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/span_flyout/index.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/span_flyout/index.tsx
index 0087b0f9d1fac..0f7a6a295601b 100644
--- a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/span_flyout/index.tsx
+++ b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/span_flyout/index.tsx
@@ -34,7 +34,7 @@ import { DurationSummaryItem } from '../../../../../../shared/summary/duration_s
import { HttpInfoSummaryItem } from '../../../../../../shared/summary/http_info_summary_item';
import { TimestampTooltip } from '../../../../../../shared/timestamp_tooltip';
import { ResponsiveFlyout } from '../responsive_flyout';
-import { SyncBadge } from '../sync_badge';
+import { SyncBadge } from '../badge/sync_badge';
import { SpanDatabase } from './span_db';
import { StickySpanProperties } from './sticky_span_properties';
import { FailureBadge } from '../failure_badge';
diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/transaction_flyout/index.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/transaction_flyout/index.tsx
index 5f1e0cacd8483..43a7ebfa2f97f 100644
--- a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/transaction_flyout/index.tsx
+++ b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/transaction_flyout/index.tsx
@@ -87,6 +87,7 @@ export function TransactionFlyout({
transaction={transactionDoc}
totalDuration={rootTransactionDuration}
errorCount={errorCount}
+ coldStartBadge={transactionDoc.faas?.coldstart}
/>
diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/waterfall_item.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/waterfall_item.tsx
index d8942cab36f77..51e9cbeaba6f7 100644
--- a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/waterfall_item.tsx
+++ b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/waterfall_item.tsx
@@ -18,7 +18,8 @@ import {
import { asDuration } from '../../../../../../../common/utils/formatters';
import { Margins } from '../../../../../shared/charts/timeline';
import { TruncateWithTooltip } from '../../../../../shared/truncate_with_tooltip';
-import { SyncBadge } from './sync_badge';
+import { SyncBadge } from './badge/sync_badge';
+import { ColdStartBadge } from './badge/cold_start_badge';
import { IWaterfallSpanOrTransaction } from './waterfall_helpers/waterfall_helpers';
import { FailureBadge } from './failure_badge';
import { useApmRouter } from '../../../../../../hooks/use_apm_router';
@@ -200,6 +201,8 @@ export function WaterfallItem({
const isCompositeSpan = item.docType === 'span' && item.doc.span.composite;
const itemBarStyle = getItemBarStyle(item, color, width, left);
+ const isServerlessColdstart =
+ item.docType === 'transaction' && item.doc.faas?.coldstart;
return (
)}
+ {isServerlessColdstart && }
);
diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall_container.stories.data.ts b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall_container.stories.data.ts
index f3331fba0ca23..d4af0e92c9054 100644
--- a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall_container.stories.data.ts
+++ b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall_container.stories.data.ts
@@ -226,6 +226,9 @@ export const simpleTrace = {
timestamp: {
us: 1584975868787052,
},
+ faas: {
+ coldstart: true,
+ },
},
{
parent: {
diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall_container.stories.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall_container.stories.tsx
index 312412a8cf827..db82e9e360207 100644
--- a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall_container.stories.tsx
+++ b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall_container.stories.tsx
@@ -19,9 +19,18 @@ import {
traceWithErrors,
urlParams as testUrlParams,
} from './waterfall_container.stories.data';
+import type { ApmPluginContextValue } from '../../../../../context/apm_plugin/apm_plugin_context';
type Args = ComponentProps;
+const apmPluginContextMock = {
+ core: {
+ http: {
+ basePath: { prepend: () => {} },
+ },
+ },
+} as unknown as ApmPluginContextValue;
+
const stories: Meta = {
title: 'app/TransactionDetails/waterfall',
component: WaterfallContainer,
@@ -32,7 +41,7 @@ const stories: Meta = {
'/services/{serviceName}/transactions/view?rangeFrom=now-15m&rangeTo=now&transactionName=testTransactionName',
]}
>
-
+
diff --git a/x-pack/plugins/apm/public/components/app/transaction_overview/index.tsx b/x-pack/plugins/apm/public/components/app/transaction_overview/index.tsx
index 39d522ca088fc..68315fc3b2b02 100644
--- a/x-pack/plugins/apm/public/components/app/transaction_overview/index.tsx
+++ b/x-pack/plugins/apm/public/components/app/transaction_overview/index.tsx
@@ -15,6 +15,7 @@ import { AggregatedTransactionsBadge } from '../../shared/aggregated_transaction
import { TransactionCharts } from '../../shared/charts/transaction_charts';
import { replace } from '../../shared/links/url_helpers';
import { TransactionsTable } from '../../shared/transactions_table';
+import { isServerlessAgent } from '../../../../common/agent_name';
export function TransactionOverview() {
const {
@@ -24,12 +25,14 @@ export function TransactionOverview() {
rangeFrom,
rangeTo,
transactionType: transactionTypeFromUrl,
+ comparisonEnabled,
+ comparisonType,
},
} = useApmParams('/services/{serviceName}/transactions');
const { start, end } = useTimeRange({ rangeFrom, rangeTo });
- const { transactionType, serviceName, fallbackToTransactions } =
+ const { transactionType, serviceName, fallbackToTransactions, runtimeName } =
useApmServiceContext();
const history = useHistory();
@@ -45,6 +48,8 @@ export function TransactionOverview() {
return null;
}
+ const isServerless = isServerlessAgent(runtimeName);
+
return (
<>
{fallbackToTransactions && (
@@ -62,6 +67,9 @@ export function TransactionOverview() {
environment={environment}
start={start}
end={end}
+ isServerlessContext={isServerless}
+ comparisonEnabled={comparisonEnabled}
+ comparisonType={comparisonType}
/>
diff --git a/x-pack/plugins/apm/public/components/routing/templates/apm_service_template/index.test.tsx b/x-pack/plugins/apm/public/components/routing/templates/apm_service_template/index.test.tsx
index 5a481b2d6f10c..03a50d6082583 100644
--- a/x-pack/plugins/apm/public/components/routing/templates/apm_service_template/index.test.tsx
+++ b/x-pack/plugins/apm/public/components/routing/templates/apm_service_template/index.test.tsx
@@ -19,6 +19,7 @@ describe('APM service template', () => {
{ agentName: 'ios/swift' },
{ agentName: 'opentelemetry/swift' },
{ agentName: 'ruby', runtimeName: 'jruby' },
+ { runtimeName: 'aws_lambda' },
].map((input) => {
it(`when input ${JSON.stringify(input)}`, () => {
expect(isMetricsTabHidden(input)).toBeTruthy();
@@ -52,6 +53,7 @@ describe('APM service template', () => {
{ agentName: 'nodejs' },
{ agentName: 'php' },
{ agentName: 'python' },
+ { runtimeName: 'aws_lambda' },
].map((input) => {
it(`when input ${JSON.stringify(input)}`, () => {
expect(isJVMsTabHidden(input)).toBeTruthy();
diff --git a/x-pack/plugins/apm/public/components/routing/templates/apm_service_template/index.tsx b/x-pack/plugins/apm/public/components/routing/templates/apm_service_template/index.tsx
index 93c222164f026..20f907e03fc37 100644
--- a/x-pack/plugins/apm/public/components/routing/templates/apm_service_template/index.tsx
+++ b/x-pack/plugins/apm/public/components/routing/templates/apm_service_template/index.tsx
@@ -21,6 +21,7 @@ import {
isJavaAgentName,
isJRubyAgent,
isRumAgentName,
+ isServerlessAgent,
} from '../../../../../common/agent_name';
import { useApmPluginContext } from '../../../../context/apm_plugin/use_apm_plugin_context';
import { ApmServiceContextProvider } from '../../../../context/apm_service/apm_service_context';
@@ -144,7 +145,8 @@ export function isMetricsTabHidden({
isRumAgentName(agentName) ||
isJavaAgentName(agentName) ||
isIosAgentName(agentName) ||
- isJRubyAgent(agentName, runtimeName)
+ isJRubyAgent(agentName, runtimeName) ||
+ isServerlessAgent(runtimeName)
);
}
@@ -155,7 +157,10 @@ export function isJVMsTabHidden({
agentName?: string;
runtimeName?: string;
}) {
- return !(isJavaAgentName(agentName) || isJRubyAgent(agentName, runtimeName));
+ return (
+ !(isJavaAgentName(agentName) || isJRubyAgent(agentName, runtimeName)) ||
+ isServerlessAgent(runtimeName)
+ );
}
function useTabs({ selectedTab }: { selectedTab: Tab['key'] }) {
diff --git a/x-pack/plugins/apm/public/components/shared/charts/transaction_charts/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/transaction_charts/index.tsx
index ff1eec200a0c2..a2bfad0175a5f 100644
--- a/x-pack/plugins/apm/public/components/shared/charts/transaction_charts/index.tsx
+++ b/x-pack/plugins/apm/public/components/shared/charts/transaction_charts/index.tsx
@@ -12,7 +12,9 @@ import { ChartPointerEventContextProvider } from '../../../../context/chart_poin
import { ServiceOverviewThroughputChart } from '../../../app/service_overview/service_overview_throughput_chart';
import { LatencyChart } from '../latency_chart';
import { TransactionBreakdownChart } from '../transaction_breakdown_chart';
+import { TransactionColdstartRateChart } from '../transaction_coldstart_rate_chart';
import { FailedTransactionRateChart } from '../failed_transaction_rate_chart';
+import { TimeRangeComparisonType } from '../../../../../common/runtime_types/comparison_type_rt';
export function TransactionCharts({
kuery,
@@ -20,12 +22,18 @@ export function TransactionCharts({
start,
end,
transactionName,
+ isServerlessContext,
+ comparisonEnabled,
+ comparisonType,
}: {
kuery: string;
environment: string;
start: string;
end: string;
transactionName?: string;
+ isServerlessContext?: boolean;
+ comparisonEnabled?: boolean;
+ comparisonType?: TimeRangeComparisonType;
}) {
return (
<>
@@ -56,12 +64,24 @@ export function TransactionCharts({
-
-
-
+ {isServerlessContext ? (
+
+
+
+ ) : (
+
+
+
+ )}
diff --git a/x-pack/plugins/apm/public/components/shared/charts/transaction_coldstart_rate_chart/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/transaction_coldstart_rate_chart/index.tsx
new file mode 100644
index 0000000000000..2b99562b67172
--- /dev/null
+++ b/x-pack/plugins/apm/public/components/shared/charts/transaction_coldstart_rate_chart/index.tsx
@@ -0,0 +1,184 @@
+/*
+ * 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 {
+ EuiPanel,
+ EuiTitle,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiIconTip,
+} from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+import React from 'react';
+import { APIReturnType } from '../../../../services/rest/create_call_apm_api';
+import { asPercent } from '../../../../../common/utils/formatters';
+import { useFetcher } from '../../../../hooks/use_fetcher';
+import { useTheme } from '../../../../hooks/use_theme';
+import { TimeseriesChart } from '../timeseries_chart';
+import { useApmServiceContext } from '../../../../context/apm_service/use_apm_service_context';
+import {
+ getComparisonChartTheme,
+ getTimeRangeComparison,
+} from '../../time_comparison/get_time_range_comparison';
+import { useApmParams } from '../../../../hooks/use_apm_params';
+import { useTimeRange } from '../../../../hooks/use_time_range';
+import { TimeRangeComparisonType } from '../../../../../common/runtime_types/comparison_type_rt';
+
+function yLabelFormat(y?: number | null) {
+ return asPercent(y || 0, 1);
+}
+
+interface Props {
+ height?: number;
+ showAnnotations?: boolean;
+ kuery: string;
+ environment: string;
+ transactionName?: string;
+ comparisonEnabled?: boolean;
+ comparisonType?: TimeRangeComparisonType;
+}
+
+type ColdstartRate =
+ APIReturnType<'GET /internal/apm/services/{serviceName}/transactions/charts/coldstart_rate'>;
+
+const INITIAL_STATE: ColdstartRate = {
+ currentPeriod: {
+ transactionColdstartRate: [],
+ average: null,
+ },
+ previousPeriod: {
+ transactionColdstartRate: [],
+ average: null,
+ },
+};
+
+export function TransactionColdstartRateChart({
+ height,
+ showAnnotations = true,
+ environment,
+ kuery,
+ transactionName,
+ comparisonEnabled,
+ comparisonType,
+}: Props) {
+ const theme = useTheme();
+
+ const {
+ query: { rangeFrom, rangeTo },
+ } = useApmParams('/services/{serviceName}');
+
+ const { start, end } = useTimeRange({ rangeFrom, rangeTo });
+
+ const { serviceName, transactionType } = useApmServiceContext();
+ const comparisonChartThem = getComparisonChartTheme();
+ const { comparisonStart, comparisonEnd } = getTimeRangeComparison({
+ start,
+ end,
+ comparisonType,
+ comparisonEnabled,
+ });
+
+ const endpoint = transactionName
+ ? ('GET /internal/apm/services/{serviceName}/transactions/charts/coldstart_rate_by_transaction_name' as const)
+ : ('GET /internal/apm/services/{serviceName}/transactions/charts/coldstart_rate' as const);
+
+ const { data = INITIAL_STATE, status } = useFetcher(
+ (callApmApi) => {
+ if (transactionType && serviceName && start && end) {
+ return callApmApi(endpoint, {
+ params: {
+ path: {
+ serviceName,
+ },
+ query: {
+ environment,
+ kuery,
+ start,
+ end,
+ transactionType,
+ comparisonStart,
+ comparisonEnd,
+ ...(transactionName ? { transactionName } : {}),
+ },
+ },
+ });
+ }
+ },
+ [
+ environment,
+ kuery,
+ serviceName,
+ start,
+ end,
+ transactionType,
+ transactionName,
+ comparisonStart,
+ comparisonEnd,
+ endpoint,
+ ]
+ );
+
+ const timeseries = [
+ {
+ data: data.currentPeriod.transactionColdstartRate,
+ type: 'linemark',
+ color: theme.eui.euiColorVis5,
+ title: i18n.translate('xpack.apm.coldstartRate.chart.coldstartRate', {
+ defaultMessage: 'Cold start rate (avg.)',
+ }),
+ },
+ ...(comparisonEnabled
+ ? [
+ {
+ data: data.previousPeriod.transactionColdstartRate,
+ type: 'area',
+ color: theme.eui.euiColorMediumShade,
+ title: i18n.translate(
+ 'xpack.apm.coldstartRate.chart.coldstartRate.previousPeriodLabel',
+ { defaultMessage: 'Previous period' }
+ ),
+ },
+ ]
+ : []),
+ ];
+
+ return (
+
+
+
+
+
+ {i18n.translate('xpack.apm.coldstartRate', {
+ defaultMessage: 'Cold start rate',
+ })}
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/x-pack/plugins/apm/public/components/shared/service_icons/cloud_details.tsx b/x-pack/plugins/apm/public/components/shared/service_icons/cloud_details.tsx
index b5938167378d7..91780fec15845 100644
--- a/x-pack/plugins/apm/public/components/shared/service_icons/cloud_details.tsx
+++ b/x-pack/plugins/apm/public/components/shared/service_icons/cloud_details.tsx
@@ -5,7 +5,13 @@
* 2.0.
*/
-import { EuiBadge, EuiDescriptionList } from '@elastic/eui';
+import {
+ EuiBadge,
+ EuiDescriptionList,
+ EuiBetaBadge,
+ EuiFlexGroup,
+ EuiFlexItem,
+} from '@elastic/eui';
import { EuiDescriptionListProps } from '@elastic/eui/src/components/description_list/description_list';
import { i18n } from '@kbn/i18n';
import React from 'react';
@@ -16,9 +22,10 @@ type ServiceDetailsReturnType =
interface Props {
cloud: ServiceDetailsReturnType['cloud'];
+ isServerless: boolean;
}
-export function CloudDetails({ cloud }: Props) {
+export function CloudDetails({ cloud, isServerless }: Props) {
if (!cloud) {
return null;
}
@@ -36,6 +43,43 @@ export function CloudDetails({ cloud }: Props) {
});
}
+ if (cloud.serviceName) {
+ listItems.push({
+ title: i18n.translate(
+ 'xpack.apm.serviceIcons.serviceDetails.cloud.serviceNameLabel',
+ {
+ defaultMessage: 'Cloud service',
+ }
+ ),
+ description: (
+
+ {cloud.serviceName}
+ {isServerless && (
+
+
+
+ )}
+
+ ),
+ });
+ }
+
if (!!cloud.availabilityZones?.length) {
listItems.push({
title: i18n.translate(
@@ -58,7 +102,29 @@ export function CloudDetails({ cloud }: Props) {
});
}
- if (cloud.machineTypes) {
+ if (!!cloud.regions?.length) {
+ listItems.push({
+ title: i18n.translate(
+ 'xpack.apm.serviceIcons.serviceDetails.cloud.regionLabel',
+ {
+ defaultMessage:
+ '{regions, plural, =0 {Region} one {Region} other {Regions}} ',
+ values: { regions: cloud.regions.length },
+ }
+ ),
+ description: (
+
+ {cloud.regions.map((region, index) => (
+ -
+ {region}
+
+ ))}
+
+ ),
+ });
+ }
+
+ if (!!cloud.machineTypes?.length) {
listItems.push({
title: i18n.translate(
'xpack.apm.serviceIcons.serviceDetails.cloud.machineTypesLabel',
diff --git a/x-pack/plugins/apm/public/components/shared/service_icons/index.test.tsx b/x-pack/plugins/apm/public/components/shared/service_icons/index.test.tsx
index 778cdeb9be3f7..2e15ea9f0db5f 100644
--- a/x-pack/plugins/apm/public/components/shared/service_icons/index.test.tsx
+++ b/x-pack/plugins/apm/public/components/shared/service_icons/index.test.tsx
@@ -189,6 +189,7 @@ describe('ServiceIcons', () => {
data: {
agentName: 'java',
containerType: 'Kubernetes',
+ serverlessType: 'lambda',
cloudProvider: 'gcp',
},
status: fetcherHook.FETCH_STATUS.SUCCESS,
@@ -220,6 +221,7 @@ describe('ServiceIcons', () => {
expect(queryAllByTestId('loading')).toHaveLength(0);
expect(getByTestId('service')).toBeInTheDocument();
expect(getByTestId('container')).toBeInTheDocument();
+ expect(getByTestId('serverless')).toBeInTheDocument();
expect(getByTestId('cloud')).toBeInTheDocument();
fireEvent.click(getByTestId('popover_Service'));
expect(getByTestId('loading-content')).toBeInTheDocument();
@@ -231,6 +233,7 @@ describe('ServiceIcons', () => {
data: {
agentName: 'java',
containerType: 'Kubernetes',
+ serverlessType: '',
cloudProvider: 'gcp',
},
status: fetcherHook.FETCH_STATUS.SUCCESS,
@@ -269,5 +272,118 @@ describe('ServiceIcons', () => {
expect(getByText('Service')).toBeInTheDocument();
expect(getByText('v1.0.0')).toBeInTheDocument();
});
+
+ it('shows serverless content', () => {
+ const apisMockData = {
+ 'GET /internal/apm/services/{serviceName}/metadata/icons': {
+ data: {
+ agentName: 'java',
+ containerType: 'Kubernetes',
+ serverlessType: 'lambda',
+ cloudProvider: 'gcp',
+ },
+ status: fetcherHook.FETCH_STATUS.SUCCESS,
+ refetch: jest.fn(),
+ },
+ 'GET /internal/apm/services/{serviceName}/metadata/details': {
+ data: {
+ serverless: {
+ type: '',
+ functionNames: ['lambda-java-dev'],
+ faasTriggerTypes: ['datasource', 'http'],
+ },
+ },
+ status: fetcherHook.FETCH_STATUS.SUCCESS,
+ refetch: jest.fn(),
+ },
+ };
+ jest
+ .spyOn(fetcherHook, 'useFetcher')
+ .mockImplementation((func: Function, deps: string[]) => {
+ return func(callApmApi(apisMockData)) || {};
+ });
+
+ const { queryAllByTestId, getByTestId, getByText } = render(
+
+
+
+
+
+ );
+ expect(queryAllByTestId('loading')).toHaveLength(0);
+ expect(getByTestId('service')).toBeInTheDocument();
+ expect(getByTestId('container')).toBeInTheDocument();
+ expect(getByTestId('serverless')).toBeInTheDocument();
+ expect(getByTestId('cloud')).toBeInTheDocument();
+
+ fireEvent.click(getByTestId('popover_Serverless'));
+ expect(queryAllByTestId('loading-content')).toHaveLength(0);
+ expect(getByText('Serverless')).toBeInTheDocument();
+ expect(getByText('lambda-java-dev')).toBeInTheDocument();
+ expect(getByText('datasource')).toBeInTheDocument();
+ expect(getByText('http')).toBeInTheDocument();
+ });
+
+ it('shows cloud content', () => {
+ const apisMockData = {
+ 'GET /internal/apm/services/{serviceName}/metadata/icons': {
+ data: {
+ agentName: 'java',
+ containerType: 'Kubernetes',
+ serverlessType: 'lambda',
+ cloudProvider: 'gcp',
+ },
+ status: fetcherHook.FETCH_STATUS.SUCCESS,
+ refetch: jest.fn(),
+ },
+ 'GET /internal/apm/services/{serviceName}/metadata/details': {
+ data: {
+ cloud: {
+ provider: 'aws',
+ projectName: '',
+ serviceName: 'lambda',
+ availabilityZones: [],
+ regions: ['us-east-1'],
+ machineTypes: [],
+ },
+ },
+ status: fetcherHook.FETCH_STATUS.SUCCESS,
+ refetch: jest.fn(),
+ },
+ };
+ jest
+ .spyOn(fetcherHook, 'useFetcher')
+ .mockImplementation((func: Function, deps: string[]) => {
+ return func(callApmApi(apisMockData)) || {};
+ });
+
+ const { queryAllByTestId, getByTestId, getByText } = render(
+
+
+
+
+
+ );
+ expect(queryAllByTestId('loading')).toHaveLength(0);
+ expect(getByTestId('service')).toBeInTheDocument();
+ expect(getByTestId('container')).toBeInTheDocument();
+ expect(getByTestId('serverless')).toBeInTheDocument();
+ expect(getByTestId('cloud')).toBeInTheDocument();
+
+ fireEvent.click(getByTestId('popover_Cloud'));
+ expect(queryAllByTestId('loading-content')).toHaveLength(0);
+ expect(getByText('Cloud')).toBeInTheDocument();
+ expect(getByText('aws')).toBeInTheDocument();
+ expect(getByText('lambda')).toBeInTheDocument();
+ expect(getByText('us-east-1')).toBeInTheDocument();
+ });
});
});
diff --git a/x-pack/plugins/apm/public/components/shared/service_icons/index.tsx b/x-pack/plugins/apm/public/components/shared/service_icons/index.tsx
index 52c5ca37d818e..2fd578f10b110 100644
--- a/x-pack/plugins/apm/public/components/shared/service_icons/index.tsx
+++ b/x-pack/plugins/apm/public/components/shared/service_icons/index.tsx
@@ -13,6 +13,7 @@ import { ContainerType } from '../../../../common/service_metadata';
import { FETCH_STATUS, useFetcher } from '../../../hooks/use_fetcher';
import { getAgentIcon } from '../agent_icon/get_agent_icon';
import { CloudDetails } from './cloud_details';
+import { ServerlessDetails } from './serverless_details';
import { ContainerDetails } from './container_details';
import { IconPopover } from './icon_popover';
import { ServiceDetails } from './service_details';
@@ -47,7 +48,7 @@ export function getContainerIcon(container?: ContainerType) {
}
}
-type Icons = 'service' | 'container' | 'cloud' | 'alerts';
+type Icons = 'service' | 'container' | 'serverless' | 'cloud' | 'alerts';
export interface PopoverItem {
key: Icons;
@@ -130,6 +131,17 @@ export function ServiceIcons({ start, end, serviceName }: Props) {
}),
component: ,
},
+ {
+ key: 'serverless',
+ icon: {
+ type: getAgentIcon(icons?.serverlessType, theme.darkMode) || 'node',
+ },
+ isVisible: !!icons?.serverlessType,
+ title: i18n.translate('xpack.apm.serviceIcons.serverless', {
+ defaultMessage: 'Serverless',
+ }),
+ component: ,
+ },
{
key: 'cloud',
icon: {
@@ -139,7 +151,12 @@ export function ServiceIcons({ start, end, serviceName }: Props) {
title: i18n.translate('xpack.apm.serviceIcons.cloud', {
defaultMessage: 'Cloud',
}),
- component: ,
+ component: (
+
+ ),
},
];
diff --git a/x-pack/plugins/apm/public/components/shared/service_icons/serverless_details.tsx b/x-pack/plugins/apm/public/components/shared/service_icons/serverless_details.tsx
new file mode 100644
index 0000000000000..d05abab6d674a
--- /dev/null
+++ b/x-pack/plugins/apm/public/components/shared/service_icons/serverless_details.tsx
@@ -0,0 +1,73 @@
+/*
+ * 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 { EuiBadge, EuiDescriptionList } from '@elastic/eui';
+import { EuiDescriptionListProps } from '@elastic/eui/src/components/description_list/description_list';
+import { i18n } from '@kbn/i18n';
+import React from 'react';
+import { APIReturnType } from '../../../services/rest/create_call_apm_api';
+
+type ServiceDetailsReturnType =
+ APIReturnType<'GET /internal/apm/services/{serviceName}/metadata/details'>;
+
+interface Props {
+ serverless: ServiceDetailsReturnType['serverless'];
+}
+
+export function ServerlessDetails({ serverless }: Props) {
+ if (!serverless) {
+ return null;
+ }
+
+ const listItems: EuiDescriptionListProps['listItems'] = [];
+
+ if (!!serverless.functionNames?.length) {
+ listItems.push({
+ title: i18n.translate(
+ 'xpack.apm.serviceIcons.serviceDetails.cloud.functionNameLabel',
+ {
+ defaultMessage:
+ '{functionNames, plural, =0 {Function name} one {Function name} other {Function names}} ',
+ values: { functionNames: serverless.functionNames.length },
+ }
+ ),
+ description: (
+
+ {serverless.functionNames.map((type, index) => (
+ -
+ {type}
+
+ ))}
+
+ ),
+ });
+ }
+
+ if (!!serverless.faasTriggerTypes?.length) {
+ listItems.push({
+ title: i18n.translate(
+ 'xpack.apm.serviceIcons.serviceDetails.cloud.faasTriggerTypeLabel',
+ {
+ defaultMessage:
+ '{triggerTypes, plural, =0 {Trigger type} one {Trigger type} other {Trigger types}} ',
+ values: { triggerTypes: serverless.faasTriggerTypes.length },
+ }
+ ),
+ description: (
+
+ {serverless.faasTriggerTypes.map((type, index) => (
+ -
+ {type}
+
+ ))}
+
+ ),
+ });
+ }
+
+ return ;
+}
diff --git a/x-pack/plugins/apm/public/components/shared/summary/transaction_summary.tsx b/x-pack/plugins/apm/public/components/shared/summary/transaction_summary.tsx
index 399121b710ce9..7ceec331ae8fe 100644
--- a/x-pack/plugins/apm/public/components/shared/summary/transaction_summary.tsx
+++ b/x-pack/plugins/apm/public/components/shared/summary/transaction_summary.tsx
@@ -14,11 +14,13 @@ import { ErrorCountSummaryItemBadge } from './error_count_summary_item_badge';
import { HttpInfoSummaryItem } from './http_info_summary_item';
import { TransactionResultSummaryItem } from './transaction_result_summary_item';
import { UserAgentSummaryItem } from './user_agent_summary_item';
+import { ColdStartBadge } from '../../app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/badge/cold_start_badge';
interface Props {
transaction: Transaction;
totalDuration: number | undefined;
errorCount: number;
+ coldStartBadge?: boolean;
}
function getTransactionResultSummaryItem(transaction: Transaction) {
@@ -39,7 +41,12 @@ function getTransactionResultSummaryItem(transaction: Transaction) {
return null;
}
-function TransactionSummary({ transaction, totalDuration, errorCount }: Props) {
+function TransactionSummary({
+ transaction,
+ totalDuration,
+ errorCount,
+ coldStartBadge,
+}: Props) {
const items = [
,
) : null,
+ coldStartBadge ? : null,
];
return ;
diff --git a/x-pack/plugins/apm/server/lib/helpers/transaction_coldstart_rate.ts b/x-pack/plugins/apm/server/lib/helpers/transaction_coldstart_rate.ts
new file mode 100644
index 0000000000000..094d5ed6350df
--- /dev/null
+++ b/x-pack/plugins/apm/server/lib/helpers/transaction_coldstart_rate.ts
@@ -0,0 +1,67 @@
+/*
+ * 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 { FAAS_COLDSTART } from '../../../common/elasticsearch_fieldnames';
+import {
+ AggregationOptionsByType,
+ AggregationResultOf,
+} from '../../../../../../src/core/types/elasticsearch';
+
+export const getColdstartAggregation = () => ({
+ terms: {
+ field: FAAS_COLDSTART,
+ },
+});
+
+type ColdstartAggregation = ReturnType;
+
+export const getTimeseriesAggregation = (
+ start: number,
+ end: number,
+ intervalString: string
+) => ({
+ date_histogram: {
+ field: '@timestamp',
+ fixed_interval: intervalString,
+ min_doc_count: 0,
+ extended_bounds: { min: start, max: end },
+ },
+ aggs: { coldstartStates: getColdstartAggregation() },
+});
+
+export function calculateTransactionColdstartRate(
+ coldstartStatesResponse: AggregationResultOf
+) {
+ const coldstartStates = Object.fromEntries(
+ coldstartStatesResponse.buckets.map(({ key, doc_count: count }) => [
+ key === 1 ? 'true' : 'false',
+ count,
+ ])
+ );
+
+ const coldstarts = coldstartStates.true ?? 0;
+ const warmstarts = coldstartStates.false ?? 0;
+
+ return coldstarts / (coldstarts + warmstarts);
+}
+
+export function getTransactionColdstartRateTimeSeries(
+ buckets: AggregationResultOf<
+ {
+ date_histogram: AggregationOptionsByType['date_histogram'];
+ aggs: { coldstartStates: ColdstartAggregation };
+ },
+ {}
+ >['buckets']
+) {
+ return buckets.map((dateBucket) => {
+ return {
+ x: dateBucket.key,
+ y: calculateTransactionColdstartRate(dateBucket.coldstartStates),
+ };
+ });
+}
diff --git a/x-pack/plugins/apm/server/lib/transaction_groups/get_coldstart_rate.ts b/x-pack/plugins/apm/server/lib/transaction_groups/get_coldstart_rate.ts
new file mode 100644
index 0000000000000..e7c9e111be7a5
--- /dev/null
+++ b/x-pack/plugins/apm/server/lib/transaction_groups/get_coldstart_rate.ts
@@ -0,0 +1,181 @@
+/*
+ * 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 {
+ FAAS_COLDSTART,
+ SERVICE_NAME,
+ TRANSACTION_NAME,
+ TRANSACTION_TYPE,
+} from '../../../common/elasticsearch_fieldnames';
+import { offsetPreviousPeriodCoordinates } from '../../../common/utils/offset_previous_period_coordinate';
+import { kqlQuery, rangeQuery } from '../../../../observability/server';
+import { environmentQuery } from '../../../common/utils/environment_query';
+import { Coordinate } from '../../../typings/timeseries';
+import {
+ getDocumentTypeFilterForTransactions,
+ getProcessorEventForTransactions,
+} from '../helpers/transactions';
+import { getBucketSizeForAggregatedTransactions } from '../helpers/get_bucket_size_for_aggregated_transactions';
+import { Setup } from '../helpers/setup_request';
+import {
+ calculateTransactionColdstartRate,
+ getColdstartAggregation,
+ getTransactionColdstartRateTimeSeries,
+} from '../helpers/transaction_coldstart_rate';
+import { termQuery } from '../../../../observability/server';
+
+export async function getColdstartRate({
+ environment,
+ kuery,
+ serviceName,
+ transactionType,
+ transactionName,
+ setup,
+ searchAggregatedTransactions,
+ start,
+ end,
+}: {
+ environment: string;
+ kuery: string;
+ serviceName: string;
+ transactionType?: string;
+ transactionName: string;
+ setup: Setup;
+ searchAggregatedTransactions: boolean;
+ start: number;
+ end: number;
+}): Promise<{
+ transactionColdstartRate: Coordinate[];
+ average: number | null;
+}> {
+ const { apmEventClient } = setup;
+
+ const filter = [
+ ...termQuery(SERVICE_NAME, serviceName),
+ { exists: { field: FAAS_COLDSTART } },
+ ...(transactionName ? termQuery(TRANSACTION_NAME, transactionName) : []),
+ ...termQuery(TRANSACTION_TYPE, transactionType),
+ ...getDocumentTypeFilterForTransactions(searchAggregatedTransactions),
+ ...rangeQuery(start, end),
+ ...environmentQuery(environment),
+ ...kqlQuery(kuery),
+ ];
+
+ const coldstartStates = getColdstartAggregation();
+
+ const params = {
+ apm: {
+ events: [getProcessorEventForTransactions(searchAggregatedTransactions)],
+ },
+ body: {
+ size: 0,
+ query: { bool: { filter } },
+ aggs: {
+ coldstartStates,
+ timeseries: {
+ date_histogram: {
+ field: '@timestamp',
+ fixed_interval: getBucketSizeForAggregatedTransactions({
+ start,
+ end,
+ searchAggregatedTransactions,
+ }).intervalString,
+ min_doc_count: 0,
+ extended_bounds: { min: start, max: end },
+ },
+ aggs: {
+ coldstartStates,
+ },
+ },
+ },
+ },
+ };
+
+ const resp = await apmEventClient.search(
+ 'get_transaction_group_coldstart_rate',
+ params
+ );
+
+ if (!resp.aggregations) {
+ return { transactionColdstartRate: [], average: null };
+ }
+
+ const transactionColdstartRate = getTransactionColdstartRateTimeSeries(
+ resp.aggregations.timeseries.buckets
+ );
+
+ const average = calculateTransactionColdstartRate(
+ resp.aggregations.coldstartStates
+ );
+
+ return { transactionColdstartRate, average };
+}
+
+export async function getColdstartRatePeriods({
+ environment,
+ kuery,
+ serviceName,
+ transactionType,
+ transactionName = '',
+ setup,
+ searchAggregatedTransactions,
+ comparisonStart,
+ comparisonEnd,
+ start,
+ end,
+}: {
+ environment: string;
+ kuery: string;
+ serviceName: string;
+ transactionType?: string;
+ transactionName?: string;
+ setup: Setup;
+ searchAggregatedTransactions: boolean;
+ comparisonStart?: number;
+ comparisonEnd?: number;
+ start: number;
+ end: number;
+}) {
+ const commonProps = {
+ environment,
+ kuery,
+ serviceName,
+ transactionType,
+ transactionName,
+ setup,
+ searchAggregatedTransactions,
+ };
+
+ const currentPeriodPromise = getColdstartRate({ ...commonProps, start, end });
+
+ const previousPeriodPromise =
+ comparisonStart && comparisonEnd
+ ? getColdstartRate({
+ ...commonProps,
+ start: comparisonStart,
+ end: comparisonEnd,
+ })
+ : { transactionColdstartRate: [], average: null };
+
+ const [currentPeriod, previousPeriod] = await Promise.all([
+ currentPeriodPromise,
+ previousPeriodPromise,
+ ]);
+
+ const firstCurrentPeriod = currentPeriod.transactionColdstartRate;
+
+ return {
+ currentPeriod,
+ previousPeriod: {
+ ...previousPeriod,
+ transactionColdstartRate: offsetPreviousPeriodCoordinates({
+ currentPeriodTimeseries: firstCurrentPeriod,
+ previousPeriodTimeseries: previousPeriod.transactionColdstartRate,
+ }),
+ },
+ };
+}
diff --git a/x-pack/plugins/apm/server/routes/services/get_service_metadata_details.ts b/x-pack/plugins/apm/server/routes/services/get_service_metadata_details.ts
index b7ce68a0de578..6b7f16084e5f3 100644
--- a/x-pack/plugins/apm/server/routes/services/get_service_metadata_details.ts
+++ b/x-pack/plugins/apm/server/routes/services/get_service_metadata_details.ts
@@ -10,7 +10,9 @@ import {
AGENT,
CLOUD,
CLOUD_AVAILABILITY_ZONE,
+ CLOUD_REGION,
CLOUD_MACHINE_TYPE,
+ CLOUD_SERVICE_NAME,
CONTAINER_ID,
HOST,
KUBERNETES,
@@ -18,6 +20,8 @@ import {
SERVICE_NAME,
SERVICE_NODE_NAME,
SERVICE_VERSION,
+ FAAS_ID,
+ FAAS_TRIGGER_TYPE,
} from '../../../common/elasticsearch_fieldnames';
import { ContainerType } from '../../../common/service_metadata';
import { rangeQuery } from '../../../../observability/server';
@@ -50,11 +54,18 @@ export interface ServiceMetadataDetails {
totalNumberInstances?: number;
type?: ContainerType;
};
+ serverless?: {
+ type?: string;
+ functionNames?: string[];
+ faasTriggerTypes?: string[];
+ };
cloud?: {
provider?: string;
availabilityZones?: string[];
+ regions?: string[];
machineTypes?: string[];
projectName?: string;
+ serviceName?: string;
};
}
@@ -104,12 +115,36 @@ export async function getServiceMetadataDetails({
size: 10,
},
},
+ regions: {
+ terms: {
+ field: CLOUD_REGION,
+ size: 10,
+ },
+ },
+ cloudServices: {
+ terms: {
+ field: CLOUD_SERVICE_NAME,
+ size: 1,
+ },
+ },
machineTypes: {
terms: {
field: CLOUD_MACHINE_TYPE,
size: 10,
},
},
+ faasTriggerTypes: {
+ terms: {
+ field: FAAS_TRIGGER_TYPE,
+ size: 10,
+ },
+ },
+ faasFunctionNames: {
+ terms: {
+ field: FAAS_ID,
+ size: 10,
+ },
+ },
totalNumberInstances: { cardinality: { field: SERVICE_NODE_NAME } },
},
},
@@ -153,13 +188,30 @@ export async function getServiceMetadataDetails({
}
: undefined;
+ const serverlessDetails =
+ !!response.aggregations?.faasTriggerTypes?.buckets.length && cloud
+ ? {
+ type: cloud.service?.name,
+ functionNames: response.aggregations?.faasFunctionNames.buckets
+ .map((bucket) => getLambdaFunctionNameFromARN(bucket.key as string))
+ .filter((name) => name),
+ faasTriggerTypes: response.aggregations?.faasTriggerTypes.buckets.map(
+ (bucket) => bucket.key as string
+ ),
+ }
+ : undefined;
+
const cloudDetails = cloud
? {
provider: cloud.provider,
projectName: cloud.project?.name,
+ serviceName: cloud.service?.name,
availabilityZones: response.aggregations?.availabilityZones.buckets.map(
(bucket) => bucket.key as string
),
+ regions: response.aggregations?.regions.buckets.map(
+ (bucket) => bucket.key as string
+ ),
machineTypes: response.aggregations?.machineTypes.buckets.map(
(bucket) => bucket.key as string
),
@@ -169,6 +221,12 @@ export async function getServiceMetadataDetails({
return {
service: serviceMetadataDetails,
container: containerDetails,
+ serverless: serverlessDetails,
cloud: cloudDetails,
};
}
+
+function getLambdaFunctionNameFromARN(arn: string) {
+ // Lambda function ARN example: arn:aws:lambda:us-west-2:123456789012:function:my-function
+ return arn.split(':')[6] || '';
+}
diff --git a/x-pack/plugins/apm/server/routes/services/get_service_metadata_icons.ts b/x-pack/plugins/apm/server/routes/services/get_service_metadata_icons.ts
index ca97e9d58f060..6082ae6c5b112 100644
--- a/x-pack/plugins/apm/server/routes/services/get_service_metadata_icons.ts
+++ b/x-pack/plugins/apm/server/routes/services/get_service_metadata_icons.ts
@@ -9,6 +9,7 @@ import { ProcessorEvent } from '../../../common/processor_event';
import {
AGENT_NAME,
CLOUD_PROVIDER,
+ CLOUD_SERVICE_NAME,
CONTAINER_ID,
KUBERNETES,
SERVICE_NAME,
@@ -29,6 +30,7 @@ type ServiceMetadataIconsRaw = Pick<
export interface ServiceMetadataIcons {
agentName?: string;
containerType?: ContainerType;
+ serverlessType?: string;
cloudProvider?: string;
}
@@ -70,7 +72,13 @@ export async function getServiceMetadataIcons({
},
body: {
size: 1,
- _source: [KUBERNETES, CLOUD_PROVIDER, CONTAINER_ID, AGENT_NAME],
+ _source: [
+ KUBERNETES,
+ CLOUD_PROVIDER,
+ CONTAINER_ID,
+ AGENT_NAME,
+ CLOUD_SERVICE_NAME,
+ ],
query: { bool: { filter, should } },
},
};
@@ -85,6 +93,7 @@ export async function getServiceMetadataIcons({
agentName: undefined,
containerType: undefined,
cloudProvider: undefined,
+ serverlessType: undefined,
};
}
@@ -98,9 +107,15 @@ export async function getServiceMetadataIcons({
containerType = 'Docker';
}
+ let serverlessType: string | undefined;
+ if (cloud?.provider === 'aws' && cloud?.service?.name === 'lambda') {
+ serverlessType = 'lambda';
+ }
+
return {
agentName: agent?.name,
containerType,
+ serverlessType,
cloudProvider: cloud?.provider,
};
}
diff --git a/x-pack/plugins/apm/server/routes/transactions/route.ts b/x-pack/plugins/apm/server/routes/transactions/route.ts
index e22e521c699ec..657387037855f 100644
--- a/x-pack/plugins/apm/server/routes/transactions/route.ts
+++ b/x-pack/plugins/apm/server/routes/transactions/route.ts
@@ -19,6 +19,7 @@ import { getTransactionBreakdown } from './breakdown';
import { getTransactionTraceSamples } from './trace_samples';
import { getLatencyPeriods } from './get_latency_charts';
import { getFailedTransactionRatePeriods } from './get_failed_transaction_rate_periods';
+import { getColdstartRatePeriods } from '../../lib/transaction_groups/get_coldstart_rate';
import { createApmServerRoute } from '../apm_routes/create_apm_server_route';
import {
comparisonRangeRt,
@@ -461,6 +462,158 @@ const transactionChartsErrorRateRoute = createApmServerRoute({
},
});
+const transactionChartsColdstartRateRoute = createApmServerRoute({
+ endpoint:
+ 'GET /internal/apm/services/{serviceName}/transactions/charts/coldstart_rate',
+ params: t.type({
+ path: t.type({
+ serviceName: t.string,
+ }),
+ query: t.intersection([
+ t.type({ transactionType: t.string }),
+ t.intersection([environmentRt, kueryRt, rangeRt, comparisonRangeRt]),
+ ]),
+ }),
+ options: { tags: ['access:apm'] },
+ handler: async (
+ resources
+ ): Promise<{
+ currentPeriod: {
+ transactionColdstartRate: Array<
+ import('../../../typings/timeseries').Coordinate
+ >;
+ average: number | null;
+ };
+ previousPeriod:
+ | {
+ transactionColdstartRate: Array<{
+ x: number;
+ y: import('../../../typings/common').Maybe;
+ }>;
+ average: number | null;
+ }
+ | {
+ transactionColdstartRate: Array<{
+ x: number;
+ y: import('../../../typings/common').Maybe;
+ }>;
+ average: null;
+ };
+ }> => {
+ const setup = await setupRequest(resources);
+
+ const { params } = resources;
+ const { serviceName } = params.path;
+ const {
+ environment,
+ kuery,
+ transactionType,
+ comparisonStart,
+ comparisonEnd,
+ start,
+ end,
+ } = params.query;
+
+ const searchAggregatedTransactions = await getSearchAggregatedTransactions({
+ ...setup,
+ kuery,
+ start,
+ end,
+ });
+
+ return getColdstartRatePeriods({
+ environment,
+ kuery,
+ serviceName,
+ transactionType,
+ setup,
+ searchAggregatedTransactions,
+ comparisonStart,
+ comparisonEnd,
+ start,
+ end,
+ });
+ },
+});
+
+const transactionChartsColdstartRateByTransactionNameRoute =
+ createApmServerRoute({
+ endpoint:
+ 'GET /internal/apm/services/{serviceName}/transactions/charts/coldstart_rate_by_transaction_name',
+ params: t.type({
+ path: t.type({
+ serviceName: t.string,
+ }),
+ query: t.intersection([
+ t.type({ transactionType: t.string, transactionName: t.string }),
+ t.intersection([environmentRt, kueryRt, rangeRt, comparisonRangeRt]),
+ ]),
+ }),
+ options: { tags: ['access:apm'] },
+ handler: async (
+ resources
+ ): Promise<{
+ currentPeriod: {
+ transactionColdstartRate: Array<
+ import('../../../typings/timeseries').Coordinate
+ >;
+ average: number | null;
+ };
+ previousPeriod:
+ | {
+ transactionColdstartRate: Array<{
+ x: number;
+ y: import('../../../typings/common').Maybe;
+ }>;
+ average: number | null;
+ }
+ | {
+ transactionColdstartRate: Array<{
+ x: number;
+ y: import('../../../typings/common').Maybe;
+ }>;
+ average: null;
+ };
+ }> => {
+ const setup = await setupRequest(resources);
+
+ const { params } = resources;
+ const { serviceName } = params.path;
+ const {
+ environment,
+ kuery,
+ transactionType,
+ transactionName,
+ comparisonStart,
+ comparisonEnd,
+ start,
+ end,
+ } = params.query;
+
+ const searchAggregatedTransactions =
+ await getSearchAggregatedTransactions({
+ ...setup,
+ kuery,
+ start,
+ end,
+ });
+
+ return getColdstartRatePeriods({
+ environment,
+ kuery,
+ serviceName,
+ transactionType,
+ transactionName,
+ setup,
+ searchAggregatedTransactions,
+ comparisonStart,
+ comparisonEnd,
+ start,
+ end,
+ });
+ },
+ });
+
export const transactionRouteRepository = {
...transactionGroupsMainStatisticsRoute,
...transactionGroupsDetailedStatisticsRoute,
@@ -468,4 +621,6 @@ export const transactionRouteRepository = {
...transactionTraceSamplesRoute,
...transactionChartsBreakdownRoute,
...transactionChartsErrorRateRoute,
+ ...transactionChartsColdstartRateRoute,
+ ...transactionChartsColdstartRateByTransactionNameRoute,
};
diff --git a/x-pack/plugins/apm/typings/es_schemas/raw/fields/cloud.ts b/x-pack/plugins/apm/typings/es_schemas/raw/fields/cloud.ts
index 8891318554e92..bc0c3ea8002ad 100644
--- a/x-pack/plugins/apm/typings/es_schemas/raw/fields/cloud.ts
+++ b/x-pack/plugins/apm/typings/es_schemas/raw/fields/cloud.ts
@@ -27,4 +27,7 @@ export interface Cloud {
image?: {
id: string;
};
+ service?: {
+ name: string;
+ };
}
diff --git a/x-pack/plugins/apm/typings/es_schemas/raw/fields/faas.ts b/x-pack/plugins/apm/typings/es_schemas/raw/fields/faas.ts
new file mode 100644
index 0000000000000..1229b8134ac13
--- /dev/null
+++ b/x-pack/plugins/apm/typings/es_schemas/raw/fields/faas.ts
@@ -0,0 +1,16 @@
+/*
+ * 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.
+ */
+
+export interface Faas {
+ id: string;
+ coldstart?: boolean;
+ execution?: string;
+ trigger?: {
+ type?: string;
+ request_id?: string;
+ };
+}
diff --git a/x-pack/plugins/apm/typings/es_schemas/raw/transaction_raw.ts b/x-pack/plugins/apm/typings/es_schemas/raw/transaction_raw.ts
index 34c391134b604..0811bfb8c1a79 100644
--- a/x-pack/plugins/apm/typings/es_schemas/raw/transaction_raw.ts
+++ b/x-pack/plugins/apm/typings/es_schemas/raw/transaction_raw.ts
@@ -19,6 +19,7 @@ import { TimestampUs } from './fields/timestamp_us';
import { Url } from './fields/url';
import { User } from './fields/user';
import { UserAgent } from './fields/user_agent';
+import { Faas } from './fields/faas';
interface Processor {
name: 'transaction';
@@ -69,4 +70,5 @@ export interface TransactionRaw extends APMBaseDoc {
user?: User;
user_agent?: UserAgent;
cloud?: Cloud;
+ faas?: Faas;
}
diff --git a/x-pack/test/apm_api_integration/tests/cold_start/cold_start.spec.ts b/x-pack/test/apm_api_integration/tests/cold_start/cold_start.spec.ts
new file mode 100644
index 0000000000000..fedf44d477d2d
--- /dev/null
+++ b/x-pack/test/apm_api_integration/tests/cold_start/cold_start.spec.ts
@@ -0,0 +1,205 @@
+/*
+ * 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 expect from '@kbn/expect';
+import { first, last } from 'lodash';
+import moment from 'moment';
+import { FtrProviderContext } from '../../common/ftr_provider_context';
+import { dataConfig, generateData } from './generate_data';
+import {
+ APIReturnType,
+ APIClientRequestParamsOf,
+} from '../../../../plugins/apm/public/services/rest/create_call_apm_api';
+import { RecursivePartial } from '../../../../plugins/apm/typings/common';
+import { isFiniteNumber } from '../../../../plugins/apm/common/utils/is_finite_number';
+
+type ColdStartRate =
+ APIReturnType<'GET /internal/apm/services/{serviceName}/transactions/charts/coldstart_rate'>;
+
+export default function ApiTest({ getService }: FtrProviderContext) {
+ const registry = getService('registry');
+ const apmApiClient = getService('apmApiClient');
+ const synthtraceEsClient = getService('synthtraceEsClient');
+
+ const { serviceName } = dataConfig;
+ const start = new Date('2021-01-01T00:00:00.000Z').getTime();
+ const end = new Date('2021-01-01T00:15:00.000Z').getTime() - 1;
+
+ async function callApi(
+ overrides?: RecursivePartial<
+ APIClientRequestParamsOf<'GET /internal/apm/services/{serviceName}/transactions/charts/coldstart_rate'>['params']
+ >
+ ) {
+ return await apmApiClient.readUser({
+ endpoint: 'GET /internal/apm/services/{serviceName}/transactions/charts/coldstart_rate',
+ params: {
+ path: { serviceName },
+ query: {
+ transactionType: 'request',
+ environment: 'ENVIRONMENT_ALL',
+ start: new Date(start).toISOString(),
+ end: new Date(end).toISOString(),
+ kuery: '',
+ ...overrides?.query,
+ },
+ },
+ });
+ }
+
+ registry.when(
+ 'Cold start rate when data is not loaded',
+ { config: 'basic', archives: [] },
+ () => {
+ it('handles empty state', async () => {
+ const { status, body } = await callApi();
+
+ expect(status).to.be(200);
+ expect(body.currentPeriod.transactionColdstartRate).to.empty();
+ expect(body.currentPeriod.average).to.be(null);
+
+ expect(body.previousPeriod.transactionColdstartRate).to.empty();
+ expect(body.previousPeriod.average).to.be(null);
+ });
+ }
+ );
+
+ registry.when(
+ 'Cold start rate when data is generated',
+ { config: 'basic', archives: ['apm_mappings_only_8.0.0'] },
+ () => {
+ describe('without comparison', () => {
+ let body: ColdStartRate;
+ let status: number;
+
+ before(async () => {
+ await generateData({
+ synthtraceEsClient,
+ start,
+ end,
+ coldStartRate: 10,
+ warmStartRate: 30,
+ });
+ const response = await callApi();
+ body = response.body;
+ status = response.status;
+ });
+
+ after(() => synthtraceEsClient.clean());
+
+ it('returns correct HTTP status', () => {
+ expect(status).to.be(200);
+ });
+
+ it('returns an array of transaction cold start rates', () => {
+ expect(body).to.have.property('currentPeriod');
+ expect(body.currentPeriod.transactionColdstartRate).to.have.length(15);
+ expect(body.currentPeriod.transactionColdstartRate.every(({ y }) => y === 0.25)).to.be(
+ true
+ );
+ });
+
+ it('returns correct average rate', () => {
+ expect(body.currentPeriod.average).to.be(0.25);
+ });
+
+ it("doesn't have data for the previous period", () => {
+ expect(body).to.have.property('previousPeriod');
+ expect(body.previousPeriod.transactionColdstartRate).to.have.length(0);
+ expect(body.previousPeriod.average).to.be(null);
+ });
+ });
+
+ describe('with comparison', () => {
+ let body: ColdStartRate;
+ let status: number;
+
+ before(async () => {
+ const startDate = moment(start).add(6, 'minutes');
+ const endDate = moment(start).add(9, 'minutes');
+ const comparisonStartDate = new Date(start);
+ const comparisonEndDate = moment(start).add(3, 'minutes');
+
+ await generateData({
+ synthtraceEsClient,
+ start: startDate.valueOf(),
+ end: endDate.valueOf(),
+ coldStartRate: 10,
+ warmStartRate: 30,
+ });
+ await generateData({
+ synthtraceEsClient,
+ start: comparisonStartDate.getTime(),
+ end: comparisonEndDate.valueOf(),
+ coldStartRate: 20,
+ warmStartRate: 20,
+ });
+
+ const response = await callApi({
+ query: {
+ start: startDate.toISOString(),
+ end: endDate.subtract(1, 'seconds').toISOString(),
+ comparisonStart: comparisonStartDate.toISOString(),
+ comparisonEnd: comparisonEndDate.subtract(1, 'seconds').toISOString(),
+ },
+ });
+ body = response.body;
+ status = response.status;
+ });
+
+ after(() => synthtraceEsClient.clean());
+
+ it('returns correct HTTP status', () => {
+ expect(status).to.be(200);
+ });
+
+ it('returns some data', () => {
+ expect(body.currentPeriod.average).not.to.be(null);
+ expect(body.currentPeriod.transactionColdstartRate.length).to.be.greaterThan(0);
+ const hasCurrentPeriodData = body.currentPeriod.transactionColdstartRate.some(({ y }) =>
+ isFiniteNumber(y)
+ );
+ expect(hasCurrentPeriodData).to.equal(true);
+
+ expect(body.previousPeriod.average).not.to.be(null);
+ expect(body.previousPeriod.transactionColdstartRate.length).to.be.greaterThan(0);
+ const hasPreviousPeriodData = body.previousPeriod.transactionColdstartRate.some(({ y }) =>
+ isFiniteNumber(y)
+ );
+ expect(hasPreviousPeriodData).to.equal(true);
+ });
+
+ it('has same start time for both periods', () => {
+ expect(first(body.currentPeriod.transactionColdstartRate)?.x).to.equal(
+ first(body.previousPeriod.transactionColdstartRate)?.x
+ );
+ });
+
+ it('has same end time for both periods', () => {
+ expect(last(body.currentPeriod.transactionColdstartRate)?.x).to.equal(
+ last(body.previousPeriod.transactionColdstartRate)?.x
+ );
+ });
+
+ it('returns an array of transaction cold start rates', () => {
+ expect(body.currentPeriod.transactionColdstartRate).to.have.length(3);
+ expect(body.currentPeriod.transactionColdstartRate.every(({ y }) => y === 0.25)).to.be(
+ true
+ );
+
+ expect(body.previousPeriod.transactionColdstartRate).to.have.length(3);
+ expect(body.previousPeriod.transactionColdstartRate.every(({ y }) => y === 0.5)).to.be(
+ true
+ );
+ });
+
+ it('has same average value for both periods', () => {
+ expect(body.currentPeriod.average).to.be(0.25);
+ expect(body.previousPeriod.average).to.be(0.5);
+ });
+ });
+ }
+ );
+}
diff --git a/x-pack/test/apm_api_integration/tests/cold_start/cold_start_by_transaction_name/cold_start_by_transaction_name.spec.ts b/x-pack/test/apm_api_integration/tests/cold_start/cold_start_by_transaction_name/cold_start_by_transaction_name.spec.ts
new file mode 100644
index 0000000000000..d577077490b88
--- /dev/null
+++ b/x-pack/test/apm_api_integration/tests/cold_start/cold_start_by_transaction_name/cold_start_by_transaction_name.spec.ts
@@ -0,0 +1,207 @@
+/*
+ * 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 expect from '@kbn/expect';
+import { first, last } from 'lodash';
+import moment from 'moment';
+import { FtrProviderContext } from '../../../common/ftr_provider_context';
+import { dataConfig, generateData } from './generate_data';
+import {
+ APIReturnType,
+ APIClientRequestParamsOf,
+} from '../../../../../plugins/apm/public/services/rest/create_call_apm_api';
+import { RecursivePartial } from '../../../../../plugins/apm/typings/common';
+import { isFiniteNumber } from '../../../../../plugins/apm/common/utils/is_finite_number';
+
+type ColdStartRate =
+ APIReturnType<'GET /internal/apm/services/{serviceName}/transactions/charts/coldstart_rate_by_transaction_name'>;
+
+export default function ApiTest({ getService }: FtrProviderContext) {
+ const registry = getService('registry');
+ const apmApiClient = getService('apmApiClient');
+ const synthtraceEsClient = getService('synthtraceEsClient');
+
+ const { serviceName, transactionName } = dataConfig;
+ const start = new Date('2021-01-01T00:00:00.000Z').getTime();
+ const end = new Date('2021-01-01T00:15:00.000Z').getTime() - 1;
+
+ async function callApi(
+ overrides?: RecursivePartial<
+ APIClientRequestParamsOf<'GET /internal/apm/services/{serviceName}/transactions/charts/coldstart_rate'>['params']
+ >
+ ) {
+ return await apmApiClient.readUser({
+ endpoint:
+ 'GET /internal/apm/services/{serviceName}/transactions/charts/coldstart_rate_by_transaction_name',
+ params: {
+ path: { serviceName },
+ query: {
+ transactionType: 'request',
+ transactionName,
+ environment: 'ENVIRONMENT_ALL',
+ start: new Date(start).toISOString(),
+ end: new Date(end).toISOString(),
+ kuery: '',
+ ...overrides?.query,
+ },
+ },
+ });
+ }
+
+ registry.when(
+ 'Cold start rate by transaction name when data is not loaded',
+ { config: 'basic', archives: [] },
+ () => {
+ it('handles empty state', async () => {
+ const { status, body } = await callApi();
+
+ expect(status).to.be(200);
+ expect(body.currentPeriod.transactionColdstartRate).to.empty();
+ expect(body.currentPeriod.average).to.be(null);
+
+ expect(body.previousPeriod.transactionColdstartRate).to.empty();
+ expect(body.previousPeriod.average).to.be(null);
+ });
+ }
+ );
+
+ registry.when(
+ 'Cold start rate by transaction name when data is generated',
+ { config: 'basic', archives: ['apm_mappings_only_8.0.0'] },
+ () => {
+ describe('without comparison', () => {
+ let body: ColdStartRate;
+ let status: number;
+
+ before(async () => {
+ await generateData({
+ synthtraceEsClient,
+ start,
+ end,
+ coldStartRate: 10,
+ warmStartRate: 30,
+ });
+ const response = await callApi();
+ body = response.body;
+ status = response.status;
+ });
+
+ after(() => synthtraceEsClient.clean());
+
+ it('returns correct HTTP status', () => {
+ expect(status).to.be(200);
+ });
+
+ it('returns an array of transaction cold start rates', () => {
+ expect(body).to.have.property('currentPeriod');
+ expect(body.currentPeriod.transactionColdstartRate).to.have.length(15);
+ expect(body.currentPeriod.transactionColdstartRate.every(({ y }) => y === 0.25)).to.be(
+ true
+ );
+ });
+
+ it('returns correct average rate', () => {
+ expect(body.currentPeriod.average).to.be(0.25);
+ });
+
+ it("doesn't have data for the previous period", () => {
+ expect(body).to.have.property('previousPeriod');
+ expect(body.previousPeriod.transactionColdstartRate).to.have.length(0);
+ expect(body.previousPeriod.average).to.be(null);
+ });
+ });
+
+ describe('with comparison', () => {
+ let body: ColdStartRate;
+ let status: number;
+
+ before(async () => {
+ const startDate = moment(start).add(6, 'minutes');
+ const endDate = moment(start).add(9, 'minutes');
+ const comparisonStartDate = new Date(start);
+ const comparisonEndDate = moment(start).add(3, 'minutes');
+
+ await generateData({
+ synthtraceEsClient,
+ start: startDate.valueOf(),
+ end: endDate.valueOf(),
+ coldStartRate: 10,
+ warmStartRate: 30,
+ });
+ await generateData({
+ synthtraceEsClient,
+ start: comparisonStartDate.getTime(),
+ end: comparisonEndDate.valueOf(),
+ coldStartRate: 20,
+ warmStartRate: 20,
+ });
+
+ const response = await callApi({
+ query: {
+ start: startDate.toISOString(),
+ end: endDate.subtract(1, 'seconds').toISOString(),
+ comparisonStart: comparisonStartDate.toISOString(),
+ comparisonEnd: comparisonEndDate.subtract(1, 'seconds').toISOString(),
+ },
+ });
+ body = response.body;
+ status = response.status;
+ });
+
+ after(() => synthtraceEsClient.clean());
+
+ it('returns correct HTTP status', () => {
+ expect(status).to.be(200);
+ });
+
+ it('returns some data', () => {
+ expect(body.currentPeriod.average).not.to.be(null);
+ expect(body.currentPeriod.transactionColdstartRate.length).to.be.greaterThan(0);
+ const hasCurrentPeriodData = body.currentPeriod.transactionColdstartRate.some(({ y }) =>
+ isFiniteNumber(y)
+ );
+ expect(hasCurrentPeriodData).to.equal(true);
+
+ expect(body.previousPeriod.average).not.to.be(null);
+ expect(body.previousPeriod.transactionColdstartRate.length).to.be.greaterThan(0);
+ const hasPreviousPeriodData = body.previousPeriod.transactionColdstartRate.some(({ y }) =>
+ isFiniteNumber(y)
+ );
+ expect(hasPreviousPeriodData).to.equal(true);
+ });
+
+ it('has same start time for both periods', () => {
+ expect(first(body.currentPeriod.transactionColdstartRate)?.x).to.equal(
+ first(body.previousPeriod.transactionColdstartRate)?.x
+ );
+ });
+
+ it('has same end time for both periods', () => {
+ expect(last(body.currentPeriod.transactionColdstartRate)?.x).to.equal(
+ last(body.previousPeriod.transactionColdstartRate)?.x
+ );
+ });
+
+ it('returns an array of transaction cold start rates', () => {
+ expect(body.currentPeriod.transactionColdstartRate).to.have.length(3);
+ expect(body.currentPeriod.transactionColdstartRate.every(({ y }) => y === 0.25)).to.be(
+ true
+ );
+
+ expect(body.previousPeriod.transactionColdstartRate).to.have.length(3);
+ expect(body.previousPeriod.transactionColdstartRate.every(({ y }) => y === 0.5)).to.be(
+ true
+ );
+ });
+
+ it('has same average value for both periods', () => {
+ expect(body.currentPeriod.average).to.be(0.25);
+ expect(body.previousPeriod.average).to.be(0.5);
+ });
+ });
+ }
+ );
+}
diff --git a/x-pack/test/apm_api_integration/tests/cold_start/cold_start_by_transaction_name/generate_data.ts b/x-pack/test/apm_api_integration/tests/cold_start/cold_start_by_transaction_name/generate_data.ts
new file mode 100644
index 0000000000000..e081c5ea2e168
--- /dev/null
+++ b/x-pack/test/apm_api_integration/tests/cold_start/cold_start_by_transaction_name/generate_data.ts
@@ -0,0 +1,64 @@
+/*
+ * 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 { apm, timerange } from '@elastic/apm-synthtrace';
+import type { ApmSynthtraceEsClient } from '@elastic/apm-synthtrace';
+
+export const dataConfig = {
+ serviceName: 'synth-go',
+ transactionName: 'GET /apple 🍎',
+ duration: 1000,
+};
+
+export async function generateData({
+ synthtraceEsClient,
+ start,
+ end,
+ coldStartRate,
+ warmStartRate,
+}: {
+ synthtraceEsClient: ApmSynthtraceEsClient;
+ start: number;
+ end: number;
+ coldStartRate: number;
+ warmStartRate: number;
+}) {
+ const { transactionName, duration, serviceName } = dataConfig;
+ const instance = apm.service(serviceName, 'production', 'go').instance('instance-a');
+
+ const traceEvents = [
+ ...timerange(start, end)
+ .interval('1m')
+ .rate(coldStartRate)
+ .flatMap((timestamp) => [
+ ...instance
+ .transaction(transactionName)
+ .defaults({
+ 'faas.coldstart': true,
+ })
+ .timestamp(timestamp)
+ .duration(duration)
+ .success()
+ .serialize(),
+ ]),
+ ...timerange(start, end)
+ .interval('1m')
+ .rate(warmStartRate)
+ .flatMap((timestamp) => [
+ ...instance
+ .transaction(transactionName)
+ .defaults({
+ 'faas.coldstart': false,
+ })
+ .timestamp(timestamp)
+ .duration(duration)
+ .success()
+ .serialize(),
+ ]),
+ ];
+
+ await synthtraceEsClient.index(traceEvents);
+}
diff --git a/x-pack/test/apm_api_integration/tests/cold_start/generate_data.ts b/x-pack/test/apm_api_integration/tests/cold_start/generate_data.ts
new file mode 100644
index 0000000000000..50861da61a9f7
--- /dev/null
+++ b/x-pack/test/apm_api_integration/tests/cold_start/generate_data.ts
@@ -0,0 +1,70 @@
+/*
+ * 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 { apm, timerange } from '@elastic/apm-synthtrace';
+import type { ApmSynthtraceEsClient } from '@elastic/apm-synthtrace';
+
+export const dataConfig = {
+ serviceName: 'synth-go',
+ coldStartTransaction: {
+ name: 'GET /apple 🍎',
+ duration: 1000,
+ },
+ warmStartTransaction: {
+ name: 'GET /banana 🍌',
+ duration: 2000,
+ },
+};
+
+export async function generateData({
+ synthtraceEsClient,
+ start,
+ end,
+ coldStartRate,
+ warmStartRate,
+}: {
+ synthtraceEsClient: ApmSynthtraceEsClient;
+ start: number;
+ end: number;
+ coldStartRate: number;
+ warmStartRate: number;
+}) {
+ const { coldStartTransaction, warmStartTransaction, serviceName } = dataConfig;
+ const instance = apm.service(serviceName, 'production', 'go').instance('instance-a');
+
+ const traceEvents = [
+ ...timerange(start, end)
+ .interval('1m')
+ .rate(coldStartRate)
+ .flatMap((timestamp) => [
+ ...instance
+ .transaction(coldStartTransaction.name)
+ .defaults({
+ 'faas.coldstart': true,
+ })
+ .timestamp(timestamp)
+ .duration(coldStartTransaction.duration)
+ .success()
+ .serialize(),
+ ]),
+ ...timerange(start, end)
+ .interval('1m')
+ .rate(warmStartRate)
+ .flatMap((timestamp) => [
+ ...instance
+ .transaction(warmStartTransaction.name)
+ .defaults({
+ 'faas.coldstart': false,
+ })
+ .timestamp(timestamp)
+ .duration(warmStartTransaction.duration)
+ .success()
+ .serialize(),
+ ]),
+ ];
+
+ await synthtraceEsClient.index(traceEvents);
+}
diff --git a/x-pack/test/apm_api_integration/tests/services/service_details.spec.ts b/x-pack/test/apm_api_integration/tests/services/service_details.spec.ts
deleted file mode 100644
index 0031569308224..0000000000000
--- a/x-pack/test/apm_api_integration/tests/services/service_details.spec.ts
+++ /dev/null
@@ -1,135 +0,0 @@
-/*
- * 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 expect from '@kbn/expect';
-import url from 'url';
-import { FtrProviderContext } from '../../common/ftr_provider_context';
-import archives from '../../common/fixtures/es_archiver/archives_metadata';
-
-export default function ApiTest({ getService }: FtrProviderContext) {
- const registry = getService('registry');
- const supertest = getService('legacySupertestAsApmReadUser');
-
- const archiveName = 'apm_8.0.0';
- const { start, end } = archives[archiveName];
-
- registry.when(
- 'Service details when data is not loaded',
- { config: 'basic', archives: [] },
- () => {
- it('handles the empty state', async () => {
- const response = await supertest.get(
- url.format({
- pathname: `/internal/apm/services/opbeans-java/metadata/details`,
- query: { start, end },
- })
- );
-
- expect(response.status).to.be(200);
- expect(response.body).to.eql({});
- });
- }
- );
-
- registry.when(
- 'Service details when data is loaded',
- { config: 'basic', archives: [archiveName] },
- () => {
- it('returns java service details', async () => {
- const response = await supertest.get(
- url.format({
- pathname: `/internal/apm/services/opbeans-java/metadata/details`,
- query: { start, end },
- })
- );
-
- expect(response.status).to.be(200);
-
- expectSnapshot(response.body).toMatchInline(`
- Object {
- "cloud": Object {
- "availabilityZones": Array [
- "europe-west1-c",
- ],
- "machineTypes": Array [
- "n1-standard-4",
- ],
- "projectName": "elastic-observability",
- "provider": "gcp",
- },
- "container": Object {
- "isContainerized": true,
- "os": "Linux",
- "totalNumberInstances": 1,
- "type": "Kubernetes",
- },
- "service": Object {
- "agent": Object {
- "ephemeral_id": "2745d454-f57f-4473-a09b-fe6bef295860",
- "name": "java",
- "version": "1.25.1-SNAPSHOT.UNKNOWN",
- },
- "runtime": Object {
- "name": "Java",
- "version": "11.0.11",
- },
- "versions": Array [
- "2021-08-03 04:26:27",
- ],
- },
- }
- `);
- });
-
- it('returns python service details', async () => {
- const response = await supertest.get(
- url.format({
- pathname: `/internal/apm/services/opbeans-python/metadata/details`,
- query: { start, end },
- })
- );
-
- expect(response.status).to.be(200);
-
- expectSnapshot(response.body).toMatchInline(`
- Object {
- "cloud": Object {
- "availabilityZones": Array [
- "europe-west1-c",
- ],
- "machineTypes": Array [
- "n1-standard-4",
- ],
- "projectName": "elastic-observability",
- "provider": "gcp",
- },
- "container": Object {
- "isContainerized": true,
- "os": "linux",
- "totalNumberInstances": 1,
- "type": "Kubernetes",
- },
- "service": Object {
- "agent": Object {
- "name": "python",
- "version": "6.3.3",
- },
- "framework": "django",
- "runtime": Object {
- "name": "CPython",
- "version": "3.9.6",
- },
- "versions": Array [
- "2021-08-03 04:26:25",
- ],
- },
- }
- `);
- });
- }
- );
-}
diff --git a/x-pack/test/apm_api_integration/tests/services/service_details/generate_data.ts b/x-pack/test/apm_api_integration/tests/services/service_details/generate_data.ts
new file mode 100644
index 0000000000000..f6035cf784dbd
--- /dev/null
+++ b/x-pack/test/apm_api_integration/tests/services/service_details/generate_data.ts
@@ -0,0 +1,130 @@
+/*
+ * 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 { apm, timerange } from '@elastic/apm-synthtrace';
+import type { ApmSynthtraceEsClient } from '@elastic/apm-synthtrace';
+
+export const dataConfig = {
+ rate: 10,
+ transaction: {
+ name: 'GET /apple 🍎',
+ duration: 1000,
+ },
+ service: {
+ name: 'lambda-python-dev-hello',
+ version: '$LATEST',
+ runtime: {
+ name: 'AWS_Lambda_python3.8',
+ version: '3.8.11',
+ },
+ framework: 'AWS Lambda',
+ agent: {
+ name: 'python',
+ version: '6.6.0',
+ },
+ },
+ containerOs: 'linux',
+ serverless: {
+ firstFunctionName: 'my-function-1',
+ secondFunctionName: 'my-function-2',
+ faasTriggerType: 'other',
+ },
+ cloud: {
+ provider: 'aws',
+ availabilityZone: 'us-central1-c',
+ region: 'us-east-1',
+ machineType: 'e2-standard-4',
+ projectName: 'elastic-observability',
+ serviceName: 'lambda',
+ },
+};
+
+export async function generateData({
+ synthtraceEsClient,
+ start,
+ end,
+}: {
+ synthtraceEsClient: ApmSynthtraceEsClient;
+ start: number;
+ end: number;
+}) {
+ const { rate, service, containerOs, serverless, cloud, transaction } = dataConfig;
+ const {
+ provider,
+ availabilityZone,
+ region,
+ machineType,
+ projectName,
+ serviceName: cloudServiceName,
+ } = cloud;
+ const { faasTriggerType, firstFunctionName, secondFunctionName } = serverless;
+ const { version, runtime, framework, agent, name: serviceName } = service;
+ const { name: serviceRunTimeName, version: serviceRunTimeVersion } = runtime;
+ const { name: agentName, version: agentVersion } = agent;
+
+ const instance = apm.service(serviceName, 'production', agentName).instance('instance-a');
+
+ const traceEvents = [
+ ...timerange(start, end)
+ .interval('30s')
+ .rate(rate)
+ .flatMap((timestamp) =>
+ instance
+ .transaction(transaction.name)
+ .timestamp(timestamp)
+ .defaults({
+ 'cloud.provider': provider,
+ 'cloud.project.name': projectName,
+ 'cloud.service.name': cloudServiceName,
+ 'cloud.availability_zone': availabilityZone,
+ 'cloud.machine.type': machineType,
+ 'cloud.region': region,
+ 'faas.id': `arn:aws:lambda:us-west-2:123456789012:function:${firstFunctionName}`,
+ 'faas.trigger.type': faasTriggerType,
+ 'host.os.platform': containerOs,
+ 'kubernetes.pod.uid': '48f4c5a5-0625-4bea-9d94-77ee94a17e70',
+ 'service.version': version,
+ 'service.runtime.name': serviceRunTimeName,
+ 'service.runtime.version': serviceRunTimeVersion,
+ 'service.framework.name': framework,
+ 'agent.version': agentVersion,
+ })
+ .duration(transaction.duration)
+ .success()
+ .serialize()
+ ),
+ ...timerange(start, end)
+ .interval('30s')
+ .rate(rate)
+ .flatMap((timestamp) =>
+ instance
+ .transaction(transaction.name)
+ .timestamp(timestamp)
+ .defaults({
+ 'cloud.provider': provider,
+ 'cloud.project.name': projectName,
+ 'cloud.service.name': cloudServiceName,
+ 'cloud.availability_zone': availabilityZone,
+ 'cloud.machine.type': machineType,
+ 'cloud.region': region,
+ 'faas.id': `arn:aws:lambda:us-west-2:123456789012:function:${secondFunctionName}`,
+ 'faas.trigger.type': faasTriggerType,
+ 'host.os.platform': containerOs,
+ 'kubernetes.pod.uid': '48f4c5a5-0625-4bea-9d94-77ee94a17e70',
+ 'service.version': version,
+ 'service.runtime.name': serviceRunTimeName,
+ 'service.runtime.version': serviceRunTimeVersion,
+ 'service.framework.name': framework,
+ 'agent.version': agentVersion,
+ })
+ .duration(transaction.duration)
+ .success()
+ .serialize()
+ ),
+ ];
+
+ await synthtraceEsClient.index(traceEvents);
+}
diff --git a/x-pack/test/apm_api_integration/tests/services/service_details/service_details.spec.ts b/x-pack/test/apm_api_integration/tests/services/service_details/service_details.spec.ts
new file mode 100644
index 0000000000000..cf59b0d39660d
--- /dev/null
+++ b/x-pack/test/apm_api_integration/tests/services/service_details/service_details.spec.ts
@@ -0,0 +1,128 @@
+/*
+ * 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 expect from '@kbn/expect';
+import { first } from 'lodash';
+import { FtrProviderContext } from '../../../common/ftr_provider_context';
+import { dataConfig, generateData } from './generate_data';
+import { APIReturnType } from '../../../../../plugins/apm/public/services/rest/create_call_apm_api';
+
+type ServiceDetails = APIReturnType<'GET /internal/apm/services/{serviceName}/metadata/details'>;
+
+export default function ApiTest({ getService }: FtrProviderContext) {
+ const registry = getService('registry');
+ const apmApiClient = getService('apmApiClient');
+ const synthtraceEsClient = getService('synthtraceEsClient');
+
+ const {
+ service: { name: serviceName },
+ } = dataConfig;
+ const start = new Date('2021-01-01T00:00:00.000Z').getTime();
+ const end = new Date('2021-01-01T00:15:00.000Z').getTime() - 1;
+
+ async function callApi() {
+ return await apmApiClient.readUser({
+ endpoint: 'GET /internal/apm/services/{serviceName}/metadata/details',
+ params: {
+ path: { serviceName },
+ query: {
+ start: new Date(start).toISOString(),
+ end: new Date(end).toISOString(),
+ },
+ },
+ });
+ }
+
+ registry.when(
+ 'Service details when data is not loaded',
+ { config: 'basic', archives: [] },
+ () => {
+ it('handles empty state', async () => {
+ const { status, body } = await callApi();
+
+ expect(status).to.be(200);
+ expect(body).to.empty();
+ });
+ }
+ );
+
+ registry.when(
+ 'Service details when data is generated',
+ { config: 'basic', archives: ['apm_mappings_only_8.0.0'] },
+ () => {
+ let body: ServiceDetails;
+ let status: number;
+
+ before(async () => {
+ await generateData({ synthtraceEsClient, start, end });
+ const response = await callApi();
+ body = response.body;
+ status = response.status;
+ });
+
+ after(() => synthtraceEsClient.clean());
+
+ it('returns correct HTTP status', () => {
+ expect(status).to.be(200);
+ });
+
+ it('returns correct cloud details', () => {
+ const { cloud } = dataConfig;
+ const {
+ provider,
+ availabilityZone,
+ region,
+ machineType,
+ projectName,
+ serviceName: cloudServiceName,
+ } = cloud;
+
+ expect(first(body?.cloud?.availabilityZones)).to.be(availabilityZone);
+ expect(first(body?.cloud?.machineTypes)).to.be(machineType);
+ expect(body?.cloud?.provider).to.be(provider);
+ expect(body?.cloud?.projectName).to.be(projectName);
+ expect(body?.cloud?.serviceName).to.be(cloudServiceName);
+ expect(first(body?.cloud?.regions)).to.be(region);
+ });
+
+ it('returns correct container details', () => {
+ const { containerOs } = dataConfig;
+
+ expect(body?.container?.isContainerized).to.be(true);
+ expect(body?.container?.os).to.be(containerOs);
+ expect(body?.container?.totalNumberInstances).to.be(1);
+ expect(body?.container?.type).to.be('Kubernetes');
+ });
+
+ it('returns correct serverless details', () => {
+ const { cloud, serverless } = dataConfig;
+ const { serviceName: cloudServiceName } = cloud;
+ const { faasTriggerType, firstFunctionName, secondFunctionName } = serverless;
+
+ expect(body?.serverless?.type).to.be(cloudServiceName);
+ expect(body?.serverless?.functionNames).to.have.length(2);
+ expect(body?.serverless?.functionNames).to.contain(firstFunctionName);
+ expect(body?.serverless?.functionNames).to.contain(secondFunctionName);
+ expect(first(body?.serverless?.faasTriggerTypes)).to.be(faasTriggerType);
+ });
+
+ it('returns correct service details', () => {
+ const { service } = dataConfig;
+ const { version, runtime, framework, agent } = service;
+ const { name: runTimeName, version: runTimeVersion } = runtime;
+ const { name: agentName, version: agentVersion } = agent;
+
+ expect(body?.service?.framework).to.be(framework);
+ expect(body?.service?.agent.name).to.be(agentName);
+ expect(body?.service?.agent.version).to.be(agentVersion);
+ expect(body?.service?.runtime?.name).to.be(runTimeName);
+ expect(body?.service?.runtime?.version).to.be(runTimeVersion);
+ expect(first(body?.service?.versions)).to.be(version);
+ });
+ }
+ );
+}
diff --git a/x-pack/test/apm_api_integration/tests/services/service_icons.spec.ts b/x-pack/test/apm_api_integration/tests/services/service_icons.spec.ts
deleted file mode 100644
index f8b66a621e83e..0000000000000
--- a/x-pack/test/apm_api_integration/tests/services/service_icons.spec.ts
+++ /dev/null
@@ -1,77 +0,0 @@
-/*
- * 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 expect from '@kbn/expect';
-import url from 'url';
-import { FtrProviderContext } from '../../common/ftr_provider_context';
-import archives from '../../common/fixtures/es_archiver/archives_metadata';
-
-export default function ApiTest({ getService }: FtrProviderContext) {
- const registry = getService('registry');
- const supertest = getService('legacySupertestAsApmReadUser');
-
- const archiveName = 'apm_8.0.0';
- const { start, end } = archives[archiveName];
-
- registry.when('Service icons when data is not loaded', { config: 'basic', archives: [] }, () => {
- it('handles the empty state', async () => {
- const response = await supertest.get(
- url.format({
- pathname: `/internal/apm/services/opbeans-java/metadata/icons`,
- query: { start, end },
- })
- );
-
- expect(response.status).to.be(200);
- expect(response.body).to.eql({});
- });
- });
-
- registry.when(
- 'Service icons when data is not loaded',
- { config: 'basic', archives: [archiveName] },
- () => {
- it('returns java service icons', async () => {
- const response = await supertest.get(
- url.format({
- pathname: `/internal/apm/services/opbeans-java/metadata/icons`,
- query: { start, end },
- })
- );
-
- expect(response.status).to.be(200);
-
- expectSnapshot(response.body).toMatchInline(`
- Object {
- "agentName": "java",
- "cloudProvider": "gcp",
- "containerType": "Kubernetes",
- }
- `);
- });
-
- it('returns python service icons', async () => {
- const response = await supertest.get(
- url.format({
- pathname: `/internal/apm/services/opbeans-python/metadata/icons`,
- query: { start, end },
- })
- );
-
- expect(response.status).to.be(200);
-
- expectSnapshot(response.body).toMatchInline(`
- Object {
- "agentName": "python",
- "cloudProvider": "gcp",
- "containerType": "Kubernetes",
- }
- `);
- });
- }
- );
-}
diff --git a/x-pack/test/apm_api_integration/tests/services/service_icons/generate_data.ts b/x-pack/test/apm_api_integration/tests/services/service_icons/generate_data.ts
new file mode 100644
index 0000000000000..93f68242caa5e
--- /dev/null
+++ b/x-pack/test/apm_api_integration/tests/services/service_icons/generate_data.ts
@@ -0,0 +1,56 @@
+/*
+ * 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 { apm, timerange } from '@elastic/apm-synthtrace';
+import type { ApmSynthtraceEsClient } from '@elastic/apm-synthtrace';
+
+export const dataConfig = {
+ serviceName: 'synth-node',
+ rate: 10,
+ transaction: {
+ name: 'GET /apple 🍎',
+ duration: 1000,
+ },
+ agentName: 'node',
+ cloud: {
+ provider: 'aws',
+ serviceName: 'lambda',
+ },
+};
+
+export async function generateData({
+ synthtraceEsClient,
+ start,
+ end,
+}: {
+ synthtraceEsClient: ApmSynthtraceEsClient;
+ start: number;
+ end: number;
+}) {
+ const { serviceName, agentName, rate, cloud, transaction } = dataConfig;
+ const { provider, serviceName: cloudServiceName } = cloud;
+
+ const instance = apm.service(serviceName, 'production', agentName).instance('instance-a');
+
+ const traceEvents = timerange(start, end)
+ .interval('30s')
+ .rate(rate)
+ .flatMap((timestamp) =>
+ instance
+ .transaction(transaction.name)
+ .defaults({
+ 'kubernetes.pod.uid': 'test',
+ 'cloud.provider': provider,
+ 'cloud.service.name': cloudServiceName,
+ })
+ .timestamp(timestamp)
+ .duration(transaction.duration)
+ .success()
+ .serialize()
+ );
+
+ await synthtraceEsClient.index(traceEvents);
+}
diff --git a/x-pack/test/apm_api_integration/tests/services/service_icons/service_icons.spec.ts b/x-pack/test/apm_api_integration/tests/services/service_icons/service_icons.spec.ts
new file mode 100644
index 0000000000000..389b8c6ac1fc8
--- /dev/null
+++ b/x-pack/test/apm_api_integration/tests/services/service_icons/service_icons.spec.ts
@@ -0,0 +1,77 @@
+/*
+ * 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 expect from '@kbn/expect';
+import { FtrProviderContext } from '../../../common/ftr_provider_context';
+import { dataConfig, generateData } from './generate_data';
+import { APIReturnType } from '../../../../../plugins/apm/public/services/rest/create_call_apm_api';
+
+type ServiceIconMetadata = APIReturnType<'GET /internal/apm/services/{serviceName}/metadata/icons'>;
+
+export default function ApiTest({ getService }: FtrProviderContext) {
+ const registry = getService('registry');
+ const apmApiClient = getService('apmApiClient');
+ const synthtraceEsClient = getService('synthtraceEsClient');
+
+ const { serviceName } = dataConfig;
+ const start = new Date('2021-01-01T00:00:00.000Z').getTime();
+ const end = new Date('2021-01-01T00:15:00.000Z').getTime() - 1;
+
+ async function callApi() {
+ return await apmApiClient.readUser({
+ endpoint: 'GET /internal/apm/services/{serviceName}/metadata/icons',
+ params: {
+ path: { serviceName },
+ query: {
+ start: new Date(start).toISOString(),
+ end: new Date(end).toISOString(),
+ },
+ },
+ });
+ }
+
+ registry.when('Service icons when data is not loaded', { config: 'basic', archives: [] }, () => {
+ it('handles empty state', async () => {
+ const { status, body } = await callApi();
+
+ expect(status).to.be(200);
+ expect(body).to.empty();
+ });
+ });
+
+ registry.when(
+ 'Service icons when data is generated',
+ { config: 'basic', archives: ['apm_mappings_only_8.0.0'] },
+ () => {
+ let body: ServiceIconMetadata;
+ let status: number;
+
+ before(async () => {
+ await generateData({ synthtraceEsClient, start, end });
+ const response = await callApi();
+ body = response.body;
+ status = response.status;
+ });
+
+ after(() => synthtraceEsClient.clean());
+
+ it('returns correct HTTP status', () => {
+ expect(status).to.be(200);
+ });
+
+ it('returns correct metadata', () => {
+ const { agentName, cloud } = dataConfig;
+ const { provider, serviceName: cloudServiceName } = cloud;
+
+ expect(body.agentName).to.be(agentName);
+ expect(body.cloudProvider).to.be(provider);
+ expect(body.containerType).to.be('Kubernetes');
+ expect(body.serverlessType).to.be(cloudServiceName);
+ });
+ }
+ );
+}
From b6d2149e99c7c0bdcf490b501e8081d806f5d4ad Mon Sep 17 00:00:00 2001
From: Anton Dosov
Date: Mon, 31 Jan 2022 13:04:32 +0100
Subject: [PATCH 11/65] Add configuration to search without `bfetch` (#123942)
---
src/plugins/bfetch/common/constants.ts | 1 +
src/plugins/bfetch/common/index.ts | 2 +-
src/plugins/bfetch/public/index.ts | 2 +
src/plugins/bfetch/server/ui_settings.ts | 16 ++-
.../search_interceptor/search_interceptor.ts | 67 ++++++++---
.../server/collectors/management/schema.ts | 4 +
.../server/collectors/management/types.ts | 1 +
src/plugins/telemetry/schema/oss_plugins.json | 6 +
.../search_examples/search_example.ts | 112 +++++++++++-------
9 files changed, 146 insertions(+), 65 deletions(-)
diff --git a/src/plugins/bfetch/common/constants.ts b/src/plugins/bfetch/common/constants.ts
index bc72ad244d9be..89d0d7c492d86 100644
--- a/src/plugins/bfetch/common/constants.ts
+++ b/src/plugins/bfetch/common/constants.ts
@@ -7,3 +7,4 @@
*/
export const DISABLE_BFETCH_COMPRESSION = 'bfetch:disableCompression';
+export const DISABLE_BFETCH = 'bfetch:disable';
diff --git a/src/plugins/bfetch/common/index.ts b/src/plugins/bfetch/common/index.ts
index 26a1734ec759c..491cabd03bf68 100644
--- a/src/plugins/bfetch/common/index.ts
+++ b/src/plugins/bfetch/common/index.ts
@@ -11,4 +11,4 @@ export type { StreamingResponseHandler } from './streaming';
export type { ItemBufferParams, TimedItemBufferParams, BatchedFunctionParams } from './buffer';
export { ItemBuffer, TimedItemBuffer, createBatchedFunction } from './buffer';
export type { ErrorLike, BatchRequestData, BatchResponseItem, BatchItemWrapper } from './batch';
-export { DISABLE_BFETCH_COMPRESSION } from './constants';
+export { DISABLE_BFETCH_COMPRESSION, DISABLE_BFETCH } from './constants';
diff --git a/src/plugins/bfetch/public/index.ts b/src/plugins/bfetch/public/index.ts
index 6ab5480327538..bba7844a3265b 100644
--- a/src/plugins/bfetch/public/index.ts
+++ b/src/plugins/bfetch/public/index.ts
@@ -14,6 +14,8 @@ export { split } from './streaming';
export type { BatchedFunc } from './batching/types';
+export { DISABLE_BFETCH } from '../common/constants';
+
export function plugin(initializerContext: PluginInitializerContext) {
return new BfetchPublicPlugin(initializerContext);
}
diff --git a/src/plugins/bfetch/server/ui_settings.ts b/src/plugins/bfetch/server/ui_settings.ts
index cf7b13a9af182..95854b9604dd3 100644
--- a/src/plugins/bfetch/server/ui_settings.ts
+++ b/src/plugins/bfetch/server/ui_settings.ts
@@ -9,13 +9,25 @@
import { i18n } from '@kbn/i18n';
import { UiSettingsParams } from 'src/core/server';
import { schema } from '@kbn/config-schema';
-import { DISABLE_BFETCH_COMPRESSION } from '../common';
+import { DISABLE_BFETCH_COMPRESSION, DISABLE_BFETCH } from '../common';
export function getUiSettings(): Record> {
return {
+ [DISABLE_BFETCH]: {
+ name: i18n.translate('bfetch.disableBfetch', {
+ defaultMessage: 'Disable request batching',
+ }),
+ value: false,
+ description: i18n.translate('bfetch.disableBfetchDesc', {
+ defaultMessage:
+ 'Disables requests batching. This increases number of HTTP requests from Kibana, but allows to debug requests individually.',
+ }),
+ schema: schema.boolean(),
+ category: [],
+ },
[DISABLE_BFETCH_COMPRESSION]: {
name: i18n.translate('bfetch.disableBfetchCompression', {
- defaultMessage: 'Disable Batch Compression',
+ defaultMessage: 'Disable batch compression',
}),
value: false,
description: i18n.translate('bfetch.disableBfetchCompressionDesc', {
diff --git a/src/plugins/data/public/search/search_interceptor/search_interceptor.ts b/src/plugins/data/public/search/search_interceptor/search_interceptor.ts
index 8c7bfe68fd54b..7dc1ce6dee078 100644
--- a/src/plugins/data/public/search/search_interceptor/search_interceptor.ts
+++ b/src/plugins/data/public/search/search_interceptor/search_interceptor.ts
@@ -21,9 +21,15 @@ import {
tap,
} from 'rxjs/operators';
import { PublicMethodsOf } from '@kbn/utility-types';
-import { CoreSetup, CoreStart, ThemeServiceSetup, ToastsSetup } from 'kibana/public';
+import {
+ CoreSetup,
+ CoreStart,
+ IHttpFetchError,
+ ThemeServiceSetup,
+ ToastsSetup,
+} from 'kibana/public';
import { i18n } from '@kbn/i18n';
-import { BatchedFunc, BfetchPublicSetup } from 'src/plugins/bfetch/public';
+import { BatchedFunc, BfetchPublicSetup, DISABLE_BFETCH } from '../../../../bfetch/public';
import {
ENHANCED_ES_SEARCH_STRATEGY,
IAsyncSearchOptions,
@@ -67,8 +73,9 @@ const MAX_CACHE_ITEMS = 50;
const MAX_CACHE_SIZE_MB = 10;
export class SearchInterceptor {
- private uiSettingsSub: Subscription;
+ private uiSettingsSubs: Subscription[] = [];
private searchTimeout: number;
+ private bFetchDisabled: boolean;
private readonly responseCache: SearchResponseCache = new SearchResponseCache(
MAX_CACHE_ITEMS,
MAX_CACHE_SIZE_MB
@@ -106,17 +113,21 @@ export class SearchInterceptor {
});
this.searchTimeout = deps.uiSettings.get(UI_SETTINGS.SEARCH_TIMEOUT);
+ this.bFetchDisabled = deps.uiSettings.get(DISABLE_BFETCH);
- this.uiSettingsSub = deps.uiSettings
- .get$(UI_SETTINGS.SEARCH_TIMEOUT)
- .subscribe((timeout: number) => {
+ this.uiSettingsSubs.push(
+ deps.uiSettings.get$(UI_SETTINGS.SEARCH_TIMEOUT).subscribe((timeout: number) => {
this.searchTimeout = timeout;
- });
+ }),
+ deps.uiSettings.get$(DISABLE_BFETCH).subscribe((bFetchDisabled: boolean) => {
+ this.bFetchDisabled = bFetchDisabled;
+ })
+ );
}
public stop() {
this.responseCache.clear();
- this.uiSettingsSub.unsubscribe();
+ this.uiSettingsSubs.forEach((s) => s.unsubscribe());
}
/*
@@ -266,13 +277,34 @@ export class SearchInterceptor {
options?: ISearchOptions
): Promise {
const { abortSignal } = options || {};
- return this.batchedFetch(
- {
- request,
- options: this.getSerializableOptions(options),
- },
- abortSignal
- );
+
+ if (this.bFetchDisabled) {
+ const { executionContext, strategy, ...searchOptions } = this.getSerializableOptions(options);
+ return this.deps.http
+ .post(`/internal/search/${strategy}${request.id ? `/${request.id}` : ''}`, {
+ signal: abortSignal,
+ context: executionContext,
+ body: JSON.stringify({
+ ...request,
+ ...searchOptions,
+ }),
+ })
+ .catch((e: IHttpFetchError) => {
+ if (e?.body) {
+ throw e.body;
+ } else {
+ throw e;
+ }
+ }) as Promise;
+ } else {
+ return this.batchedFetch(
+ {
+ request,
+ options: this.getSerializableOptions(options),
+ },
+ abortSignal
+ );
+ }
}
/**
@@ -319,9 +351,12 @@ export class SearchInterceptor {
*/
public search({ id, ...request }: IKibanaSearchRequest, options: IAsyncSearchOptions = {}) {
const searchOptions = {
- strategy: ENHANCED_ES_SEARCH_STRATEGY,
...options,
};
+ if (!searchOptions.strategy) {
+ searchOptions.strategy = ENHANCED_ES_SEARCH_STRATEGY;
+ }
+
const { sessionId, abortSignal } = searchOptions;
return this.createRequestHash$(request, searchOptions).pipe(
diff --git a/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts b/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts
index 27e44cba1094f..bde8a10e4dd2e 100644
--- a/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts
+++ b/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts
@@ -392,6 +392,10 @@ export const stackManagementSchema: MakeSchemaFrom = {
type: 'boolean',
_meta: { description: 'Non-default value of setting.' },
},
+ 'bfetch:disable': {
+ type: 'boolean',
+ _meta: { description: 'Non-default value of setting.' },
+ },
'visualization:visualize:legacyPieChartsLibrary': {
type: 'boolean',
_meta: { description: 'Non-default value of setting.' },
diff --git a/src/plugins/kibana_usage_collection/server/collectors/management/types.ts b/src/plugins/kibana_usage_collection/server/collectors/management/types.ts
index 8776bad89f8a6..47656c568bf4c 100644
--- a/src/plugins/kibana_usage_collection/server/collectors/management/types.ts
+++ b/src/plugins/kibana_usage_collection/server/collectors/management/types.ts
@@ -23,6 +23,7 @@ export interface UsageStats {
/**
* non-sensitive settings
*/
+ 'bfetch:disable': boolean;
'bfetch:disableCompression': boolean;
'autocomplete:useTimeRange': boolean;
'autocomplete:valueSuggestionMethod': string;
diff --git a/src/plugins/telemetry/schema/oss_plugins.json b/src/plugins/telemetry/schema/oss_plugins.json
index 9c2c71898dee7..3269452ca8cc3 100644
--- a/src/plugins/telemetry/schema/oss_plugins.json
+++ b/src/plugins/telemetry/schema/oss_plugins.json
@@ -7736,6 +7736,12 @@
"description": "Non-default value of setting."
}
},
+ "bfetch:disable": {
+ "type": "boolean",
+ "_meta": {
+ "description": "Non-default value of setting."
+ }
+ },
"visualization:visualize:legacyPieChartsLibrary": {
"type": "boolean",
"_meta": {
diff --git a/x-pack/test/examples/search_examples/search_example.ts b/x-pack/test/examples/search_examples/search_example.ts
index ac0a9e47fa2f1..f78b2ffbdf545 100644
--- a/x-pack/test/examples/search_examples/search_example.ts
+++ b/x-pack/test/examples/search_examples/search_example.ts
@@ -17,62 +17,82 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
const toasts = getService('toasts');
describe('Search example', () => {
- const appId = 'searchExamples';
-
- before(async function () {
- await PageObjects.common.navigateToApp(appId, { insertTimestamp: false });
- await comboBox.setCustom('dataViewSelector', 'logstash-*');
- await comboBox.set('searchBucketField', 'geo.src');
- await comboBox.set('searchMetricField', 'memory');
- await PageObjects.timePicker.setAbsoluteRange(
- 'Mar 1, 2015 @ 00:00:00.000',
- 'Nov 1, 2015 @ 00:00:00.000'
- );
+ describe('with bfetch', () => {
+ testSearchExample();
});
- beforeEach(async () => {
- await toasts.dismissAllToasts();
- await retry.waitFor('toasts gone', async () => {
- return (await toasts.getToastCount()) === 0;
+ describe('no bfetch', () => {
+ const kibanaServer = getService('kibanaServer');
+ before(async () => {
+ await kibanaServer.uiSettings.replace({
+ 'bfetch:disable': true,
+ });
});
+ after(async () => {
+ await kibanaServer.uiSettings.unset('bfetch:disable');
+ });
+
+ testSearchExample();
});
- it('should have an other bucket', async () => {
- await testSubjects.click('searchSourceWithOther');
- await testSubjects.click('responseTab');
- const codeBlock = await testSubjects.find('responseCodeBlock');
- await retry.waitFor('get code block', async () => {
- const visibleText = await codeBlock.getVisibleText();
- const parsedResponse = JSON.parse(visibleText);
- const buckets = parsedResponse.aggregations[1].buckets;
- return (
- buckets.length === 3 && buckets[2].key === '__other__' && buckets[2].doc_count === 9039
+ const appId = 'searchExamples';
+
+ function testSearchExample() {
+ before(async function () {
+ await PageObjects.common.navigateToApp(appId, { insertTimestamp: false });
+ await comboBox.setCustom('dataViewSelector', 'logstash-*');
+ await comboBox.set('searchBucketField', 'geo.src');
+ await comboBox.set('searchMetricField', 'memory');
+ await PageObjects.timePicker.setAbsoluteRange(
+ 'Mar 1, 2015 @ 00:00:00.000',
+ 'Nov 1, 2015 @ 00:00:00.000'
);
});
- });
- it('should not have an other bucket', async () => {
- await testSubjects.click('searchSourceWithoutOther');
- await testSubjects.click('responseTab');
- const codeBlock = await testSubjects.find('responseCodeBlock');
- await retry.waitFor('get code block', async () => {
- const visibleText = await codeBlock.getVisibleText();
- const parsedResponse = JSON.parse(visibleText);
- const buckets = parsedResponse.aggregations[1].buckets;
- return buckets.length === 2;
+ beforeEach(async () => {
+ await toasts.dismissAllToasts();
+ await retry.waitFor('toasts gone', async () => {
+ return (await toasts.getToastCount()) === 0;
+ });
});
- });
- it('should handle warnings', async () => {
- await testSubjects.click('searchWithWarning');
- await retry.waitFor('', async () => {
- const toastCount = await toasts.getToastCount();
- return toastCount > 1;
+ it('should have an other bucket', async () => {
+ await testSubjects.click('searchSourceWithOther');
+ await testSubjects.click('responseTab');
+ const codeBlock = await testSubjects.find('responseCodeBlock');
+ await retry.waitFor('get code block', async () => {
+ const visibleText = await codeBlock.getVisibleText();
+ const parsedResponse = JSON.parse(visibleText);
+ const buckets = parsedResponse.aggregations[1].buckets;
+ return (
+ buckets.length === 3 && buckets[2].key === '__other__' && buckets[2].doc_count === 9039
+ );
+ });
});
- const warningToast = await toasts.getToastElement(2);
- const textEl = await warningToast.findByClassName('euiToastBody');
- const text: string = await textEl.getVisibleText();
- expect(text).to.contain('Watch out!');
- });
+
+ it('should not have an other bucket', async () => {
+ await testSubjects.click('searchSourceWithoutOther');
+ await testSubjects.click('responseTab');
+ const codeBlock = await testSubjects.find('responseCodeBlock');
+ await retry.waitFor('get code block', async () => {
+ const visibleText = await codeBlock.getVisibleText();
+ const parsedResponse = JSON.parse(visibleText);
+ const buckets = parsedResponse.aggregations[1].buckets;
+ return buckets.length === 2;
+ });
+ });
+
+ it('should handle warnings', async () => {
+ await testSubjects.click('searchWithWarning');
+ await retry.waitFor('', async () => {
+ const toastCount = await toasts.getToastCount();
+ return toastCount > 1;
+ });
+ const warningToast = await toasts.getToastElement(2);
+ const textEl = await warningToast.findByClassName('euiToastBody');
+ const text: string = await textEl.getVisibleText();
+ expect(text).to.contain('Watch out!');
+ });
+ }
});
}
From 1f3f1c6fc6f8b4b8036c92161854b10814758708 Mon Sep 17 00:00:00 2001
From: Vitalii Dmyterko <92328789+vitaliidm@users.noreply.github.com>
Date: Mon, 31 Jan 2022 12:42:42 +0000
Subject: [PATCH 12/65] [Security Solution][Detections]Adds bulk edit index
patterns/tags (#122635)
Issues:
- https://github.com/elastic/security-team/issues/2076
- https://github.com/elastic/security-team/issues/2077
- https://github.com/elastic/security-team/issues/2079
- https://github.com/elastic/security-team/issues/2081
## Summary
- add ids parameter to Bulk edit API, so now rules can be bulk edited by their ids
- adds unit/functional tests for ids parameter
- adds Bulk edit UI for tags and index patterns
- Bulk edit UI is hidden behind feature enabled `rulesBulkEditEnabled`, which is set to `true` by default.
If it will be decided to set it to disable, it can be enabled in Kibana config
```yml
xpack.securitySolution.enableExperimental: ['rulesBulkEditEnabled']
```
- for bulk edit, UI is not getting frozen, instead only single rules and bulk actions are. We also display a warning toast, if action takes too long(after 5s) to inform user
'Unfrozen' UI screen
**Notes to frozen UI:**
1. It applied only to bulk edit action for now
2. User still can change pages/apply filters/open rule. We will lose all rules blocking UI in such case. This is a current limitation of the solution.
## TODO
- add unit/cypress tests
- lift table context to wrap rule details, so loading rules state can be persistent(?)
### Checklist
Delete any items that are not applicable to this PR.
- [x] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)
- [x] Any UI touched in this PR is usable by keyboard only (learn more about [keyboard accessibility](https://webaim.org/techniques/keyboard/))
- [x] Any UI touched in this PR does not create any new axe failures (run axe in browser: [FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/), [Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US))
- [x] This renders correctly on smaller devices using a responsive layout. (You can test this [in your browser](https://www.browserstack.com/guide/responsive-testing-on-local-server))
- [x] This was checked for [cross-browser compatibility](https://www.elastic.co/support/matrix#matrix_browsers)
### Release notes
Adds ability to bulk edit rules in rules management/monitoring tables. Available for bulk edit fields: tags and index patterns
---
.../security_solution/common/constants.ts | 10 +
.../schemas/common/schemas.ts | 6 +
.../perform_bulk_action_schema.mock.ts | 2 +
.../perform_bulk_action_schema.test.ts | 15 +-
.../request/perform_bulk_action_schema.ts | 1 +
.../response/perform_bulk_action_schema.ts | 37 ++
.../common/detection_engine/types.ts | 4 +
.../common/experimental_features.ts | 1 +
.../detection_rules/sorting.spec.ts | 12 +-
.../cypress/tasks/alerts_detection_rules.ts | 8 +-
.../utility_bar/utility_bar_action.tsx | 95 ++--
.../public/common/hooks/use_app_toasts.ts | 8 +-
.../public/common/mock/test_providers.tsx | 28 +-
.../containers/detection_engine/rules/api.ts | 11 +-
.../rules/rules_table/rules_table_context.tsx | 2 +-
.../detection_engine/rules/types.ts | 6 +-
.../detection_engine/rules/all/actions.tsx | 82 ++-
.../bulk_actions/bulk_edit_confirmation.tsx | 76 +++
.../all/bulk_actions/bulk_edit_flyout.tsx | 45 ++
.../forms/bulk_edit_form_wrapper.tsx | 76 +++
.../forms/index_patterns_form.tsx | 158 ++++++
.../all/bulk_actions/forms/tags_form.tsx | 148 +++++
.../all/bulk_actions/use_bulk_actions.tsx | 515 ++++++++++++++++++
.../bulk_actions/use_bulk_edit_form_flyout.ts | 53 ++
.../bulk_actions/use_custom_rules_count.ts | 50 ++
.../rules/all/exceptions/exceptions_table.tsx | 2 +-
.../detection_engine/rules/all/helpers.ts | 12 +-
.../rules_table_filters.test.tsx | 6 +
.../rules_table_filters.tsx | 8 +-
.../rules/all/rules_tables.tsx | 83 ++-
.../rules/all/use_bulk_actions.tsx | 266 ---------
.../rules/all/use_columns.tsx | 5 +-
.../rules/all/utility_bar.test.tsx | 26 +-
.../rules/all/utility_bar.tsx | 45 +-
.../detection_engine/rules/translations.ts | 313 ++++++++++-
.../rules/perform_bulk_action_route.test.ts | 91 ++++
.../routes/rules/perform_bulk_action_route.ts | 139 ++++-
.../translations/translations/ja-JP.json | 6 -
.../translations/translations/zh-CN.json | 6 -
.../tests/perform_bulk_action.ts | 33 ++
40 files changed, 2041 insertions(+), 449 deletions(-)
create mode 100644 x-pack/plugins/security_solution/common/detection_engine/schemas/response/perform_bulk_action_schema.ts
create mode 100644 x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/bulk_actions/bulk_edit_confirmation.tsx
create mode 100644 x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/bulk_actions/bulk_edit_flyout.tsx
create mode 100644 x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/bulk_actions/forms/bulk_edit_form_wrapper.tsx
create mode 100644 x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/bulk_actions/forms/index_patterns_form.tsx
create mode 100644 x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/bulk_actions/forms/tags_form.tsx
create mode 100644 x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/bulk_actions/use_bulk_actions.tsx
create mode 100644 x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/bulk_actions/use_bulk_edit_form_flyout.ts
create mode 100644 x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/bulk_actions/use_custom_rules_count.ts
delete mode 100644 x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/use_bulk_actions.tsx
diff --git a/x-pack/plugins/security_solution/common/constants.ts b/x-pack/plugins/security_solution/common/constants.ts
index 12df1874754f0..ecd5889c3485b 100644
--- a/x-pack/plugins/security_solution/common/constants.ts
+++ b/x-pack/plugins/security_solution/common/constants.ts
@@ -411,3 +411,13 @@ export const WARNING_TRANSFORM_STATES = new Set([
export const MAX_RULES_TO_UPDATE_IN_PARALLEL = 50;
export const LIMITED_CONCURRENCY_ROUTE_TAG_PREFIX = `${APP_ID}:limitedConcurrency`;
+
+/**
+ * Max number of rules to display on UI in table, max number of rules that can be edited in a single bulk edit API request
+ * We limit number of rules in bulk edit API, because rulesClient doesn't support bulkGet of rules by ids.
+ * Given this limitation, current implementation fetches each rule separately through rulesClient.resolve method.
+ * As max number of rules displayed on a page is 100, max 100 rules can be bulk edited by passing their ids to API.
+ * We decided add this limit(number of ids less than 100) in bulk edit API as well, to prevent a huge number of single rule fetches
+ */
+export const RULES_TABLE_MAX_PAGE_SIZE = 100;
+export const RULES_TABLE_PAGE_SIZE_OPTIONS = [5, 10, 20, 50, RULES_TABLE_MAX_PAGE_SIZE];
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 f063d14822c84..9765f890e7474 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
@@ -442,6 +442,8 @@ const bulkActionEditPayloadTags = t.type({
value: tags,
});
+export type BulkActionEditPayloadTags = t.TypeOf;
+
const bulkActionEditPayloadIndexPatterns = t.type({
type: t.union([
t.literal(BulkActionEditType.add_index_patterns),
@@ -451,6 +453,10 @@ const bulkActionEditPayloadIndexPatterns = t.type({
value: index,
});
+export type BulkActionEditPayloadIndexPatterns = t.TypeOf<
+ typeof bulkActionEditPayloadIndexPatterns
+>;
+
const bulkActionEditPayloadTimeline = t.type({
type: t.literal(BulkActionEditType.set_timeline),
value: t.type({
diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/perform_bulk_action_schema.mock.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/perform_bulk_action_schema.mock.ts
index b6c241dfd15d2..4c6bdef3a4e80 100644
--- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/perform_bulk_action_schema.mock.ts
+++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/perform_bulk_action_schema.mock.ts
@@ -10,11 +10,13 @@ import { PerformBulkActionSchema } from './perform_bulk_action_schema';
export const getPerformBulkActionSchemaMock = (): PerformBulkActionSchema => ({
query: '',
+ ids: undefined,
action: BulkAction.disable,
});
export const getPerformBulkActionEditSchemaMock = (): PerformBulkActionSchema => ({
query: '',
+ ids: undefined,
action: BulkAction.edit,
[BulkAction.edit]: [{ type: BulkActionEditType.add_tags, value: ['tag1'] }],
});
diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/perform_bulk_action_schema.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/perform_bulk_action_schema.test.ts
index 855b7ea506d81..35083052141a4 100644
--- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/perform_bulk_action_schema.test.ts
+++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/perform_bulk_action_schema.test.ts
@@ -61,11 +61,22 @@ describe('perform_bulk_action_schema', () => {
const payload = {
query: 'name: test',
action: BulkAction.enable,
- ids: ['id'],
+ mock: ['id'],
};
const message = retrieveValidationMessage(payload);
- expect(getPaths(left(message.errors))).toEqual(['invalid keys "ids,["id"]"']);
+ expect(getPaths(left(message.errors))).toEqual(['invalid keys "mock,["id"]"']);
+ expect(message.schema).toEqual({});
+ });
+
+ test('invalid request: wrong type for ids', () => {
+ const payload = {
+ ids: 'mock',
+ action: BulkAction.enable,
+ };
+ const message = retrieveValidationMessage(payload);
+
+ expect(getPaths(left(message.errors))).toEqual(['Invalid value "mock" supplied to "ids"']);
expect(message.schema).toEqual({});
});
});
diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/perform_bulk_action_schema.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/perform_bulk_action_schema.ts
index 02de2f845b85d..167ed8efec7a9 100644
--- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/perform_bulk_action_schema.ts
+++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/perform_bulk_action_schema.ts
@@ -14,6 +14,7 @@ export const performBulkActionSchema = t.intersection([
query: queryOrUndefined,
})
),
+ t.exact(t.partial({ ids: t.array(t.string) })),
t.union([
t.exact(
t.type({
diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/response/perform_bulk_action_schema.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/perform_bulk_action_schema.ts
new file mode 100644
index 0000000000000..2bc3ea8448ee9
--- /dev/null
+++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/perform_bulk_action_schema.ts
@@ -0,0 +1,37 @@
+/*
+ * 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 * as t from 'io-ts';
+import { PositiveInteger, PositiveIntegerGreaterThanZero } from '@kbn/securitysolution-io-ts-types';
+
+const rule = t.type({
+ id: t.string,
+ name: t.union([t.string, t.undefined]),
+});
+
+const error = t.type({
+ status_code: PositiveIntegerGreaterThanZero,
+ message: t.string,
+ rules: t.array(rule),
+});
+
+export const bulkActionPartialErrorResponseSchema = t.exact(
+ t.type({
+ status_code: PositiveIntegerGreaterThanZero,
+ message: t.string,
+ attributes: t.type({
+ errors: t.array(error),
+ rules: t.type({
+ failed: PositiveInteger,
+ succeeded: PositiveInteger,
+ total: PositiveInteger,
+ }),
+ }),
+ })
+);
+
+export type BulkActionPartialErrorResponse = t.TypeOf;
diff --git a/x-pack/plugins/security_solution/common/detection_engine/types.ts b/x-pack/plugins/security_solution/common/detection_engine/types.ts
index e7b8cca8d5a97..eca1ee10bc6d8 100644
--- a/x-pack/plugins/security_solution/common/detection_engine/types.ts
+++ b/x-pack/plugins/security_solution/common/detection_engine/types.ts
@@ -54,3 +54,7 @@ export interface EqlSearchResponse {
events?: Array>;
};
}
+
+export interface HTTPError extends Error {
+ body?: unknown;
+}
diff --git a/x-pack/plugins/security_solution/common/experimental_features.ts b/x-pack/plugins/security_solution/common/experimental_features.ts
index 3f19b888d3901..a3a3c3a23acf0 100644
--- a/x-pack/plugins/security_solution/common/experimental_features.ts
+++ b/x-pack/plugins/security_solution/common/experimental_features.ts
@@ -22,6 +22,7 @@ export const allowedExperimentalValues = Object.freeze({
riskyHostsEnabled: false,
securityRulesCancelEnabled: false,
pendingActionResponsesWithAck: true,
+ rulesBulkEditEnabled: true,
policyListEnabled: false,
});
diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_rules/sorting.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_rules/sorting.spec.ts
index 23a02eb6dd1be..1a8fa2e972f89 100644
--- a/x-pack/plugins/security_solution/cypress/integration/detection_rules/sorting.spec.ts
+++ b/x-pack/plugins/security_solution/cypress/integration/detection_rules/sorting.spec.ts
@@ -17,11 +17,11 @@ import {
import { goToManageAlertsDetectionRules, waitForAlertsPanelToBeLoaded } from '../../tasks/alerts';
import {
- activateRule,
+ enableRule,
changeRowsPerPageTo,
checkAutoRefresh,
goToPage,
- sortByActivatedRules,
+ sortByEnabledRules,
waitForRulesTableToBeLoaded,
waitForRuleToChangeStatus,
} from '../../tasks/alerts_detection_rules';
@@ -50,19 +50,19 @@ describe('Alerts detection rules', () => {
createCustomRule(getNewThresholdRule(), '4');
});
- it('Sorts by activated rules', () => {
+ it('Sorts by enabled rules', () => {
goToManageAlertsDetectionRules();
waitForRulesTableToBeLoaded();
- activateRule(SECOND_RULE);
+ enableRule(SECOND_RULE);
waitForRuleToChangeStatus();
- activateRule(FOURTH_RULE);
+ enableRule(FOURTH_RULE);
waitForRuleToChangeStatus();
cy.get(RULE_SWITCH).eq(SECOND_RULE).should('have.attr', 'role', 'switch');
cy.get(RULE_SWITCH).eq(FOURTH_RULE).should('have.attr', 'role', 'switch');
- sortByActivatedRules();
+ sortByEnabledRules();
cy.get(RULE_SWITCH).eq(FIRST_RULE).should('have.attr', 'role', 'switch');
cy.get(RULE_SWITCH).eq(SECOND_RULE).should('have.attr', 'role', 'switch');
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 377abd53b1af8..03d64ab22608c 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
@@ -49,7 +49,7 @@ import {
import { ALL_ACTIONS } from '../screens/rule_details';
import { LOADING_INDICATOR } from '../screens/security_header';
-export const activateRule = (rulePosition: number) => {
+export const enableRule = (rulePosition: number) => {
cy.get(RULE_SWITCH).eq(rulePosition).click({ force: true });
};
@@ -191,9 +191,9 @@ export const confirmRulesDelete = () => {
cy.get(RULES_DELETE_CONFIRMATION_MODAL).should('not.exist');
};
-export const sortByActivatedRules = () => {
- cy.get(SORT_RULES_BTN).contains('Activated').click({ force: true });
- cy.get(SORT_RULES_BTN).contains('Activated').click({ force: true });
+export const sortByEnabledRules = () => {
+ cy.get(SORT_RULES_BTN).contains('Enabled').click({ force: true });
+ cy.get(SORT_RULES_BTN).contains('Enabled').click({ force: true });
};
export const waitForRulesTableToBeLoaded = () => {
diff --git a/x-pack/plugins/security_solution/public/common/components/utility_bar/utility_bar_action.tsx b/x-pack/plugins/security_solution/public/common/components/utility_bar/utility_bar_action.tsx
index ac30818ff1c3f..faa4733a0bf3e 100644
--- a/x-pack/plugins/security_solution/public/common/components/utility_bar/utility_bar_action.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/utility_bar/utility_bar_action.tsx
@@ -5,12 +5,24 @@
* 2.0.
*/
-import { EuiPopover } from '@elastic/eui';
+import { EuiPopover, PanelPaddingSize, EuiButtonEmpty } from '@elastic/eui';
import React, { useCallback, useState } from 'react';
+import styled, { css } from 'styled-components';
import { LinkIcon, LinkIconProps } from '../link_icon';
import { BarAction } from './styles';
+const LoadingButtonEmpty = styled(EuiButtonEmpty)`
+ ${({ theme }) => css`
+ &.euiButtonEmpty.euiButtonEmpty--xSmall {
+ height: ${theme.eui.euiSize};
+ .euiButtonEmpty__content {
+ padding: 0;
+ }
+ }
+ `}
+`;
+
const Popover = React.memo(
({
children,
@@ -22,6 +34,7 @@ const Popover = React.memo(
disabled,
ownFocus,
dataTestSubj,
+ popoverPanelPaddingSize,
}) => {
const [popoverState, setPopoverState] = useState(false);
@@ -30,6 +43,7 @@ const Popover = React.memo(
return (
void) => React.ReactNode;
+ popoverPanelPaddingSize?: PanelPaddingSize;
dataTestSubj: string;
ownFocus?: boolean;
+ inProgress?: boolean;
}
export const UtilityBarAction = React.memo(
@@ -74,37 +90,52 @@ export const UtilityBarAction = React.memo(
ownFocus,
onClick,
popoverContent,
- }) => (
-
- {popoverContent ? (
-
- {children}
-
- ) : (
-
- {children}
-
- )}
-
- )
+ popoverPanelPaddingSize,
+ inProgress,
+ }) => {
+ if (inProgress) {
+ return (
+
+
+ {children}
+
+
+ );
+ }
+
+ return (
+
+ {popoverContent ? (
+
+ {children}
+
+ ) : (
+
+ {children}
+
+ )}
+
+ );
+ }
);
UtilityBarAction.displayName = 'UtilityBarAction';
diff --git a/x-pack/plugins/security_solution/public/common/hooks/use_app_toasts.ts b/x-pack/plugins/security_solution/public/common/hooks/use_app_toasts.ts
index 0c2721e6ad416..b0719960ff2c8 100644
--- a/x-pack/plugins/security_solution/public/common/hooks/use_app_toasts.ts
+++ b/x-pack/plugins/security_solution/public/common/hooks/use_app_toasts.ts
@@ -5,7 +5,7 @@
* 2.0.
*/
-import { useCallback, useRef } from 'react';
+import { useCallback, useRef, useMemo } from 'react';
import { isString } from 'lodash/fp';
import {
AppError,
@@ -44,7 +44,11 @@ export const useAppToasts = (): UseAppToasts => {
},
[addError]
);
- return { api: toasts, addError: _addError, addSuccess, addWarning };
+
+ return useMemo(
+ () => ({ api: toasts, addError: _addError, addSuccess, addWarning }),
+ [_addError, addSuccess, addWarning, toasts]
+ );
};
/**
diff --git a/x-pack/plugins/security_solution/public/common/mock/test_providers.tsx b/x-pack/plugins/security_solution/public/common/mock/test_providers.tsx
index 1909388aea27a..f183198e6f5c5 100644
--- a/x-pack/plugins/security_solution/public/common/mock/test_providers.tsx
+++ b/x-pack/plugins/security_solution/public/common/mock/test_providers.tsx
@@ -15,6 +15,7 @@ import { Store } from 'redux';
import { BehaviorSubject } from 'rxjs';
import { ThemeProvider } from 'styled-components';
import { Capabilities } from 'src/core/public';
+import { QueryClient, QueryClientProvider } from 'react-query';
import { createStore, State } from '../store';
import { mockGlobalState } from './global_state';
@@ -50,17 +51,22 @@ export const TestProvidersComponent: React.FC = ({
children,
store = createStore(state, SUB_PLUGINS_REDUCER, kibanaObservable, storage),
onDragEnd = jest.fn(),
-}) => (
-
-
-
- ({ eui: euiDarkVars, darkMode: true })}>
- {children}
-
-
-
-
-);
+}) => {
+ const queryClient = new QueryClient();
+ return (
+
+
+
+ ({ eui: euiDarkVars, darkMode: true })}>
+
+ {children}
+
+
+
+
+
+ );
+};
/**
* A utility for wrapping children in the providers required to run most tests
diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.ts
index ad51ec009acbf..c765db6eaecd8 100644
--- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.ts
+++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.ts
@@ -255,6 +255,8 @@ export const duplicateRules = async ({ rules }: DuplicateRulesProps): Promise({
action,
query,
+ edit,
+ ids,
}: BulkActionProps): Promise> =>
KibanaServices.get().http.fetch>(DETECTION_ENGINE_RULES_BULK_ACTION, {
method: 'POST',
- body: JSON.stringify({ action, query }),
+ body: JSON.stringify({
+ action,
+ ...(edit ? { edit } : {}),
+ ...(ids ? { ids } : {}),
+ ...(query !== undefined ? { query } : {}),
+ }),
});
/**
diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/rules_table/rules_table_context.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/rules_table/rules_table_context.tsx
index 41aee63ea5c65..3f91387f52d07 100644
--- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/rules_table/rules_table_context.tsx
+++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/rules_table/rules_table_context.tsx
@@ -168,7 +168,7 @@ export const RulesTableContextProvider = ({
const isActionInProgress = useMemo(() => {
if (loadingRules.ids.length > 0) {
- return !(loadingRules.action === 'disable' || loadingRules.action === 'enable');
+ return !['disable', 'enable', 'edit'].includes(loadingRules.action ?? '');
}
return false;
}, [loadingRules.action, loadingRules.ids.length]);
diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/types.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/types.ts
index 009a95fe31367..de2cc3c7d93d7 100644
--- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/types.ts
+++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/types.ts
@@ -30,8 +30,10 @@ import {
timestamp_override,
threshold,
BulkAction,
+ BulkActionEditPayload,
ruleExecutionSummary,
} from '../../../../../common/detection_engine/schemas/common';
+
import {
CreateRulesSchema,
PatchRulesSchema,
@@ -230,7 +232,9 @@ export interface DuplicateRulesProps {
export interface BulkActionProps {
action: Action;
- query: string;
+ query?: string;
+ ids?: string[];
+ edit?: BulkActionEditPayload[];
}
export interface BulkActionResult {
diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/actions.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/actions.tsx
index 93bc806e7ae0d..024b3aba613af 100644
--- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/actions.tsx
+++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/actions.tsx
@@ -6,9 +6,14 @@
*/
import { Dispatch } from 'react';
-import { NavigateToAppOptions } from '../../../../../../../../../src/core/public';
+import type { NavigateToAppOptions } from '../../../../../../../../../src/core/public';
+
+import type { UseAppToasts } from '../../../../../common/hooks/use_app_toasts';
import { APP_UI_ID } from '../../../../../../common/constants';
-import { BulkAction } from '../../../../../../common/detection_engine/schemas/common/schemas';
+import {
+ BulkAction,
+ BulkActionEditPayload,
+} from '../../../../../../common/detection_engine/schemas/common/schemas';
import { CreateRulesSchema } from '../../../../../../common/detection_engine/schemas/request';
import { SecurityPageName } from '../../../../../app/types';
import { getEditRuleUrl } from '../../../../../common/components/link_to/redirect_to_detection_engine';
@@ -31,7 +36,7 @@ import {
import { RulesTableActions } from '../../../../containers/detection_engine/rules/rules_table/rules_table_context';
import { transformOutput } from '../../../../containers/detection_engine/rules/transforms';
import * as i18n from '../translations';
-import { bucketRulesResponse, getExportedRulesCount } from './helpers';
+import { bucketRulesResponse, getExportedRulesCounts } from './helpers';
export const editRuleAction = (
ruleId: string,
@@ -84,9 +89,9 @@ export const exportRulesAction = async (
const blob = await exportRules({ ids: exportRuleId });
downloadBlob(blob, `${i18n.EXPORT_FILENAME}.ndjson`);
- const exportedRulesCount = await getExportedRulesCount(blob);
+ const { exported } = await getExportedRulesCounts(blob);
displaySuccessToast(
- i18n.SUCCESSFULLY_EXPORTED_RULES(exportedRulesCount, exportRuleId.length),
+ i18n.SUCCESSFULLY_EXPORTED_RULES(exported, exportRuleId.length),
dispatchToaster
);
} catch (e) {
@@ -171,32 +176,67 @@ export const enableRulesAction = async (
}
};
-export const rulesBulkActionByQuery = async (
- visibleRuleIds: string[],
- selectedItemsCount: number,
- query: string,
- action: BulkAction,
- dispatchToaster: Dispatch,
- setLoadingRules: RulesTableActions['setLoadingRules']
-) => {
+interface ExecuteRulesBulkActionArgs {
+ visibleRuleIds: string[];
+ action: BulkAction;
+ toasts: UseAppToasts;
+ search: { query: string } | { ids: string[] };
+ payload?: { edit?: BulkActionEditPayload[] };
+ onSuccess?: (arg: { rulesCount: number }) => void;
+ onError?: (error: Error) => void;
+ setLoadingRules: RulesTableActions['setLoadingRules'];
+}
+
+const executeRulesBulkAction = async ({
+ visibleRuleIds,
+ action,
+ setLoadingRules,
+ toasts,
+ search,
+ payload,
+ onSuccess,
+ onError,
+}: ExecuteRulesBulkActionArgs) => {
try {
setLoadingRules({ ids: visibleRuleIds, action });
if (action === BulkAction.export) {
- const blob = await performBulkAction({ query, action });
+ const blob = await performBulkAction({ ...search, action });
downloadBlob(blob, `${i18n.EXPORT_FILENAME}.ndjson`);
+ const { exported, total } = await getExportedRulesCounts(blob);
- const exportedRulesCount = await getExportedRulesCount(blob);
- displaySuccessToast(
- i18n.SUCCESSFULLY_EXPORTED_RULES(exportedRulesCount, selectedItemsCount),
- dispatchToaster
- );
+ toasts.addSuccess(i18n.SUCCESSFULLY_EXPORTED_RULES(exported, total));
} else {
- await performBulkAction({ query, action });
+ const response = await performBulkAction({ ...search, action, edit: payload?.edit });
+
+ onSuccess?.({ rulesCount: response.rules_count });
}
} catch (e) {
- displayErrorToast(i18n.BULK_ACTION_FAILED, [e.message], dispatchToaster);
+ if (onError) {
+ onError(e);
+ } else {
+ toasts.addError(e, { title: i18n.BULK_ACTION_FAILED });
+ }
} finally {
setLoadingRules({ ids: [], action: null });
}
};
+
+export const initRulesBulkAction = (params: Omit) => {
+ const byQuery = (query: string) =>
+ executeRulesBulkAction({
+ ...params,
+ search: { query },
+ });
+
+ const byIds = (ids: string[]) =>
+ executeRulesBulkAction({
+ ...params,
+ search: { ids },
+ });
+
+ return {
+ byQuery,
+ byIds,
+ };
+};
diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/bulk_actions/bulk_edit_confirmation.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/bulk_actions/bulk_edit_confirmation.tsx
new file mode 100644
index 0000000000000..445bd33860be2
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/bulk_actions/bulk_edit_confirmation.tsx
@@ -0,0 +1,76 @@
+/*
+ * 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 React, { useEffect } from 'react';
+import { EuiConfirmModal } from '@elastic/eui';
+import { FormattedMessage } from '@kbn/i18n-react';
+
+import * as i18n from '../../translations';
+
+interface BulkEditConfirmationProps {
+ customRulesCount: number;
+ elasticRulesCount: number;
+ onCancel: () => void;
+ onConfirm: () => void;
+}
+const BulkEditConfirmationComponent = ({
+ onCancel,
+ onConfirm,
+ customRulesCount,
+ elasticRulesCount,
+}: BulkEditConfirmationProps) => {
+ useEffect(() => {
+ if (elasticRulesCount === 0) {
+ setTimeout(onConfirm, 0);
+ }
+ }, [elasticRulesCount, onConfirm]);
+
+ // proceed straight to edit flyout if there is no Elastic rules
+ if (elasticRulesCount === 0) {
+ return null;
+ }
+
+ if (customRulesCount === 0) {
+ return (
+
+
+
+ );
+ }
+
+ return (
+
+
+
+ );
+};
+
+export const BulkEditConfirmation = React.memo(BulkEditConfirmationComponent);
+
+BulkEditConfirmation.displayName = 'BulkEditConfirmation';
diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/bulk_actions/bulk_edit_flyout.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/bulk_actions/bulk_edit_flyout.tsx
new file mode 100644
index 0000000000000..c4e7005f8e71a
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/bulk_actions/bulk_edit_flyout.tsx
@@ -0,0 +1,45 @@
+/*
+ * 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 React from 'react';
+
+import {
+ BulkActionEditType,
+ BulkActionEditPayload,
+} from '../../../../../../../common/detection_engine/schemas/common/schemas';
+
+import { IndexPatternsForm } from './forms/index_patterns_form';
+import { TagsForm } from './forms/tags_form';
+
+interface BulkEditFlyoutProps {
+ onClose: () => void;
+ onConfirm: (bulkactionEditPayload: BulkActionEditPayload) => void;
+ editAction: BulkActionEditType;
+ rulesCount: number;
+ tags: string[];
+}
+
+const BulkEditFlyoutComponent = ({ editAction, tags, ...props }: BulkEditFlyoutProps) => {
+ switch (editAction) {
+ case BulkActionEditType.add_index_patterns:
+ case BulkActionEditType.delete_index_patterns:
+ case BulkActionEditType.set_index_patterns:
+ return ;
+
+ case BulkActionEditType.add_tags:
+ case BulkActionEditType.delete_tags:
+ case BulkActionEditType.set_tags:
+ return ;
+
+ default:
+ return null;
+ }
+};
+
+export const BulkEditFlyout = React.memo(BulkEditFlyoutComponent);
+
+BulkEditFlyout.displayName = 'BulkEditFlyout';
diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/bulk_actions/forms/bulk_edit_form_wrapper.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/bulk_actions/forms/bulk_edit_form_wrapper.tsx
new file mode 100644
index 0000000000000..c6d60be416408
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/bulk_actions/forms/bulk_edit_form_wrapper.tsx
@@ -0,0 +1,76 @@
+/*
+ * 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 React, { FC } from 'react';
+import {
+ useGeneratedHtmlId,
+ EuiFlyout,
+ EuiFlyoutFooter,
+ EuiFlexGroup,
+ EuiButtonEmpty,
+ EuiFlexItem,
+ EuiButton,
+ EuiFlyoutHeader,
+ EuiTitle,
+ EuiFlyoutBody,
+} from '@elastic/eui';
+
+import { Form, FormHook } from '../../../../../../../shared_imports';
+
+import * as i18n from '../../../translations';
+
+interface BulkEditFormWrapperProps {
+ onClose: () => void;
+ onSubmit: () => void;
+ title: string;
+ form: FormHook;
+ children: React.ReactNode;
+}
+
+const BulkEditFormWrapperComponent: FC = ({
+ form,
+ onClose,
+ onSubmit,
+ children,
+ title,
+}) => {
+ const simpleFlyoutTitleId = useGeneratedHtmlId({
+ prefix: 'BulkEditForm',
+ });
+
+ const { isValid } = form;
+ return (
+
+
+
+ {title}
+
+
+
+
+
+
+
+
+
+ {i18n.BULK_EDIT_FLYOUT_FORM_CLOSE}
+
+
+
+
+ {i18n.BULK_EDIT_FLYOUT_FORM_SAVE}
+
+
+
+
+
+ );
+};
+
+export const BulkEditFormWrapper = React.memo(BulkEditFormWrapperComponent);
+
+BulkEditFormWrapper.displayName = 'BulkEditFormWrapper';
diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/bulk_actions/forms/index_patterns_form.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/bulk_actions/forms/index_patterns_form.tsx
new file mode 100644
index 0000000000000..d25ea6e661180
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/bulk_actions/forms/index_patterns_form.tsx
@@ -0,0 +1,158 @@
+/*
+ * 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 React from 'react';
+import { EuiFormRow, EuiCallOut } from '@elastic/eui';
+import { FormattedMessage } from '@kbn/i18n-react';
+
+import * as i18n from '../../../translations';
+
+import { DEFAULT_INDEX_KEY } from '../../../../../../../../common/constants';
+import { useKibana } from '../../../../../../../common/lib/kibana';
+
+import {
+ BulkActionEditType,
+ BulkActionEditPayload,
+} from '../../../../../../../../common/detection_engine/schemas/common/schemas';
+
+import {
+ Field,
+ getUseField,
+ useFormData,
+ useForm,
+ FIELD_TYPES,
+ fieldValidators,
+ FormSchema,
+} from '../../../../../../../shared_imports';
+
+import { BulkEditFormWrapper } from './bulk_edit_form_wrapper';
+const CommonUseField = getUseField({ component: Field });
+
+type IndexPatternsEditActions =
+ | BulkActionEditType.add_index_patterns
+ | BulkActionEditType.delete_index_patterns
+ | BulkActionEditType.set_index_patterns;
+
+interface IndexPatternsFormData {
+ index: string[];
+ overwrite: boolean;
+}
+
+const schema: FormSchema = {
+ index: {
+ fieldsToValidateOnChange: ['index'],
+ type: FIELD_TYPES.COMBO_BOX,
+ validations: [
+ {
+ validator: fieldValidators.emptyField(
+ i18n.BULK_EDIT_FLYOUT_FORM_ADD_INDEX_PATTERNS_REQUIRED_ERROR
+ ),
+ },
+ ],
+ },
+ overwrite: {
+ type: FIELD_TYPES.CHECKBOX,
+ label: i18n.BULK_EDIT_FLYOUT_FORM_ADD_INDEX_PATTERNS_OVERWRITE_LABEL,
+ },
+};
+
+const initialFormData: IndexPatternsFormData = { index: [], overwrite: false };
+
+const getFormConfig = (editAction: IndexPatternsEditActions) =>
+ editAction === BulkActionEditType.add_index_patterns
+ ? {
+ indexLabel: i18n.BULK_EDIT_FLYOUT_FORM_ADD_INDEX_PATTERNS_LABEL,
+ indexHelpText: i18n.BULK_EDIT_FLYOUT_FORM_ADD_INDEX_PATTERNS_HELP_TEXT,
+ formTitle: i18n.BULK_EDIT_FLYOUT_FORM_ADD_INDEX_PATTERNS_TITLE,
+ }
+ : {
+ indexLabel: i18n.BULK_EDIT_FLYOUT_FORM_DELETE_INDEX_PATTERNS_LABEL,
+ indexHelpText: i18n.BULK_EDIT_FLYOUT_FORM_DELETE_INDEX_PATTERNS_HELP_TEXT,
+ formTitle: i18n.BULK_EDIT_FLYOUT_FORM_DELETE_INDEX_PATTERNS_TITLE,
+ };
+
+interface IndexPatternsFormProps {
+ editAction: IndexPatternsEditActions;
+ rulesCount: number;
+ onClose: () => void;
+ onConfirm: (bulkactionEditPayload: BulkActionEditPayload) => void;
+}
+
+const IndexPatternsFormComponent = ({
+ editAction,
+ rulesCount,
+ onClose,
+ onConfirm,
+}: IndexPatternsFormProps) => {
+ const { form } = useForm({
+ defaultValue: initialFormData,
+ schema,
+ });
+
+ const { indexHelpText, indexLabel, formTitle } = getFormConfig(editAction);
+
+ const [{ overwrite }] = useFormData({ form, watch: ['overwrite'] });
+ const { uiSettings } = useKibana().services;
+ const defaultPatterns = uiSettings.get(DEFAULT_INDEX_KEY);
+
+ const handleSubmit = async () => {
+ const { data, isValid } = await form.submit();
+ if (!isValid) {
+ return;
+ }
+
+ const payload = {
+ value: data.index,
+ type: data.overwrite ? BulkActionEditType.set_index_patterns : editAction,
+ };
+
+ onConfirm(payload);
+ };
+
+ return (
+
+ ({ label })),
+ },
+ }}
+ />
+ {editAction === BulkActionEditType.add_index_patterns && (
+
+ )}
+ {overwrite && (
+
+
+
+
+
+ )}
+
+ );
+};
+
+export const IndexPatternsForm = React.memo(IndexPatternsFormComponent);
+IndexPatternsForm.displayName = 'IndexPatternsForm';
diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/bulk_actions/forms/tags_form.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/bulk_actions/forms/tags_form.tsx
new file mode 100644
index 0000000000000..de3aede948425
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/bulk_actions/forms/tags_form.tsx
@@ -0,0 +1,148 @@
+/*
+ * 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 React, { useMemo } from 'react';
+import { EuiFormRow, EuiCallOut } from '@elastic/eui';
+import { FormattedMessage } from '@kbn/i18n-react';
+
+import * as i18n from '../../../translations';
+import { caseInsensitiveSort } from '../../helpers';
+import {
+ BulkActionEditType,
+ BulkActionEditPayload,
+} from '../../../../../../../../common/detection_engine/schemas/common/schemas';
+
+import {
+ useForm,
+ Field,
+ getUseField,
+ useFormData,
+ FIELD_TYPES,
+ fieldValidators,
+ FormSchema,
+} from '../../../../../../../shared_imports';
+
+import { BulkEditFormWrapper } from './bulk_edit_form_wrapper';
+
+type TagsEditActions =
+ | BulkActionEditType.add_tags
+ | BulkActionEditType.delete_tags
+ | BulkActionEditType.set_tags;
+
+const CommonUseField = getUseField({ component: Field });
+
+export interface TagsFormData {
+ tags: string[];
+ overwrite: boolean;
+}
+
+const schema: FormSchema = {
+ tags: {
+ fieldsToValidateOnChange: ['tags'],
+ type: FIELD_TYPES.COMBO_BOX,
+ validations: [
+ {
+ validator: fieldValidators.emptyField(i18n.BULK_EDIT_FLYOUT_FORM_TAGS_REQUIRED_ERROR),
+ },
+ ],
+ },
+ overwrite: {
+ type: FIELD_TYPES.CHECKBOX,
+ label: i18n.BULK_EDIT_FLYOUT_FORM_ADD_TAGS_OVERWRITE_LABEL,
+ },
+};
+
+const initialFormData: TagsFormData = { tags: [], overwrite: false };
+
+const getFormConfig = (editAction: TagsEditActions) =>
+ editAction === BulkActionEditType.add_tags
+ ? {
+ tagsLabel: i18n.BULK_EDIT_FLYOUT_FORM_ADD_TAGS_LABEL,
+ tagsHelpText: i18n.BULK_EDIT_FLYOUT_FORM_ADD_TAGS_HELP_TEXT,
+ formTitle: i18n.BULK_EDIT_FLYOUT_FORM_ADD_TAGS_TITLE,
+ }
+ : {
+ tagsLabel: i18n.BULK_EDIT_FLYOUT_FORM_DELETE_TAGS_LABEL,
+ tagsHelpText: i18n.BULK_EDIT_FLYOUT_FORM_DELETE_TAGS_HELP_TEXT,
+ formTitle: i18n.BULK_EDIT_FLYOUT_FORM_DELETE_TAGS_TITLE,
+ };
+
+interface TagsFormProps {
+ editAction: TagsEditActions;
+ rulesCount: number;
+ onClose: () => void;
+ onConfirm: (bulkactionEditPayload: BulkActionEditPayload) => void;
+ tags: string[];
+}
+
+const TagsFormComponent = ({ editAction, rulesCount, onClose, onConfirm, tags }: TagsFormProps) => {
+ const { form } = useForm({
+ defaultValue: initialFormData,
+ schema,
+ });
+ const [{ overwrite }] = useFormData({ form, watch: ['overwrite'] });
+ const sortedTags = useMemo(() => caseInsensitiveSort(tags), [tags]);
+
+ const { tagsLabel, tagsHelpText, formTitle } = getFormConfig(editAction);
+
+ const handleSubmit = async () => {
+ const { data, isValid } = await form.submit();
+ if (!isValid) {
+ return;
+ }
+
+ const payload = {
+ value: data.tags,
+ type: data.overwrite ? BulkActionEditType.set_tags : editAction,
+ };
+
+ onConfirm(payload);
+ };
+
+ return (
+
+ ({ label })),
+ },
+ }}
+ />
+ {editAction === BulkActionEditType.add_tags ? (
+
+ ) : null}
+ {overwrite && (
+
+
+
+
+
+ )}
+
+ );
+};
+
+export const TagsForm = React.memo(TagsFormComponent);
+TagsForm.displayName = 'TagsForm';
diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/bulk_actions/use_bulk_actions.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/bulk_actions/use_bulk_actions.tsx
new file mode 100644
index 0000000000000..f197043a7a291
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/bulk_actions/use_bulk_actions.tsx
@@ -0,0 +1,515 @@
+/*
+ * 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.
+ */
+/* eslint-disable complexity */
+
+import React, { useCallback } from 'react';
+import { useQueryClient } from 'react-query';
+import {
+ EuiTextColor,
+ EuiContextMenuPanelDescriptor,
+ EuiFlexGroup,
+ EuiButton,
+ EuiFlexItem,
+} from '@elastic/eui';
+import { euiThemeVars } from '@kbn/ui-theme';
+import { useIsMounted } from '@kbn/securitysolution-hook-utils';
+
+import type { Toast } from '../../../../../../../../../../src/core/public';
+import { mountReactNode } from '../../../../../../../../../../src/core/public/utils';
+import {
+ BulkAction,
+ BulkActionEditType,
+ BulkActionEditPayload,
+} from '../../../../../../../common/detection_engine/schemas/common/schemas';
+import { isMlRule } from '../../../../../../../common/machine_learning/helpers';
+import { displayWarningToast, useStateToaster } from '../../../../../../common/components/toasters';
+import { canEditRuleWithActions } from '../../../../../../common/utils/privileges';
+import { useRulesTableContext } from '../../../../../containers/detection_engine/rules/rules_table/rules_table_context';
+import * as detectionI18n from '../../../translations';
+import * as i18n from '../../translations';
+import {
+ deleteRulesAction,
+ duplicateRulesAction,
+ enableRulesAction,
+ exportRulesAction,
+ initRulesBulkAction,
+} from '../actions';
+import { useHasActionsPrivileges } from '../use_has_actions_privileges';
+import { useHasMlPermissions } from '../use_has_ml_permissions';
+import { getCustomRulesCountFromCache } from './use_custom_rules_count';
+import { useAppToasts } from '../../../../../../common/hooks/use_app_toasts';
+import { useIsExperimentalFeatureEnabled } from '../../../../../../common/hooks/use_experimental_features';
+import { convertRulesFilterToKQL } from '../../../../../containers/detection_engine/rules/utils';
+
+import type { FilterOptions } from '../../../../../containers/detection_engine/rules/types';
+import type { BulkActionPartialErrorResponse } from '../../../../../../../common/detection_engine/schemas/response/perform_bulk_action_schema';
+import type { HTTPError } from '../../../../../../../common/detection_engine/types';
+
+interface UseBulkActionsArgs {
+ filterOptions: FilterOptions;
+ confirmDeletion: () => Promise;
+ confirmBulkEdit: () => Promise;
+ completeBulkEditForm: (
+ bulkActionEditType: BulkActionEditType
+ ) => Promise;
+ reFetchTags: () => void;
+}
+
+export const useBulkActions = ({
+ filterOptions,
+ confirmDeletion,
+ confirmBulkEdit,
+ completeBulkEditForm,
+ reFetchTags,
+}: UseBulkActionsArgs) => {
+ const queryClient = useQueryClient();
+ const hasMlPermissions = useHasMlPermissions();
+ const rulesTableContext = useRulesTableContext();
+ const [, dispatchToaster] = useStateToaster();
+ const hasActionsPrivileges = useHasActionsPrivileges();
+ const toasts = useAppToasts();
+ const isRulesBulkEditEnabled = useIsExperimentalFeatureEnabled('rulesBulkEditEnabled');
+ const getIsMounted = useIsMounted();
+ const filterQuery = convertRulesFilterToKQL(filterOptions);
+
+ // refetch tags if edit action is related to tags: add_tags/delete_tags/set_tags
+ const resolveTagsRefetch = useCallback(
+ async (bulkEditActionType: BulkActionEditType) => {
+ const isTagsAction = [
+ BulkActionEditType.add_tags,
+ BulkActionEditType.set_tags,
+ BulkActionEditType.delete_tags,
+ ].includes(bulkEditActionType);
+
+ return isTagsAction ? reFetchTags() : null;
+ },
+ [reFetchTags]
+ );
+
+ const {
+ state: { isAllSelected, rules, loadingRuleIds, selectedRuleIds },
+ actions: { reFetchRules, setLoadingRules, updateRules, setIsRefreshOn },
+ } = rulesTableContext;
+
+ return useCallback(
+ (closePopover: () => void): EuiContextMenuPanelDescriptor[] => {
+ const selectedRules = rules.filter(({ id }) => selectedRuleIds.includes(id));
+
+ const containsEnabled = selectedRules.some(({ enabled }) => enabled);
+ const containsDisabled = selectedRules.some(({ enabled }) => !enabled);
+ const containsLoading = selectedRuleIds.some((id) => loadingRuleIds.includes(id));
+ const containsImmutable = selectedRules.some(({ immutable }) => immutable);
+
+ const missingActionPrivileges =
+ !hasActionsPrivileges &&
+ selectedRules.some((rule) => !canEditRuleWithActions(rule, hasActionsPrivileges));
+
+ const handleActivateAction = async () => {
+ closePopover();
+ const deactivatedRules = selectedRules.filter(({ enabled }) => !enabled);
+ const deactivatedRulesNoML = deactivatedRules.filter(({ type }) => !isMlRule(type));
+
+ const mlRuleCount = deactivatedRules.length - deactivatedRulesNoML.length;
+ if (!hasMlPermissions && mlRuleCount > 0) {
+ displayWarningToast(detectionI18n.ML_RULES_UNAVAILABLE(mlRuleCount), dispatchToaster);
+ }
+
+ const ruleIds = hasMlPermissions
+ ? deactivatedRules.map(({ id }) => id)
+ : deactivatedRulesNoML.map(({ id }) => id);
+
+ if (isAllSelected) {
+ const rulesBulkAction = initRulesBulkAction({
+ visibleRuleIds: ruleIds,
+ action: BulkAction.enable,
+ setLoadingRules,
+ toasts,
+ });
+
+ await rulesBulkAction.byQuery(filterQuery);
+ await reFetchRules();
+ } else {
+ await enableRulesAction(ruleIds, true, dispatchToaster, setLoadingRules, updateRules);
+ }
+ };
+
+ const handleDeactivateActions = async () => {
+ closePopover();
+ const activatedIds = selectedRules.filter(({ enabled }) => enabled).map(({ id }) => id);
+ if (isAllSelected) {
+ const rulesBulkAction = initRulesBulkAction({
+ visibleRuleIds: activatedIds,
+ action: BulkAction.disable,
+ setLoadingRules,
+ toasts,
+ });
+
+ await rulesBulkAction.byQuery(filterQuery);
+ await reFetchRules();
+ } else {
+ await enableRulesAction(
+ activatedIds,
+ false,
+ dispatchToaster,
+ setLoadingRules,
+ updateRules
+ );
+ }
+ };
+
+ const handleDuplicateAction = async () => {
+ closePopover();
+ if (isAllSelected) {
+ const rulesBulkAction = initRulesBulkAction({
+ visibleRuleIds: selectedRuleIds,
+ action: BulkAction.duplicate,
+ setLoadingRules,
+ toasts,
+ });
+
+ await rulesBulkAction.byQuery(filterQuery);
+ } else {
+ await duplicateRulesAction(
+ selectedRules,
+ selectedRuleIds,
+ dispatchToaster,
+ setLoadingRules
+ );
+ }
+ await reFetchRules();
+ };
+
+ const handleDeleteAction = async () => {
+ closePopover();
+ if (isAllSelected) {
+ if ((await confirmDeletion()) === false) {
+ // User has cancelled deletion
+ return;
+ }
+
+ const rulesBulkAction = initRulesBulkAction({
+ visibleRuleIds: selectedRuleIds,
+ action: BulkAction.delete,
+ setLoadingRules,
+ toasts,
+ });
+
+ await rulesBulkAction.byQuery(filterQuery);
+ } else {
+ await deleteRulesAction(selectedRuleIds, dispatchToaster, setLoadingRules);
+ }
+ await reFetchRules();
+ };
+
+ const handleExportAction = async () => {
+ closePopover();
+ if (isAllSelected) {
+ const rulesBulkAction = initRulesBulkAction({
+ visibleRuleIds: selectedRuleIds,
+ action: BulkAction.export,
+ setLoadingRules,
+ toasts,
+ });
+
+ await rulesBulkAction.byQuery(filterQuery);
+ } else {
+ await exportRulesAction(
+ selectedRules.map((r) => r.rule_id),
+ dispatchToaster,
+ setLoadingRules
+ );
+ }
+ };
+
+ const handleBulkEdit = (bulkEditActionType: BulkActionEditType) => async () => {
+ let longTimeWarningToast: Toast;
+ let isBulkEditFinished = false;
+ try {
+ // disabling auto-refresh so user's selected rules won't disappear after table refresh
+ setIsRefreshOn(false);
+ closePopover();
+
+ const customSelectedRuleIds = selectedRules
+ .filter((rule) => rule.immutable === false)
+ .map((rule) => rule.id);
+
+ // User has cancelled edit action or there are no custom rules to proceed
+ if ((await confirmBulkEdit()) === false) {
+ setIsRefreshOn(true);
+ return;
+ }
+
+ const editPayload = await completeBulkEditForm(bulkEditActionType);
+ if (editPayload == null) {
+ throw Error('Bulk edit payload is empty');
+ }
+
+ const hideWarningToast = () => {
+ if (longTimeWarningToast) {
+ toasts.api.remove(longTimeWarningToast);
+ }
+ };
+
+ const customRulesCount = isAllSelected
+ ? getCustomRulesCountFromCache(queryClient)
+ : customSelectedRuleIds.length;
+
+ // show warning toast only if bulk edit action exceeds 5s
+ // if bulkAction already finished, we won't show toast at all (hence flag "isBulkEditFinished")
+ setTimeout(() => {
+ if (isBulkEditFinished) {
+ return;
+ }
+ longTimeWarningToast = toasts.addWarning(
+ {
+ title: i18n.BULK_EDIT_WARNING_TOAST_TITLE,
+ text: mountReactNode(
+ <>
+ {i18n.BULK_EDIT_WARNING_TOAST_DESCRIPTION(customRulesCount)}
+
+
+
+ {i18n.BULK_EDIT_WARNING_TOAST_NOTIFY}
+
+
+
+ >
+ ),
+ iconType: undefined,
+ },
+ { toastLifeTimeMs: 10 * 60 * 1000 }
+ );
+ }, 5 * 1000);
+
+ const rulesBulkAction = initRulesBulkAction({
+ visibleRuleIds: selectedRuleIds,
+ action: BulkAction.edit,
+ setLoadingRules,
+ toasts,
+ payload: { edit: [editPayload] },
+ onSuccess: ({ rulesCount }) => {
+ hideWarningToast();
+ toasts.addSuccess({
+ title: i18n.BULK_EDIT_SUCCESS_TOAST_TITLE,
+ text: i18n.BULK_EDIT_SUCCESS_TOAST_DESCRIPTION(rulesCount),
+ iconType: undefined,
+ });
+ },
+ onError: (error: HTTPError) => {
+ hideWarningToast();
+
+ // if response doesn't have number of failed rules, it means the whole bulk action failed
+ // and general error toast will be shown. Otherwise - error toast for partial failure
+ const failedRulesCount = (error?.body as BulkActionPartialErrorResponse)?.attributes
+ ?.rules?.failed;
+
+ if (isNaN(failedRulesCount)) {
+ toasts.addError(error, { title: i18n.BULK_ACTION_FAILED });
+ } else {
+ try {
+ error.stack = JSON.stringify(error.body, null, 2);
+ toasts.addError(error, {
+ title: i18n.BULK_EDIT_ERROR_TOAST_TITLE,
+ toastMessage: i18n.BULK_EDIT_ERROR_TOAST_DESCIRPTION(failedRulesCount),
+ });
+ } catch (e) {
+ // toast error has failed
+ }
+ }
+ },
+ });
+
+ // only edit custom rules, as elastic rule are immutable
+ if (isAllSelected) {
+ const customRulesOnlyFilterQuery = convertRulesFilterToKQL({
+ ...filterOptions,
+ showCustomRules: true,
+ });
+ await rulesBulkAction.byQuery(customRulesOnlyFilterQuery);
+ } else {
+ await rulesBulkAction.byIds(customSelectedRuleIds);
+ }
+
+ isBulkEditFinished = true;
+ if (getIsMounted()) {
+ await Promise.allSettled([reFetchRules(), resolveTagsRefetch(bulkEditActionType)]);
+ }
+ } catch (e) {
+ // user has cancelled form or error has occured
+ } finally {
+ isBulkEditFinished = true;
+ if (getIsMounted()) {
+ setIsRefreshOn(true);
+ }
+ }
+ };
+
+ const isDeleteDisabled = containsLoading || selectedRuleIds.length === 0;
+ const isEditDisabled =
+ missingActionPrivileges || containsLoading || selectedRuleIds.length === 0;
+
+ return [
+ {
+ id: 0,
+ title: isRulesBulkEditEnabled ? i18n.BULK_ACTION_MENU_TITLE : undefined,
+ items: [
+ {
+ key: i18n.BULK_ACTION_ENABLE,
+ name: i18n.BULK_ACTION_ENABLE,
+ 'data-test-subj': 'activateRuleBulk',
+ disabled:
+ missingActionPrivileges || containsLoading || (!containsDisabled && !isAllSelected),
+ onClick: handleActivateAction,
+ toolTipContent: missingActionPrivileges ? i18n.EDIT_RULE_SETTINGS_TOOLTIP : undefined,
+ toolTipPosition: 'right',
+ icon: isRulesBulkEditEnabled ? undefined : 'checkInCircleFilled',
+ },
+ {
+ key: i18n.BULK_ACTION_DUPLICATE,
+ name: i18n.BULK_ACTION_DUPLICATE,
+ 'data-test-subj': 'duplicateRuleBulk',
+ disabled: isEditDisabled,
+ onClick: handleDuplicateAction,
+ toolTipContent: missingActionPrivileges ? i18n.EDIT_RULE_SETTINGS_TOOLTIP : undefined,
+ toolTipPosition: 'right',
+ icon: isRulesBulkEditEnabled ? undefined : 'crossInACircleFilled',
+ },
+ ...(isRulesBulkEditEnabled
+ ? [
+ {
+ key: i18n.BULK_ACTION_INDEX_PATTERNS,
+ name: i18n.BULK_ACTION_INDEX_PATTERNS,
+ 'data-test-subj': 'indexPatternsBulkEditRule',
+ disabled: isEditDisabled,
+ panel: 2,
+ },
+ {
+ key: i18n.BULK_ACTION_TAGS,
+ name: i18n.BULK_ACTION_TAGS,
+ 'data-test-subj': 'tagsBulkEditRule',
+ disabled: isEditDisabled,
+ panel: 1,
+ },
+ ]
+ : []),
+ {
+ key: i18n.BULK_ACTION_EXPORT,
+ name: i18n.BULK_ACTION_EXPORT,
+ 'data-test-subj': 'exportRuleBulk',
+ disabled:
+ (containsImmutable && !isAllSelected) ||
+ containsLoading ||
+ selectedRuleIds.length === 0,
+ onClick: handleExportAction,
+ icon: isRulesBulkEditEnabled ? undefined : 'exportAction',
+ },
+ {
+ key: i18n.BULK_ACTION_DISABLE,
+ name: i18n.BULK_ACTION_DISABLE,
+ 'data-test-subj': 'deactivateRuleBulk',
+ disabled:
+ missingActionPrivileges || containsLoading || (!containsEnabled && !isAllSelected),
+ onClick: handleDeactivateActions,
+ toolTipContent: missingActionPrivileges ? i18n.EDIT_RULE_SETTINGS_TOOLTIP : undefined,
+ toolTipPosition: 'right',
+ icon: isRulesBulkEditEnabled ? undefined : 'copy',
+ },
+ {
+ key: i18n.BULK_ACTION_DELETE,
+ name: (
+
+ {i18n.BULK_ACTION_DELETE}
+
+ ),
+ 'data-test-subj': 'deleteRuleBulk',
+ disabled: isDeleteDisabled,
+ onClick: handleDeleteAction,
+ toolTipContent: containsImmutable
+ ? i18n.BATCH_ACTION_DELETE_SELECTED_IMMUTABLE
+ : undefined,
+ toolTipPosition: 'right',
+ icon: isRulesBulkEditEnabled ? undefined : 'trash',
+ },
+ ],
+ },
+ {
+ id: 1,
+ title: i18n.BULK_ACTION_MENU_TITLE,
+ items: [
+ {
+ key: i18n.BULK_ACTION_ADD_TAGS,
+ name: i18n.BULK_ACTION_ADD_TAGS,
+ 'data-test-subj': 'addTagsBulkEditRule',
+ onClick: handleBulkEdit(BulkActionEditType.add_tags),
+ disabled: isEditDisabled,
+ toolTipContent: missingActionPrivileges ? i18n.EDIT_RULE_SETTINGS_TOOLTIP : undefined,
+ toolTipPosition: 'right',
+ },
+ {
+ key: i18n.BULK_ACTION_DELETE_TAGS,
+ name: i18n.BULK_ACTION_DELETE_TAGS,
+ 'data-test-subj': 'deleteTagsBulkEditRule',
+ onClick: handleBulkEdit(BulkActionEditType.delete_tags),
+ disabled: isEditDisabled,
+ toolTipContent: missingActionPrivileges ? i18n.EDIT_RULE_SETTINGS_TOOLTIP : undefined,
+ toolTipPosition: 'right',
+ },
+ ],
+ },
+ {
+ id: 2,
+ title: i18n.BULK_ACTION_MENU_TITLE,
+ items: [
+ {
+ key: i18n.BULK_ACTION_ADD_INDEX_PATTERNS,
+ name: i18n.BULK_ACTION_ADD_INDEX_PATTERNS,
+ 'data-test-subj': 'addIndexPatternsBulkEditRule',
+ onClick: handleBulkEdit(BulkActionEditType.add_index_patterns),
+ disabled: isEditDisabled,
+ toolTipContent: missingActionPrivileges ? i18n.EDIT_RULE_SETTINGS_TOOLTIP : undefined,
+ toolTipPosition: 'right',
+ },
+ {
+ key: i18n.BULK_ACTION_DELETE_INDEX_PATTERNS,
+ name: i18n.BULK_ACTION_DELETE_INDEX_PATTERNS,
+ 'data-test-subj': 'deleteIndexPatternsBulkEditRule',
+ onClick: handleBulkEdit(BulkActionEditType.delete_index_patterns),
+ disabled: isEditDisabled,
+ toolTipContent: missingActionPrivileges ? i18n.EDIT_RULE_SETTINGS_TOOLTIP : undefined,
+ toolTipPosition: 'right',
+ },
+ ],
+ },
+ ];
+ },
+ [
+ rules,
+ selectedRuleIds,
+ hasActionsPrivileges,
+ isAllSelected,
+ loadingRuleIds,
+ hasMlPermissions,
+ dispatchToaster,
+ filterQuery,
+ setLoadingRules,
+ reFetchRules,
+ updateRules,
+ confirmDeletion,
+ isRulesBulkEditEnabled,
+ toasts,
+ filterOptions,
+ completeBulkEditForm,
+ confirmBulkEdit,
+ resolveTagsRefetch,
+ setIsRefreshOn,
+ getIsMounted,
+ queryClient,
+ ]
+ );
+};
diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/bulk_actions/use_bulk_edit_form_flyout.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/bulk_actions/use_bulk_edit_form_flyout.ts
new file mode 100644
index 0000000000000..1f3dad4d50aae
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/bulk_actions/use_bulk_edit_form_flyout.ts
@@ -0,0 +1,53 @@
+/*
+ * 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 { useState, useCallback, useRef } from 'react';
+import { useAsyncConfirmation } from '../../../../../containers/detection_engine/rules/rules_table/use_async_confirmation';
+
+import {
+ BulkActionEditType,
+ BulkActionEditPayload,
+} from '../../../../../../../common/detection_engine/schemas/common/schemas';
+import { useBoolState } from '../../../../../../common/hooks/use_bool_state';
+
+export const useBulkEditFormFlyout = () => {
+ const dataFormRef = useRef(null);
+ const [actionType, setActionType] = useState();
+ const [isBulkEditFlyoutVisible, showBulkEditFlyout, hideBulkEditFlyout] = useBoolState();
+
+ const [confirmForm, onConfirm, onCancel] = useAsyncConfirmation({
+ onInit: showBulkEditFlyout,
+ onFinish: hideBulkEditFlyout,
+ });
+
+ const completeBulkEditForm = useCallback(
+ async (editActionType: BulkActionEditType) => {
+ setActionType(editActionType);
+ if ((await confirmForm()) === true) {
+ return dataFormRef.current;
+ } else {
+ throw Error('Form is cancelled');
+ }
+ },
+ [confirmForm]
+ );
+
+ const handleBulkEditFormConfirm = useCallback(
+ (data: BulkActionEditPayload) => {
+ dataFormRef.current = data;
+ onConfirm();
+ },
+ [onConfirm]
+ );
+
+ return {
+ bulkEditActionType: actionType,
+ isBulkEditFlyoutVisible,
+ handleBulkEditFormConfirm,
+ handleBulkEditFormCancel: onCancel,
+ completeBulkEditForm,
+ };
+};
diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/bulk_actions/use_custom_rules_count.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/bulk_actions/use_custom_rules_count.ts
new file mode 100644
index 0000000000000..761d7a2a917ea
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/bulk_actions/use_custom_rules_count.ts
@@ -0,0 +1,50 @@
+/*
+ * 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 { useQuery, QueryClient } from 'react-query';
+
+import { fetchRules } from '../../../../../containers/detection_engine/rules/api';
+import type { FilterOptions } from '../../../../../containers/detection_engine/rules/types';
+
+const CUSTOM_RULES_COUNT_QUERY_KEY = 'customRulesCount';
+interface CustomRulesCountData {
+ customRulesCount: number;
+}
+
+export const getCustomRulesCountFromCache = (queryClient: QueryClient) =>
+ queryClient.getQueryData(CUSTOM_RULES_COUNT_QUERY_KEY)?.customRulesCount ??
+ 0;
+
+type UseCustomRulesCount = (arg: { filterOptions: FilterOptions; enabled: boolean }) => {
+ customRulesCount: number;
+ isCustomRulesCountLoading: boolean;
+};
+
+export const useCustomRulesCount: UseCustomRulesCount = ({ filterOptions, enabled }) => {
+ const { data, isFetching } = useQuery(
+ [CUSTOM_RULES_COUNT_QUERY_KEY],
+ async ({ signal }) => {
+ const res = await fetchRules({
+ pagination: { perPage: 1, page: 1 },
+ filterOptions: { ...filterOptions, showCustomRules: true },
+ signal,
+ });
+
+ return {
+ customRulesCount: res.total,
+ };
+ },
+ {
+ enabled,
+ }
+ );
+
+ return {
+ customRulesCount: data?.customRulesCount ?? 0,
+ isCustomRulesCountLoading: isFetching,
+ };
+};
diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/exceptions_table.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/exceptions_table.tsx
index de9da5c293fc1..f7b1127129b3b 100644
--- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/exceptions_table.tsx
+++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/exceptions_table.tsx
@@ -370,7 +370,7 @@ export const ExceptionListsTable = React.memo(() => {
) : (
<>
=> {
+export const getExportedRulesCounts = async (blob: Blob) => {
const details = await getExportedRulesDetails(blob);
- return details.exported_rules_count;
+ return {
+ exported: details.exported_rules_count,
+ missing: details.missing_rules_count,
+ total: details.exported_rules_count + details.missing_rules_count,
+ };
};
diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_table_filters/rules_table_filters.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_table_filters/rules_table_filters.test.tsx
index a84a60af51b39..e627ce3815e59 100644
--- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_table_filters/rules_table_filters.test.tsx
+++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_table_filters/rules_table_filters.test.tsx
@@ -31,6 +31,9 @@ describe('RulesTableFilters', () => {
rulesCustomInstalled={null}
rulesInstalled={null}
currentFilterTags={[]}
+ tags={[]}
+ isLoadingTags={false}
+ reFetchTags={() => ({})}
/>
);
@@ -51,6 +54,9 @@ describe('RulesTableFilters', () => {
rulesCustomInstalled={10}
rulesInstalled={9}
currentFilterTags={[]}
+ tags={[]}
+ isLoadingTags={false}
+ reFetchTags={() => ({})}
/>
);
diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_table_filters/rules_table_filters.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_table_filters/rules_table_filters.tsx
index a7e1f1b663521..5987cd75d303e 100644
--- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_table_filters/rules_table_filters.tsx
+++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_table_filters/rules_table_filters.tsx
@@ -19,7 +19,6 @@ import { isEqual } from 'lodash/fp';
import * as i18n from '../../translations';
import { FilterOptions } from '../../../../../containers/detection_engine/rules';
-import { useTags } from '../../../../../containers/detection_engine/rules/use_tags';
import { TagsFilterPopover } from './tags_filter_popover';
interface RulesTableFiltersProps {
@@ -27,6 +26,9 @@ interface RulesTableFiltersProps {
rulesCustomInstalled: number | null;
rulesInstalled: number | null;
currentFilterTags: string[];
+ tags: string[];
+ isLoadingTags: boolean;
+ reFetchTags: () => void;
}
/**
@@ -40,12 +42,14 @@ const RulesTableFiltersComponent = ({
rulesCustomInstalled,
rulesInstalled,
currentFilterTags,
+ tags,
+ isLoadingTags,
+ reFetchTags,
}: RulesTableFiltersProps) => {
const [filter, setFilter] = useState('');
const [selectedTags, setSelectedTags] = useState([]);
const [showCustomRules, setShowCustomRules] = useState(false);
const [showElasticRules, setShowElasticRules] = useState(false);
- const [isLoadingTags, tags, reFetchTags] = useTags();
useEffect(() => {
reFetchTags();
diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_tables.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_tables.tsx
index c21680242b818..b1595da0088ba 100644
--- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_tables.tsx
+++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_tables.tsx
@@ -5,6 +5,8 @@
* 2.0.
*/
+/* eslint-disable complexity */
+
import {
EuiBasicTable,
EuiConfirmModal,
@@ -13,6 +15,8 @@ import {
EuiProgress,
} from '@elastic/eui';
import React, { useCallback, useEffect, useMemo, useRef } from 'react';
+import { partition } from 'lodash/fp';
+
import { AllRulesTabs } from '.';
import { HeaderSection } from '../../../../../common/components/header_section';
import { Loader } from '../../../../../common/components/loader';
@@ -28,15 +32,20 @@ import {
} from '../../../../containers/detection_engine/rules';
import { useRulesTableContext } from '../../../../containers/detection_engine/rules/rules_table/rules_table_context';
import { useAsyncConfirmation } from '../../../../containers/detection_engine/rules/rules_table/use_async_confirmation';
-import { convertRulesFilterToKQL } from '../../../../containers/detection_engine/rules/utils';
import { getPrePackagedRuleStatus } from '../helpers';
import * as i18n from '../translations';
import { EuiBasicTableOnChange } from '../types';
-import { useBulkActions } from './use_bulk_actions';
import { useMonitoringColumns, useRulesColumns } from './use_columns';
import { showRulesTable } from './helpers';
import { RulesTableFilters } from './rules_table_filters/rules_table_filters';
import { AllRulesUtilityBar } from './utility_bar';
+import { RULES_TABLE_PAGE_SIZE_OPTIONS } from '../../../../../../common/constants';
+import { useTags } from '../../../../containers/detection_engine/rules/use_tags';
+import { useCustomRulesCount } from './bulk_actions/use_custom_rules_count';
+import { useBulkEditFormFlyout } from './bulk_actions/use_bulk_edit_form_flyout';
+import { BulkEditConfirmation } from './bulk_actions/bulk_edit_confirmation';
+import { BulkEditFlyout } from './bulk_actions/bulk_edit_flyout';
+import { useBulkActions } from './bulk_actions/use_bulk_actions';
const INITIAL_SORT_FIELD = 'enabled';
@@ -95,6 +104,7 @@ export const RulesTables = React.memo(
isRefreshOn,
lastUpdated,
loadingRuleIds,
+ loadingRulesAction,
pagination,
selectedRuleIds,
sortingOptions,
@@ -117,6 +127,8 @@ export const RulesTables = React.memo(
rulesNotUpdated
);
+ const [isLoadingTags, tags, reFetchTags] = useTags();
+
const [isDeleteConfirmationVisible, showDeleteConfirmation, hideDeleteConfirmation] =
useBoolState();
@@ -125,13 +137,42 @@ export const RulesTables = React.memo(
onFinish: hideDeleteConfirmation,
});
+ const [isBulkEditConfirmationVisible, showBulkEditonfirmation, hideBulkEditConfirmation] =
+ useBoolState();
+
+ const [confirmBulkEdit, handleBulkEditConfirm, handleBulkEditCancel] = useAsyncConfirmation({
+ onInit: showBulkEditonfirmation,
+ onFinish: hideBulkEditConfirmation,
+ });
+
+ const { customRulesCount, isCustomRulesCountLoading } = useCustomRulesCount({
+ enabled: isBulkEditConfirmationVisible && isAllSelected,
+ filterOptions,
+ });
+
+ const {
+ bulkEditActionType,
+ isBulkEditFlyoutVisible,
+ handleBulkEditFormConfirm,
+ handleBulkEditFormCancel,
+ completeBulkEditForm,
+ } = useBulkEditFormFlyout();
+
const selectedItemsCount = isAllSelected ? pagination.total : selectedRuleIds.length;
const hasPagination = pagination.total > pagination.perPage;
- const getBatchItemsPopoverContent = useBulkActions({
- filterQuery: convertRulesFilterToKQL(filterOptions),
+ const [selectedElasticRuleIds, selectedCustomRuleIds] = useMemo(() => {
+ const ruleImmutablityMap = new Map(rules.map((rule) => [rule.id, rule.immutable]));
+ const predicate = (id: string) => ruleImmutablityMap.get(id);
+ return partition(predicate, selectedRuleIds);
+ }, [rules, selectedRuleIds]);
+
+ const getBulkItemsPopoverContent = useBulkActions({
+ filterOptions,
confirmDeletion,
- selectedItemsCount,
+ confirmBulkEdit,
+ completeBulkEditForm,
+ reFetchTags,
});
const paginationMemo = useMemo(
@@ -139,7 +180,7 @@ export const RulesTables = React.memo(
pageIndex: pagination.page - 1,
pageSize: pagination.perPage,
totalItemCount: pagination.total,
- pageSizeOptions: [5, 10, 20, 50, 100],
+ pageSizeOptions: RULES_TABLE_PAGE_SIZE_OPTIONS,
}),
[pagination]
);
@@ -281,6 +322,9 @@ export const RulesTables = React.memo(
rulesCustomInstalled={rulesCustomInstalled}
rulesInstalled={rulesInstalled}
currentFilterTags={filterOptions.tags}
+ isLoadingTags={isLoadingTags}
+ tags={tags}
+ reFetchTags={reFetchTags}
/>
)}
@@ -308,6 +352,27 @@ export const RulesTables = React.memo(
{i18n.DELETE_CONFIRMATION_BODY}
)}
+ {isBulkEditConfirmationVisible && !isCustomRulesCountLoading && (
+
+ )}
+ {isBulkEditFlyoutVisible && bulkEditActionType !== undefined && (
+
+ )}
{shouldShowRulesTable && (
<>
(
hasPagination={hasPagination}
paginationTotal={pagination.total ?? 0}
numberSelectedItems={selectedItemsCount}
- onGetBatchItemsPopoverContent={getBatchItemsPopoverContent}
+ onGetBulkItemsPopoverContent={getBulkItemsPopoverContent}
onRefresh={reFetchRules}
isAutoRefreshOn={isRefreshOn}
onRefreshSwitch={handleAutoRefreshSwitch}
isAllSelected={isAllSelected}
onToggleSelectAll={toggleSelectAll}
- showBulkActions
+ isBulkActionInProgress={isCustomRulesCountLoading || loadingRulesAction != null}
+ hasDisabledActions={loadingRulesAction != null}
+ hasBulkActions
/>
Promise;
- selectedItemsCount: number;
-}
-
-export const useBulkActions = ({
- filterQuery,
- confirmDeletion,
- selectedItemsCount,
-}: UseBulkActionsArgs) => {
- const hasMlPermissions = useHasMlPermissions();
- const rulesTableContext = useRulesTableContext();
- const [, dispatchToaster] = useStateToaster();
- const hasActionsPrivileges = useHasActionsPrivileges();
-
- const {
- state: { isAllSelected, rules, loadingRuleIds, selectedRuleIds },
- actions: { reFetchRules, setLoadingRules, updateRules },
- } = rulesTableContext;
-
- return useCallback(
- (closePopover: () => void): JSX.Element[] => {
- const selectedRules = rules.filter(({ id }) => selectedRuleIds.includes(id));
-
- const containsEnabled = selectedRules.some(({ enabled }) => enabled);
- const containsDisabled = selectedRules.some(({ enabled }) => !enabled);
- const containsLoading = selectedRuleIds.some((id) => loadingRuleIds.includes(id));
- const containsImmutable = selectedRules.some(({ immutable }) => immutable);
-
- const missingActionPrivileges =
- !hasActionsPrivileges &&
- selectedRules.some((rule) => !canEditRuleWithActions(rule, hasActionsPrivileges));
-
- const handleActivateAction = async () => {
- closePopover();
- const deactivatedRules = selectedRules.filter(({ enabled }) => !enabled);
- const deactivatedRulesNoML = deactivatedRules.filter(({ type }) => !isMlRule(type));
-
- const mlRuleCount = deactivatedRules.length - deactivatedRulesNoML.length;
- if (!hasMlPermissions && mlRuleCount > 0) {
- displayWarningToast(detectionI18n.ML_RULES_UNAVAILABLE(mlRuleCount), dispatchToaster);
- }
-
- const ruleIds = hasMlPermissions
- ? deactivatedRules.map(({ id }) => id)
- : deactivatedRulesNoML.map(({ id }) => id);
-
- if (isAllSelected) {
- await rulesBulkActionByQuery(
- ruleIds,
- selectedItemsCount,
- filterQuery,
- BulkAction.enable,
- dispatchToaster,
- setLoadingRules
- );
- await reFetchRules();
- } else {
- await enableRulesAction(ruleIds, true, dispatchToaster, setLoadingRules, updateRules);
- }
- };
-
- const handleDeactivateActions = async () => {
- closePopover();
- const activatedIds = selectedRules.filter(({ enabled }) => enabled).map(({ id }) => id);
- if (isAllSelected) {
- await rulesBulkActionByQuery(
- activatedIds,
- selectedItemsCount,
- filterQuery,
- BulkAction.disable,
- dispatchToaster,
- setLoadingRules
- );
- await reFetchRules();
- } else {
- await enableRulesAction(
- activatedIds,
- false,
- dispatchToaster,
- setLoadingRules,
- updateRules
- );
- }
- };
-
- const handleDuplicateAction = async () => {
- closePopover();
- if (isAllSelected) {
- await rulesBulkActionByQuery(
- selectedRuleIds,
- selectedItemsCount,
- filterQuery,
- BulkAction.duplicate,
- dispatchToaster,
- setLoadingRules
- );
- await reFetchRules();
- } else {
- await duplicateRulesAction(
- selectedRules,
- selectedRuleIds,
- dispatchToaster,
- setLoadingRules
- );
- }
- await reFetchRules();
- };
-
- const handleDeleteAction = async () => {
- closePopover();
- if (isAllSelected) {
- if ((await confirmDeletion()) === false) {
- // User has cancelled deletion
- return;
- }
-
- await rulesBulkActionByQuery(
- selectedRuleIds,
- selectedItemsCount,
- filterQuery,
- BulkAction.delete,
- dispatchToaster,
- setLoadingRules
- );
- } else {
- await deleteRulesAction(selectedRuleIds, dispatchToaster, setLoadingRules);
- }
- await reFetchRules();
- };
-
- const handleExportAction = async () => {
- closePopover();
- if (isAllSelected) {
- await rulesBulkActionByQuery(
- selectedRuleIds,
- selectedItemsCount,
- filterQuery,
- BulkAction.export,
- dispatchToaster,
- setLoadingRules
- );
- } else {
- await exportRulesAction(
- selectedRules.map((r) => r.rule_id),
- dispatchToaster,
- setLoadingRules
- );
- }
- };
-
- return [
-
-
- <>{i18n.BATCH_ACTION_ACTIVATE_SELECTED}>
-
- ,
-
-
- <>{i18n.BATCH_ACTION_DEACTIVATE_SELECTED}>
-
- ,
-
- {i18n.BATCH_ACTION_EXPORT_SELECTED}
- ,
-
-
-
- <>{i18n.BATCH_ACTION_DUPLICATE_SELECTED}>
-
- ,
-
- {i18n.BATCH_ACTION_DELETE_SELECTED}
- ,
- ];
- },
- [
- rules,
- selectedRuleIds,
- hasActionsPrivileges,
- isAllSelected,
- loadingRuleIds,
- hasMlPermissions,
- dispatchToaster,
- selectedItemsCount,
- filterQuery,
- setLoadingRules,
- reFetchRules,
- updateRules,
- confirmDeletion,
- ]
- );
-};
diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/use_columns.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/use_columns.tsx
index fa3e02c183516..d7477a792a115 100644
--- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/use_columns.tsx
+++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/use_columns.tsx
@@ -58,15 +58,14 @@ const useEnabledColumn = ({ hasPermissions }: ColumnsProps): TableColumn => {
const { loadingRulesAction, loadingRuleIds } = useRulesTableContext().state;
const loadingIds = useMemo(
- () =>
- loadingRulesAction === 'enable' || loadingRulesAction === 'disable' ? loadingRuleIds : [],
+ () => (['disable', 'enable', 'edit'].includes(loadingRulesAction ?? '') ? loadingRuleIds : []),
[loadingRuleIds, loadingRulesAction]
);
return useMemo(
() => ({
field: 'enabled',
- name: i18n.COLUMN_ACTIVATE,
+ name: i18n.COLUMN_ENABLE,
render: (_, rule: Rule) => (
{
onRefresh={jest.fn()}
paginationTotal={4}
numberSelectedItems={1}
- onGetBatchItemsPopoverContent={jest.fn()}
+ onGetBulkItemsPopoverContent={jest.fn()}
isAutoRefreshOn={true}
onRefreshSwitch={jest.fn()}
- showBulkActions
+ hasBulkActions
/>
);
@@ -40,7 +40,7 @@ describe('AllRules', () => {
);
});
- it('does not render total selected and bulk actions when "showBulkActions" is false', () => {
+ it('does not render total selected and bulk actions when "hasBulkActions" is false', () => {
const wrapper = mount(
{
onRefresh={jest.fn()}
paginationTotal={4}
numberSelectedItems={1}
- onGetBatchItemsPopoverContent={jest.fn()}
+ onGetBulkItemsPopoverContent={jest.fn()}
isAutoRefreshOn={true}
onRefreshSwitch={jest.fn()}
- showBulkActions={false}
+ hasBulkActions={false}
/>
);
@@ -71,10 +71,10 @@ describe('AllRules', () => {
onRefresh={jest.fn()}
paginationTotal={4}
numberSelectedItems={1}
- onGetBatchItemsPopoverContent={jest.fn()}
+ onGetBulkItemsPopoverContent={jest.fn()}
isAutoRefreshOn={true}
onRefreshSwitch={jest.fn()}
- showBulkActions
+ hasBulkActions
/>
);
@@ -90,10 +90,10 @@ describe('AllRules', () => {
onRefresh={jest.fn()}
paginationTotal={4}
numberSelectedItems={1}
- onGetBatchItemsPopoverContent={jest.fn()}
+ onGetBulkItemsPopoverContent={jest.fn()}
isAutoRefreshOn={true}
onRefreshSwitch={jest.fn()}
- showBulkActions
+ hasBulkActions
/>
);
@@ -110,10 +110,10 @@ describe('AllRules', () => {
onRefresh={mockRefresh}
paginationTotal={4}
numberSelectedItems={1}
- onGetBatchItemsPopoverContent={jest.fn()}
+ onGetBulkItemsPopoverContent={jest.fn()}
isAutoRefreshOn={true}
onRefreshSwitch={jest.fn()}
- showBulkActions
+ hasBulkActions
/>
);
@@ -132,10 +132,10 @@ describe('AllRules', () => {
onRefresh={jest.fn()}
paginationTotal={4}
numberSelectedItems={1}
- onGetBatchItemsPopoverContent={jest.fn()}
+ onGetBulkItemsPopoverContent={jest.fn()}
isAutoRefreshOn={true}
onRefreshSwitch={mockSwitch}
- showBulkActions
+ hasBulkActions
/>
);
diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/utility_bar.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/utility_bar.tsx
index 2e53091dc97df..7276415b852b2 100644
--- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/utility_bar.tsx
+++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/utility_bar.tsx
@@ -5,7 +5,13 @@
* 2.0.
*/
-import { EuiContextMenuPanel, EuiSwitch, EuiSwitchEvent } from '@elastic/eui';
+import {
+ EuiContextMenu,
+ EuiContextMenuPanel,
+ EuiSwitch,
+ EuiSwitchEvent,
+ EuiContextMenuPanelDescriptor,
+} from '@elastic/eui';
import React, { useCallback } from 'react';
import {
@@ -22,13 +28,15 @@ interface AllRulesUtilityBarProps {
isAllSelected?: boolean;
isAutoRefreshOn?: boolean;
numberSelectedItems: number;
- onGetBatchItemsPopoverContent?: (closePopover: () => void) => JSX.Element[];
+ onGetBulkItemsPopoverContent?: (closePopover: () => void) => EuiContextMenuPanelDescriptor[];
onRefresh?: () => void;
onRefreshSwitch?: (checked: boolean) => void;
onToggleSelectAll?: () => void;
paginationTotal: number;
- showBulkActions: boolean;
+ hasBulkActions: boolean;
hasPagination?: boolean;
+ isBulkActionInProgress?: boolean;
+ hasDisabledActions?: boolean;
}
export const AllRulesUtilityBar = React.memo(
@@ -37,23 +45,30 @@ export const AllRulesUtilityBar = React.memo(
isAllSelected,
isAutoRefreshOn,
numberSelectedItems,
- onGetBatchItemsPopoverContent,
+ onGetBulkItemsPopoverContent,
onRefresh,
onRefreshSwitch,
onToggleSelectAll,
paginationTotal,
- showBulkActions = true,
+ hasBulkActions = true,
hasPagination,
+ isBulkActionInProgress,
+ hasDisabledActions,
}) => {
- const handleGetBatchItemsPopoverContent = useCallback(
+ const handleGetBuIktemsPopoverContent = useCallback(
(closePopover: () => void): JSX.Element | null => {
- if (onGetBatchItemsPopoverContent != null) {
- return ;
+ if (onGetBulkItemsPopoverContent != null) {
+ return (
+
+ );
} else {
return null;
}
},
- [onGetBatchItemsPopoverContent]
+ [onGetBulkItemsPopoverContent]
);
const handleAutoRefreshSwitch = useCallback(
@@ -88,7 +103,7 @@ export const AllRulesUtilityBar = React.memo(
- {showBulkActions ? (
+ {hasBulkActions ? (
{i18n.SHOWING_RULES(paginationTotal)}
@@ -99,7 +114,7 @@ export const AllRulesUtilityBar = React.memo(
)}
- {showBulkActions ? (
+ {hasBulkActions ? (
<>
@@ -108,6 +123,7 @@ export const AllRulesUtilityBar = React.memo(
{canBulkEdit && onToggleSelectAll && hasPagination && (
(
{canBulkEdit && (
{i18n.BATCH_ACTIONS}
)}
(
{i18n.REFRESH}
+export const BULK_ACTION_DISABLE = i18n.translate(
+ 'xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.disableTitle',
+ {
+ defaultMessage: 'Disable',
+ }
+);
+
+export const BULK_ACTION_EXPORT = i18n.translate(
+ 'xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.exportTitle',
+ {
+ defaultMessage: 'Export',
+ }
+);
+
+export const BULK_ACTION_DUPLICATE = i18n.translate(
+ 'xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.duplicateTitle',
+ {
+ defaultMessage: 'Duplicate',
+ }
+);
+
+export const BULK_ACTION_DELETE = i18n.translate(
+ 'xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.deleteTitle',
+ {
+ defaultMessage: 'Delete',
+ }
+);
+
+export const BULK_ACTION_INDEX_PATTERNS = i18n.translate(
+ 'xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.indexPatternsTitle',
+ {
+ defaultMessage: 'Index patterns',
+ }
+);
+
+export const BULK_ACTION_TAGS = i18n.translate(
+ 'xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.tagsTitle',
+ {
+ defaultMessage: 'Tags',
+ }
+);
+
+export const BULK_ACTION_ADD_INDEX_PATTERNS = i18n.translate(
+ 'xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.addIndexPatternsTitle',
+ {
+ defaultMessage: 'Add index patterns',
+ }
+);
+
+export const BULK_ACTION_DELETE_INDEX_PATTERNS = i18n.translate(
+ 'xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.deleteIndexPatternsTitle',
+ {
+ defaultMessage: 'Delete index patterns',
+ }
+);
+
+export const BULK_ACTION_ADD_TAGS = i18n.translate(
+ 'xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.addTagsTitle',
+ {
+ defaultMessage: 'Add tags',
+ }
+);
+
+export const BULK_ACTION_DELETE_TAGS = i18n.translate(
+ 'xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.deleteTagsTitle',
+ {
+ defaultMessage: 'Delete tags',
+ }
+);
+
+export const BULK_ACTION_MENU_TITLE = i18n.translate(
+ 'xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.contextMenuTitle',
+ {
+ defaultMessage: 'Options',
+ }
+);
+
+export const BULK_EDIT_SUCCESS_TOAST_TITLE = i18n.translate(
+ 'xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.bulkEditSuccessToastTitle',
+ {
+ defaultMessage: 'Rules changes updated',
+ }
+);
+
+export const BULK_EDIT_SUCCESS_TOAST_DESCRIPTION = (rulesCount: number) =>
i18n.translate(
- 'xpack.securitySolution.detectionEngine.rules.allRules.batchActions.activateSelectedErrorTitle',
+ 'xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.bulkEditSuccessToastDescription',
{
- values: { totalRules },
- defaultMessage: 'Error activating {totalRules, plural, =1 {rule} other {rules}}',
+ values: { rulesCount },
+ defaultMessage:
+ 'You’ve successfully updated {rulesCount, plural, =1 {# rule} other {# rules}}.',
}
);
-export const BATCH_ACTION_DEACTIVATE_SELECTED = i18n.translate(
- 'xpack.securitySolution.detectionEngine.rules.allRules.batchActions.deactivateSelectedTitle',
+export const BULK_EDIT_WARNING_TOAST_TITLE = i18n.translate(
+ 'xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.bulkEditWarningToastTitle',
{
- defaultMessage: 'Deactivate selected',
+ defaultMessage: 'Rules updates are in progress',
}
);
-export const BATCH_ACTION_DEACTIVATE_SELECTED_ERROR = (totalRules: number) =>
+export const BULK_EDIT_WARNING_TOAST_DESCRIPTION = (rulesCount: number) =>
i18n.translate(
- 'xpack.securitySolution.detectionEngine.rules.allRules.batchActions.deactivateSelectedErrorTitle',
+ 'xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.bulkEditWarningToastDescription',
{
- values: { totalRules },
- defaultMessage: 'Error deactivating {totalRules, plural, =1 {rule} other {rules}}',
+ values: { rulesCount },
+ defaultMessage: '{rulesCount, plural, =1 {# rule is} other {# rules are}} updating.',
}
);
-export const BATCH_ACTION_EXPORT_SELECTED = i18n.translate(
- 'xpack.securitySolution.detectionEngine.rules.allRules.batchActions.exportSelectedTitle',
+export const BULK_EDIT_WARNING_TOAST_NOTIFY = i18n.translate(
+ 'xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.bulkEditWarningToastNotifyButtonLabel',
+ {
+ defaultMessage: `Notify me when done`,
+ }
+);
+
+export const BULK_EDIT_ERROR_TOAST_TITLE = i18n.translate(
+ 'xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.bulkEditErrorToastTitle',
+ {
+ defaultMessage: 'Rule updates failed',
+ }
+);
+
+export const BULK_EDIT_ERROR_TOAST_DESCIRPTION = (rulesCount: number) =>
+ i18n.translate(
+ 'xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.bulkEditErrorToastDescription',
+ {
+ values: { rulesCount },
+ defaultMessage: '{rulesCount, plural, =1 {# rule is} other {# rules are}} failed to update.',
+ }
+ );
+
+export const BULK_EDIT_CONFIRMATION_TITLE = (elasticRulesCount: number) =>
+ i18n.translate(
+ 'xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.bulkEditConfirmationTitle',
+ {
+ values: { elasticRulesCount },
+ defaultMessage:
+ '{elasticRulesCount, plural, =1 {# Elastic rule} other {# Elastic rules}} cannot be edited',
+ }
+ );
+
+export const BULK_EDIT_CONFIRMATION_CANCEL = i18n.translate(
+ 'xpack.securitySolution.detectionEngine.components.allRules.bulkActions.bulkEditConfirmationCancelButtonLabel',
+ {
+ defaultMessage: 'Cancel',
+ }
+);
+
+export const BULK_EDIT_CONFIRMATION_CONFIRM = i18n.translate(
+ 'xpack.securitySolution.detectionEngine.components.allRules.bulkActions.bulkEditConfirmation.confirmButtonLabel',
+ {
+ defaultMessage: 'Edit custom rules',
+ }
+);
+
+export const BULK_EDIT_FLYOUT_FORM_SAVE = i18n.translate(
+ 'xpack.securitySolution.detectionEngine.components.allRules.bulkActions.bulkEditFlyoutForm.saveButtonLabel',
+ {
+ defaultMessage: 'Save',
+ }
+);
+
+export const BULK_EDIT_FLYOUT_FORM_CLOSE = i18n.translate(
+ 'xpack.securitySolution.detectionEngine.components.allRules.bulkActions.bulkEditFlyoutForm.closeButtonLabel',
+ {
+ defaultMessage: 'Close',
+ }
+);
+
+export const BULK_EDIT_FLYOUT_FORM_ADD_INDEX_PATTERNS_HELP_TEXT = i18n.translate(
+ 'xpack.securitySolution.detectionEngine.components.allRules.bulkActions.bulkEditFlyoutForm.addIndexPatternsComboboxHelpText',
+ {
+ defaultMessage:
+ 'Select default index patterns of Elasticsearch indices from the dropdown. You can add custom index patterns and hit Enter to begin a new one.',
+ }
+);
+
+export const BULK_EDIT_FLYOUT_FORM_DELETE_INDEX_PATTERNS_HELP_TEXT = i18n.translate(
+ 'xpack.securitySolution.detectionEngine.components.allRules.bulkActions.bulkEditFlyoutForm.deleteIndexPatternsComboboxHelpText',
+ {
+ defaultMessage:
+ 'Delete default index patterns of Elasticsearch indices from the dropdown. You can add custom index patterns and hit Enter to begin a new one.',
+ }
+);
+
+export const BULK_EDIT_FLYOUT_FORM_ADD_INDEX_PATTERNS_LABEL = i18n.translate(
+ 'xpack.securitySolution.detectionEngine.components.allRules.bulkActions.bulkEditFlyoutForm.addIndexPatternsComboboxLabel',
{
- defaultMessage: 'Export selected',
+ defaultMessage: 'Add index patterns for selected rules',
}
);
-export const BATCH_ACTION_DUPLICATE_SELECTED = i18n.translate(
- 'xpack.securitySolution.detectionEngine.rules.allRules.batchActions.duplicateSelectedTitle',
+export const BULK_EDIT_FLYOUT_FORM_ADD_INDEX_PATTERNS_REQUIRED_ERROR = i18n.translate(
+ 'xpack.securitySolution.detectionEngine.components.allRules.bulkActions.bulkEditFlyoutForm.indexPatternsRequiredErrorMessage',
{
- defaultMessage: 'Duplicate selected',
+ defaultMessage: 'A minimum of one index pattern is required.',
}
);
-export const BATCH_ACTION_DELETE_SELECTED = i18n.translate(
- 'xpack.securitySolution.detectionEngine.rules.allRules.batchActions.deleteSelectedTitle',
+export const BULK_EDIT_FLYOUT_FORM_ADD_INDEX_PATTERNS_TITLE = i18n.translate(
+ 'xpack.securitySolution.detectionEngine.components.allRules.bulkActions.bulkEditFlyoutForm.addIndexPatternsTitle',
{
- defaultMessage: 'Delete selected',
+ defaultMessage: 'Add index patterns',
}
);
+export const BULK_EDIT_FLYOUT_FORM_ADD_INDEX_PATTERNS_OVERWRITE_LABEL = i18n.translate(
+ 'xpack.securitySolution.detectionEngine.components.allRules.bulkActions.bulkEditFlyoutForm.addIndexPatternsOverwriteCheckboxLabel',
+ {
+ defaultMessage: 'Overwrite all selected rules index patterns',
+ }
+);
+
+export const BULK_EDIT_FLYOUT_FORM_DELETE_INDEX_PATTERNS_LABEL = i18n.translate(
+ 'xpack.securitySolution.detectionEngine.components.allRules.bulkActions.bulkEditFlyoutForm.deleteIndexPatternsComboboxLabel',
+ {
+ defaultMessage: 'Delete index patterns for selected rules',
+ }
+);
+
+export const BULK_EDIT_FLYOUT_FORM_DELETE_INDEX_PATTERNS_TITLE = i18n.translate(
+ 'xpack.securitySolution.detectionEngine.components.allRules.bulkActions.bulkEditFlyoutForm.deleteIndexPatternsTitle',
+ {
+ defaultMessage: 'Delete index patterns',
+ }
+);
+
+export const BULK_EDIT_FLYOUT_FORM_ADD_TAGS_HELP_TEXT = i18n.translate(
+ 'xpack.securitySolution.detectionEngine.components.allRules.bulkActions.bulkEditFlyoutForm.addTagsComboboxHelpText',
+ {
+ defaultMessage:
+ 'Add one or more custom identifying tags for selected rules. Press enter after each tag to begin a new one.',
+ }
+);
+
+export const BULK_EDIT_FLYOUT_FORM_DELETE_TAGS_HELP_TEXT = i18n.translate(
+ 'xpack.securitySolution.detectionEngine.components.allRules.bulkActions.bulkEditFlyoutForm.deleteTagsComboboxHelpText',
+ {
+ defaultMessage:
+ 'Delete one or more custom identifying tags for selected rules. Press enter after each tag to begin a new one.',
+ }
+);
+
+export const BULK_EDIT_FLYOUT_FORM_ADD_TAGS_LABEL = i18n.translate(
+ 'xpack.securitySolution.detectionEngine.components.allRules.bulkActions.bulkEditFlyoutForm.addTagsComboboxLabel',
+ {
+ defaultMessage: 'Add tags for selected rules',
+ }
+);
+
+export const BULK_EDIT_FLYOUT_FORM_TAGS_REQUIRED_ERROR = i18n.translate(
+ 'xpack.securitySolution.detectionEngine.components.allRules.bulkActions.bulkEditFlyoutForm.tagsComoboxRequiredErrorMessage',
+ {
+ defaultMessage: 'A minimum of one tag is required.',
+ }
+);
+
+export const BULK_EDIT_FLYOUT_FORM_ADD_TAGS_TITLE = i18n.translate(
+ 'xpack.securitySolution.detectionEngine.components.allRules.bulkActions.bulkEditFlyoutForm.addTagsTitle',
+ {
+ defaultMessage: 'Add tags',
+ }
+);
+
+export const BULK_EDIT_FLYOUT_FORM_ADD_TAGS_OVERWRITE_LABEL = i18n.translate(
+ 'xpack.securitySolution.detectionEngine.components.allRules.bulkActions.bulkEditFlyoutForm.addTagsOverwriteCheckboxLabel',
+ {
+ defaultMessage: 'Overwrite all selected rules tags',
+ }
+);
+
+export const BULK_EDIT_FLYOUT_FORM_DELETE_TAGS_LABEL = i18n.translate(
+ 'xpack.securitySolution.detectionEngine.components.allRules.bulkActions.bulkEditFlyoutForm.deleteTagsComboboxLabel',
+ {
+ defaultMessage: 'Delete tags for selected rules',
+ }
+);
+
+export const BULK_EDIT_FLYOUT_FORM_DELETE_TAGS_TITLE = i18n.translate(
+ 'xpack.securitySolution.detectionEngine.components.allRules.bulkActions.bulkEditFlyoutForm.deleteTagsTitle',
+ {
+ defaultMessage: 'Delete tags',
+ }
+);
+
+export const BATCH_ACTION_ACTIVATE_SELECTED_ERROR = (totalRules: number) =>
+ i18n.translate(
+ 'xpack.securitySolution.detectionEngine.rules.allRules.batchActions.activateSelectedErrorTitle',
+ {
+ values: { totalRules },
+ defaultMessage: 'Error enabling {totalRules, plural, =1 {rule} other {rules}}',
+ }
+ );
+
+export const BATCH_ACTION_DEACTIVATE_SELECTED_ERROR = (totalRules: number) =>
+ i18n.translate(
+ 'xpack.securitySolution.detectionEngine.rules.allRules.batchActions.deactivateSelectedErrorTitle',
+ {
+ values: { totalRules },
+ defaultMessage: 'Error disabling {totalRules, plural, =1 {rule} other {rules}}',
+ }
+ );
+
export const BATCH_ACTION_DELETE_SELECTED_IMMUTABLE = i18n.translate(
'xpack.securitySolution.detectionEngine.rules.allRules.batchActions.deleteSelectedImmutableTitle',
{
@@ -304,7 +563,7 @@ export const DUPLICATE_RULE_ERROR = i18n.translate(
export const BULK_ACTION_FAILED = i18n.translate(
'xpack.securitySolution.detectionEngine.rules.allRules.actions.bulkActionFailedDescription',
{
- defaultMessage: 'Failed to execte bulk action',
+ defaultMessage: 'Failed to execute bulk action',
}
);
@@ -385,10 +644,10 @@ export const COLUMN_SEE_ALL_POPOVER = i18n.translate(
}
);
-export const COLUMN_ACTIVATE = i18n.translate(
- 'xpack.securitySolution.detectionEngine.rules.allRules.columns.activateTitle',
+export const COLUMN_ENABLE = i18n.translate(
+ 'xpack.securitySolution.detectionEngine.rules.allRules.columns.enabledTitle',
{
- defaultMessage: 'Activated',
+ defaultMessage: 'Enabled',
}
);
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/perform_bulk_action_route.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/perform_bulk_action_route.test.ts
index 4019e519e9db4..0d968eb402717 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/perform_bulk_action_route.test.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/perform_bulk_action_route.test.ts
@@ -20,15 +20,18 @@ import { performBulkActionRoute } from './perform_bulk_action_route';
import { getPerformBulkActionSchemaMock } from '../../../../../common/detection_engine/schemas/request/perform_bulk_action_schema.mock';
import { loggingSystemMock } from 'src/core/server/mocks';
import { isElasticRule } from '../../../../usage/detections';
+import { readRules } from '../../rules/read_rules';
jest.mock('../../../machine_learning/authz', () => mockMlAuthzFactory.create());
jest.mock('../../../../usage/detections', () => ({ isElasticRule: jest.fn() }));
+jest.mock('../../rules/read_rules', () => ({ readRules: jest.fn() }));
describe.each([
['Legacy', false],
['RAC', true],
])('perform_bulk_action - %s', (_, isRuleRegistryEnabled) => {
const isElasticRuleMock = isElasticRule as jest.Mock;
+ const readRulesMock = readRules as jest.Mock;
let server: ReturnType;
let { clients, context } = requestContextMock.createTools();
let ml: ReturnType;
@@ -249,6 +252,48 @@ describe.each([
expect(response.status).toEqual(500);
expect(response.body.attributes.errors[0].message.length).toEqual(1000);
});
+
+ it('returns partial failure error if one if rules from ids params can`t be fetched', async () => {
+ readRulesMock
+ .mockImplementationOnce(() => Promise.resolve(mockRule))
+ .mockImplementationOnce(() => Promise.resolve(null));
+
+ const request = requestMock.create({
+ method: 'patch',
+ path: DETECTION_ENGINE_RULES_BULK_ACTION,
+ body: {
+ ...getPerformBulkActionSchemaMock(),
+ ids: [mockRule.id, 'failed-mock-id'],
+ query: undefined,
+ },
+ });
+
+ const response = await server.inject(request, context);
+
+ expect(response.status).toEqual(500);
+ expect(response.body).toEqual({
+ attributes: {
+ rules: {
+ failed: 1,
+ succeeded: 1,
+ total: 2,
+ },
+ errors: [
+ {
+ message: 'Can`t fetch a rule',
+ status_code: 500,
+ rules: [
+ {
+ id: 'failed-mock-id',
+ },
+ ],
+ },
+ ],
+ },
+ message: 'Bulk edit partially failed',
+ status_code: 500,
+ });
+ });
});
describe('request validation', () => {
@@ -297,6 +342,52 @@ describe.each([
expect(result.ok).toHaveBeenCalled();
});
+
+ it('rejects payloads with incorrect typing for ids', async () => {
+ const request = requestMock.create({
+ method: 'patch',
+ path: DETECTION_ENGINE_RULES_BULK_ACTION,
+ body: { ...getPerformBulkActionSchemaMock(), ids: 'test fake' },
+ });
+ const result = server.validate(request);
+ expect(result.badRequest).toHaveBeenCalledWith('Invalid value "test fake" supplied to "ids"');
+ });
+
+ it('rejects payload if there is more than 100 ids in payload', async () => {
+ const request = requestMock.create({
+ method: 'patch',
+ path: DETECTION_ENGINE_RULES_BULK_ACTION,
+ body: {
+ ...getPerformBulkActionSchemaMock(),
+ query: undefined,
+ ids: Array.from({ length: 101 }).map(() => 'fake-id'),
+ },
+ });
+
+ const response = await server.inject(request, context);
+
+ expect(response.status).toEqual(400);
+ expect(response.body.message).toEqual('More than 100 ids sent for bulk edit action.');
+ });
+
+ it('rejects payload if both query and ids defined', async () => {
+ const request = requestMock.create({
+ method: 'patch',
+ path: DETECTION_ENGINE_RULES_BULK_ACTION,
+ body: {
+ ...getPerformBulkActionSchemaMock(),
+ query: '',
+ ids: ['fake-id'],
+ },
+ });
+
+ const response = await server.inject(request, context);
+
+ expect(response.status).toEqual(400);
+ expect(response.body.message).toEqual(
+ 'Both query and ids are sent. Define either ids or query in request payload.'
+ );
+ });
});
it('should process large number of rules, larger than configured concurrency', async () => {
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/perform_bulk_action_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/perform_bulk_action_route.ts
index 6e9d9d0e02d52..d4866082148a2 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/perform_bulk_action_route.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/perform_bulk_action_route.ts
@@ -11,9 +11,12 @@ import { Logger } from 'src/core/server';
import { RuleAlertType as Rule } from '../../rules/types';
+import type { RulesClient } from '../../../../../../alerting/server';
+
import {
DETECTION_ENGINE_RULES_BULK_ACTION,
MAX_RULES_TO_UPDATE_IN_PARALLEL,
+ RULES_TABLE_MAX_PAGE_SIZE,
} from '../../../../../common/constants';
import { BulkAction } from '../../../../../common/detection_engine/schemas/common/schemas';
import { performBulkActionSchema } from '../../../../../common/detection_engine/schemas/request/perform_bulk_action_schema';
@@ -29,6 +32,7 @@ import { deleteRules } from '../../rules/delete_rules';
import { duplicateRule } from '../../rules/duplicate_rule';
import { enableRule } from '../../rules/enable_rule';
import { findRules } from '../../rules/find_rules';
+import { readRules } from '../../rules/read_rules';
import { patchRules } from '../../rules/patch_rules';
import { appplyBulkActionEditToRule } from '../../rules/bulk_action_edit';
import { getExportByObjectIds } from '../../rules/get_export_by_object_ids';
@@ -129,6 +133,87 @@ const executeBulkAction = async (rules: Rule[], action: RuleActionFn, abortSigna
abortSignal,
});
+const getRulesByIds = async ({
+ ids,
+ rulesClient,
+ isRuleRegistryEnabled,
+ abortSignal,
+}: {
+ ids: string[];
+ rulesClient: RulesClient;
+ isRuleRegistryEnabled: boolean;
+ abortSignal: AbortSignal;
+}) => {
+ const readRulesExecutor = async (id: string) => {
+ try {
+ const rule = await readRules({ id, rulesClient, isRuleRegistryEnabled, ruleId: undefined });
+ if (rule == null) {
+ throw Error('Can`t fetch a rule');
+ }
+ return { rule };
+ } catch (err) {
+ const { message, statusCode } = transformError(err);
+ return {
+ error: { message, statusCode },
+ rule: { id },
+ };
+ }
+ };
+
+ const { results } = await initPromisePool({
+ concurrency: MAX_RULES_TO_UPDATE_IN_PARALLEL,
+ items: ids,
+ executor: readRulesExecutor,
+ abortSignal,
+ });
+
+ return {
+ total: ids.length,
+ rules: results.filter((rule) => rule.error === undefined).map(({ rule }) => rule) as Rule[],
+ fetchErrors: results.filter((rule): rule is RuleActionError => rule.error !== undefined),
+ };
+};
+
+const fetchRules = async ({
+ query,
+ ids,
+ rulesClient,
+ isRuleRegistryEnabled,
+ abortSignal,
+}: {
+ query: string | undefined;
+ ids: string[] | undefined;
+ rulesClient: RulesClient;
+ isRuleRegistryEnabled: boolean;
+ abortSignal: AbortSignal;
+}) => {
+ if (ids) {
+ return getRulesByIds({
+ ids,
+ rulesClient,
+ isRuleRegistryEnabled,
+ abortSignal,
+ });
+ }
+
+ const { data, total } = await findRules({
+ isRuleRegistryEnabled,
+ rulesClient,
+ perPage: MAX_RULES_TO_PROCESS_TOTAL,
+ filter: query !== '' ? query : undefined,
+ page: undefined,
+ sortField: undefined,
+ sortOrder: undefined,
+ fields: undefined,
+ });
+
+ return {
+ rules: data,
+ total,
+ fetchErrors: [] as RuleActionError[],
+ };
+};
+
export const performBulkActionRoute = (
router: SecuritySolutionPluginRouter,
ml: SetupPlugins['ml'],
@@ -151,6 +236,21 @@ export const performBulkActionRoute = (
async (context, request, response) => {
const { body } = request;
const siemResponse = buildSiemResponse(response);
+
+ if (body?.ids && body.ids.length > RULES_TABLE_MAX_PAGE_SIZE) {
+ return siemResponse.error({
+ body: `More than ${RULES_TABLE_MAX_PAGE_SIZE} ids sent for bulk edit action.`,
+ statusCode: 400,
+ });
+ }
+
+ if (body?.ids && body.query !== undefined) {
+ return siemResponse.error({
+ body: `Both query and ids are sent. Define either ids or query in request payload.`,
+ statusCode: 400,
+ });
+ }
+
const abortController = new AbortController();
// subscribing to completed$, because it handles both cases when request was completed and aborted.
@@ -170,18 +270,15 @@ export const performBulkActionRoute = (
savedObjectsClient,
});
- const rules = await findRules({
+ const { rules, total, fetchErrors } = await fetchRules({
isRuleRegistryEnabled,
rulesClient,
- perPage: MAX_RULES_TO_PROCESS_TOTAL,
- filter: body.query !== '' ? body.query : undefined,
- page: undefined,
- sortField: undefined,
- sortOrder: undefined,
- fields: undefined,
+ query: body.query,
+ ids: body.ids,
+ abortSignal: abortController.signal,
});
- if (rules.total > MAX_RULES_TO_PROCESS_TOTAL) {
+ if (total > MAX_RULES_TO_PROCESS_TOTAL) {
return siemResponse.error({
body: `More than ${MAX_RULES_TO_PROCESS_TOTAL} rules matched the filter query. Try to narrow it down.`,
statusCode: 400,
@@ -196,7 +293,7 @@ export const performBulkActionRoute = (
switch (body.action) {
case BulkAction.enable:
processingResponse = await executeBulkAction(
- rules.data,
+ rules,
async (rule) => {
if (!rule.enabled) {
throwHttpError(await mlAuthz.validateRuleType(rule.params.type));
@@ -211,7 +308,7 @@ export const performBulkActionRoute = (
break;
case BulkAction.disable:
processingResponse = await executeBulkAction(
- rules.data,
+ rules,
async (rule) => {
if (rule.enabled) {
throwHttpError(await mlAuthz.validateRuleType(rule.params.type));
@@ -223,7 +320,7 @@ export const performBulkActionRoute = (
break;
case BulkAction.delete:
processingResponse = await executeBulkAction(
- rules.data,
+ rules,
async (rule) => {
await deleteRules({
ruleId: rule.id,
@@ -236,7 +333,7 @@ export const performBulkActionRoute = (
break;
case BulkAction.duplicate:
processingResponse = await executeBulkAction(
- rules.data,
+ rules,
async (rule) => {
throwHttpError(await mlAuthz.validateRuleType(rule.params.type));
@@ -252,7 +349,7 @@ export const performBulkActionRoute = (
rulesClient,
exceptionsClient,
savedObjectsClient,
- rules.data.map(({ params }) => ({ rule_id: params.ruleId })),
+ rules.map(({ params }) => ({ rule_id: params.ruleId })),
logger,
isRuleRegistryEnabled
);
@@ -268,7 +365,7 @@ export const performBulkActionRoute = (
});
case BulkAction.edit:
processingResponse = await executeBulkAction(
- rules.data,
+ rules,
async (rule) => {
throwHttpError({
valid: !isElasticRule(rule.tags),
@@ -302,13 +399,15 @@ export const performBulkActionRoute = (
throw Error('Bulk action was aborted');
}
- const errors = processingResponse.results.filter(
- (resp): resp is RuleActionError => resp?.error !== undefined
- );
- const rulesCount = rules.data.length;
+ const errors = [
+ ...fetchErrors,
+ ...processingResponse.results.filter(
+ (resp): resp is RuleActionError => resp?.error !== undefined
+ ),
+ ];
if (errors.length > 0) {
- const responseBody = getErrorResponseBody(errors, rulesCount);
+ const responseBody = getErrorResponseBody(errors, total);
return response.custom({
headers: {
@@ -322,7 +421,7 @@ export const performBulkActionRoute = (
return response.ok({
body: {
success: true,
- rules_count: rulesCount,
+ rules_count: total,
},
});
} catch (err) {
diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json
index cc4035fbaf301..8e47a12f9ba4a 100644
--- a/x-pack/plugins/translations/translations/ja-JP.json
+++ b/x-pack/plugins/translations/translations/ja-JP.json
@@ -22806,15 +22806,9 @@
"xpack.securitySolution.detectionEngine.rules.allRules.actions.editRuleSettingsToolTip": "Kibana アクション特権がありません",
"xpack.securitySolution.detectionEngine.rules.allRules.actions.exportRuleDescription": "ルールのエクスポート",
"xpack.securitySolution.detectionEngine.rules.allRules.activeRuleDescription": "アクティブ",
- "xpack.securitySolution.detectionEngine.rules.allRules.batchActions.activateSelectedTitle": "選択した項目の有効化",
- "xpack.securitySolution.detectionEngine.rules.allRules.batchActions.deactivateSelectedTitle": "選択した項目の無効化",
"xpack.securitySolution.detectionEngine.rules.allRules.batchActions.deleteSelectedImmutableTitle": "選択には削除できないイミュータブルルールがあります",
- "xpack.securitySolution.detectionEngine.rules.allRules.batchActions.deleteSelectedTitle": "選択した項目を削除",
- "xpack.securitySolution.detectionEngine.rules.allRules.batchActions.duplicateSelectedTitle": "選択した項目の複製",
- "xpack.securitySolution.detectionEngine.rules.allRules.batchActions.exportSelectedTitle": "選択した項目のエクスポート",
"xpack.securitySolution.detectionEngine.rules.allRules.batchActionsTitle": "一斉アクション",
"xpack.securitySolution.detectionEngine.rules.allRules.clearSelectionTitle": "選択した項目をクリア",
- "xpack.securitySolution.detectionEngine.rules.allRules.columns.activateTitle": "有効化",
"xpack.securitySolution.detectionEngine.rules.allRules.columns.gap": "最後のギャップ(該当する場合)",
"xpack.securitySolution.detectionEngine.rules.allRules.columns.gapTooltip": "ルール実行の最新のギャップの期間。ルールルックバックを調整するか、ギャップの軽減については{seeDocs}してください。",
"xpack.securitySolution.detectionEngine.rules.allRules.columns.indexingTimes": "インデックス時間(ミリ秒)",
diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json
index 7cffdf21f4ccc..f2b19cf866e83 100644
--- a/x-pack/plugins/translations/translations/zh-CN.json
+++ b/x-pack/plugins/translations/translations/zh-CN.json
@@ -23157,17 +23157,11 @@
"xpack.securitySolution.detectionEngine.rules.allRules.actions.exportRuleDescription": "导出规则",
"xpack.securitySolution.detectionEngine.rules.allRules.activeRuleDescription": "活动",
"xpack.securitySolution.detectionEngine.rules.allRules.batchActions.activateSelectedErrorTitle": "激活{totalRules, plural, other {规则}}时出错",
- "xpack.securitySolution.detectionEngine.rules.allRules.batchActions.activateSelectedTitle": "激活所选",
"xpack.securitySolution.detectionEngine.rules.allRules.batchActions.deactivateSelectedErrorTitle": "停用{totalRules, plural, other {规则}}时出错",
- "xpack.securitySolution.detectionEngine.rules.allRules.batchActions.deactivateSelectedTitle": "停用所选",
"xpack.securitySolution.detectionEngine.rules.allRules.batchActions.deleteSelectedErrorTitle": "删除{totalRules, plural, other {规则}}时出错",
"xpack.securitySolution.detectionEngine.rules.allRules.batchActions.deleteSelectedImmutableTitle": "选择内容包含无法删除的不可变规则",
- "xpack.securitySolution.detectionEngine.rules.allRules.batchActions.deleteSelectedTitle": "删除所选",
- "xpack.securitySolution.detectionEngine.rules.allRules.batchActions.duplicateSelectedTitle": "复制所选",
- "xpack.securitySolution.detectionEngine.rules.allRules.batchActions.exportSelectedTitle": "导出所选",
"xpack.securitySolution.detectionEngine.rules.allRules.batchActionsTitle": "批处理操作",
"xpack.securitySolution.detectionEngine.rules.allRules.clearSelectionTitle": "清除所选内容",
- "xpack.securitySolution.detectionEngine.rules.allRules.columns.activateTitle": "已激活",
"xpack.securitySolution.detectionEngine.rules.allRules.columns.gap": "上一缺口(如果有)",
"xpack.securitySolution.detectionEngine.rules.allRules.columns.gapTooltip": "规则执行中最近缺口的持续时间。调整规则回查或{seeDocs}以缩小缺口。",
"xpack.securitySolution.detectionEngine.rules.allRules.columns.indexingTimes": "索引时间 (ms)",
diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/perform_bulk_action.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/perform_bulk_action.ts
index 1643c4851c024..06223fbca7070 100644
--- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/perform_bulk_action.ts
+++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/perform_bulk_action.ts
@@ -326,5 +326,38 @@ export default ({ getService }: FtrProviderContext): void => {
expect(responses.filter((r) => r.body.statusCode === 429).length).to.eql(5);
});
+
+ it('should bulk update rule by id', async () => {
+ const ruleId = 'ruleId';
+ const timelineId = '91832785-286d-4ebe-b884-1a208d111a70';
+ const timelineTitle = 'Test timeline';
+ await createRule(supertest, log, getSimpleRule(ruleId));
+ const {
+ body: { id },
+ } = await fetchRule(ruleId);
+
+ const { body } = await postBulkAction()
+ .send({
+ ids: [id],
+ action: BulkAction.edit,
+ [BulkAction.edit]: [
+ {
+ type: BulkActionEditType.set_timeline,
+ value: {
+ timeline_id: timelineId,
+ timeline_title: timelineTitle,
+ },
+ },
+ ],
+ })
+ .expect(200);
+
+ expect(body).to.eql({ success: true, rules_count: 1 });
+
+ const { body: rule } = await fetchRule(ruleId).expect(200);
+
+ expect(rule.timeline_id).to.eql(timelineId);
+ expect(rule.timeline_title).to.eql(timelineTitle);
+ });
});
};
From 363ad1490aa0106c215061ff9cd43044d82b671e Mon Sep 17 00:00:00 2001
From: Stratoula Kalafateli
Date: Mon, 31 Jan 2022 14:49:05 +0200
Subject: [PATCH 13/65] [Lens] Fixes heatmap tests flakiness (#124080)
---
x-pack/test/functional/apps/lens/heatmap.ts | 13 ++++++-------
1 file changed, 6 insertions(+), 7 deletions(-)
diff --git a/x-pack/test/functional/apps/lens/heatmap.ts b/x-pack/test/functional/apps/lens/heatmap.ts
index deb344e7eb8b5..618f1141d8431 100644
--- a/x-pack/test/functional/apps/lens/heatmap.ts
+++ b/x-pack/test/functional/apps/lens/heatmap.ts
@@ -9,13 +9,12 @@ import expect from '@kbn/expect';
import { FtrProviderContext } from '../../ftr_provider_context';
export default function ({ getService, getPageObjects }: FtrProviderContext) {
- const PageObjects = getPageObjects(['visualize', 'lens', 'common', 'header']);
+ const PageObjects = getPageObjects(['visualize', 'lens', 'common']);
const elasticChart = getService('elasticChart');
const testSubjects = getService('testSubjects');
const retry = getService('retry');
- // FLAKY: https://github.com/elastic/kibana/issues/124075
- describe.skip('lens heatmap', () => {
+ describe('lens heatmap', () => {
before(async () => {
await PageObjects.visualize.navigateToNewVisualization();
await PageObjects.visualize.clickVisType('lens');
@@ -99,7 +98,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
it('should not change when passing from percentage to number', async () => {
await testSubjects.click('lnsPalettePanel_dynamicColoring_rangeType_groups_number');
- await PageObjects.header.waitUntilLoadingHasFinished();
+ await PageObjects.lens.waitForVisualization();
const debugState = await PageObjects.lens.getCurrentChartDebugState();
@@ -125,7 +124,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await testSubjects.setValue('lnsPalettePanel_dynamicColoring_range_value_0', '0', {
clearWithKeyboard: true,
});
- await PageObjects.header.waitUntilLoadingHasFinished();
+ await PageObjects.lens.waitForVisualization();
const debugState = await PageObjects.lens.getCurrentChartDebugState();
@@ -145,7 +144,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
it('should reset stop numbers when changing palette', async () => {
await PageObjects.lens.changePaletteTo('status');
- await PageObjects.header.waitUntilLoadingHasFinished();
+ await PageObjects.lens.waitForVisualization();
const debugState = await PageObjects.lens.getCurrentChartDebugState();
@@ -165,7 +164,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
it('should not change when passing from number to percent', async () => {
await testSubjects.click('lnsPalettePanel_dynamicColoring_rangeType_groups_percent');
- await PageObjects.header.waitUntilLoadingHasFinished();
+ await PageObjects.lens.waitForVisualization();
const debugState = await PageObjects.lens.getCurrentChartDebugState();
From 920b4fcb04d6293f02d054d8e4ba7569a39dc96b Mon Sep 17 00:00:00 2001
From: Joe Reuter
Date: Mon, 31 Jan 2022 14:03:56 +0100
Subject: [PATCH 14/65] remove leftover deprecated imports (#124006)
---
.../public/components/sidebar/sidebar.tsx | 2 +-
.../public/components/sidebar/state/reducers.ts | 6 +++---
src/plugins/visualizations/common/locator.ts | 3 +--
src/plugins/visualizations/public/vis.test.ts | 2 +-
src/plugins/visualizations/public/vis.ts | 2 +-
5 files changed, 7 insertions(+), 8 deletions(-)
diff --git a/src/plugins/vis_default_editor/public/components/sidebar/sidebar.tsx b/src/plugins/vis_default_editor/public/components/sidebar/sidebar.tsx
index f1eebbbdf2116..5fcf3d4ecf133 100644
--- a/src/plugins/vis_default_editor/public/components/sidebar/sidebar.tsx
+++ b/src/plugins/vis_default_editor/public/components/sidebar/sidebar.tsx
@@ -104,7 +104,7 @@ function DefaultEditorSideBarComponent({
...vis.serialize(),
params: state.params,
data: {
- aggs: state.data.aggs ? (state.data.aggs.aggs.map((agg) => agg.toJSON()) as any) : [],
+ aggs: state.data.aggs ? (state.data.aggs.aggs.map((agg) => agg.serialize()) as any) : [],
},
});
embeddableHandler.reload();
diff --git a/src/plugins/vis_default_editor/public/components/sidebar/state/reducers.ts b/src/plugins/vis_default_editor/public/components/sidebar/state/reducers.ts
index ee2b715fad25f..54012b9d89590 100644
--- a/src/plugins/vis_default_editor/public/components/sidebar/state/reducers.ts
+++ b/src/plugins/vis_default_editor/public/components/sidebar/state/reducers.ts
@@ -58,7 +58,7 @@ const createEditorStateReducer =
if (agg.id === aggId) {
agg.type = value;
- return agg.toJSON();
+ return agg.serialize();
}
return agg;
@@ -78,7 +78,7 @@ const createEditorStateReducer =
const newAggs = state.data.aggs!.aggs.map((agg) => {
if (agg.id === aggId) {
- const parsedAgg = agg.toJSON();
+ const parsedAgg = agg.serialize();
return {
...parsedAgg,
@@ -169,7 +169,7 @@ const createEditorStateReducer =
const newAggs = state.data.aggs!.aggs.map((agg) => {
if (agg.id === aggId) {
- const parsedAgg = agg.toJSON();
+ const parsedAgg = agg.serialize();
return {
...parsedAgg,
diff --git a/src/plugins/visualizations/common/locator.ts b/src/plugins/visualizations/common/locator.ts
index b294019e83392..a1d15ee5188d3 100644
--- a/src/plugins/visualizations/common/locator.ts
+++ b/src/plugins/visualizations/common/locator.ts
@@ -11,10 +11,9 @@ import { omitBy } from 'lodash';
import type { ParsedQuery } from 'query-string';
import { stringify } from 'query-string';
import rison from 'rison-node';
-import { Filter } from '@kbn/es-query';
+import { Filter, isFilterPinned } from '@kbn/es-query';
import type { Query, RefreshInterval, TimeRange } from 'src/plugins/data/common';
import type { LocatorDefinition, LocatorPublic } from 'src/plugins/share/common';
-import { isFilterPinned } from '../../data/common';
import { url } from '../../kibana_utils/common';
import { GLOBAL_STATE_STORAGE_KEY, STATE_STORAGE_KEY, VisualizeConstants } from './constants';
import type { SavedVisState } from './types';
diff --git a/src/plugins/visualizations/public/vis.test.ts b/src/plugins/visualizations/public/vis.test.ts
index bfe69f9c59a36..a3fb8abf76bcd 100644
--- a/src/plugins/visualizations/public/vis.test.ts
+++ b/src/plugins/visualizations/public/vis.test.ts
@@ -38,7 +38,7 @@ jest.mock('./services', () => {
getTypes: () => ({ get: () => visType }),
getAggs: () => ({
createAggConfigs: (indexPattern: any, cfg: any) => ({
- aggs: cfg.map((aggConfig: any) => ({ ...aggConfig, toJSON: () => aggConfig })),
+ aggs: cfg.map((aggConfig: any) => ({ ...aggConfig, serialize: () => aggConfig })),
}),
}),
getSearch: () => ({
diff --git a/src/plugins/visualizations/public/vis.ts b/src/plugins/visualizations/public/vis.ts
index fd9bb434a7cf0..821becf3da9c1 100644
--- a/src/plugins/visualizations/public/vis.ts
+++ b/src/plugins/visualizations/public/vis.ts
@@ -189,7 +189,7 @@ export class Vis {
}
serialize(): SerializedVis {
- const aggs = this.data.aggs ? this.data.aggs.aggs.map((agg) => agg.toJSON()) : [];
+ const aggs = this.data.aggs ? this.data.aggs.aggs.map((agg) => agg.serialize()) : [];
return {
id: this.id,
title: this.title,
From 89fc72f955462b01a35facfe543084056036bc7a Mon Sep 17 00:00:00 2001
From: Joe Reuter
Date: Mon, 31 Jan 2022 14:12:39 +0100
Subject: [PATCH 15/65] prevent leakage of moment timezone (#123694)
---
.../response_processors/series/time_shift.js | 42 +++++++------
.../series/time_shift.test.js | 5 ++
.../expressions/time_scale/time_scale.test.ts | 30 ++++++++++
.../expressions/time_scale/time_scale_fn.ts | 60 +++++++++++--------
4 files changed, 92 insertions(+), 45 deletions(-)
diff --git a/src/plugins/vis_types/timeseries/server/lib/vis_data/response_processors/series/time_shift.js b/src/plugins/vis_types/timeseries/server/lib/vis_data/response_processors/series/time_shift.js
index 429050fab36cc..6936ea7e1c5d7 100644
--- a/src/plugins/vis_types/timeseries/server/lib/vis_data/response_processors/series/time_shift.js
+++ b/src/plugins/vis_types/timeseries/server/lib/vis_data/response_processors/series/time_shift.js
@@ -27,28 +27,32 @@ export function timeShift(
const offsetValue = matches[1];
const offsetUnit = matches[2];
- let defaultTimezone;
- if (!panel.ignore_daylight_time) {
- // the datemath plugin always parses dates by using the current default moment time zone.
- // to use the configured time zone, we are switching just for the bounds calculation.
- defaultTimezone = moment().zoneName();
- moment.tz.setDefault(timezone);
- }
+ const defaultTimezone = moment().zoneName();
+ try {
+ if (!panel.ignore_daylight_time) {
+ // the datemath plugin always parses dates by using the current default moment time zone.
+ // to use the configured time zone, we are switching just for the bounds calculation.
- results.forEach((item) => {
- if (startsWith(item.id, series.id)) {
- item.data = item.data.map((row) => [
- (panel.ignore_daylight_time ? moment.utc : moment)(row[0])
- .add(offsetValue, offsetUnit)
- .valueOf(),
- row[1],
- ]);
+ // The code between this call and the reset in the finally block is not allowed to get async,
+ // otherwise the timezone setting can leak out of this function.
+ moment.tz.setDefault(timezone);
}
- });
- if (!panel.ignore_daylight_time) {
- // reset default moment timezone
- moment.tz.setDefault(defaultTimezone);
+ results.forEach((item) => {
+ if (startsWith(item.id, series.id)) {
+ item.data = item.data.map((row) => [
+ (panel.ignore_daylight_time ? moment.utc : moment)(row[0])
+ .add(offsetValue, offsetUnit)
+ .valueOf(),
+ row[1],
+ ]);
+ }
+ });
+ } finally {
+ if (!panel.ignore_daylight_time) {
+ // reset default moment timezone
+ moment.tz.setDefault(defaultTimezone);
+ }
}
}
}
diff --git a/src/plugins/vis_types/timeseries/server/lib/vis_data/response_processors/series/time_shift.test.js b/src/plugins/vis_types/timeseries/server/lib/vis_data/response_processors/series/time_shift.test.js
index 7fff2603cf47a..f3000e5589b6d 100644
--- a/src/plugins/vis_types/timeseries/server/lib/vis_data/response_processors/series/time_shift.test.js
+++ b/src/plugins/vis_types/timeseries/server/lib/vis_data/response_processors/series/time_shift.test.js
@@ -128,4 +128,9 @@ describe('timeShift(resp, panel, series)', () => {
[dateAfterDST + 1000 * 60 * 60 * 24, 2],
]);
});
+
+ test('processor is sync to avoid timezone setting leakage', () => {
+ const result = timeShift(resp, panel, series, {})((results) => results)([]);
+ expect(Array.isArray(result)).toBe(true);
+ });
});
diff --git a/x-pack/plugins/lens/common/expressions/time_scale/time_scale.test.ts b/x-pack/plugins/lens/common/expressions/time_scale/time_scale.test.ts
index d51f2594b4267..9bf647b194282 100644
--- a/x-pack/plugins/lens/common/expressions/time_scale/time_scale.test.ts
+++ b/x-pack/plugins/lens/common/expressions/time_scale/time_scale.test.ts
@@ -388,4 +388,34 @@ describe('time_scale', () => {
expect(result.rows.map(({ scaledMetric }) => scaledMetric)).toEqual([1, 1, 1, 1, 1]);
});
+
+ it('should be sync except for timezone getter to prevent timezone leakage', async () => {
+ let resolveTimezonePromise: (value: string | PromiseLike) => void;
+ const timezonePromise = new Promise((res) => {
+ resolveTimezonePromise = res;
+ });
+ const timeScaleResolved = jest.fn((x) => x);
+ const delayedTimeScale = getTimeScale(() => timezonePromise);
+ const delayedTimeScaleWrapper = functionWrapper(delayedTimeScale);
+ const result = delayedTimeScaleWrapper(
+ {
+ ...emptyTable,
+ },
+ {
+ ...defaultArgs,
+ }
+ ).then(timeScaleResolved) as Promise;
+
+ expect(result instanceof Promise).toBe(true);
+ // wait a tick
+ await new Promise((r) => setTimeout(r, 0));
+ // time scale is not done yet because it's waiting for the timezone
+ expect(timeScaleResolved).not.toHaveBeenCalled();
+ // resolve timezone
+ resolveTimezonePromise!('UTC');
+ // wait a tick
+ await new Promise((r) => setTimeout(r, 0));
+ // should resolve now without another async dependency
+ expect(timeScaleResolved).toHaveBeenCalled();
+ });
});
diff --git a/x-pack/plugins/lens/common/expressions/time_scale/time_scale_fn.ts b/x-pack/plugins/lens/common/expressions/time_scale/time_scale_fn.ts
index 78d2e9896d4c3..5217d6a7db167 100644
--- a/x-pack/plugins/lens/common/expressions/time_scale/time_scale_fn.ts
+++ b/x-pack/plugins/lens/common/expressions/time_scale/time_scale_fn.ts
@@ -9,6 +9,7 @@ import moment from 'moment-timezone';
import { i18n } from '@kbn/i18n';
import {
buildResultColumns,
+ Datatable,
ExecutionContext,
} from '../../../../../../src/plugins/expressions/common';
import {
@@ -76,38 +77,45 @@ export const timeScaleFn =
}
// the datemath plugin always parses dates by using the current default moment time zone.
// to use the configured time zone, we are switching just for the bounds calculation.
+
+ // The code between this call and the reset in the finally block is not allowed to get async,
+ // otherwise the timezone setting can leak out of this function.
const defaultTimezone = moment().zoneName();
- moment.tz.setDefault(timeInfo.timeZone);
+ let result: Datatable;
+ try {
+ moment.tz.setDefault(timeInfo.timeZone);
- const timeBounds = timeInfo.timeRange && calculateBounds(timeInfo.timeRange);
+ const timeBounds = timeInfo.timeRange && calculateBounds(timeInfo.timeRange);
- const result = {
- ...input,
- columns: resultColumns,
- rows: input.rows.map((row) => {
- const newRow = { ...row };
+ result = {
+ ...input,
+ columns: resultColumns,
+ rows: input.rows.map((row) => {
+ const newRow = { ...row };
- let startOfBucket = moment(row[dateColumnId]);
- let endOfBucket = startOfBucket.clone().add(intervalDuration);
- if (timeBounds && timeBounds.min) {
- startOfBucket = moment.max(startOfBucket, timeBounds.min);
- }
- if (timeBounds && timeBounds.max) {
- endOfBucket = moment.min(endOfBucket, timeBounds.max);
- }
- const bucketSize = endOfBucket.diff(startOfBucket);
- const factor = bucketSize / targetUnitInMs;
+ let startOfBucket = moment(row[dateColumnId]);
+ let endOfBucket = startOfBucket.clone().add(intervalDuration);
+ if (timeBounds && timeBounds.min) {
+ startOfBucket = moment.max(startOfBucket, timeBounds.min);
+ }
+ if (timeBounds && timeBounds.max) {
+ endOfBucket = moment.min(endOfBucket, timeBounds.max);
+ }
+ const bucketSize = endOfBucket.diff(startOfBucket);
+ const factor = bucketSize / targetUnitInMs;
- const currentValue = newRow[inputColumnId];
- if (currentValue != null) {
- newRow[outputColumnId] = Number(currentValue) / factor;
- }
+ const currentValue = newRow[inputColumnId];
+ if (currentValue != null) {
+ newRow[outputColumnId] = Number(currentValue) / factor;
+ }
- return newRow;
- }),
- };
- // reset default moment timezone
- moment.tz.setDefault(defaultTimezone);
+ return newRow;
+ }),
+ };
+ } finally {
+ // reset default moment timezone
+ moment.tz.setDefault(defaultTimezone);
+ }
return result;
};
From fabaa8879138dddcb2db41c99c76b272439adfc1 Mon Sep 17 00:00:00 2001
From: Joe Reuter
Date: Mon, 31 Jan 2022 14:31:39 +0100
Subject: [PATCH 16/65] [Lens] Fix records field name and migrate (#123894)
---
x-pack/plugins/lens/common/constants.ts | 3 +
.../datapanel.test.tsx | 5 +-
.../dimension_panel/dimension_panel.test.tsx | 18 ++---
.../droppable/droppable.test.ts | 20 ++---
.../dimension_panel/field_input.test.tsx | 2 +-
.../indexpattern_datasource/document_field.ts | 5 +-
.../field_item.test.tsx | 3 +-
.../indexpattern.test.ts | 34 ++++----
.../indexpattern_suggestions.test.tsx | 14 ++--
.../operations/definitions.test.ts | 2 +-
.../definitions/filters/filters.test.tsx | 2 +-
.../formula/editor/math_completion.test.ts | 2 +-
.../definitions/formula/formula.test.tsx | 2 +-
.../definitions/last_value.test.tsx | 8 +-
.../definitions/ranges/ranges.test.tsx | 2 +-
.../definitions/terms/helpers.test.ts | 2 +-
.../definitions/terms/terms.test.tsx | 10 +--
.../operations/layer_helpers.test.ts | 42 +++++-----
.../operations/time_scale_utils.test.ts | 2 +-
.../make_lens_embeddable_factory.ts | 7 +-
.../server/migrations/common_migrations.ts | 25 +++++-
.../saved_object_migrations.test.ts | 80 ++++++++++++++++++-
.../migrations/saved_object_migrations.ts | 16 ++--
23 files changed, 207 insertions(+), 99 deletions(-)
diff --git a/x-pack/plugins/lens/common/constants.ts b/x-pack/plugins/lens/common/constants.ts
index edf7654deb7b7..f0db3385cefc1 100644
--- a/x-pack/plugins/lens/common/constants.ts
+++ b/x-pack/plugins/lens/common/constants.ts
@@ -22,6 +22,9 @@ export const layerTypes: Record = {
REFERENCELINE: 'referenceLine',
};
+// might collide with user-supplied field names, try to make as unique as possible
+export const DOCUMENT_FIELD_NAME = '___records___';
+
export function getBasePath() {
return `#/`;
}
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.test.tsx
index 27d03a1e3edc8..7fe115847b2b5 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.test.tsx
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.test.tsx
@@ -27,6 +27,7 @@ import { indexPatternFieldEditorPluginMock } from '../../../../../src/plugins/da
import { getFieldByNameFactory } from './pure_helpers';
import { uiActionsPluginMock } from '../../../../../src/plugins/ui_actions/public/mocks';
import { TermsIndexPatternColumn } from './operations';
+import { DOCUMENT_FIELD_NAME } from '../../common';
const fieldsOne = [
{
@@ -640,7 +641,7 @@ describe('IndexPattern Data Panel', () => {
});
it('should list all supported fields in the pattern sorted alphabetically in groups', async () => {
const wrapper = mountWithIntl();
- expect(wrapper.find(FieldItem).first().prop('field').name).toEqual('Records');
+ expect(wrapper.find(FieldItem).first().prop('field').displayName).toEqual('Records');
expect(
wrapper
.find('[data-test-subj="lnsIndexPatternAvailableFields"]')
@@ -813,7 +814,7 @@ describe('IndexPattern Data Panel', () => {
wrapper.find('[data-test-subj="typeFilter-document"]').first().simulate('click');
expect(wrapper.find(FieldItem).map((fieldItem) => fieldItem.prop('field').name)).toEqual([
- 'Records',
+ DOCUMENT_FIELD_NAME,
]);
expect(wrapper.find(NoFieldsCallout).length).toEqual(3);
});
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx
index dd16b0be6ce61..975e08e5ca1a9 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx
@@ -350,7 +350,7 @@ describe('IndexPatternDimensionEditorPanel', () => {
.filter('[data-test-subj="indexPattern-dimension-field"]')
.prop('options');
- expect(options![0]['data-test-subj']).toEqual('lns-fieldOptionIncompatible-Records');
+ expect(options![0]['data-test-subj']).toEqual('lns-fieldOptionIncompatible-___records___');
expect(
options![1].options!.filter(({ label }) => label === 'timestampLabel')[0]['data-test-subj']
@@ -481,7 +481,7 @@ describe('IndexPatternDimensionEditorPanel', () => {
dataType: 'number',
isBucketed: false,
operationType: 'count',
- sourceField: 'Records',
+ sourceField: '___records___',
},
})}
/>
@@ -991,7 +991,7 @@ describe('IndexPatternDimensionEditorPanel', () => {
const newColumnState = setState.mock.calls[0][0](state).layers.first.columns.col2;
expect(newColumnState.operationType).toEqual('count');
- expect(newColumnState.sourceField).toEqual('Records');
+ expect(newColumnState.sourceField).toEqual('___records___');
});
it('should indicate document and field compatibility with selected document operation', () => {
@@ -1004,7 +1004,7 @@ describe('IndexPatternDimensionEditorPanel', () => {
isBucketed: false,
label: '',
operationType: 'count',
- sourceField: 'Records',
+ sourceField: '___records___',
},
})}
columnId="col2"
@@ -1090,7 +1090,7 @@ describe('IndexPatternDimensionEditorPanel', () => {
isBucketed: false,
label: 'Count of records',
operationType: 'count',
- sourceField: 'Records',
+ sourceField: '___records___',
...colOverrides,
} as GenericIndexPatternColumn,
}),
@@ -1320,7 +1320,7 @@ describe('IndexPatternDimensionEditorPanel', () => {
isBucketed: false,
label: 'Count of records',
operationType: 'count',
- sourceField: 'Records',
+ sourceField: '___records___',
...colOverrides,
} as GenericIndexPatternColumn,
}),
@@ -1337,7 +1337,7 @@ describe('IndexPatternDimensionEditorPanel', () => {
isBucketed: false,
label: 'Count of records',
operationType: 'count',
- sourceField: 'Records',
+ sourceField: '___records___',
} as GenericIndexPatternColumn,
}),
columnId: 'col2',
@@ -1524,7 +1524,7 @@ describe('IndexPatternDimensionEditorPanel', () => {
isBucketed: false,
label: 'Count of records',
operationType: 'count',
- sourceField: 'Records',
+ sourceField: '___records___',
...colOverrides,
} as GenericIndexPatternColumn,
}),
@@ -1871,7 +1871,7 @@ describe('IndexPatternDimensionEditorPanel', () => {
isBucketed: false,
label: '',
operationType: 'count',
- sourceField: 'Records',
+ sourceField: '___records___',
},
})}
columnId={'col2'}
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable/droppable.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable/droppable.test.ts
index fab431e886d8e..3871715cf31e5 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable/droppable.test.ts
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable/droppable.test.ts
@@ -713,7 +713,7 @@ describe('IndexPatternDimensionEditorPanel', () => {
label: 'Count of records',
dataType: 'number',
isBucketed: false,
- sourceField: 'Records',
+ sourceField: '___records___',
operationType: 'count',
},
},
@@ -1186,7 +1186,7 @@ describe('IndexPatternDimensionEditorPanel', () => {
label: 'Count of records',
dataType: 'number',
isBucketed: false,
- sourceField: 'Records',
+ sourceField: '___records___',
operationType: 'count',
},
},
@@ -1249,7 +1249,7 @@ describe('IndexPatternDimensionEditorPanel', () => {
label: 'Count of records',
dataType: 'number',
isBucketed: false,
- sourceField: 'Records',
+ sourceField: '___records___',
operationType: 'count',
},
ref2: {
@@ -1329,7 +1329,7 @@ describe('IndexPatternDimensionEditorPanel', () => {
label: 'Count of records',
dataType: 'number',
isBucketed: false,
- sourceField: 'Records',
+ sourceField: '___records___',
operationType: 'count',
},
ref2: {
@@ -1415,7 +1415,7 @@ describe('IndexPatternDimensionEditorPanel', () => {
label: 'Count of records',
dataType: 'number',
isBucketed: false,
- sourceField: 'Records',
+ sourceField: '___records___',
operationType: 'count',
},
col2: {
@@ -1605,7 +1605,7 @@ describe('IndexPatternDimensionEditorPanel', () => {
// Private
operationType: 'count',
- sourceField: 'Records',
+ sourceField: '___records___',
customLabel: true,
},
},
@@ -1822,7 +1822,7 @@ describe('IndexPatternDimensionEditorPanel', () => {
operationType: 'count',
label: '',
isBucketed: false,
- sourceField: 'Records',
+ sourceField: '___records___',
customLabel: true,
},
col6: {
@@ -1830,7 +1830,7 @@ describe('IndexPatternDimensionEditorPanel', () => {
operationType: 'count',
label: '',
isBucketed: false,
- sourceField: 'Records',
+ sourceField: '___records___',
customLabel: true,
},
},
@@ -1862,14 +1862,14 @@ describe('IndexPatternDimensionEditorPanel', () => {
operationType: 'count',
label: '',
isBucketed: false,
- sourceField: 'Records',
+ sourceField: '___records___',
}),
col6: expect.objectContaining({
dataType: 'number',
operationType: 'count',
label: '',
isBucketed: false,
- sourceField: 'Records',
+ sourceField: '___records___',
}),
},
},
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/field_input.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/field_input.test.tsx
index cf409ebfd680d..730342ae694e8 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/field_input.test.tsx
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/field_input.test.tsx
@@ -98,7 +98,7 @@ function getCountOperationColumn(): GenericIndexPatternColumn {
label: 'Count',
dataType: 'number',
isBucketed: false,
- sourceField: 'Records',
+ sourceField: '___records___',
operationType: 'count',
};
}
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/document_field.ts b/x-pack/plugins/lens/public/indexpattern_datasource/document_field.ts
index fef78b1d5c6de..1f69d1df5602a 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/document_field.ts
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/document_field.ts
@@ -6,6 +6,7 @@
*/
import { i18n } from '@kbn/i18n';
+import { DOCUMENT_FIELD_NAME } from '../../common';
import { IndexPatternField } from './types';
/**
@@ -16,9 +17,7 @@ export const documentField: IndexPatternField = {
displayName: i18n.translate('xpack.lens.indexPattern.records', {
defaultMessage: 'Records',
}),
- name: i18n.translate('xpack.lens.indexPattern.records', {
- defaultMessage: 'Records',
- }),
+ name: DOCUMENT_FIELD_NAME,
type: 'document',
aggregatable: true,
searchable: true,
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/field_item.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/field_item.test.tsx
index 868c5864a6730..cb721416a1df9 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/field_item.test.tsx
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/field_item.test.tsx
@@ -18,6 +18,7 @@ import { chartPluginMock } from '../../../../../src/plugins/charts/public/mocks'
import { documentField } from './document_field';
import { uiActionsPluginMock } from '../../../../../src/plugins/ui_actions/public/mocks';
import { FieldFormatsStart } from '../../../../../src/plugins/field_formats/public';
+import { DOCUMENT_FIELD_NAME } from '../../common';
const chartsThemeService = chartPluginMock.createSetupContract().theme;
@@ -254,7 +255,7 @@ describe('IndexPattern Field Item', () => {
it('should not request field stats for document field', async () => {
const wrapper = mountWithIntl();
- clickField(wrapper, 'Records');
+ clickField(wrapper, DOCUMENT_FIELD_NAME);
expect(core.http.post).not.toHaveBeenCalled();
expect(wrapper.find(EuiPopover).prop('isOpen')).toEqual(true);
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts
index 4e358564c9f76..404c31010278b 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts
@@ -224,7 +224,7 @@ describe('IndexPattern Data Source', () => {
isBucketed: false,
label: 'Foo',
operationType: 'count',
- sourceField: 'Records',
+ sourceField: '___records___',
};
const map = indexPatternDatasource.uniqueLabels({
layers: {
@@ -353,7 +353,7 @@ describe('IndexPattern Data Source', () => {
label: 'Count of records',
dataType: 'number',
isBucketed: false,
- sourceField: 'Records',
+ sourceField: '___records___',
operationType: 'count',
},
col2: {
@@ -479,7 +479,7 @@ describe('IndexPattern Data Source', () => {
Object {
"arguments": Object {
"idMap": Array [
- "{\\"col-0-0\\":{\\"label\\":\\"Count of records\\",\\"dataType\\":\\"number\\",\\"isBucketed\\":false,\\"sourceField\\":\\"Records\\",\\"operationType\\":\\"count\\",\\"id\\":\\"col1\\"},\\"col-1-1\\":{\\"label\\":\\"Date\\",\\"dataType\\":\\"date\\",\\"isBucketed\\":true,\\"operationType\\":\\"date_histogram\\",\\"sourceField\\":\\"timestamp\\",\\"params\\":{\\"interval\\":\\"1d\\"},\\"id\\":\\"col2\\"}}",
+ "{\\"col-0-0\\":{\\"label\\":\\"Count of records\\",\\"dataType\\":\\"number\\",\\"isBucketed\\":false,\\"sourceField\\":\\"___records___\\",\\"operationType\\":\\"count\\",\\"id\\":\\"col1\\"},\\"col-1-1\\":{\\"label\\":\\"Date\\",\\"dataType\\":\\"date\\",\\"isBucketed\\":true,\\"operationType\\":\\"date_histogram\\",\\"sourceField\\":\\"timestamp\\",\\"params\\":{\\"interval\\":\\"1d\\"},\\"id\\":\\"col2\\"}}",
],
},
"function": "lens_rename_columns",
@@ -503,7 +503,7 @@ describe('IndexPattern Data Source', () => {
label: 'Count of records',
dataType: 'number',
isBucketed: false,
- sourceField: 'Records',
+ sourceField: '___records___',
operationType: 'count',
},
col2: {
@@ -549,7 +549,7 @@ describe('IndexPattern Data Source', () => {
label: 'Count of records',
dataType: 'number',
isBucketed: false,
- sourceField: 'Records',
+ sourceField: '___records___',
operationType: 'count',
timeShift: '1d',
},
@@ -586,7 +586,7 @@ describe('IndexPattern Data Source', () => {
label: 'Count of records',
dataType: 'number',
isBucketed: false,
- sourceField: 'Records',
+ sourceField: '___records___',
operationType: 'count',
timeScale: 'h',
filter: {
@@ -716,7 +716,7 @@ describe('IndexPattern Data Source', () => {
label: 'Count of records',
dataType: 'number',
isBucketed: false,
- sourceField: 'Records',
+ sourceField: '___records___',
operationType: 'count',
timeScale: 'h',
},
@@ -809,7 +809,7 @@ describe('IndexPattern Data Source', () => {
label: 'Count of records',
dataType: 'number',
isBucketed: false,
- sourceField: 'Records',
+ sourceField: '___records___',
operationType: 'count',
timeScale: 'h',
},
@@ -848,7 +848,7 @@ describe('IndexPattern Data Source', () => {
label: 'Count of records',
dataType: 'number',
isBucketed: false,
- sourceField: 'Records',
+ sourceField: '___records___',
operationType: 'count',
},
bucket1: {
@@ -1025,7 +1025,7 @@ describe('IndexPattern Data Source', () => {
operationType: 'count',
isBucketed: false,
scale: 'ratio',
- sourceField: 'Records',
+ sourceField: '___records___',
customLabel: true,
},
date: {
@@ -1057,7 +1057,7 @@ describe('IndexPattern Data Source', () => {
operationType: 'count',
isBucketed: false,
scale: 'ratio',
- sourceField: 'Records',
+ sourceField: '___records___',
customLabel: true,
},
math: {
@@ -1589,7 +1589,7 @@ describe('IndexPattern Data Source', () => {
label: 'Count of records2',
dataType: 'number',
isBucketed: false,
- sourceField: 'Records',
+ sourceField: '___records___',
operationType: 'count',
},
},
@@ -1602,7 +1602,7 @@ describe('IndexPattern Data Source', () => {
label: 'Count of records',
dataType: 'number',
isBucketed: false,
- sourceField: 'Records',
+ sourceField: '___records___',
operationType: 'count',
},
bucket1: {
@@ -1645,7 +1645,7 @@ describe('IndexPattern Data Source', () => {
label: 'Count of records',
dataType: 'number',
isBucketed: false,
- sourceField: 'Records',
+ sourceField: '___records___',
operationType: 'count',
},
},
@@ -1669,7 +1669,7 @@ describe('IndexPattern Data Source', () => {
label: 'Count of records',
dataType: 'number',
isBucketed: false,
- sourceField: 'Records',
+ sourceField: '___records___',
operationType: 'count',
},
},
@@ -1698,7 +1698,7 @@ describe('IndexPattern Data Source', () => {
label: 'Count of records',
dataType: 'number',
isBucketed: false,
- sourceField: 'Records',
+ sourceField: '___records___',
operationType: 'count',
},
},
@@ -1753,7 +1753,7 @@ describe('IndexPattern Data Source', () => {
operationType: 'count',
isBucketed: false,
scale: 'ratio',
- sourceField: 'Records',
+ sourceField: '___records___',
},
},
columnOrder: ['fa649155-d7f5-49d9-af26-508287431244'],
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.test.tsx
index f9f720cfa922a..ab7801d0d8760 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.test.tsx
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.test.tsx
@@ -1063,7 +1063,7 @@ describe('IndexPattern Data Source suggestions', () => {
isBucketed: false,
operationType: 'count',
- sourceField: 'Records',
+ sourceField: '___records___',
},
},
},
@@ -2483,7 +2483,7 @@ describe('IndexPattern Data Source suggestions', () => {
dataType: 'number',
isBucketed: false,
operationType: 'count',
- sourceField: 'Records',
+ sourceField: '___records___',
},
ref: {
label: '',
@@ -2665,7 +2665,7 @@ describe('IndexPattern Data Source suggestions', () => {
dataType: 'number',
isBucketed: false,
operationType: 'count',
- sourceField: 'Records',
+ sourceField: '___records___',
},
ref: {
label: '',
@@ -2679,21 +2679,21 @@ describe('IndexPattern Data Source suggestions', () => {
dataType: 'number',
isBucketed: false,
operationType: 'count',
- sourceField: 'Records',
+ sourceField: '___records___',
},
metric3: {
label: '',
dataType: 'number',
isBucketed: false,
operationType: 'count',
- sourceField: 'Records',
+ sourceField: '___records___',
},
metric4: {
label: '',
dataType: 'number',
isBucketed: false,
operationType: 'count',
- sourceField: 'Records',
+ sourceField: '___records___',
},
ref2: {
label: '',
@@ -2842,7 +2842,7 @@ describe('IndexPattern Data Source suggestions', () => {
dataType: 'number',
isBucketed: false,
operationType: 'count',
- sourceField: 'Records',
+ sourceField: '___records___',
},
},
},
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions.test.ts
index 974e37c2aea8e..1a0da126f28e3 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions.test.ts
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions.test.ts
@@ -91,7 +91,7 @@ const baseColumnArgs: {
// Private
operationType: 'count',
- sourceField: 'Records',
+ sourceField: '___records___',
},
indexPattern,
layer: {
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filters.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filters.test.tsx
index 8d0a07cffd2e1..5a3700c165d20 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filters.test.tsx
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filters.test.tsx
@@ -79,7 +79,7 @@ describe('filters', () => {
label: 'Count',
dataType: 'number',
isBucketed: false,
- sourceField: 'Records',
+ sourceField: '___records___',
operationType: 'count',
},
},
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/math_completion.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/math_completion.test.ts
index 9b7f34b583631..612ae892146fc 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/math_completion.test.ts
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/editor/math_completion.test.ts
@@ -58,7 +58,7 @@ const operationDefinitionMap: Record = {
input: 'field',
buildColumn: buildGenericColumn('count'),
getPossibleOperationForField: (field: IndexPatternField) =>
- field.name === 'Records' ? numericOperation() : null,
+ field.name === '___records___' ? numericOperation() : null,
} as unknown as GenericOperationDefinition,
last_value: {
type: 'last_value',
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/formula.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/formula.test.tsx
index d1561e93aa807..a0e5fca114c37 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/formula.test.tsx
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/formula.test.tsx
@@ -265,7 +265,7 @@ describe('formula', () => {
previousColumn: {
...layer.columns.col1,
operationType: 'count',
- sourceField: 'Records',
+ sourceField: '___records___',
filter: {
language: 'lucene',
query: `*`,
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/last_value.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/last_value.test.tsx
index 26074b47e0f48..652c644d653fb 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/last_value.test.tsx
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/last_value.test.tsx
@@ -242,7 +242,7 @@ describe('last_value', () => {
label: 'Count',
dataType: 'number',
isBucketed: false,
- sourceField: 'Records',
+ sourceField: '___records___',
operationType: 'count',
},
},
@@ -296,7 +296,7 @@ describe('last_value', () => {
label: 'Count',
dataType: 'number',
isBucketed: false,
- sourceField: 'Records',
+ sourceField: '___records___',
operationType: 'count',
},
},
@@ -359,7 +359,7 @@ describe('last_value', () => {
label: 'Count',
dataType: 'number',
isBucketed: false,
- sourceField: 'Records',
+ sourceField: '___records___',
operationType: 'count',
},
},
@@ -378,7 +378,7 @@ describe('last_value', () => {
label: 'Count',
dataType: 'number',
isBucketed: false,
- sourceField: 'Records',
+ sourceField: '___records___',
operationType: 'count',
params: {
format: {
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/ranges.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/ranges.test.tsx
index 9b1fde8561363..79103841ef4ba 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/ranges.test.tsx
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/ranges.test.tsx
@@ -159,7 +159,7 @@ describe('ranges', () => {
label: 'Count',
dataType: 'number',
isBucketed: false,
- sourceField: 'Records',
+ sourceField: '___records___',
operationType: 'count',
},
},
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/helpers.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/helpers.test.ts
index 4468953a26d17..628a5d6a7740a 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/helpers.test.ts
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/helpers.test.ts
@@ -86,7 +86,7 @@ function getCountOperationColumn(
label: 'Count',
dataType: 'number',
isBucketed: false,
- sourceField: 'Records',
+ sourceField: '___records___',
operationType: 'count',
...params,
};
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/terms.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/terms.test.tsx
index bc1106ad3debf..fb3943289437d 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/terms.test.tsx
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/terms.test.tsx
@@ -82,7 +82,7 @@ describe('terms', () => {
label: 'Count of records',
dataType: 'number',
isBucketed: false,
- sourceField: 'Records',
+ sourceField: '___records___',
operationType: 'count',
},
},
@@ -434,7 +434,7 @@ describe('terms', () => {
label: 'Count',
dataType: 'number',
isBucketed: false,
- sourceField: 'Records',
+ sourceField: '___records___',
operationType: 'count',
},
},
@@ -544,7 +544,7 @@ describe('terms', () => {
label: 'Count',
dataType: 'number',
isBucketed: false,
- sourceField: 'Records',
+ sourceField: '___records___',
operationType: 'count',
},
},
@@ -630,7 +630,7 @@ describe('terms', () => {
dataType: 'number',
isBucketed: false,
operationType: 'count',
- sourceField: 'Records',
+ sourceField: '___records___',
},
},
columnOrder: [],
@@ -779,7 +779,7 @@ describe('terms', () => {
label: 'Count',
dataType: 'number',
isBucketed: false,
- sourceField: 'Records',
+ sourceField: '___records___',
operationType: 'count',
},
},
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.test.ts
index 9066641b522d9..dad1250b39e14 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.test.ts
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.test.ts
@@ -378,7 +378,7 @@ describe('state_helpers', () => {
// Private
operationType: 'count',
- sourceField: 'Records',
+ sourceField: '___records___',
},
},
};
@@ -414,7 +414,7 @@ describe('state_helpers', () => {
// Private
operationType: 'count',
- sourceField: 'Records',
+ sourceField: '___records___',
},
},
};
@@ -575,7 +575,7 @@ describe('state_helpers', () => {
// Private
operationType: 'count',
- sourceField: 'Records',
+ sourceField: '___records___',
},
},
},
@@ -1292,7 +1292,7 @@ describe('state_helpers', () => {
label: 'Count',
dataType: 'number',
isBucketed: false,
- sourceField: 'Records',
+ sourceField: '___records___',
operationType: 'count',
},
},
@@ -1351,7 +1351,7 @@ describe('state_helpers', () => {
label: 'Count of records',
dataType: 'number',
isBucketed: false,
- sourceField: 'Records',
+ sourceField: '___records___',
operationType: 'count',
},
},
@@ -1376,7 +1376,7 @@ describe('state_helpers', () => {
id1: expect.objectContaining({
dataType: 'number',
isBucketed: false,
- sourceField: 'Records',
+ sourceField: '___records___',
operationType: 'count',
}),
willBeReference: expect.objectContaining({
@@ -1435,7 +1435,7 @@ describe('state_helpers', () => {
customLabel: true,
dataType: 'number' as const,
isBucketed: false,
- sourceField: 'Records',
+ sourceField: '___records___',
operationType: 'count' as const,
};
@@ -1472,7 +1472,7 @@ describe('state_helpers', () => {
customLabel: true,
dataType: 'number' as const,
isBucketed: false,
- sourceField: 'Records',
+ sourceField: '___records___',
operationType: 'count' as const,
};
@@ -1607,7 +1607,7 @@ describe('state_helpers', () => {
label: 'Count',
dataType: 'number' as const,
isBucketed: false,
- sourceField: 'Records',
+ sourceField: '___records___',
operationType: 'count' as const,
},
},
@@ -1647,7 +1647,7 @@ describe('state_helpers', () => {
label: 'Count',
dataType: 'number' as const,
isBucketed: false,
- sourceField: 'Records',
+ sourceField: '___records___',
operationType: 'count' as const,
},
},
@@ -1720,7 +1720,7 @@ describe('state_helpers', () => {
isBucketed: false,
operationType: 'count' as const,
- sourceField: 'Records',
+ sourceField: '___records___',
},
invalid: {
label: 'Test reference',
@@ -1772,7 +1772,7 @@ describe('state_helpers', () => {
isBucketed: false,
operationType: 'count' as const,
- sourceField: 'Records',
+ sourceField: '___records___',
},
invalid: {
label: 'Test reference',
@@ -1990,7 +1990,7 @@ describe('state_helpers', () => {
isBucketed: false,
operationType: 'count' as const,
- sourceField: 'Records',
+ sourceField: '___records___',
filter: { language: 'kuery', query: 'bytes > 4000' },
timeShift: '3h',
};
@@ -2168,7 +2168,7 @@ describe('state_helpers', () => {
columnOrder: ['col1', 'col2'],
columns: {
col1: expect.objectContaining({
- sourceField: 'Records',
+ sourceField: '___records___',
operationType: 'count',
}),
col2: expect.objectContaining({ references: ['col1'] }),
@@ -2228,7 +2228,7 @@ describe('state_helpers', () => {
label: 'Count of records',
dataType: 'number',
isBucketed: false,
- sourceField: 'Records',
+ sourceField: '___records___',
operationType: 'count',
},
},
@@ -2282,7 +2282,7 @@ describe('state_helpers', () => {
label: 'Count',
dataType: 'number',
isBucketed: false,
- sourceField: 'Records',
+ sourceField: '___records___',
operationType: 'count',
},
},
@@ -2309,7 +2309,7 @@ describe('state_helpers', () => {
isBucketed: false,
operationType: 'count',
- sourceField: 'Records',
+ sourceField: '___records___',
},
col2: {
label: 'Test reference',
@@ -2337,7 +2337,7 @@ describe('state_helpers', () => {
isBucketed: false,
operationType: 'count',
- sourceField: 'Records',
+ sourceField: '___records___',
},
col2: {
label: 'Changed label',
@@ -2382,7 +2382,7 @@ describe('state_helpers', () => {
isBucketed: false,
operationType: 'count',
- sourceField: 'Records',
+ sourceField: '___records___',
},
col2: {
label: 'Test reference',
@@ -2566,7 +2566,7 @@ describe('state_helpers', () => {
operationType: 'count',
isBucketed: false,
scale: 'ratio',
- sourceField: 'Records',
+ sourceField: '___records___',
customLabel: true,
},
date: {
@@ -2598,7 +2598,7 @@ describe('state_helpers', () => {
operationType: 'count',
isBucketed: false,
scale: 'ratio',
- sourceField: 'Records',
+ sourceField: '___records___',
customLabel: true,
},
math: {
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/time_scale_utils.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/time_scale_utils.test.ts
index 30f8c85a81b90..1eb02fa82ceef 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/time_scale_utils.test.ts
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/time_scale_utils.test.ts
@@ -99,7 +99,7 @@ describe('time scale utils', () => {
describe('adjustTimeScaleOnOtherColumnChange', () => {
const baseColumn: GenericIndexPatternColumn = {
operationType: 'count',
- sourceField: 'Records',
+ sourceField: '___records___',
label: 'Count of records per second',
dataType: 'number',
isBucketed: false,
diff --git a/x-pack/plugins/lens/server/embeddable/make_lens_embeddable_factory.ts b/x-pack/plugins/lens/server/embeddable/make_lens_embeddable_factory.ts
index f516a99b078f2..332f20a00f66d 100644
--- a/x-pack/plugins/lens/server/embeddable/make_lens_embeddable_factory.ts
+++ b/x-pack/plugins/lens/server/embeddable/make_lens_embeddable_factory.ts
@@ -17,6 +17,7 @@ import {
commonRemoveTimezoneDateHistogramParam,
commonRenameFilterReferences,
commonRenameOperationsForFormula,
+ commonRenameRecordsField,
commonUpdateVisLayerType,
getLensFilterMigrations,
} from '../migrations/common_migrations';
@@ -68,8 +69,10 @@ export const makeLensEmbeddableFactory =
} as unknown as SerializableRecord;
},
'8.1.0': (state) => {
- const lensState = state as unknown as { attributes: LensDocShape715 };
- const migratedLensState = commonRenameFilterReferences(lensState.attributes);
+ const lensState = state as unknown as { attributes: LensDocShape715 };
+ const migratedLensState = commonRenameRecordsField(
+ commonRenameFilterReferences(lensState.attributes)
+ );
return {
...lensState,
attributes: migratedLensState,
diff --git a/x-pack/plugins/lens/server/migrations/common_migrations.ts b/x-pack/plugins/lens/server/migrations/common_migrations.ts
index 82cde025e31ed..5f3ea4d57fa2c 100644
--- a/x-pack/plugins/lens/server/migrations/common_migrations.ts
+++ b/x-pack/plugins/lens/server/migrations/common_migrations.ts
@@ -22,8 +22,9 @@ import {
VisStatePost715,
VisStatePre715,
VisState716,
+ LensDocShape810,
} from './types';
-import { CustomPaletteParams, layerTypes } from '../../common';
+import { CustomPaletteParams, DOCUMENT_FIELD_NAME, layerTypes } from '../../common';
import { LensDocShape } from './saved_object_migrations';
export const commonRenameOperationsForFormula = (
@@ -162,13 +163,31 @@ export const commonMakeReversePaletteAsCustom = (
return newAttributes;
};
-export const commonRenameFilterReferences = (attributes: LensDocShape715) => {
+export const commonRenameRecordsField = (attributes: LensDocShape810) => {
+ const newAttributes = cloneDeep(attributes);
+ Object.keys(newAttributes.state?.datasourceStates?.indexpattern?.layers || {}).forEach(
+ (layerId) => {
+ newAttributes.state.datasourceStates.indexpattern.layers[layerId].columnOrder.forEach(
+ (columnId) => {
+ const column =
+ newAttributes.state.datasourceStates.indexpattern.layers[layerId].columns[columnId];
+ if (column && column.operationType === 'count') {
+ column.sourceField = DOCUMENT_FIELD_NAME;
+ }
+ }
+ );
+ }
+ );
+ return newAttributes;
+};
+
+export const commonRenameFilterReferences = (attributes: LensDocShape715): LensDocShape810 => {
const newAttributes = cloneDeep(attributes);
for (const filter of newAttributes.state.filters) {
filter.meta.index = filter.meta.indexRefName;
delete filter.meta.indexRefName;
}
- return newAttributes;
+ return newAttributes as LensDocShape810;
};
const getApplyFilterMigrationToLens = (filterMigration: MigrateFunction) => {
diff --git a/x-pack/plugins/lens/server/migrations/saved_object_migrations.test.ts b/x-pack/plugins/lens/server/migrations/saved_object_migrations.test.ts
index 9a6407ae30552..a78cfa5e72237 100644
--- a/x-pack/plugins/lens/server/migrations/saved_object_migrations.test.ts
+++ b/x-pack/plugins/lens/server/migrations/saved_object_migrations.test.ts
@@ -12,7 +12,13 @@ import {
SavedObjectMigrationFn,
SavedObjectUnsanitizedDoc,
} from 'src/core/server';
-import { LensDocShape715, VisState716, VisStatePost715, VisStatePre715 } from './types';
+import {
+ LensDocShape715,
+ LensDocShape810,
+ VisState716,
+ VisStatePost715,
+ VisStatePre715,
+} from './types';
import { CustomPaletteParams, layerTypes } from '../../common';
import { PaletteOutput } from 'src/plugins/charts/common';
import { Filter } from '@kbn/es-query';
@@ -1513,6 +1519,78 @@ describe('Lens migrations', () => {
});
});
+ describe('8.1.0 rename records field', () => {
+ const context = { log: { warning: () => {} } } as unknown as SavedObjectMigrationContext;
+ const example = {
+ type: 'lens',
+ id: 'mocked-saved-object-id',
+ attributes: {
+ savedObjectId: '1',
+ title: 'MyRenamedOps',
+ description: '',
+ visualizationType: null,
+ state: {
+ datasourceMetaData: {
+ filterableIndexPatterns: [],
+ },
+ datasourceStates: {
+ indexpattern: {
+ currentIndexPatternId: 'logstash-*',
+ layers: {
+ '2': {
+ columns: {
+ '3': {
+ label: '@timestamp',
+ dataType: 'date',
+ operationType: 'date_histogram',
+ sourceField: '@timestamp',
+ isBucketed: true,
+ scale: 'interval',
+ params: { interval: 'auto', timeZone: 'Europe/Berlin' },
+ },
+ '4': {
+ label: 'Anzahl der Aufnahmen',
+ dataType: 'number',
+ operationType: 'count',
+ sourceField: 'Aufnahmen',
+ isBucketed: false,
+ scale: 'ratio',
+ },
+ '5': {
+ label: 'Sum of bytes',
+ dataType: 'numver',
+ operationType: 'sum',
+ sourceField: 'bytes',
+ isBucketed: false,
+ scale: 'ratio',
+ },
+ },
+ columnOrder: ['3', '4', '5'],
+ },
+ },
+ },
+ },
+ visualization: {},
+ query: { query: '', language: 'kuery' },
+ filters: [],
+ },
+ },
+ } as unknown as SavedObjectUnsanitizedDoc;
+
+ it('should change field for count operations but not for others, not changing the vis', () => {
+ const result = migrations['8.1.0'](example, context) as ReturnType<
+ SavedObjectMigrationFn
+ >;
+
+ expect(
+ Object.values(
+ result.attributes.state.datasourceStates.indexpattern.layers['2'].columns
+ ).map((column) => column.sourceField)
+ ).toEqual(['@timestamp', '___records___', 'bytes']);
+ expect(example.attributes.state.visualization).toEqual(result.attributes.state.visualization);
+ });
+ });
+
test('should properly apply a filter migration within a lens visualization', () => {
const migrationVersion = 'some-version';
diff --git a/x-pack/plugins/lens/server/migrations/saved_object_migrations.ts b/x-pack/plugins/lens/server/migrations/saved_object_migrations.ts
index fc1718c1df2fa..d3efb4df44575 100644
--- a/x-pack/plugins/lens/server/migrations/saved_object_migrations.ts
+++ b/x-pack/plugins/lens/server/migrations/saved_object_migrations.ts
@@ -5,7 +5,7 @@
* 2.0.
*/
-import { cloneDeep, mergeWith } from 'lodash';
+import { cloneDeep, flow, mergeWith } from 'lodash';
import { fromExpression, toExpression, Ast, AstFunction } from '@kbn/interpreter';
import {
SavedObjectMigrationMap,
@@ -27,6 +27,7 @@ import {
VisStatePost715,
VisStatePre715,
VisState716,
+ LensDocShape810,
} from './types';
import {
commonRenameOperationsForFormula,
@@ -35,6 +36,7 @@ import {
commonMakeReversePaletteAsCustom,
commonRenameFilterReferences,
getLensFilterMigrations,
+ commonRenameRecordsField,
} from './common_migrations';
interface LensDocShapePre710 {
@@ -444,14 +446,16 @@ const moveDefaultReversedPaletteToCustom: SavedObjectMigrationFn<
return { ...newDoc, attributes: commonMakeReversePaletteAsCustom(newDoc.attributes) };
};
-const renameFilterReferences: SavedObjectMigrationFn<
- LensDocShape715,
- LensDocShape715
-> = (doc) => {
+const renameFilterReferences: SavedObjectMigrationFn = (doc) => {
const newDoc = cloneDeep(doc);
return { ...newDoc, attributes: commonRenameFilterReferences(newDoc.attributes) };
};
+const renameRecordsField: SavedObjectMigrationFn = (doc) => {
+ const newDoc = cloneDeep(doc);
+ return { ...newDoc, attributes: commonRenameRecordsField(newDoc.attributes) };
+};
+
const lensMigrations: SavedObjectMigrationMap = {
'7.7.0': removeInvalidAccessors,
// The order of these migrations matter, since the timefield migration relies on the aggConfigs
@@ -465,7 +469,7 @@ const lensMigrations: SavedObjectMigrationMap = {
'7.14.0': removeTimezoneDateHistogramParam,
'7.15.0': addLayerTypeToVisualization,
'7.16.0': moveDefaultReversedPaletteToCustom,
- '8.1.0': renameFilterReferences,
+ '8.1.0': flow(renameFilterReferences, renameRecordsField),
};
export const mergeSavedObjectMigrationMaps = (
From 940c4e28330a3005c35ea615a70adcde9e9fb02b Mon Sep 17 00:00:00 2001
From: Clint Andrew Hall
Date: Mon, 31 Jan 2022 08:07:04 -0600
Subject: [PATCH 17/65] [cloud] Create IFRAME chat component; add to Unified
Integrations (#123772)
* [cloud] Create IFRAME chat component; add to Unified Integrations
* Provide chat user data from endpoint
* Apply suggestions from code review
Co-authored-by: Luke Elmers
* Addressing feedback
* Addressing feedback
* Fixing package.json
* Addressing feedback
* Wrap chat config in an observable
* Add tests
* Add integration tests, docs
Co-authored-by: Sergei Poluektov
Co-authored-by: Luke Elmers
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
---
.../steps/storybooks/build_and_upload.js | 1 +
api_docs/cloud.devdocs.json | 123 +++++++++++++++---
package.json | 1 +
src/dev/storybook/aliases.ts | 1 +
.../lib/create_jwt_assertion.test.ts | 2 +-
.../lib/create_jwt_assertion.ts | 2 +-
x-pack/plugins/cloud/.storybook/decorator.tsx | 35 +++++
x-pack/plugins/cloud/.storybook/index.ts | 8 ++
x-pack/plugins/cloud/.storybook/main.ts | 10 ++
x-pack/plugins/cloud/.storybook/manager.ts | 20 +++
x-pack/plugins/cloud/.storybook/preview.ts | 11 ++
x-pack/plugins/cloud/common/constants.ts | 1 +
x-pack/plugins/cloud/common/types.ts | 12 ++
.../public/components/chat/chat.stories.tsx | 20 +++
.../cloud/public/components/chat/chat.tsx | 111 ++++++++++++++++
.../components/chat/get_chat_context.test.ts | 65 +++++++++
.../components/chat/get_chat_context.ts | 36 +++++
.../cloud/public/components/chat/index.ts | 12 ++
.../plugins/cloud/public/components/index.tsx | 26 ++++
x-pack/plugins/cloud/public/index.ts | 5 +-
x-pack/plugins/cloud/public/mocks.ts | 22 ----
x-pack/plugins/cloud/public/mocks.tsx | 49 +++++++
x-pack/plugins/cloud/public/plugin.test.ts | 111 ++++++++++++++++
.../cloud/public/{plugin.ts => plugin.tsx} | 87 ++++++++++++-
.../plugins/cloud/public/services/index.tsx | 46 +++++++
x-pack/plugins/cloud/server/config.ts | 24 ++--
x-pack/plugins/cloud/server/plugin.ts | 16 ++-
.../plugins/cloud/server/routes/chat.test.ts | 109 ++++++++++++++++
x-pack/plugins/cloud/server/routes/chat.ts | 64 +++++++++
.../cloud/server/util/generate_jwt.test.ts | 23 ++++
.../plugins/cloud/server/util/generate_jwt.ts | 24 ++++
x-pack/plugins/cloud/tsconfig.json | 2 +
.../fleet/.storybook/context/index.tsx | 5 +-
x-pack/plugins/fleet/kibana.json | 2 +-
.../public/applications/integrations/app.tsx | 29 +++--
.../public/mock/fleet_start_services.tsx | 11 +-
.../fleet/public/mock/plugin_dependencies.ts | 10 +-
x-pack/plugins/fleet/public/plugin.ts | 20 ++-
x-pack/test/cloud_integration/config.ts | 18 ++-
x-pack/test/cloud_integration/tests/chat.ts | 30 +++++
40 files changed, 1125 insertions(+), 79 deletions(-)
create mode 100644 x-pack/plugins/cloud/.storybook/decorator.tsx
create mode 100644 x-pack/plugins/cloud/.storybook/index.ts
create mode 100644 x-pack/plugins/cloud/.storybook/main.ts
create mode 100644 x-pack/plugins/cloud/.storybook/manager.ts
create mode 100644 x-pack/plugins/cloud/.storybook/preview.ts
create mode 100644 x-pack/plugins/cloud/common/types.ts
create mode 100644 x-pack/plugins/cloud/public/components/chat/chat.stories.tsx
create mode 100644 x-pack/plugins/cloud/public/components/chat/chat.tsx
create mode 100644 x-pack/plugins/cloud/public/components/chat/get_chat_context.test.ts
create mode 100644 x-pack/plugins/cloud/public/components/chat/get_chat_context.ts
create mode 100644 x-pack/plugins/cloud/public/components/chat/index.ts
create mode 100644 x-pack/plugins/cloud/public/components/index.tsx
delete mode 100644 x-pack/plugins/cloud/public/mocks.ts
create mode 100644 x-pack/plugins/cloud/public/mocks.tsx
rename x-pack/plugins/cloud/public/{plugin.ts => plugin.tsx} (80%)
create mode 100644 x-pack/plugins/cloud/public/services/index.tsx
create mode 100644 x-pack/plugins/cloud/server/routes/chat.test.ts
create mode 100644 x-pack/plugins/cloud/server/routes/chat.ts
create mode 100644 x-pack/plugins/cloud/server/util/generate_jwt.test.ts
create mode 100644 x-pack/plugins/cloud/server/util/generate_jwt.ts
create mode 100644 x-pack/test/cloud_integration/tests/chat.ts
diff --git a/.buildkite/scripts/steps/storybooks/build_and_upload.js b/.buildkite/scripts/steps/storybooks/build_and_upload.js
index 328c769d81c66..0af75e72de78a 100644
--- a/.buildkite/scripts/steps/storybooks/build_and_upload.js
+++ b/.buildkite/scripts/steps/storybooks/build_and_upload.js
@@ -15,6 +15,7 @@ const STORYBOOKS = [
'apm',
'canvas',
'ci_composite',
+ 'cloud',
'codeeditor',
'custom_integrations',
'dashboard_enhanced',
diff --git a/api_docs/cloud.devdocs.json b/api_docs/cloud.devdocs.json
index 6c6dbf6f48003..ce0ded5fa875b 100644
--- a/api_docs/cloud.devdocs.json
+++ b/api_docs/cloud.devdocs.json
@@ -2,7 +2,24 @@
"id": "cloud",
"client": {
"classes": [],
- "functions": [],
+ "functions": [
+ {
+ "parentPluginId": "cloud",
+ "id": "def-public.Chat",
+ "type": "Function",
+ "tags": [],
+ "label": "Chat",
+ "description": [],
+ "signature": [
+ "() => JSX.Element"
+ ],
+ "path": "x-pack/plugins/cloud/public/components/index.tsx",
+ "deprecated": false,
+ "children": [],
+ "returnComment": [],
+ "initialIsOpen": false
+ }
+ ],
"interfaces": [
{
"parentPluginId": "cloud",
@@ -11,7 +28,7 @@
"tags": [],
"label": "CloudConfigType",
"description": [],
- "path": "x-pack/plugins/cloud/public/plugin.ts",
+ "path": "x-pack/plugins/cloud/public/plugin.tsx",
"deprecated": false,
"children": [
{
@@ -24,7 +41,7 @@
"signature": [
"string | undefined"
],
- "path": "x-pack/plugins/cloud/public/plugin.ts",
+ "path": "x-pack/plugins/cloud/public/plugin.tsx",
"deprecated": false
},
{
@@ -37,7 +54,7 @@
"signature": [
"string | undefined"
],
- "path": "x-pack/plugins/cloud/public/plugin.ts",
+ "path": "x-pack/plugins/cloud/public/plugin.tsx",
"deprecated": false
},
{
@@ -50,7 +67,7 @@
"signature": [
"string | undefined"
],
- "path": "x-pack/plugins/cloud/public/plugin.ts",
+ "path": "x-pack/plugins/cloud/public/plugin.tsx",
"deprecated": false
},
{
@@ -63,7 +80,7 @@
"signature": [
"string | undefined"
],
- "path": "x-pack/plugins/cloud/public/plugin.ts",
+ "path": "x-pack/plugins/cloud/public/plugin.tsx",
"deprecated": false
},
{
@@ -76,7 +93,7 @@
"signature": [
"string | undefined"
],
- "path": "x-pack/plugins/cloud/public/plugin.ts",
+ "path": "x-pack/plugins/cloud/public/plugin.tsx",
"deprecated": false
},
{
@@ -89,7 +106,7 @@
"signature": [
"string | undefined"
],
- "path": "x-pack/plugins/cloud/public/plugin.ts",
+ "path": "x-pack/plugins/cloud/public/plugin.tsx",
"deprecated": false
},
{
@@ -102,8 +119,78 @@
"signature": [
"{ enabled: boolean; org_id?: string | undefined; }"
],
- "path": "x-pack/plugins/cloud/public/plugin.ts",
+ "path": "x-pack/plugins/cloud/public/plugin.tsx",
"deprecated": false
+ },
+ {
+ "parentPluginId": "cloud",
+ "id": "def-public.CloudConfigType.chat",
+ "type": "Object",
+ "tags": [],
+ "label": "chat",
+ "description": [],
+ "signature": [
+ "{ enabled: boolean; chatURL: string; }"
+ ],
+ "path": "x-pack/plugins/cloud/public/plugin.tsx",
+ "deprecated": false
+ }
+ ],
+ "initialIsOpen": false
+ },
+ {
+ "parentPluginId": "cloud",
+ "id": "def-public.CloudStart",
+ "type": "Interface",
+ "tags": [],
+ "label": "CloudStart",
+ "description": [],
+ "path": "x-pack/plugins/cloud/public/plugin.tsx",
+ "deprecated": false,
+ "children": [
+ {
+ "parentPluginId": "cloud",
+ "id": "def-public.CloudStart.CloudContextProvider",
+ "type": "Function",
+ "tags": [],
+ "label": "CloudContextProvider",
+ "description": [
+ "\nA React component that provides a pre-wired `React.Context` which connects components to Cloud services."
+ ],
+ "signature": [
+ "React.FunctionComponent<{}>"
+ ],
+ "path": "x-pack/plugins/cloud/public/plugin.tsx",
+ "deprecated": false,
+ "returnComment": [],
+ "children": [
+ {
+ "parentPluginId": "cloud",
+ "id": "def-public.CloudStart.CloudContextProvider.$1",
+ "type": "CompoundType",
+ "tags": [],
+ "label": "props",
+ "description": [],
+ "signature": [
+ "P & { children?: React.ReactNode; }"
+ ],
+ "path": "node_modules/@types/react/index.d.ts",
+ "deprecated": false
+ },
+ {
+ "parentPluginId": "cloud",
+ "id": "def-public.CloudStart.CloudContextProvider.$2",
+ "type": "Any",
+ "tags": [],
+ "label": "context",
+ "description": [],
+ "signature": [
+ "any"
+ ],
+ "path": "node_modules/@types/react/index.d.ts",
+ "deprecated": false
+ }
+ ]
}
],
"initialIsOpen": false
@@ -119,7 +206,7 @@
"tags": [],
"label": "CloudSetup",
"description": [],
- "path": "x-pack/plugins/cloud/public/plugin.ts",
+ "path": "x-pack/plugins/cloud/public/plugin.tsx",
"deprecated": false,
"children": [
{
@@ -132,7 +219,7 @@
"signature": [
"string | undefined"
],
- "path": "x-pack/plugins/cloud/public/plugin.ts",
+ "path": "x-pack/plugins/cloud/public/plugin.tsx",
"deprecated": false
},
{
@@ -145,7 +232,7 @@
"signature": [
"string | undefined"
],
- "path": "x-pack/plugins/cloud/public/plugin.ts",
+ "path": "x-pack/plugins/cloud/public/plugin.tsx",
"deprecated": false
},
{
@@ -158,7 +245,7 @@
"signature": [
"string | undefined"
],
- "path": "x-pack/plugins/cloud/public/plugin.ts",
+ "path": "x-pack/plugins/cloud/public/plugin.tsx",
"deprecated": false
},
{
@@ -171,7 +258,7 @@
"signature": [
"string | undefined"
],
- "path": "x-pack/plugins/cloud/public/plugin.ts",
+ "path": "x-pack/plugins/cloud/public/plugin.tsx",
"deprecated": false
},
{
@@ -184,7 +271,7 @@
"signature": [
"string | undefined"
],
- "path": "x-pack/plugins/cloud/public/plugin.ts",
+ "path": "x-pack/plugins/cloud/public/plugin.tsx",
"deprecated": false
},
{
@@ -197,7 +284,7 @@
"signature": [
"string | undefined"
],
- "path": "x-pack/plugins/cloud/public/plugin.ts",
+ "path": "x-pack/plugins/cloud/public/plugin.tsx",
"deprecated": false
},
{
@@ -210,7 +297,7 @@
"signature": [
"string | undefined"
],
- "path": "x-pack/plugins/cloud/public/plugin.ts",
+ "path": "x-pack/plugins/cloud/public/plugin.tsx",
"deprecated": false
},
{
@@ -220,7 +307,7 @@
"tags": [],
"label": "isCloudEnabled",
"description": [],
- "path": "x-pack/plugins/cloud/public/plugin.ts",
+ "path": "x-pack/plugins/cloud/public/plugin.tsx",
"deprecated": false
}
],
diff --git a/package.json b/package.json
index c5c8a81a82017..3a8673b15de1e 100644
--- a/package.json
+++ b/package.json
@@ -278,6 +278,7 @@
"json-stable-stringify": "^1.0.1",
"json-stringify-pretty-compact": "1.2.0",
"json-stringify-safe": "5.0.1",
+ "jsonwebtoken": "^8.3.0",
"jsts": "^1.6.2",
"kea": "^2.4.2",
"load-json-file": "^6.2.0",
diff --git a/src/dev/storybook/aliases.ts b/src/dev/storybook/aliases.ts
index 896ed6b0bb536..d6526781e7373 100644
--- a/src/dev/storybook/aliases.ts
+++ b/src/dev/storybook/aliases.ts
@@ -11,6 +11,7 @@ export const storybookAliases = {
apm: 'x-pack/plugins/apm/.storybook',
canvas: 'x-pack/plugins/canvas/storybook',
ci_composite: '.ci/.storybook',
+ cloud: 'x-pack/plugins/cloud/.storybook',
codeeditor: 'src/plugins/kibana_react/public/code_editor/.storybook',
controls: 'src/plugins/controls/storybook',
custom_integrations: 'src/plugins/custom_integrations/storybook',
diff --git a/x-pack/plugins/actions/server/builtin_action_types/lib/create_jwt_assertion.test.ts b/x-pack/plugins/actions/server/builtin_action_types/lib/create_jwt_assertion.test.ts
index abeb14de4c43b..ed6497573a362 100644
--- a/x-pack/plugins/actions/server/builtin_action_types/lib/create_jwt_assertion.test.ts
+++ b/x-pack/plugins/actions/server/builtin_action_types/lib/create_jwt_assertion.test.ts
@@ -8,7 +8,7 @@
jest.mock('jsonwebtoken', () => ({
sign: jest.fn(),
}));
-// eslint-disable-next-line import/no-extraneous-dependencies
+
import jwt from 'jsonwebtoken';
import { Logger } from '../../../../../../src/core/server';
import { loggingSystemMock } from '../../../../../../src/core/server/mocks';
diff --git a/x-pack/plugins/actions/server/builtin_action_types/lib/create_jwt_assertion.ts b/x-pack/plugins/actions/server/builtin_action_types/lib/create_jwt_assertion.ts
index 9f5102b336eda..79230a99a3822 100644
--- a/x-pack/plugins/actions/server/builtin_action_types/lib/create_jwt_assertion.ts
+++ b/x-pack/plugins/actions/server/builtin_action_types/lib/create_jwt_assertion.ts
@@ -4,7 +4,7 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
-// eslint-disable-next-line import/no-extraneous-dependencies
+
import jwt, { Algorithm } from 'jsonwebtoken';
import { Logger } from '../../../../../../src/core/server';
diff --git a/x-pack/plugins/cloud/.storybook/decorator.tsx b/x-pack/plugins/cloud/.storybook/decorator.tsx
new file mode 100644
index 0000000000000..4489b58f75759
--- /dev/null
+++ b/x-pack/plugins/cloud/.storybook/decorator.tsx
@@ -0,0 +1,35 @@
+/*
+ * 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 React from 'react';
+import { DecoratorFn } from '@storybook/react';
+import { ServicesProvider, CloudServices } from '../public/services';
+
+// TODO: move to a storybook implementation of the service using parameters.
+const services: CloudServices = {
+ chat: {
+ enabled: true,
+ chatURL: 'https://elasticcloud-production-chat-us-east-1.s3.amazonaws.com/drift-iframe.html',
+ user: {
+ id: 'user-id',
+ email: 'test-user@elastic.co',
+ // this doesn't affect chat appearance,
+ // but a user identity in Drift only
+ jwt: 'identity-jwt',
+ },
+ },
+};
+
+export const getCloudContextProvider: () => React.FC =
+ () =>
+ ({ children }) =>
+ {children};
+
+export const getCloudContextDecorator: DecoratorFn = (storyFn) => {
+ const CloudContextProvider = getCloudContextProvider();
+ return {storyFn()};
+};
diff --git a/x-pack/plugins/cloud/.storybook/index.ts b/x-pack/plugins/cloud/.storybook/index.ts
new file mode 100644
index 0000000000000..321df983cb20d
--- /dev/null
+++ b/x-pack/plugins/cloud/.storybook/index.ts
@@ -0,0 +1,8 @@
+/*
+ * 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.
+ */
+
+export { getCloudContextDecorator, getCloudContextProvider } from './decorator';
diff --git a/x-pack/plugins/cloud/.storybook/main.ts b/x-pack/plugins/cloud/.storybook/main.ts
new file mode 100644
index 0000000000000..bf63e08d64c32
--- /dev/null
+++ b/x-pack/plugins/cloud/.storybook/main.ts
@@ -0,0 +1,10 @@
+/*
+ * 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 { defaultConfig } from '@kbn/storybook';
+
+module.exports = defaultConfig;
diff --git a/x-pack/plugins/cloud/.storybook/manager.ts b/x-pack/plugins/cloud/.storybook/manager.ts
new file mode 100644
index 0000000000000..54c3d31c2002f
--- /dev/null
+++ b/x-pack/plugins/cloud/.storybook/manager.ts
@@ -0,0 +1,20 @@
+/*
+ * 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 { addons } from '@storybook/addons';
+import { create } from '@storybook/theming';
+import { PANEL_ID } from '@storybook/addon-actions';
+
+addons.setConfig({
+ theme: create({
+ base: 'light',
+ brandTitle: 'Cloud Storybook',
+ brandUrl: 'https://github.com/elastic/kibana/tree/main/x-pack/plugins/cloud',
+ }),
+ showPanel: true.valueOf,
+ selectedPanel: PANEL_ID,
+});
diff --git a/x-pack/plugins/cloud/.storybook/preview.ts b/x-pack/plugins/cloud/.storybook/preview.ts
new file mode 100644
index 0000000000000..83c512e516d5a
--- /dev/null
+++ b/x-pack/plugins/cloud/.storybook/preview.ts
@@ -0,0 +1,11 @@
+/*
+ * 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 { addDecorator } from '@storybook/react';
+import { getCloudContextDecorator } from './decorator';
+
+addDecorator(getCloudContextDecorator);
diff --git a/x-pack/plugins/cloud/common/constants.ts b/x-pack/plugins/cloud/common/constants.ts
index fc37906299d14..09333e3773fe9 100644
--- a/x-pack/plugins/cloud/common/constants.ts
+++ b/x-pack/plugins/cloud/common/constants.ts
@@ -6,6 +6,7 @@
*/
export const ELASTIC_SUPPORT_LINK = 'https://cloud.elastic.co/support';
+export const GET_CHAT_USER_DATA_ROUTE_PATH = '/internal/cloud/chat_user';
/**
* This is the page for managing your snapshots on Cloud.
diff --git a/x-pack/plugins/cloud/common/types.ts b/x-pack/plugins/cloud/common/types.ts
new file mode 100644
index 0000000000000..38ebeaf5f467c
--- /dev/null
+++ b/x-pack/plugins/cloud/common/types.ts
@@ -0,0 +1,12 @@
+/*
+ * 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.
+ */
+
+export interface GetChatUserDataResponseBody {
+ token: string;
+ email: string;
+ id: string;
+}
diff --git a/x-pack/plugins/cloud/public/components/chat/chat.stories.tsx b/x-pack/plugins/cloud/public/components/chat/chat.stories.tsx
new file mode 100644
index 0000000000000..668017e134e75
--- /dev/null
+++ b/x-pack/plugins/cloud/public/components/chat/chat.stories.tsx
@@ -0,0 +1,20 @@
+/*
+ * 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 React from 'react';
+
+import { Chat } from './chat';
+
+export default {
+ title: 'Chat Widget',
+ description: '',
+ parameters: {},
+};
+
+export const Component = () => {
+ return ;
+};
diff --git a/x-pack/plugins/cloud/public/components/chat/chat.tsx b/x-pack/plugins/cloud/public/components/chat/chat.tsx
new file mode 100644
index 0000000000000..99b53f553e75f
--- /dev/null
+++ b/x-pack/plugins/cloud/public/components/chat/chat.tsx
@@ -0,0 +1,111 @@
+/*
+ * 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 React, { useEffect, useRef, useState, CSSProperties } from 'react';
+import { css } from '@emotion/react';
+import { useChat } from '../../services';
+import { getChatContext } from './get_chat_context';
+
+type UseChatType =
+ | { enabled: false }
+ | {
+ enabled: true;
+ src: string;
+ ref: React.MutableRefObject;
+ style: CSSProperties;
+ isReady: boolean;
+ };
+
+const MESSAGE_READY = 'driftIframeReady';
+const MESSAGE_RESIZE = 'driftIframeResize';
+const MESSAGE_SET_CONTEXT = 'driftSetContext';
+
+const useChatConfig = (): UseChatType => {
+ const ref = useRef(null);
+ const chat = useChat();
+ const [style, setStyle] = useState({});
+ const [isReady, setIsReady] = useState(false);
+
+ useEffect(() => {
+ const handleMessage = (event: MessageEvent): void => {
+ const { current: chatIframe } = ref;
+
+ if (
+ !chat.enabled ||
+ !chatIframe?.contentWindow ||
+ event.source !== chatIframe?.contentWindow
+ ) {
+ return;
+ }
+
+ const context = getChatContext();
+ const { data: message } = event;
+ const { user: userConfig } = chat;
+ const { id, email, jwt } = userConfig;
+
+ switch (message.type) {
+ case MESSAGE_READY: {
+ const user = {
+ id,
+ attributes: {
+ email,
+ },
+ jwt,
+ };
+
+ chatIframe.contentWindow.postMessage(
+ {
+ type: MESSAGE_SET_CONTEXT,
+ data: { context, user },
+ },
+ '*'
+ );
+
+ setIsReady(true);
+
+ break;
+ }
+
+ case MESSAGE_RESIZE: {
+ const styles = message.data.styles || ({} as CSSProperties);
+ setStyle({ ...style, ...styles });
+ break;
+ }
+
+ default:
+ break;
+ }
+ };
+
+ window.addEventListener('message', handleMessage);
+
+ return () => window.removeEventListener('message', handleMessage);
+ }, [chat, style]);
+
+ if (chat.enabled) {
+ return { enabled: true, src: chat.chatURL, ref, style, isReady };
+ }
+
+ return { enabled: false };
+};
+
+export const Chat = () => {
+ const config = useChatConfig();
+
+ if (!config.enabled) {
+ return null;
+ }
+
+ const iframeStyle = css`
+ position: fixed;
+ botton: 30px;
+ right: 30px;
+ visibility: ${config.isReady ? 'visible' : 'hidden'};
+ `;
+
+ return ;
+};
diff --git a/x-pack/plugins/cloud/public/components/chat/get_chat_context.test.ts b/x-pack/plugins/cloud/public/components/chat/get_chat_context.test.ts
new file mode 100644
index 0000000000000..e3e2675d0291c
--- /dev/null
+++ b/x-pack/plugins/cloud/public/components/chat/get_chat_context.test.ts
@@ -0,0 +1,65 @@
+/*
+ * 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 { getChatContext } from './get_chat_context';
+
+const PROTOCOL = 'http:';
+const PORT = '1234';
+const HASH = '#/discover?_g=()&_a=()';
+const HOST_NAME = 'www.kibana.com';
+const PATH_NAME = '/app/kibana';
+const HOST = `${HOST_NAME}:${PORT}`;
+const ORIGIN = `${PROTOCOL}//${HOST}`;
+const HREF = `${ORIGIN}${PATH_NAME}${HASH}`;
+const USER_AGENT = 'user-agent';
+const LANGUAGE = 'la-ng';
+const TITLE = 'title';
+const REFERRER = 'referrer';
+
+describe('getChatContext', () => {
+ const url = new URL(HREF);
+
+ test('retreive the context', () => {
+ Object.defineProperty(window, 'location', { value: url });
+ Object.defineProperty(window, 'navigator', {
+ value: {
+ language: LANGUAGE,
+ userAgent: USER_AGENT,
+ },
+ });
+ Object.defineProperty(window.document, 'referrer', { value: REFERRER });
+ window.document.title = TITLE;
+
+ const context = getChatContext();
+
+ expect(context).toStrictEqual({
+ window: {
+ location: {
+ hash: HASH,
+ host: HOST,
+ hostname: HOST_NAME,
+ href: HREF,
+ origin: ORIGIN,
+ pathname: PATH_NAME,
+ port: PORT,
+ protocol: PROTOCOL,
+ search: '',
+ },
+ navigator: {
+ language: LANGUAGE,
+ userAgent: USER_AGENT,
+ },
+ innerHeight: 768,
+ innerWidth: 1024,
+ },
+ document: {
+ title: TITLE,
+ referrer: REFERRER,
+ },
+ });
+ });
+});
diff --git a/x-pack/plugins/cloud/public/components/chat/get_chat_context.ts b/x-pack/plugins/cloud/public/components/chat/get_chat_context.ts
new file mode 100644
index 0000000000000..e29a2efa24803
--- /dev/null
+++ b/x-pack/plugins/cloud/public/components/chat/get_chat_context.ts
@@ -0,0 +1,36 @@
+/*
+ * 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.
+ */
+
+export const getChatContext = () => {
+ const { location, navigator, innerHeight, innerWidth } = window;
+ const { hash, host, hostname, href, origin, pathname, port, protocol, search } = location;
+ const { language, userAgent } = navigator;
+ const { title, referrer } = document;
+
+ return {
+ window: {
+ location: {
+ hash,
+ host,
+ hostname,
+ href,
+ origin,
+ pathname,
+ port,
+ protocol,
+ search,
+ },
+ navigator: { language, userAgent },
+ innerHeight,
+ innerWidth,
+ },
+ document: {
+ title,
+ referrer,
+ },
+ };
+};
diff --git a/x-pack/plugins/cloud/public/components/chat/index.ts b/x-pack/plugins/cloud/public/components/chat/index.ts
new file mode 100644
index 0000000000000..ceed215208b64
--- /dev/null
+++ b/x-pack/plugins/cloud/public/components/chat/index.ts
@@ -0,0 +1,12 @@
+/*
+ * 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.
+ */
+
+/**
+ * A component that will display a trigger that will allow the user to chat with a human operator,
+ * when the service is enabled; otherwise, it renders nothing.
+ */
+export { Chat } from './chat';
diff --git a/x-pack/plugins/cloud/public/components/index.tsx b/x-pack/plugins/cloud/public/components/index.tsx
new file mode 100644
index 0000000000000..e0aab671de279
--- /dev/null
+++ b/x-pack/plugins/cloud/public/components/index.tsx
@@ -0,0 +1,26 @@
+/*
+ * 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 React, { Suspense } from 'react';
+import { EuiErrorBoundary } from '@elastic/eui';
+
+/**
+ * A suspense-compatible version of the Chat component.
+ */
+export const LazyChat = React.lazy(() => import('./chat').then(({ Chat }) => ({ default: Chat })));
+
+/**
+ * A lazily-loaded component that will display a trigger that will allow the user to chat with a
+ * human operator when the service is enabled; otherwise, it renders nothing.
+ */
+export const Chat = () => (
+
+ }>
+
+
+
+);
diff --git a/x-pack/plugins/cloud/public/index.ts b/x-pack/plugins/cloud/public/index.ts
index d51def6fa6641..73aa3a350e8ea 100644
--- a/x-pack/plugins/cloud/public/index.ts
+++ b/x-pack/plugins/cloud/public/index.ts
@@ -8,7 +8,10 @@
import { PluginInitializerContext } from '../../../../src/core/public';
import { CloudPlugin } from './plugin';
-export type { CloudSetup, CloudConfigType } from './plugin';
+export type { CloudSetup, CloudConfigType, CloudStart } from './plugin';
+
export function plugin(initializerContext: PluginInitializerContext) {
return new CloudPlugin(initializerContext);
}
+
+export { Chat } from './components';
diff --git a/x-pack/plugins/cloud/public/mocks.ts b/x-pack/plugins/cloud/public/mocks.ts
deleted file mode 100644
index 52a027e899d0d..0000000000000
--- a/x-pack/plugins/cloud/public/mocks.ts
+++ /dev/null
@@ -1,22 +0,0 @@
-/*
- * 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.
- */
-
-function createSetupMock() {
- return {
- cloudId: 'mock-cloud-id',
- isCloudEnabled: true,
- cname: 'cname',
- baseUrl: 'base-url',
- deploymentUrl: 'deployment-url',
- profileUrl: 'profile-url',
- organizationUrl: 'organization-url',
- };
-}
-
-export const cloudMock = {
- createSetup: createSetupMock,
-};
diff --git a/x-pack/plugins/cloud/public/mocks.tsx b/x-pack/plugins/cloud/public/mocks.tsx
new file mode 100644
index 0000000000000..5bef215ffb0ea
--- /dev/null
+++ b/x-pack/plugins/cloud/public/mocks.tsx
@@ -0,0 +1,49 @@
+/*
+ * 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 React from 'react';
+
+import { CloudStart } from '.';
+import { ServicesProvider } from '../public/services';
+
+function createSetupMock() {
+ return {
+ cloudId: 'mock-cloud-id',
+ isCloudEnabled: true,
+ cname: 'cname',
+ baseUrl: 'base-url',
+ deploymentUrl: 'deployment-url',
+ profileUrl: 'profile-url',
+ organizationUrl: 'organization-url',
+ };
+}
+
+const config = {
+ chat: {
+ enabled: true,
+ chatURL: 'chat-url',
+ user: {
+ id: 'user-id',
+ email: 'test-user@elastic.co',
+ jwt: 'identity-jwt',
+ },
+ },
+};
+
+const getContextProvider: () => React.FC =
+ () =>
+ ({ children }) =>
+ {children};
+
+const createStartMock = (): jest.Mocked => ({
+ CloudContextProvider: jest.fn(getContextProvider()),
+});
+
+export const cloudMock = {
+ createSetup: createSetupMock,
+ createStart: createStartMock,
+};
diff --git a/x-pack/plugins/cloud/public/plugin.test.ts b/x-pack/plugins/cloud/public/plugin.test.ts
index 070202782411d..7198fb6f8a774 100644
--- a/x-pack/plugins/cloud/public/plugin.test.ts
+++ b/x-pack/plugins/cloud/public/plugin.test.ts
@@ -40,6 +40,9 @@ describe('Cloud Plugin', () => {
full_story: {
enabled: false,
},
+ chat: {
+ enabled: false,
+ },
...config,
});
@@ -47,11 +50,15 @@ describe('Cloud Plugin', () => {
const coreSetup = coreMock.createSetup();
const coreStart = coreMock.createStart();
+
if (currentAppId$) {
coreStart.application.currentAppId$ = currentAppId$;
}
+
coreSetup.getStartServices.mockResolvedValue([coreStart, {}, undefined]);
+
const securitySetup = securityMock.createSetup();
+
securitySetup.authc.getCurrentUser.mockResolvedValue(
securityMock.createMockAuthenticatedUser(currentUserProps)
);
@@ -212,6 +219,101 @@ describe('Cloud Plugin', () => {
});
});
+ describe('setupChat', () => {
+ let consoleMock: jest.SpyInstance;
+
+ beforeEach(() => {
+ consoleMock = jest.spyOn(console, 'debug').mockImplementation(() => {});
+ });
+
+ afterEach(() => {
+ consoleMock.mockRestore();
+ });
+
+ const setupPlugin = async ({
+ config = {},
+ securityEnabled = true,
+ currentUserProps = {},
+ isCloudEnabled = true,
+ failHttp = false,
+ }: {
+ config?: Partial;
+ securityEnabled?: boolean;
+ currentUserProps?: Record;
+ isCloudEnabled?: boolean;
+ failHttp?: boolean;
+ }) => {
+ const initContext = coreMock.createPluginInitializerContext({
+ id: isCloudEnabled ? 'cloud-id' : null,
+ base_url: 'https://cloud.elastic.co',
+ deployment_url: '/abc123',
+ profile_url: '/profile/alice',
+ organization_url: '/org/myOrg',
+ full_story: {
+ enabled: false,
+ },
+ chat: {
+ enabled: false,
+ },
+ ...config,
+ });
+
+ const plugin = new CloudPlugin(initContext);
+
+ const coreSetup = coreMock.createSetup();
+ const coreStart = coreMock.createStart();
+
+ if (failHttp) {
+ coreSetup.http.get.mockImplementation(() => {
+ throw new Error('HTTP request failed');
+ });
+ }
+
+ coreSetup.getStartServices.mockResolvedValue([coreStart, {}, undefined]);
+
+ const securitySetup = securityMock.createSetup();
+ securitySetup.authc.getCurrentUser.mockResolvedValue(
+ securityMock.createMockAuthenticatedUser(currentUserProps)
+ );
+
+ const setup = plugin.setup(coreSetup, securityEnabled ? { security: securitySetup } : {});
+
+ return { initContext, plugin, setup, coreSetup };
+ };
+
+ it('chatConfig is not retrieved if cloud is not enabled', async () => {
+ const { coreSetup } = await setupPlugin({ isCloudEnabled: false });
+ expect(coreSetup.http.get).not.toHaveBeenCalled();
+ });
+
+ it('chatConfig is not retrieved if security is not enabled', async () => {
+ const { coreSetup } = await setupPlugin({ securityEnabled: false });
+ expect(coreSetup.http.get).not.toHaveBeenCalled();
+ });
+
+ it('chatConfig is not retrieved if chat is enabled but url is not provided', async () => {
+ // @ts-expect-error 2741
+ const { coreSetup } = await setupPlugin({ config: { chat: { enabled: true } } });
+ expect(coreSetup.http.get).not.toHaveBeenCalled();
+ });
+
+ it('chatConfig is not retrieved if internal API fails', async () => {
+ const { coreSetup } = await setupPlugin({
+ config: { chat: { enabled: true, chatURL: 'http://chat.elastic.co' } },
+ failHttp: true,
+ });
+ expect(coreSetup.http.get).toHaveBeenCalled();
+ expect(consoleMock).toHaveBeenCalled();
+ });
+
+ it('chatConfig is retrieved if chat is enabled and url is provided', async () => {
+ const { coreSetup } = await setupPlugin({
+ config: { chat: { enabled: true, chatURL: 'http://chat.elastic.co' } },
+ });
+ expect(coreSetup.http.get).toHaveBeenCalled();
+ });
+ });
+
describe('interface', () => {
const setupPlugin = () => {
const initContext = coreMock.createPluginInitializerContext({
@@ -221,6 +323,12 @@ describe('Cloud Plugin', () => {
deployment_url: '/abc123',
profile_url: '/user/settings/',
organization_url: '/account/',
+ chat: {
+ enabled: false,
+ },
+ full_story: {
+ enabled: false,
+ },
});
const plugin = new CloudPlugin(initContext);
@@ -284,6 +392,9 @@ describe('Cloud Plugin', () => {
full_story: {
enabled: false,
},
+ chat: {
+ enabled: false,
+ },
})
);
const coreSetup = coreMock.createSetup();
diff --git a/x-pack/plugins/cloud/public/plugin.ts b/x-pack/plugins/cloud/public/plugin.tsx
similarity index 80%
rename from x-pack/plugins/cloud/public/plugin.ts
rename to x-pack/plugins/cloud/public/plugin.tsx
index 81aad8bf79ccc..991a7c1f8b565 100644
--- a/x-pack/plugins/cloud/public/plugin.ts
+++ b/x-pack/plugins/cloud/public/plugin.tsx
@@ -5,6 +5,7 @@
* 2.0.
*/
+import React, { FC } from 'react';
import {
CoreSetup,
CoreStart,
@@ -15,17 +16,24 @@ import {
ApplicationStart,
} from 'src/core/public';
import { i18n } from '@kbn/i18n';
-import { Subscription } from 'rxjs';
+import useObservable from 'react-use/lib/useObservable';
+import { BehaviorSubject, Subscription } from 'rxjs';
import type {
AuthenticatedUser,
SecurityPluginSetup,
SecurityPluginStart,
} from '../../security/public';
import { getIsCloudEnabled } from '../common/is_cloud_enabled';
-import { ELASTIC_SUPPORT_LINK, CLOUD_SNAPSHOTS_PATH } from '../common/constants';
+import {
+ ELASTIC_SUPPORT_LINK,
+ CLOUD_SNAPSHOTS_PATH,
+ GET_CHAT_USER_DATA_ROUTE_PATH,
+} from '../common/constants';
+import type { GetChatUserDataResponseBody } from '../common/types';
import { HomePublicPluginSetup } from '../../../../src/plugins/home/public';
import { createUserMenuLinks } from './user_menu_links';
import { getFullCloudUrl } from './utils';
+import { ChatConfig, ServicesProvider } from './services';
export interface CloudConfigType {
id?: string;
@@ -38,6 +46,13 @@ export interface CloudConfigType {
enabled: boolean;
org_id?: string;
};
+ /** Configuration to enable live chat in Cloud-enabled instances of Kibana. */
+ chat: {
+ /** Determines if chat is enabled. */
+ enabled: boolean;
+ /** The URL to the remotely-hosted chat application. */
+ chatURL: string;
+ };
}
interface CloudSetupDependencies {
@@ -49,6 +64,13 @@ interface CloudStartDependencies {
security?: SecurityPluginStart;
}
+export interface CloudStart {
+ /**
+ * A React component that provides a pre-wired `React.Context` which connects components to Cloud services.
+ */
+ CloudContextProvider: FC<{}>;
+}
+
export interface CloudSetup {
cloudId?: string;
cname?: string;
@@ -65,10 +87,15 @@ interface SetupFullstoryDeps extends CloudSetupDependencies {
basePath: IBasePath;
}
+interface SetupChatDeps extends Pick {
+ http: CoreSetup['http'];
+}
+
export class CloudPlugin implements Plugin {
private config!: CloudConfigType;
private isCloudEnabled: boolean;
private appSubscription?: Subscription;
+ private chatConfig$ = new BehaviorSubject({ enabled: false });
constructor(private readonly initializerContext: PluginInitializerContext) {
this.config = this.initializerContext.config.get();
@@ -79,6 +106,7 @@ export class CloudPlugin implements Plugin {
const application = core.getStartServices().then(([coreStart]) => {
return coreStart.application;
});
+
this.setupFullstory({ basePath: core.http.basePath, security, application }).catch((e) =>
// eslint-disable-next-line no-console
console.debug(`Error setting up FullStory: ${e.toString()}`)
@@ -95,6 +123,11 @@ export class CloudPlugin implements Plugin {
this.isCloudEnabled = getIsCloudEnabled(id);
+ this.setupChat({ http: core.http, security }).catch((e) =>
+ // eslint-disable-next-line no-console
+ console.debug(`Error setting up Chat: ${e.toString()}`)
+ );
+
if (home) {
home.environment.update({ cloud: this.isCloudEnabled });
if (this.isCloudEnabled) {
@@ -119,7 +152,7 @@ export class CloudPlugin implements Plugin {
};
}
- public start(coreStart: CoreStart, { security }: CloudStartDependencies) {
+ public start(coreStart: CoreStart, { security }: CloudStartDependencies): CloudStart {
const { deployment_url: deploymentUrl, base_url: baseUrl } = this.config;
coreStart.chrome.setHelpSupportUrl(ELASTIC_SUPPORT_LINK);
@@ -147,6 +180,17 @@ export class CloudPlugin implements Plugin {
// In the event of an unexpected error, fail *open*.
// Cloud admin console will always perform the actual authorization checks.
.catch(() => setLinks(true));
+
+ // There's a risk that the request for chat config will take too much time to complete, and the provider
+ // will maintain a stale value. To avoid this, we'll use an Observable.
+ const CloudContextProvider: FC = ({ children }) => {
+ const chatConfig = useObservable(this.chatConfig$, { enabled: false });
+ return {children};
+ };
+
+ return {
+ CloudContextProvider,
+ };
}
public stop() {
@@ -266,6 +310,43 @@ export class CloudPlugin implements Plugin {
...memoryInfo,
});
}
+
+ private async setupChat({ http, security }: SetupChatDeps) {
+ if (!this.isCloudEnabled) {
+ return;
+ }
+
+ const { enabled, chatURL } = this.config.chat;
+
+ if (!security || !enabled || !chatURL) {
+ return;
+ }
+
+ try {
+ const {
+ email,
+ id,
+ token: jwt,
+ } = await http.get(GET_CHAT_USER_DATA_ROUTE_PATH);
+
+ if (!email || !id || !jwt) {
+ return;
+ }
+
+ this.chatConfig$.next({
+ enabled,
+ chatURL,
+ user: {
+ email,
+ id,
+ jwt,
+ },
+ });
+ } catch (e) {
+ // eslint-disable-next-line no-console
+ console.debug(`[cloud.chat] Could not retrieve chat config: ${e.res.status} ${e.message}`, e);
+ }
+ }
}
/** @internal exported for testing */
diff --git a/x-pack/plugins/cloud/public/services/index.tsx b/x-pack/plugins/cloud/public/services/index.tsx
new file mode 100644
index 0000000000000..96b80ae308883
--- /dev/null
+++ b/x-pack/plugins/cloud/public/services/index.tsx
@@ -0,0 +1,46 @@
+/*
+ * 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 React, { FC, createContext, useContext } from 'react';
+
+interface WithoutChat {
+ enabled: false;
+}
+
+interface WithChat {
+ enabled: true;
+ chatURL: string;
+ user: {
+ jwt: string;
+ id: string;
+ email: string;
+ };
+}
+
+export type ChatConfig = WithChat | WithoutChat;
+
+export interface CloudServices {
+ chat: ChatConfig;
+}
+
+const ServicesContext = createContext({ chat: { enabled: false } });
+
+export const ServicesProvider: FC = ({ children, ...services }) => (
+ {children}
+);
+
+/**
+ * React hook for accessing the pre-wired `CloudServices`.
+ */
+export function useServices() {
+ return useContext(ServicesContext);
+}
+
+export function useChat(): ChatConfig {
+ const { chat } = useServices();
+ return chat;
+}
diff --git a/x-pack/plugins/cloud/server/config.ts b/x-pack/plugins/cloud/server/config.ts
index 109987cd72d44..c5a122c2ec702 100644
--- a/x-pack/plugins/cloud/server/config.ts
+++ b/x-pack/plugins/cloud/server/config.ts
@@ -28,28 +28,36 @@ const fullStoryConfigSchema = schema.object({
),
});
+const chatConfigSchema = schema.object({
+ enabled: schema.boolean({ defaultValue: false }),
+ chatURL: schema.maybe(schema.string()),
+});
+
const configSchema = schema.object({
- id: schema.maybe(schema.string()),
apm: schema.maybe(apmConfigSchema),
- cname: schema.maybe(schema.string()),
base_url: schema.maybe(schema.string()),
- profile_url: schema.maybe(schema.string()),
+ chat: chatConfigSchema,
+ chatIdentitySecret: schema.maybe(schema.string()),
+ cname: schema.maybe(schema.string()),
deployment_url: schema.maybe(schema.string()),
- organization_url: schema.maybe(schema.string()),
full_story: fullStoryConfigSchema,
+ id: schema.maybe(schema.string()),
+ organization_url: schema.maybe(schema.string()),
+ profile_url: schema.maybe(schema.string()),
});
export type CloudConfigType = TypeOf;
export const config: PluginConfigDescriptor = {
exposeToBrowser: {
- id: true,
- cname: true,
base_url: true,
- profile_url: true,
+ chat: true,
+ cname: true,
deployment_url: true,
- organization_url: true,
full_story: true,
+ id: true,
+ organization_url: true,
+ profile_url: true,
},
schema: configSchema,
};
diff --git a/x-pack/plugins/cloud/server/plugin.ts b/x-pack/plugins/cloud/server/plugin.ts
index 4c0b7b7f7eca6..a5cf423fb8295 100644
--- a/x-pack/plugins/cloud/server/plugin.ts
+++ b/x-pack/plugins/cloud/server/plugin.ts
@@ -7,14 +7,17 @@
import { UsageCollectionSetup } from 'src/plugins/usage_collection/server';
import { CoreSetup, Logger, Plugin, PluginInitializerContext } from 'src/core/server';
+import type { SecurityPluginSetup } from '../../security/server';
import { CloudConfigType } from './config';
import { registerCloudUsageCollector } from './collectors';
import { getIsCloudEnabled } from '../common/is_cloud_enabled';
import { parseDeploymentIdFromDeploymentUrl } from './utils';
import { registerFullstoryRoute } from './routes/fullstory';
+import { registerChatRoute } from './routes/chat';
interface PluginsSetup {
usageCollection?: UsageCollectionSetup;
+ security?: SecurityPluginSetup;
}
export interface CloudSetup {
@@ -30,13 +33,15 @@ export interface CloudSetup {
export class CloudPlugin implements Plugin {
private readonly logger: Logger;
private readonly config: CloudConfigType;
+ private isDev: boolean;
constructor(private readonly context: PluginInitializerContext) {
this.logger = this.context.logger.get();
this.config = this.context.config.get();
+ this.isDev = this.context.env.mode.dev;
}
- public setup(core: CoreSetup, { usageCollection }: PluginsSetup) {
+ public setup(core: CoreSetup, { usageCollection, security }: PluginsSetup) {
this.logger.debug('Setting up Cloud plugin');
const isCloudEnabled = getIsCloudEnabled(this.config.id);
registerCloudUsageCollector(usageCollection, { isCloudEnabled });
@@ -48,6 +53,15 @@ export class CloudPlugin implements Plugin {
});
}
+ if (this.config.chat.enabled && this.config.chatIdentitySecret) {
+ registerChatRoute({
+ router: core.http.createRouter(),
+ chatIdentitySecret: this.config.chatIdentitySecret,
+ security,
+ isDev: this.isDev,
+ });
+ }
+
return {
cloudId: this.config.id,
deploymentId: parseDeploymentIdFromDeploymentUrl(this.config.deployment_url),
diff --git a/x-pack/plugins/cloud/server/routes/chat.test.ts b/x-pack/plugins/cloud/server/routes/chat.test.ts
new file mode 100644
index 0000000000000..9ed76eff6d081
--- /dev/null
+++ b/x-pack/plugins/cloud/server/routes/chat.test.ts
@@ -0,0 +1,109 @@
+/*
+ * 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.
+ */
+
+jest.mock('jsonwebtoken', () => ({
+ sign: () => {
+ return 'json-web-token';
+ },
+}));
+
+import { httpServiceMock, httpServerMock } from '../../../../../src/core/server/mocks';
+import { securityMock } from '../../../security/server/mocks';
+import { kibanaResponseFactory } from 'src/core/server';
+import { registerChatRoute } from './chat';
+
+describe('chat route', () => {
+ test('do not add the route if security is not enabled', async () => {
+ const router = httpServiceMock.createRouter();
+ registerChatRoute({ router, isDev: false, chatIdentitySecret: 'secret' });
+ expect(router.get.mock.calls).toEqual([]);
+ });
+
+ test('error if no user', async () => {
+ const security = securityMock.createSetup();
+ security.authc.getCurrentUser.mockReturnValueOnce(null);
+
+ const router = httpServiceMock.createRouter();
+ registerChatRoute({ router, security, isDev: false, chatIdentitySecret: 'secret' });
+
+ const [_config, handler] = router.get.mock.calls[0];
+
+ await expect(handler({}, httpServerMock.createKibanaRequest(), kibanaResponseFactory)).resolves
+ .toMatchInlineSnapshot(`
+ KibanaResponse {
+ "options": Object {
+ "body": "User has no email or username",
+ },
+ "payload": "User has no email or username",
+ "status": 400,
+ }
+ `);
+ });
+
+ test('returns user information and a token', async () => {
+ const security = securityMock.createSetup();
+ const username = 'user.name';
+ const email = 'user@elastic.co';
+
+ security.authc.getCurrentUser.mockReturnValueOnce({
+ username,
+ email,
+ });
+
+ const router = httpServiceMock.createRouter();
+ registerChatRoute({ router, security, isDev: false, chatIdentitySecret: 'secret' });
+ const [_config, handler] = router.get.mock.calls[0];
+ await expect(handler({}, httpServerMock.createKibanaRequest(), kibanaResponseFactory)).resolves
+ .toMatchInlineSnapshot(`
+ KibanaResponse {
+ "options": Object {
+ "body": Object {
+ "email": "${email}",
+ "id": "${username}",
+ "token": "json-web-token",
+ },
+ },
+ "payload": Object {
+ "email": "${email}",
+ "id": "${username}",
+ "token": "json-web-token",
+ },
+ "status": 200,
+ }
+ `);
+ });
+
+ test('returns placeholder user information and a token in dev mode', async () => {
+ const security = securityMock.createSetup();
+ const username = 'first.last';
+ const email = 'test+first.last@elasticsearch.com';
+
+ security.authc.getCurrentUser.mockReturnValueOnce({});
+
+ const router = httpServiceMock.createRouter();
+ registerChatRoute({ router, security, isDev: true, chatIdentitySecret: 'secret' });
+ const [_config, handler] = router.get.mock.calls[0];
+ await expect(handler({}, httpServerMock.createKibanaRequest(), kibanaResponseFactory)).resolves
+ .toMatchInlineSnapshot(`
+ KibanaResponse {
+ "options": Object {
+ "body": Object {
+ "email": "${email}",
+ "id": "${username}",
+ "token": "json-web-token",
+ },
+ },
+ "payload": Object {
+ "email": "${email}",
+ "id": "${username}",
+ "token": "json-web-token",
+ },
+ "status": 200,
+ }
+ `);
+ });
+});
diff --git a/x-pack/plugins/cloud/server/routes/chat.ts b/x-pack/plugins/cloud/server/routes/chat.ts
new file mode 100644
index 0000000000000..62c4475c92ae5
--- /dev/null
+++ b/x-pack/plugins/cloud/server/routes/chat.ts
@@ -0,0 +1,64 @@
+/*
+ * 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 { IRouter } from '../../../../../src/core/server';
+import type { SecurityPluginSetup } from '../../../security/server';
+import { GET_CHAT_USER_DATA_ROUTE_PATH } from '../../common/constants';
+import type { GetChatUserDataResponseBody } from '../../common/types';
+import { generateSignedJwt } from '../util/generate_jwt';
+
+export const registerChatRoute = ({
+ router,
+ chatIdentitySecret,
+ security,
+ isDev,
+}: {
+ router: IRouter;
+ chatIdentitySecret: string;
+ security?: SecurityPluginSetup;
+ isDev: boolean;
+}) => {
+ if (!security) {
+ return;
+ }
+
+ router.get(
+ {
+ path: GET_CHAT_USER_DATA_ROUTE_PATH,
+ validate: {},
+ },
+ async (_context, request, response) => {
+ const user = security.authc.getCurrentUser(request);
+ let { email: userEmail, username: userId } = user || {};
+
+ // In local development, these values are not populated. This is a workaround
+ // to allow for local testing.
+ if (isDev) {
+ if (!userId) {
+ userId = 'first.last';
+ }
+ if (!userEmail) {
+ userEmail = userEmail || `test+${userId}@elasticsearch.com`;
+ }
+ }
+
+ if (!userEmail || !userId) {
+ return response.badRequest({
+ body: 'User has no email or username',
+ });
+ }
+
+ const token = generateSignedJwt(userId, chatIdentitySecret);
+ const body: GetChatUserDataResponseBody = {
+ token,
+ email: userEmail,
+ id: userId,
+ };
+ return response.ok({ body });
+ }
+ );
+};
diff --git a/x-pack/plugins/cloud/server/util/generate_jwt.test.ts b/x-pack/plugins/cloud/server/util/generate_jwt.test.ts
new file mode 100644
index 0000000000000..65fecc05c84ca
--- /dev/null
+++ b/x-pack/plugins/cloud/server/util/generate_jwt.test.ts
@@ -0,0 +1,23 @@
+/*
+ * 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.
+ */
+
+jest.mock('jsonwebtoken', () => ({
+ sign: (payload: {}, secret: string, options: {}) => {
+ return `${JSON.stringify(payload)}.${secret}.${JSON.stringify(options)}`;
+ },
+}));
+
+import { generateSignedJwt } from './generate_jwt';
+
+describe('generateSignedJwt', () => {
+ test('creating a JWT token', () => {
+ const jwtToken = generateSignedJwt('test', '123456');
+ expect(jwtToken).toEqual(
+ '{"sub":"test"}.123456.{"header":{"alg":"HS256","typ":"JWT"},"expiresIn":300}'
+ );
+ });
+});
diff --git a/x-pack/plugins/cloud/server/util/generate_jwt.ts b/x-pack/plugins/cloud/server/util/generate_jwt.ts
new file mode 100644
index 0000000000000..1001a89beb7db
--- /dev/null
+++ b/x-pack/plugins/cloud/server/util/generate_jwt.ts
@@ -0,0 +1,24 @@
+/*
+ * 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 jwt from 'jsonwebtoken';
+
+export const generateSignedJwt = (userId: string, secret: string): string => {
+ const options = {
+ header: {
+ alg: 'HS256',
+ typ: 'JWT',
+ },
+ expiresIn: 5 * 60, // 5m
+ };
+
+ const payload = {
+ sub: userId,
+ };
+
+ return jwt.sign(payload, secret, options);
+};
diff --git a/x-pack/plugins/cloud/tsconfig.json b/x-pack/plugins/cloud/tsconfig.json
index d1ff8c63e84cb..e743b46ac17eb 100644
--- a/x-pack/plugins/cloud/tsconfig.json
+++ b/x-pack/plugins/cloud/tsconfig.json
@@ -7,9 +7,11 @@
"declarationMap": true,
},
"include": [
+ ".storybook/**/*",
"common/**/*",
"public/**/*",
"server/**/*",
+ "../../../typings/**/*"
],
"references": [
{ "path": "../../../src/core/tsconfig.json" },
diff --git a/x-pack/plugins/fleet/.storybook/context/index.tsx b/x-pack/plugins/fleet/.storybook/context/index.tsx
index 68348a4d8d07a..eb19a1145ba75 100644
--- a/x-pack/plugins/fleet/.storybook/context/index.tsx
+++ b/x-pack/plugins/fleet/.storybook/context/index.tsx
@@ -53,7 +53,10 @@ export const StorybookContext: React.FC<{ storyContext?: StoryContext }> = ({
...stubbedStartServices,
application: getApplication(),
chrome: getChrome(),
- cloud: getCloud({ isCloudEnabled }),
+ cloud: {
+ ...getCloud({ isCloudEnabled }),
+ CloudContextProvider: () => <>>,
+ },
customIntegrations: {
ContextProvider: getStorybookContextProvider(),
},
diff --git a/x-pack/plugins/fleet/kibana.json b/x-pack/plugins/fleet/kibana.json
index 01b78e107a467..ec03442ce860f 100644
--- a/x-pack/plugins/fleet/kibana.json
+++ b/x-pack/plugins/fleet/kibana.json
@@ -11,5 +11,5 @@
"requiredPlugins": ["licensing", "data", "encryptedSavedObjects", "navigation", "customIntegrations", "share", "spaces", "security"],
"optionalPlugins": ["features", "cloud", "usageCollection", "home", "globalSearch", "telemetry"],
"extraPublicDirs": ["common"],
- "requiredBundles": ["kibanaReact", "esUiShared", "home", "infra", "kibanaUtils", "usageCollection"]
+ "requiredBundles": ["kibanaReact", "cloud", "esUiShared", "home", "infra", "kibanaUtils", "usageCollection"]
}
diff --git a/x-pack/plugins/fleet/public/applications/integrations/app.tsx b/x-pack/plugins/fleet/public/applications/integrations/app.tsx
index e4724ca13b411..2c5b3b98c3e9a 100644
--- a/x-pack/plugins/fleet/public/applications/integrations/app.tsx
+++ b/x-pack/plugins/fleet/public/applications/integrations/app.tsx
@@ -21,6 +21,7 @@ import {
RedirectAppLinks,
} from '../../../../../../src/plugins/kibana_react/public';
import { EuiThemeProvider } from '../../../../../../src/plugins/kibana_react/common';
+import { Chat } from '../../../../cloud/public';
import { KibanaThemeProvider } from '../../../../../../src/plugins/kibana_react/public';
@@ -33,6 +34,8 @@ import { EPMApp } from './sections/epm';
import { PackageInstallProvider, UIExtensionsContext } from './hooks';
import { IntegrationsHeader } from './components/header';
+const EmptyContext = () => <>>;
+
/**
* Fleet Application context all the way down to the Router, but with no permissions or setup checks
* and no routes defined
@@ -60,6 +63,7 @@ export const IntegrationsAppContext: React.FC<{
theme$,
}) => {
const isDarkMode = useObservable(startServices.uiSettings.get$('theme:darkMode'));
+ const CloudContext = startServices.cloud?.CloudContextProvider || EmptyContext;
return (
@@ -73,17 +77,20 @@ export const IntegrationsAppContext: React.FC<{
-
-
-
-
- {children}
-
-
-
+
+
+
+
+
+ {children}
+
+
+
+
+
diff --git a/x-pack/plugins/fleet/public/mock/fleet_start_services.tsx b/x-pack/plugins/fleet/public/mock/fleet_start_services.tsx
index f810b0369c161..4d8f74fa3b04a 100644
--- a/x-pack/plugins/fleet/public/mock/fleet_start_services.tsx
+++ b/x-pack/plugins/fleet/public/mock/fleet_start_services.tsx
@@ -16,7 +16,7 @@ import { setHttpClient } from '../hooks/use_request';
import type { FleetAuthz } from '../../common';
-import { createStartDepsMock } from './plugin_dependencies';
+import { createStartDepsMock, createSetupDepsMock } from './plugin_dependencies';
import type { MockedFleetStartServices } from './types';
// Taken from core. See: src/plugins/kibana_utils/public/storage/storage.test.ts
@@ -71,9 +71,16 @@ const configureStartServices = (services: MockedFleetStartServices): void => {
};
export const createStartServices = (basePath: string = '/mock'): MockedFleetStartServices => {
+ const { cloud: cloudStart, ...startDeps } = createStartDepsMock();
+ const { cloud: cloudSetup } = createSetupDepsMock();
+
const startServices: MockedFleetStartServices = {
...coreMock.createStart({ basePath }),
- ...createStartDepsMock(),
+ ...startDeps,
+ cloud: {
+ ...cloudStart,
+ ...cloudSetup,
+ },
storage: new Storage(createMockStore()) as jest.Mocked,
authz: fleetAuthzMock,
};
diff --git a/x-pack/plugins/fleet/public/mock/plugin_dependencies.ts b/x-pack/plugins/fleet/public/mock/plugin_dependencies.ts
index 0bf0213905e72..bd119c147aec7 100644
--- a/x-pack/plugins/fleet/public/mock/plugin_dependencies.ts
+++ b/x-pack/plugins/fleet/public/mock/plugin_dependencies.ts
@@ -7,27 +7,29 @@
import { dataPluginMock } from '../../../../../src/plugins/data/public/mocks';
import { licensingMock } from '../../../licensing/public/mocks';
+import { cloudMock } from '../../../cloud/public/mocks';
import { homePluginMock } from '../../../../../src/plugins/home/public/mocks';
import { navigationPluginMock } from '../../../../../src/plugins/navigation/public/mocks';
import { customIntegrationsMock } from '../../../../../src/plugins/custom_integrations/public/mocks';
import { sharePluginMock } from '../../../../../src/plugins/share/public/mocks';
-import type { MockedFleetSetupDeps, MockedFleetStartDeps } from './types';
-
-export const createSetupDepsMock = (): MockedFleetSetupDeps => {
+export const createSetupDepsMock = () => {
+ const cloud = cloudMock.createSetup();
return {
licensing: licensingMock.createSetup(),
data: dataPluginMock.createSetupContract(),
home: homePluginMock.createSetupContract(),
customIntegrations: customIntegrationsMock.createSetup(),
+ cloud,
};
};
-export const createStartDepsMock = (): MockedFleetStartDeps => {
+export const createStartDepsMock = () => {
return {
data: dataPluginMock.createStartContract(),
navigation: navigationPluginMock.createStartContract(),
customIntegrations: customIntegrationsMock.createStart(),
share: sharePluginMock.createStartContract(),
+ cloud: cloudMock.createStart(),
};
};
diff --git a/x-pack/plugins/fleet/public/plugin.ts b/x-pack/plugins/fleet/public/plugin.ts
index a15be583d7a1e..548319e7bfaba 100644
--- a/x-pack/plugins/fleet/public/plugin.ts
+++ b/x-pack/plugins/fleet/public/plugin.ts
@@ -26,6 +26,8 @@ import type { SharePluginStart } from 'src/plugins/share/public';
import { once } from 'lodash';
+import type { CloudStart } from '../../cloud/public';
+
import type { UsageCollectionSetup } from '../../../../src/plugins/usage_collection/public';
import { DEFAULT_APP_CATEGORIES, AppNavLinkStatus } from '../../../../src/core/public';
@@ -94,12 +96,13 @@ export interface FleetStartDeps {
navigation: NavigationPublicPluginStart;
customIntegrations: CustomIntegrationsStart;
share: SharePluginStart;
+ cloud?: CloudStart;
}
-export interface FleetStartServices extends CoreStart, FleetStartDeps {
+export interface FleetStartServices extends CoreStart, Exclude {
storage: Storage;
share: SharePluginStart;
- cloud?: CloudSetup;
+ cloud?: CloudSetup & CloudStart;
authz: FleetAuthz;
}
@@ -141,11 +144,16 @@ export class FleetPlugin implements Plugin {
const [coreStartServices, startDepsServices, fleetStart] = await core.getStartServices();
+ const cloud =
+ deps.cloud && startDepsServices.cloud
+ ? { ...deps.cloud, ...startDepsServices.cloud }
+ : undefined;
+
const startServices: FleetStartServices = {
...coreStartServices,
...startDepsServices,
storage: this.storage,
- cloud: deps.cloud,
+ cloud,
authz: await fleetStart.authz,
};
const { renderApp, teardownIntegrations } = await import('./applications/integrations');
@@ -178,11 +186,15 @@ export class FleetPlugin implements Plugin {
const [coreStartServices, startDepsServices, fleetStart] = await core.getStartServices();
+ const cloud =
+ deps.cloud && startDepsServices.cloud
+ ? { ...deps.cloud, ...startDepsServices.cloud }
+ : undefined;
const startServices: FleetStartServices = {
...coreStartServices,
...startDepsServices,
storage: this.storage,
- cloud: deps.cloud,
+ cloud,
authz: await fleetStart.authz,
};
const { renderApp, teardownFleet } = await import('./applications/fleet');
diff --git a/x-pack/test/cloud_integration/config.ts b/x-pack/test/cloud_integration/config.ts
index a012dfd1ad34b..102d276b34584 100644
--- a/x-pack/test/cloud_integration/config.ts
+++ b/x-pack/test/cloud_integration/config.ts
@@ -14,6 +14,10 @@ const FULLSTORY_ORG_ID = process.env.FULLSTORY_ORG_ID;
const FULLSTORY_API_KEY = process.env.FULLSTORY_API_KEY;
const RUN_FULLSTORY_TESTS = Boolean(FULLSTORY_ORG_ID && FULLSTORY_API_KEY);
+const CHAT_URL = process.env.CHAT_URL;
+const CHAT_IDENTITY_SECRET = process.env.CHAT_IDENTITY_SECRET;
+const RUN_CHAT_TESTS = Boolean(CHAT_URL);
+
// the default export of config files must be a config provider
// that returns an object with the projects config values
export default async function ({ readConfigFile }: FtrConfigProviderContext) {
@@ -29,7 +33,11 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) {
const samlIdPPlugin = resolve(__dirname, './fixtures/saml/saml_provider');
return {
- testFiles: [...(RUN_FULLSTORY_TESTS ? [resolve(__dirname, './tests/fullstory')] : [])],
+ testFiles: [
+ ...(RUN_FULLSTORY_TESTS ? [resolve(__dirname, './tests/fullstory')] : []),
+ ...(RUN_CHAT_TESTS ? [resolve(__dirname, './tests/chat')] : []),
+ ...(!RUN_CHAT_TESTS ? [resolve(__dirname, './tests/chat_disabled')] : []),
+ ],
services,
pageObjects,
@@ -69,6 +77,14 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) {
`--xpack.cloud.full_story.org_id=${FULLSTORY_ORG_ID}`,
]
: []),
+ ...(RUN_CHAT_TESTS
+ ? [
+ '--xpack.cloud.id=5b2de169-2785-441b-ae8c-186a1936b17d',
+ '--xpack.cloud.chat.enabled=true',
+ `--xpack.cloud.chat.chatURL=${CHAT_URL}`,
+ `--xpack.cloud.chatIdentitySecret=${CHAT_IDENTITY_SECRET}`,
+ ]
+ : []),
],
},
uiSettings: {
diff --git a/x-pack/test/cloud_integration/tests/chat.ts b/x-pack/test/cloud_integration/tests/chat.ts
new file mode 100644
index 0000000000000..3411ebc8174a6
--- /dev/null
+++ b/x-pack/test/cloud_integration/tests/chat.ts
@@ -0,0 +1,30 @@
+/*
+ * 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 expect from '@kbn/expect';
+import { FtrProviderContext } from '../ftr_provider_context';
+
+export default function ({ getService, getPageObjects }: FtrProviderContext) {
+ const find = getService('find');
+ const PageObjects = getPageObjects(['common']);
+
+ describe('Cloud Chat integration', function () {
+ before(async () => {
+ // Create role mapping so user gets superuser access
+ await getService('esSupertest')
+ .post('/_security/role_mapping/saml1')
+ .send({ roles: ['superuser'], enabled: true, rules: { field: { 'realm.name': 'saml1' } } })
+ .expect(200);
+ });
+
+ it('chat widget is present when enabled', async () => {
+ PageObjects.common.navigateToUrl('integrations', 'browse', { useActualUrl: true });
+ const chat = await find.byCssSelector('[data-test-subj="floatingChatTrigger"]', 20000);
+ expect(chat).to.not.be(null);
+ });
+ });
+}
From 997988fac202d130a5b519fad4249efbdff378f1 Mon Sep 17 00:00:00 2001
From: Paul Tavares <56442535+paul-tavares@users.noreply.github.com>
Date: Mon, 31 Jan 2022 09:13:25 -0500
Subject: [PATCH 18/65] [Security Solution][Endpoint] Add Host Isolation
Exceptions api validations `get`, `find`, `delete`, `export`, `summary` and
`import` (#123954)
* validation for Pre GET one of host isolation exceptions.
* adjust checks for host isolation validation
* Add validation for import for all artifacts
* Validate host isolation exceptions exports
* Validate host isolation exceptions multi list find
* Validate host isolation exceptions single list find
* Validate host isolation exceptions Summary
* add FTR tests to validate authz
* Update all exception extension point handlers to use the ExceptionListClient passed in on context
* Refactored ExceptionListItemGenerator a bit and added methods to get Host Isolation exceptions
* Update handlers to immediately exit if the namespace_type is not `agnostic`
* Improved `log.info` messages in artifact and policy services
* Add `lists-summary` to Security solution `all` feature privilege (was missing)
---
x-pack/plugins/lists/server/types.ts | 3 +-
.../exceptions_list_item_generator.ts | 225 ++++++------------
.../endpoint/service/artifacts/constants.ts | 12 +
.../host_isolation_exceptions/constants.ts | 8 +-
.../host_isolation_exceptions/service.ts | 4 +-
.../security_solution/server/features.ts | 2 +-
.../exceptions_pre_delete_item_handler.ts | 31 ++-
.../handlers/exceptions_pre_export_handler.ts | 28 ++-
.../exceptions_pre_get_one_handler.ts | 35 ++-
.../handlers/exceptions_pre_import_handler.ts | 31 +++
.../exceptions_pre_multi_list_find_handler.ts | 19 +-
...exceptions_pre_single_list_find_handler.ts | 19 +-
.../exceptions_pre_summary_handler.ts | 23 +-
.../handlers/exceptions_pre_update_handler.ts | 25 +-
.../register_endpoint_extension_points.ts | 7 +
.../host_isolation_exceptions_validator.ts | 46 ++++
.../security_solution/server/plugin.ts | 21 +-
.../services/endpoint_artifacts.ts | 18 +-
.../services/endpoint_policy.ts | 8 +
.../apis/endpoint_artifacts.ts | 8 +-
...int_host_isolation_exceptions_artifacts.ts | 155 ++++++++++++
.../apis/index.ts | 1 +
22 files changed, 525 insertions(+), 204 deletions(-)
create mode 100644 x-pack/plugins/security_solution/server/lists_integration/endpoint/handlers/exceptions_pre_import_handler.ts
create mode 100644 x-pack/plugins/security_solution/server/lists_integration/endpoint/validators/host_isolation_exceptions_validator.ts
create mode 100644 x-pack/test/security_solution_endpoint_api_int/apis/endpoint_host_isolation_exceptions_artifacts.ts
diff --git a/x-pack/plugins/lists/server/types.ts b/x-pack/plugins/lists/server/types.ts
index 8e2070f8d1b70..0c395c438750d 100644
--- a/x-pack/plugins/lists/server/types.ts
+++ b/x-pack/plugins/lists/server/types.ts
@@ -39,7 +39,8 @@ export type GetListClientType = (
export type GetExceptionListClientType = (
savedObjectsClient: SavedObjectsClientContract,
user: string,
- disableServerExtensionPoints?: boolean
+ /** Default is `true` - processing of server extension points are always on by default */
+ enableServerExtensionPoints?: boolean
) => ExceptionListClient;
export interface ListPluginSetup {
diff --git a/x-pack/plugins/security_solution/common/endpoint/data_generators/exceptions_list_item_generator.ts b/x-pack/plugins/security_solution/common/endpoint/data_generators/exceptions_list_item_generator.ts
index e2f9a42b014c2..de93a247a6b45 100644
--- a/x-pack/plugins/security_solution/common/endpoint/data_generators/exceptions_list_item_generator.ts
+++ b/x-pack/plugins/security_solution/common/endpoint/data_generators/exceptions_list_item_generator.ts
@@ -13,6 +13,7 @@ import type {
import {
ENDPOINT_EVENT_FILTERS_LIST_ID,
ENDPOINT_TRUSTED_APPS_LIST_ID,
+ ENDPOINT_HOST_ISOLATION_EXCEPTIONS_LIST_ID,
} from '@kbn/securitysolution-list-constants';
import { BaseDataGenerator } from './base_data_generator';
import { ConditionEntryField } from '../types';
@@ -37,6 +38,55 @@ type UpdateExceptionListItemSchemaWithNonNullProps = NonNullableTypeProperties<
> &
Pick;
+const exceptionItemToCreateExceptionItem = (
+ exceptionItem: ExceptionListItemSchema
+): CreateExceptionListItemSchemaWithNonNullProps => {
+ const {
+ /* eslint-disable @typescript-eslint/naming-convention */
+ description,
+ entries,
+ list_id,
+ name,
+ type,
+ comments,
+ item_id,
+ meta,
+ namespace_type,
+ os_types,
+ tags,
+ /* eslint-enable @typescript-eslint/naming-convention */
+ } = exceptionItem;
+
+ return {
+ description,
+ entries,
+ list_id,
+ name,
+ type,
+ comments,
+ item_id,
+ meta,
+ namespace_type,
+ os_types,
+ tags,
+ };
+};
+
+const exceptionItemToUpdateExceptionItem = (
+ exceptionItem: ExceptionListItemSchema
+): UpdateExceptionListItemSchemaWithNonNullProps => {
+ // eslint-disable-next-line @typescript-eslint/naming-convention
+ const { id, item_id, _version } = exceptionItem;
+ const { list_id: _, ...updateAttributes } = exceptionItemToCreateExceptionItem(exceptionItem);
+
+ return {
+ ...updateAttributes,
+ id,
+ item_id,
+ _version: _version ?? 'some value',
+ };
+};
+
export class ExceptionsListItemGenerator extends BaseDataGenerator {
generate(overrides: Partial = {}): ExceptionListItemSchema {
const exceptionItem: ExceptionListItemSchema = {
@@ -85,82 +135,22 @@ export class ExceptionsListItemGenerator extends BaseDataGenerator = {}
): CreateExceptionListItemSchemaWithNonNullProps {
- const {
- /* eslint-disable @typescript-eslint/naming-convention */
- description,
- entries,
- list_id,
- name,
- type,
- comments,
- item_id,
- meta,
- namespace_type,
- os_types,
- tags,
- /* eslint-enable @typescript-eslint/naming-convention */
- } = this.generate();
-
- return {
- description,
- entries,
- list_id,
- name,
- type,
- comments,
- item_id,
- meta,
- namespace_type,
- os_types,
- tags,
- ...overrides,
- };
+ return Object.assign(exceptionItemToCreateExceptionItem(this.generate()), overrides);
}
generateTrustedApp(overrides: Partial = {}): ExceptionListItemSchema {
- const trustedApp = this.generate(overrides);
-
- return {
- ...trustedApp,
+ return this.generate({
name: `Trusted app (${this.randomString(5)})`,
list_id: ENDPOINT_TRUSTED_APPS_LIST_ID,
- // Remove the hash field which the generator above currently still sets to a field that is not
- // actually valid when used with the Exception List
- entries: trustedApp.entries.filter((entry) => entry.field !== ConditionEntryField.HASH),
- };
+ ...overrides,
+ });
}
generateTrustedAppForCreate(
overrides: Partial = {}
): CreateExceptionListItemSchemaWithNonNullProps {
- const {
- /* eslint-disable @typescript-eslint/naming-convention */
- description,
- entries,
- list_id,
- name,
- type,
- comments,
- item_id,
- meta,
- namespace_type,
- os_types,
- tags,
- /* eslint-enable @typescript-eslint/naming-convention */
- } = this.generateTrustedApp();
-
return {
- description,
- entries,
- list_id,
- name,
- type,
- comments,
- item_id,
- meta,
- namespace_type,
- os_types,
- tags,
+ ...exceptionItemToCreateExceptionItem(this.generateTrustedApp()),
...overrides,
};
}
@@ -168,36 +158,8 @@ export class ExceptionsListItemGenerator extends BaseDataGenerator = {}
): UpdateExceptionListItemSchemaWithNonNullProps {
- const {
- /* eslint-disable @typescript-eslint/naming-convention */
- description,
- entries,
- name,
- type,
- comments,
- id,
- item_id,
- meta,
- namespace_type,
- os_types,
- tags,
- _version,
- /* eslint-enable @typescript-eslint/naming-convention */
- } = this.generateTrustedApp();
-
return {
- description,
- entries,
- name,
- type,
- comments,
- id,
- item_id,
- meta,
- namespace_type,
- os_types,
- tags,
- _version: _version ?? 'some value',
+ ...exceptionItemToUpdateExceptionItem(this.generateTrustedApp()),
...overrides,
};
}
@@ -215,33 +177,8 @@ export class ExceptionsListItemGenerator extends BaseDataGenerator = {}
): CreateExceptionListItemSchemaWithNonNullProps {
- const {
- /* eslint-disable @typescript-eslint/naming-convention */
- description,
- entries,
- list_id,
- name,
- type,
- comments,
- item_id,
- meta,
- namespace_type,
- os_types,
- tags,
- /* eslint-enable @typescript-eslint/naming-convention */
- } = this.generateEventFilter();
return {
- description,
- entries,
- list_id,
- name,
- type,
- comments,
- item_id,
- meta,
- namespace_type,
- os_types,
- tags,
+ ...exceptionItemToCreateExceptionItem(this.generateEventFilter()),
...overrides,
};
}
@@ -249,35 +186,27 @@ export class ExceptionsListItemGenerator extends BaseDataGenerator = {}
): UpdateExceptionListItemSchemaWithNonNullProps {
- const {
- /* eslint-disable @typescript-eslint/naming-convention */
- description,
- entries,
- name,
- type,
- comments,
- id,
- item_id,
- meta,
- namespace_type,
- os_types,
- tags,
- _version,
- /* eslint-enable @typescript-eslint/naming-convention */
- } = this.generateEventFilter();
return {
- description,
- entries,
- name,
- type,
- comments,
- id,
- item_id,
- meta,
- namespace_type,
- os_types,
- tags,
- _version: _version ?? 'some value',
+ ...exceptionItemToUpdateExceptionItem(this.generateEventFilter()),
+ ...overrides,
+ };
+ }
+
+ generateHostIsolationException(
+ overrides: Partial = {}
+ ): ExceptionListItemSchema {
+ return this.generate({
+ name: `Host Isolation (${this.randomString(5)})`,
+ list_id: ENDPOINT_HOST_ISOLATION_EXCEPTIONS_LIST_ID,
+ ...overrides,
+ });
+ }
+
+ generateHostIsolationExceptionForCreate(
+ overrides: Partial = {}
+ ): CreateExceptionListItemSchemaWithNonNullProps {
+ return {
+ ...exceptionItemToCreateExceptionItem(this.generateHostIsolationException()),
...overrides,
};
}
diff --git a/x-pack/plugins/security_solution/common/endpoint/service/artifacts/constants.ts b/x-pack/plugins/security_solution/common/endpoint/service/artifacts/constants.ts
index eadc52941da05..c62e9e15621be 100644
--- a/x-pack/plugins/security_solution/common/endpoint/service/artifacts/constants.ts
+++ b/x-pack/plugins/security_solution/common/endpoint/service/artifacts/constants.ts
@@ -5,6 +5,18 @@
* 2.0.
*/
+import {
+ ENDPOINT_TRUSTED_APPS_LIST_ID,
+ ENDPOINT_EVENT_FILTERS_LIST_ID,
+ ENDPOINT_HOST_ISOLATION_EXCEPTIONS_LIST_ID,
+} from '@kbn/securitysolution-list-constants';
+
export const BY_POLICY_ARTIFACT_TAG_PREFIX = 'policy:';
export const GLOBAL_ARTIFACT_TAG = `${BY_POLICY_ARTIFACT_TAG_PREFIX}all`;
+
+export const ALL_ENDPOINT_ARTIFACT_LIST_IDS: readonly string[] = [
+ ENDPOINT_TRUSTED_APPS_LIST_ID,
+ ENDPOINT_EVENT_FILTERS_LIST_ID,
+ ENDPOINT_HOST_ISOLATION_EXCEPTIONS_LIST_ID,
+];
diff --git a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/constants.ts b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/constants.ts
index dab3b528a181b..8ba18f3df976c 100644
--- a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/constants.ts
+++ b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/constants.ts
@@ -4,7 +4,11 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
-import { ExceptionListType, ExceptionListTypeEnum } from '@kbn/securitysolution-io-ts-list-types';
+import {
+ ExceptionListType,
+ ExceptionListTypeEnum,
+ CreateExceptionListSchema,
+} from '@kbn/securitysolution-io-ts-list-types';
import {
ENDPOINT_HOST_ISOLATION_EXCEPTIONS_LIST_DESCRIPTION,
ENDPOINT_HOST_ISOLATION_EXCEPTIONS_LIST_ID,
@@ -14,7 +18,7 @@ import {
export const HOST_ISOLATION_EXCEPTIONS_LIST_TYPE: ExceptionListType =
ExceptionListTypeEnum.ENDPOINT_HOST_ISOLATION_EXCEPTIONS;
-export const HOST_ISOLATION_EXCEPTIONS_LIST = {
+export const HOST_ISOLATION_EXCEPTIONS_LIST_DEFINITION: CreateExceptionListSchema = {
name: ENDPOINT_HOST_ISOLATION_EXCEPTIONS_LIST_NAME,
namespace_type: 'agnostic',
description: ENDPOINT_HOST_ISOLATION_EXCEPTIONS_LIST_DESCRIPTION,
diff --git a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/service.ts b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/service.ts
index e8c78e049d865..6fd0b77abfffc 100644
--- a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/service.ts
+++ b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/service.ts
@@ -16,12 +16,12 @@ import { ENDPOINT_HOST_ISOLATION_EXCEPTIONS_LIST_ID } from '@kbn/securitysolutio
import { transformNewItemOutput, transformOutput } from '@kbn/securitysolution-list-hooks';
import { HttpStart } from 'kibana/public';
import { EXCEPTION_LIST_ITEM_URL, EXCEPTION_LIST_URL } from '../event_filters/constants';
-import { HOST_ISOLATION_EXCEPTIONS_LIST } from './constants';
+import { HOST_ISOLATION_EXCEPTIONS_LIST_DEFINITION } from './constants';
async function createHostIsolationExceptionList(http: HttpStart): Promise {
try {
await http.post(EXCEPTION_LIST_URL, {
- body: JSON.stringify(HOST_ISOLATION_EXCEPTIONS_LIST),
+ body: JSON.stringify(HOST_ISOLATION_EXCEPTIONS_LIST_DEFINITION),
});
} catch (err) {
// Ignore 409 errors. List already created
diff --git a/x-pack/plugins/security_solution/server/features.ts b/x-pack/plugins/security_solution/server/features.ts
index 01f11517ca919..6fe65ce8692e5 100644
--- a/x-pack/plugins/security_solution/server/features.ts
+++ b/x-pack/plugins/security_solution/server/features.ts
@@ -118,7 +118,7 @@ export const getKibanaPrivilegesFeaturePrivileges = (ruleTypes: string[]): Kiban
all: {
app: [APP_ID, 'kibana'],
catalogue: [APP_ID],
- api: [APP_ID, 'lists-all', 'lists-read', 'rac'],
+ api: [APP_ID, 'lists-all', 'lists-read', 'lists-summary', 'rac'],
savedObject: {
all: [
'alert',
diff --git a/x-pack/plugins/security_solution/server/lists_integration/endpoint/handlers/exceptions_pre_delete_item_handler.ts b/x-pack/plugins/security_solution/server/lists_integration/endpoint/handlers/exceptions_pre_delete_item_handler.ts
index 17502d5d2af74..d328219eed4ef 100644
--- a/x-pack/plugins/security_solution/server/lists_integration/endpoint/handlers/exceptions_pre_delete_item_handler.ts
+++ b/x-pack/plugins/security_solution/server/lists_integration/endpoint/handlers/exceptions_pre_delete_item_handler.ts
@@ -5,14 +5,37 @@
* 2.0.
*/
+import { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types';
import { EndpointAppContextService } from '../../../endpoint/endpoint_app_context_services';
-import { ExtensionPoint } from '../../../../../lists/server';
+import { ExceptionsListPreDeleteItemServerExtension } from '../../../../../lists/server';
+import { HostIsolationExceptionsValidator } from '../validators/host_isolation_exceptions_validator';
export const getExceptionsPreDeleteItemHandler = (
endpointAppContext: EndpointAppContextService
-): (ExtensionPoint & { type: 'exceptionsListPreDeleteItem' })['callback'] => {
- return async function ({ data }) {
- // Individual validators here
+): ExceptionsListPreDeleteItemServerExtension['callback'] => {
+ return async function ({ data, context: { request, exceptionListClient } }) {
+ if (data.namespaceType !== 'agnostic') {
+ return data;
+ }
+
+ const exceptionItem: ExceptionListItemSchema | null =
+ await exceptionListClient.getExceptionListItem({
+ id: data.id,
+ itemId: data.itemId,
+ namespaceType: data.namespaceType,
+ });
+
+ if (!exceptionItem) {
+ return data;
+ }
+
+ // Host Isolation Exception
+ if (HostIsolationExceptionsValidator.isHostIsolationException(exceptionItem.list_id)) {
+ await new HostIsolationExceptionsValidator(
+ endpointAppContext,
+ request
+ ).validatePreDeleteItem();
+ }
return data;
};
diff --git a/x-pack/plugins/security_solution/server/lists_integration/endpoint/handlers/exceptions_pre_export_handler.ts b/x-pack/plugins/security_solution/server/lists_integration/endpoint/handlers/exceptions_pre_export_handler.ts
index 32e9c51d4241b..6904e7942ac20 100644
--- a/x-pack/plugins/security_solution/server/lists_integration/endpoint/handlers/exceptions_pre_export_handler.ts
+++ b/x-pack/plugins/security_solution/server/lists_integration/endpoint/handlers/exceptions_pre_export_handler.ts
@@ -6,13 +6,31 @@
*/
import { EndpointAppContextService } from '../../../endpoint/endpoint_app_context_services';
-import { ExtensionPoint } from '../../../../../lists/server';
+import { ExceptionsListPreExportServerExtension } from '../../../../../lists/server';
+import { HostIsolationExceptionsValidator } from '../validators/host_isolation_exceptions_validator';
export const getExceptionsPreExportHandler = (
- endpointAppContext: EndpointAppContextService
-): (ExtensionPoint & { type: 'exceptionsListPreExport' })['callback'] => {
- return async function ({ data }) {
- // Individual validators here
+ endpointAppContextService: EndpointAppContextService
+): ExceptionsListPreExportServerExtension['callback'] => {
+ return async function ({ data, context: { request, exceptionListClient } }) {
+ const { listId: maybeListId, id } = data;
+ let listId: string | null | undefined = maybeListId;
+
+ if (!listId && id) {
+ listId = (await exceptionListClient.getExceptionList(data))?.list_id ?? null;
+ }
+
+ if (!listId) {
+ return data;
+ }
+
+ // Host Isolation Exceptions validations
+ if (HostIsolationExceptionsValidator.isHostIsolationException(listId)) {
+ await new HostIsolationExceptionsValidator(
+ endpointAppContextService,
+ request
+ ).validatePreExport();
+ }
return data;
};
diff --git a/x-pack/plugins/security_solution/server/lists_integration/endpoint/handlers/exceptions_pre_get_one_handler.ts b/x-pack/plugins/security_solution/server/lists_integration/endpoint/handlers/exceptions_pre_get_one_handler.ts
index 0a74aeceb734c..9c72e6b5f44c7 100644
--- a/x-pack/plugins/security_solution/server/lists_integration/endpoint/handlers/exceptions_pre_get_one_handler.ts
+++ b/x-pack/plugins/security_solution/server/lists_integration/endpoint/handlers/exceptions_pre_get_one_handler.ts
@@ -5,14 +5,39 @@
* 2.0.
*/
+import { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types';
import { EndpointAppContextService } from '../../../endpoint/endpoint_app_context_services';
-import { ExtensionPoint } from '../../../../../lists/server';
+import { ExceptionsListPreGetOneItemServerExtension } from '../../../../../lists/server';
+import { HostIsolationExceptionsValidator } from '../validators/host_isolation_exceptions_validator';
export const getExceptionsPreGetOneHandler = (
- endpointAppContext: EndpointAppContextService
-): (ExtensionPoint & { type: 'exceptionsListPreGetOneItem' })['callback'] => {
- return async function ({ data }) {
- // Individual validators here
+ endpointAppContextService: EndpointAppContextService
+): ExceptionsListPreGetOneItemServerExtension['callback'] => {
+ return async function ({ data, context: { request, exceptionListClient } }) {
+ if (data.namespaceType !== 'agnostic') {
+ return data;
+ }
+
+ const exceptionItem: ExceptionListItemSchema | null =
+ await exceptionListClient.getExceptionListItem({
+ id: data.id,
+ itemId: data.itemId,
+ namespaceType: data.namespaceType,
+ });
+
+ if (!exceptionItem) {
+ return data;
+ }
+
+ // validate Host Isolation Exception
+ if (HostIsolationExceptionsValidator.isHostIsolationException(exceptionItem.list_id)) {
+ await new HostIsolationExceptionsValidator(
+ endpointAppContextService,
+ request
+ ).validatePreGetOneItem();
+
+ return data;
+ }
return data;
};
diff --git a/x-pack/plugins/security_solution/server/lists_integration/endpoint/handlers/exceptions_pre_import_handler.ts b/x-pack/plugins/security_solution/server/lists_integration/endpoint/handlers/exceptions_pre_import_handler.ts
new file mode 100644
index 0000000000000..c1fe425abef8d
--- /dev/null
+++ b/x-pack/plugins/security_solution/server/lists_integration/endpoint/handlers/exceptions_pre_import_handler.ts
@@ -0,0 +1,31 @@
+/*
+ * 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 { ExceptionsListPreImportServerExtension } from '../../../../../lists/server';
+import { EndpointArtifactExceptionValidationError } from '../validators/errors';
+import { ALL_ENDPOINT_ARTIFACT_LIST_IDS } from '../../../../common/endpoint/service/artifacts/constants';
+
+export const getExceptionsPreImportHandler =
+ (): ExceptionsListPreImportServerExtension['callback'] => {
+ return async ({ data }) => {
+ const hasEndpointArtifactListOrListItems = [...data.lists, ...data.items].some((item) => {
+ if ('list_id' in item) {
+ return ALL_ENDPOINT_ARTIFACT_LIST_IDS.includes(item.list_id);
+ }
+
+ return false;
+ });
+
+ if (hasEndpointArtifactListOrListItems) {
+ throw new EndpointArtifactExceptionValidationError(
+ 'Import is not supported for Endpoint artifact exceptions'
+ );
+ }
+
+ return data;
+ };
+ };
diff --git a/x-pack/plugins/security_solution/server/lists_integration/endpoint/handlers/exceptions_pre_multi_list_find_handler.ts b/x-pack/plugins/security_solution/server/lists_integration/endpoint/handlers/exceptions_pre_multi_list_find_handler.ts
index e167b6df72e8a..6ce8f5b3ecc2e 100644
--- a/x-pack/plugins/security_solution/server/lists_integration/endpoint/handlers/exceptions_pre_multi_list_find_handler.ts
+++ b/x-pack/plugins/security_solution/server/lists_integration/endpoint/handlers/exceptions_pre_multi_list_find_handler.ts
@@ -6,13 +6,24 @@
*/
import { EndpointAppContextService } from '../../../endpoint/endpoint_app_context_services';
-import { ExtensionPoint } from '../../../../../lists/server';
+import { ExceptionsListPreMultiListFindServerExtension } from '../../../../../lists/server';
+import { HostIsolationExceptionsValidator } from '../validators/host_isolation_exceptions_validator';
export const getExceptionsPreMultiListFindHandler = (
endpointAppContext: EndpointAppContextService
-): (ExtensionPoint & { type: 'exceptionsListPreMultiListFind' })['callback'] => {
- return async function ({ data }) {
- // Individual validators here
+): ExceptionsListPreMultiListFindServerExtension['callback'] => {
+ return async function ({ data, context: { request } }) {
+ if (!data.namespaceType.includes('agnostic')) {
+ return data;
+ }
+
+ // Validate Host Isolation Exceptions
+ if (data.listId.some(HostIsolationExceptionsValidator.isHostIsolationException)) {
+ await new HostIsolationExceptionsValidator(
+ endpointAppContext,
+ request
+ ).validatePreMultiListFind();
+ }
return data;
};
diff --git a/x-pack/plugins/security_solution/server/lists_integration/endpoint/handlers/exceptions_pre_single_list_find_handler.ts b/x-pack/plugins/security_solution/server/lists_integration/endpoint/handlers/exceptions_pre_single_list_find_handler.ts
index 5fd3fa08ec321..3f4ca0c99642a 100644
--- a/x-pack/plugins/security_solution/server/lists_integration/endpoint/handlers/exceptions_pre_single_list_find_handler.ts
+++ b/x-pack/plugins/security_solution/server/lists_integration/endpoint/handlers/exceptions_pre_single_list_find_handler.ts
@@ -6,13 +6,24 @@
*/
import { EndpointAppContextService } from '../../../endpoint/endpoint_app_context_services';
-import { ExtensionPoint } from '../../../../../lists/server';
+import { ExceptionsListPreSingleListFindServerExtension } from '../../../../../lists/server';
+import { HostIsolationExceptionsValidator } from '../validators/host_isolation_exceptions_validator';
export const getExceptionsPreSingleListFindHandler = (
endpointAppContext: EndpointAppContextService
-): (ExtensionPoint & { type: 'exceptionsListPreSingleListFind' })['callback'] => {
- return async function ({ data }) {
- // Individual validators here
+): ExceptionsListPreSingleListFindServerExtension['callback'] => {
+ return async function ({ data, context: { request } }) {
+ if (data.namespaceType !== 'agnostic') {
+ return data;
+ }
+
+ // Validate Host Isolation Exceptions
+ if (HostIsolationExceptionsValidator.isHostIsolationException(data.listId)) {
+ await new HostIsolationExceptionsValidator(
+ endpointAppContext,
+ request
+ ).validatePreSingleListFind();
+ }
return data;
};
diff --git a/x-pack/plugins/security_solution/server/lists_integration/endpoint/handlers/exceptions_pre_summary_handler.ts b/x-pack/plugins/security_solution/server/lists_integration/endpoint/handlers/exceptions_pre_summary_handler.ts
index d98fbff5471d3..a7e495469c626 100644
--- a/x-pack/plugins/security_solution/server/lists_integration/endpoint/handlers/exceptions_pre_summary_handler.ts
+++ b/x-pack/plugins/security_solution/server/lists_integration/endpoint/handlers/exceptions_pre_summary_handler.ts
@@ -6,13 +6,28 @@
*/
import { EndpointAppContextService } from '../../../endpoint/endpoint_app_context_services';
-import { ExtensionPoint } from '../../../../../lists/server';
+import { ExceptionsListPreSummaryServerExtension } from '../../../../../lists/server';
+import { HostIsolationExceptionsValidator } from '../validators/host_isolation_exceptions_validator';
export const getExceptionsPreSummaryHandler = (
endpointAppContext: EndpointAppContextService
-): (ExtensionPoint & { type: 'exceptionsListPreSummary' })['callback'] => {
- return async function ({ data }) {
- // Individual validators here
+): ExceptionsListPreSummaryServerExtension['callback'] => {
+ return async function ({ data, context: { request, exceptionListClient } }) {
+ const { listId: maybeListId, id } = data;
+ let listId: string | null | undefined = maybeListId;
+
+ if (!listId && id) {
+ listId = (await exceptionListClient.getExceptionList(data))?.list_id ?? null;
+ }
+
+ if (!listId) {
+ return data;
+ }
+
+ // Host Isolation Exceptions
+ if (HostIsolationExceptionsValidator.isHostIsolationException(listId)) {
+ await new HostIsolationExceptionsValidator(endpointAppContext, request).validatePreSummary();
+ }
return data;
};
diff --git a/x-pack/plugins/security_solution/server/lists_integration/endpoint/handlers/exceptions_pre_update_handler.ts b/x-pack/plugins/security_solution/server/lists_integration/endpoint/handlers/exceptions_pre_update_handler.ts
index 3aceb5afbae84..4e3de46fdbf98 100644
--- a/x-pack/plugins/security_solution/server/lists_integration/endpoint/handlers/exceptions_pre_update_handler.ts
+++ b/x-pack/plugins/security_solution/server/lists_integration/endpoint/handlers/exceptions_pre_update_handler.ts
@@ -15,16 +15,21 @@ import { EventFilterValidator, TrustedAppValidator } from '../validators';
export const getExceptionsPreUpdateItemHandler = (
endpointAppContextService: EndpointAppContextService
): ExceptionsListPreUpdateItemServerExtension['callback'] => {
- return async function ({ data, context: { request } }): Promise {
- const currentSavedItem = await endpointAppContextService
- .getExceptionListsClient()
- .getExceptionListItem({
- id: data.id,
- itemId: data.itemId,
- namespaceType: data.namespaceType,
- });
-
- // We don't want to `throw` here becuase we don't know for sure that the item is one we care about.
+ return async function ({
+ data,
+ context: { request, exceptionListClient },
+ }): Promise {
+ if (data.namespaceType !== 'agnostic') {
+ return data;
+ }
+
+ const currentSavedItem = await exceptionListClient.getExceptionListItem({
+ id: data.id,
+ itemId: data.itemId,
+ namespaceType: data.namespaceType,
+ });
+
+ // We don't want to `throw` here because we don't know for sure that the item is one we care about.
// So we just return the data and the Lists plugin will likely error out because it can't find the item
if (!currentSavedItem) {
return data;
diff --git a/x-pack/plugins/security_solution/server/lists_integration/endpoint/register_endpoint_extension_points.ts b/x-pack/plugins/security_solution/server/lists_integration/endpoint/register_endpoint_extension_points.ts
index f076270cd8503..98d509ecf928f 100644
--- a/x-pack/plugins/security_solution/server/lists_integration/endpoint/register_endpoint_extension_points.ts
+++ b/x-pack/plugins/security_solution/server/lists_integration/endpoint/register_endpoint_extension_points.ts
@@ -15,6 +15,7 @@ import { getExceptionsPreDeleteItemHandler } from './handlers/exceptions_pre_del
import { getExceptionsPreExportHandler } from './handlers/exceptions_pre_export_handler';
import { getExceptionsPreMultiListFindHandler } from './handlers/exceptions_pre_multi_list_find_handler';
import { getExceptionsPreSingleListFindHandler } from './handlers/exceptions_pre_single_list_find_handler';
+import { getExceptionsPreImportHandler } from './handlers/exceptions_pre_import_handler';
export const registerListsPluginEndpointExtensionPoints = (
registerListsExtensionPoint: ListsServerExtensionRegistrar,
@@ -67,4 +68,10 @@ export const registerListsPluginEndpointExtensionPoints = (
type: 'exceptionsListPreSingleListFind',
callback: getExceptionsPreSingleListFindHandler(endpointAppContextService),
});
+
+ // PRE-IMPORT
+ registerListsExtensionPoint({
+ type: 'exceptionsListPreImport',
+ callback: getExceptionsPreImportHandler(),
+ });
};
diff --git a/x-pack/plugins/security_solution/server/lists_integration/endpoint/validators/host_isolation_exceptions_validator.ts b/x-pack/plugins/security_solution/server/lists_integration/endpoint/validators/host_isolation_exceptions_validator.ts
new file mode 100644
index 0000000000000..6de3f37e78be5
--- /dev/null
+++ b/x-pack/plugins/security_solution/server/lists_integration/endpoint/validators/host_isolation_exceptions_validator.ts
@@ -0,0 +1,46 @@
+/*
+ * 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 { ENDPOINT_HOST_ISOLATION_EXCEPTIONS_LIST_ID } from '@kbn/securitysolution-list-constants';
+import { BaseValidator } from './base_validator';
+import { EndpointArtifactExceptionValidationError } from './errors';
+
+export class HostIsolationExceptionsValidator extends BaseValidator {
+ static isHostIsolationException(listId: string): boolean {
+ return listId === ENDPOINT_HOST_ISOLATION_EXCEPTIONS_LIST_ID;
+ }
+
+ async validatePreGetOneItem(): Promise {
+ await this.validateCanManageEndpointArtifacts();
+ }
+
+ async validatePreSummary(): Promise {
+ await this.validateCanManageEndpointArtifacts();
+ }
+
+ async validatePreDeleteItem(): Promise {
+ await this.validateCanManageEndpointArtifacts();
+ }
+
+ async validatePreExport(): Promise {
+ await this.validateCanManageEndpointArtifacts();
+ }
+
+ async validatePreSingleListFind(): Promise {
+ await this.validateCanManageEndpointArtifacts();
+ }
+
+ async validatePreMultiListFind(): Promise {
+ await this.validateCanManageEndpointArtifacts();
+ }
+
+ async validatePreImport(): Promise {
+ throw new EndpointArtifactExceptionValidationError(
+ 'Import is not supported for Endpoint artifact exceptions'
+ );
+ }
+}
diff --git a/x-pack/plugins/security_solution/server/plugin.ts b/x-pack/plugins/security_solution/server/plugin.ts
index 784e2ed759798..1748956136b6d 100644
--- a/x-pack/plugins/security_solution/server/plugin.ts
+++ b/x-pack/plugins/security_solution/server/plugin.ts
@@ -348,6 +348,19 @@ export class Plugin implements ISecuritySolutionPlugin {
const savedObjectsClient = new SavedObjectsClient(core.savedObjects.createInternalRepository());
const registerIngestCallback = plugins.fleet?.registerExternalCallback;
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ const exceptionListClient = this.lists!.getExceptionListClient(
+ savedObjectsClient,
+ 'kibana',
+ // execution of Lists plugin server extension points callbacks should be turned off
+ // here because most of the uses of this client will be in contexts where some endpoint
+ // validations (specifically those around authz) can not be done (due ot the lack of a `KibanaRequest`
+ // from where authz can be derived)
+ false
+ );
+ const { authz, agentService, packageService, packagePolicyService, agentPolicyService } =
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ plugins.fleet!;
let manifestManager: ManifestManager | undefined;
this.licensing$ = plugins.licensing.license$;
@@ -355,7 +368,6 @@ export class Plugin implements ISecuritySolutionPlugin {
if (this.lists && plugins.taskManager && plugins.fleet) {
// Exceptions, Artifacts and Manifests start
const taskManager = plugins.taskManager;
- const exceptionListClient = this.lists.getExceptionListClient(savedObjectsClient, 'kibana');
const artifactClient = new EndpointArtifactClient(
plugins.fleet.createArtifactsClient('endpoint')
);
@@ -396,13 +408,6 @@ export class Plugin implements ISecuritySolutionPlugin {
this.policyWatcher.start(licenseService);
}
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- const exceptionListClient = this.lists!.getExceptionListClient(savedObjectsClient, 'kibana');
-
- const { authz, agentService, packageService, packagePolicyService, agentPolicyService } =
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- plugins.fleet!;
-
this.endpointAppContextService.start({
fleetAuthzService: authz,
agentService,
diff --git a/x-pack/test/security_solution_endpoint/services/endpoint_artifacts.ts b/x-pack/test/security_solution_endpoint/services/endpoint_artifacts.ts
index fa75dcd4b7bc5..d8500ecc3e53b 100644
--- a/x-pack/test/security_solution_endpoint/services/endpoint_artifacts.ts
+++ b/x-pack/test/security_solution_endpoint/services/endpoint_artifacts.ts
@@ -17,6 +17,7 @@ import { ExceptionsListItemGenerator } from '../../../plugins/security_solution/
import { TRUSTED_APPS_EXCEPTION_LIST_DEFINITION } from '../../../plugins/security_solution/public/management/pages/trusted_apps/constants';
import { EndpointError } from '../../../plugins/security_solution/common/endpoint/errors';
import { EVENT_FILTER_LIST } from '../../../plugins/security_solution/public/management/pages/event_filters/constants';
+import { HOST_ISOLATION_EXCEPTIONS_LIST_DEFINITION } from '../../../plugins/security_solution/public/management/pages/host_isolation_exceptions/constants';
export interface ArtifactTestData {
artifact: ExceptionListItemSchema;
@@ -59,9 +60,9 @@ export class EndpointArtifactsTestResources extends FtrService {
.then(this.getHttpResponseFailureHandler())
.then((response) => response.body as ExceptionListItemSchema);
- const { item_id: itemId, namespace_type: namespaceType } = artifact;
+ const { item_id: itemId, namespace_type: namespaceType, list_id: listId } = artifact;
- this.log.info(`Created exception list item: ${itemId}`);
+ this.log.info(`Created exception list item [${listId}]: ${itemId}`);
const cleanup = async () => {
const deleteResponse = await this.supertest
@@ -70,7 +71,9 @@ export class EndpointArtifactsTestResources extends FtrService {
.send()
.then(this.getHttpResponseFailureHandler([404]));
- this.log.info(`Deleted exception list item: ${itemId} (${deleteResponse.status})`);
+ this.log.info(
+ `Deleted exception list item [${listId}]: ${itemId} (${deleteResponse.status})`
+ );
};
return {
@@ -96,4 +99,13 @@ export class EndpointArtifactsTestResources extends FtrService {
return this.createExceptionItem(eventFilter);
}
+
+ async createHostIsolationException(
+ overrides: Partial = {}
+ ): Promise {
+ await this.ensureListExists(HOST_ISOLATION_EXCEPTIONS_LIST_DEFINITION);
+ const artifact = this.exceptionsGenerator.generateHostIsolationExceptionForCreate(overrides);
+
+ return this.createExceptionItem(artifact);
+ }
}
diff --git a/x-pack/test/security_solution_endpoint/services/endpoint_policy.ts b/x-pack/test/security_solution_endpoint/services/endpoint_policy.ts
index 6232f82181509..a376072cddb3c 100644
--- a/x-pack/test/security_solution_endpoint/services/endpoint_policy.ts
+++ b/x-pack/test/security_solution_endpoint/services/endpoint_policy.ts
@@ -102,6 +102,9 @@ export function EndpointPolicyTestResourcesProvider({ getService }: FtrProviderC
`Endpoint package was not in response from ${SECURITY_PACKAGES_ROUTE}`
);
}
+
+ log.info(`Endpoint package version: ${endpointPackageInfo.version}`);
+
return Promise.resolve(endpointPackageInfo);
});
});
@@ -217,6 +220,11 @@ export function EndpointPolicyTestResourcesProvider({ getService }: FtrProviderC
return logSupertestApiErrorAndThrow(`Unable to create Package Policy via Ingest!`, error);
}
+ log.info(
+ `Created Fleet Agent Policy: ${agentPolicy.id}\n`,
+ `Created Fleet Endpoint Package Policy: ${packagePolicy.id}`
+ );
+
return {
agentPolicy,
packagePolicy,
diff --git a/x-pack/test/security_solution_endpoint_api_int/apis/endpoint_artifacts.ts b/x-pack/test/security_solution_endpoint_api_int/apis/endpoint_artifacts.ts
index 58f6fbf58ccb5..1e9fb0c20d0ba 100644
--- a/x-pack/test/security_solution_endpoint_api_int/apis/endpoint_artifacts.ts
+++ b/x-pack/test/security_solution_endpoint_api_int/apis/endpoint_artifacts.ts
@@ -64,12 +64,12 @@ export default function ({ getService }: FtrProviderContext) {
const exceptionsGenerator = new ExceptionsListItemGenerator();
let trustedAppData: ArtifactTestData;
- type TrustedAppApiCallsInterface = Array<{
+ type TrustedAppApiCallsInterface = Array<{
method: keyof Pick;
path: string;
// The body just needs to have the properties we care about in the tests. This should cover most
// mocks used for testing that support different interfaces
- getBody: () => Pick;
+ getBody: () => BodyReturnType;
}>;
beforeEach(async () => {
@@ -84,7 +84,9 @@ export default function ({ getService }: FtrProviderContext) {
}
});
- const trustedAppApiCalls: TrustedAppApiCallsInterface = [
+ const trustedAppApiCalls: TrustedAppApiCallsInterface<
+ Pick
+ > = [
{
method: 'post',
path: EXCEPTION_LIST_ITEM_URL,
diff --git a/x-pack/test/security_solution_endpoint_api_int/apis/endpoint_host_isolation_exceptions_artifacts.ts b/x-pack/test/security_solution_endpoint_api_int/apis/endpoint_host_isolation_exceptions_artifacts.ts
new file mode 100644
index 0000000000000..9ec40ccdaf06b
--- /dev/null
+++ b/x-pack/test/security_solution_endpoint_api_int/apis/endpoint_host_isolation_exceptions_artifacts.ts
@@ -0,0 +1,155 @@
+/*
+ * 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 { EXCEPTION_LIST_ITEM_URL, EXCEPTION_LIST_URL } from '@kbn/securitysolution-list-constants';
+import { FtrProviderContext } from '../ftr_provider_context';
+import { PolicyTestResourceInfo } from '../../security_solution_endpoint/services/endpoint_policy';
+import { ArtifactTestData } from '../../security_solution_endpoint/services/endpoint_artifacts';
+import { BY_POLICY_ARTIFACT_TAG_PREFIX } from '../../../plugins/security_solution/common/endpoint/service/artifacts';
+import {
+ createUserAndRole,
+ deleteUserAndRole,
+ ROLES,
+} from '../../common/services/security_solution';
+import {
+ getImportExceptionsListSchemaMock,
+ toNdJsonString,
+} from '../../../plugins/lists/common/schemas/request/import_exceptions_schema.mock';
+
+export default function ({ getService }: FtrProviderContext) {
+ const USER = ROLES.detections_admin;
+
+ const supertest = getService('supertest');
+ const supertestWithoutAuth = getService('supertestWithoutAuth');
+ const endpointPolicyTestResources = getService('endpointPolicyTestResources');
+ const endpointArtifactTestResources = getService('endpointArtifactTestResources');
+
+ type ApiCallsInterface = Array<{
+ method: keyof Pick;
+ info?: string;
+ path: string;
+ // The body just needs to have the properties we care about in the tests. This should cover most
+ // mocks used for testing that support different interfaces
+ getBody: () => BodyReturnType;
+ }>;
+
+ describe('Endpoint Host Isolation Exceptions artifacts (via lists plugin)', () => {
+ let fleetEndpointPolicy: PolicyTestResourceInfo;
+ let existingExceptionData: ArtifactTestData;
+
+ before(async () => {
+ // Create an endpoint policy in fleet we can work with
+ fleetEndpointPolicy = await endpointPolicyTestResources.createPolicy();
+
+ // create role/user
+ await createUserAndRole(getService, USER);
+ });
+
+ after(async () => {
+ if (fleetEndpointPolicy) {
+ await fleetEndpointPolicy.cleanup();
+ }
+
+ // delete role/user
+ await deleteUserAndRole(getService, USER);
+ });
+
+ beforeEach(async () => {
+ existingExceptionData = await endpointArtifactTestResources.createHostIsolationException({
+ tags: [`${BY_POLICY_ARTIFACT_TAG_PREFIX}${fleetEndpointPolicy.packagePolicy.id}`],
+ });
+ });
+
+ afterEach(async () => {
+ if (existingExceptionData) {
+ await existingExceptionData.cleanup();
+ }
+ });
+
+ it('should return 400 for import of endpoint exceptions', async () => {
+ await supertest
+ .post(`${EXCEPTION_LIST_URL}/_import?overwrite=false`)
+ .set('kbn-xsrf', 'true')
+ .attach(
+ 'file',
+ Buffer.from(
+ toNdJsonString([
+ getImportExceptionsListSchemaMock(existingExceptionData.artifact.list_id),
+ ])
+ ),
+ 'exceptions.ndjson'
+ )
+ .expect(400, {
+ status_code: 400,
+ message:
+ 'EndpointArtifactError: Import is not supported for Endpoint artifact exceptions',
+ });
+ });
+
+ describe(`and user (${USER}) DOES NOT have authorization to manage endpoint security`, () => {
+ // Define a new array that includes the prior set from above, plus additional API calls that
+ // only have Authz validations setup
+ const allApiCalls: ApiCallsInterface = [
+ {
+ method: 'get',
+ info: 'single item',
+ get path() {
+ return `${EXCEPTION_LIST_ITEM_URL}?item_id=${existingExceptionData.artifact.item_id}&namespace_type=${existingExceptionData.artifact.namespace_type}`;
+ },
+ getBody: () => undefined,
+ },
+ {
+ method: 'get',
+ info: 'list summary',
+ get path() {
+ return `${EXCEPTION_LIST_URL}/summary?list_id=${existingExceptionData.artifact.list_id}&namespace_type=${existingExceptionData.artifact.namespace_type}`;
+ },
+ getBody: () => undefined,
+ },
+ {
+ method: 'delete',
+ info: 'single item',
+ get path() {
+ return `${EXCEPTION_LIST_ITEM_URL}?item_id=${existingExceptionData.artifact.item_id}&namespace_type=${existingExceptionData.artifact.namespace_type}`;
+ },
+ getBody: () => undefined,
+ },
+ {
+ method: 'post',
+ info: 'list export',
+ get path() {
+ return `${EXCEPTION_LIST_URL}/_export?list_id=${existingExceptionData.artifact.list_id}&namespace_type=${existingExceptionData.artifact.namespace_type}&id=1`;
+ },
+ getBody: () => undefined,
+ },
+ {
+ method: 'get',
+ info: 'find items',
+ get path() {
+ return `${EXCEPTION_LIST_ITEM_URL}/_find?list_id=${existingExceptionData.artifact.list_id}&namespace_type=${existingExceptionData.artifact.namespace_type}&page=1&per_page=1&sort_field=name&sort_order=asc`;
+ },
+ getBody: () => undefined,
+ },
+ ];
+
+ for (const apiCall of allApiCalls) {
+ it(`should error on [${apiCall.method}]${
+ apiCall.info ? ` ${apiCall.info}` : ''
+ }`, async () => {
+ await supertestWithoutAuth[apiCall.method](apiCall.path)
+ .auth(ROLES.detections_admin, 'changeme')
+ .set('kbn-xsrf', 'true')
+ .send(apiCall.getBody())
+ .expect(403, {
+ status_code: 403,
+ message: 'EndpointArtifactError: Endpoint authorization failure',
+ });
+ });
+ }
+ });
+ });
+}
diff --git a/x-pack/test/security_solution_endpoint_api_int/apis/index.ts b/x-pack/test/security_solution_endpoint_api_int/apis/index.ts
index c8dbabe7e8a6f..9b982351c6c57 100644
--- a/x-pack/test/security_solution_endpoint_api_int/apis/index.ts
+++ b/x-pack/test/security_solution_endpoint_api_int/apis/index.ts
@@ -34,5 +34,6 @@ export default function endpointAPIIntegrationTests(providerContext: FtrProvider
loadTestFile(require.resolve('./endpoint_authz'));
loadTestFile(require.resolve('./endpoint_artifacts'));
loadTestFile(require.resolve('./endpoint_artifacts/event_filter'));
+ loadTestFile(require.resolve('./endpoint_host_isolation_exceptions_artifacts'));
});
}
From 54895a2ef81e3c947cc6894e634c033cd7778d4a Mon Sep 17 00:00:00 2001
From: Jean-Louis Leysens
Date: Mon, 31 Jan 2022 15:24:44 +0100
Subject: [PATCH 19/65] [Docs] Add missing reporting config (#123917)
* added docs for `xpack.reporting.csv.escapeFormulaValues` config and `xpack.reporting.csv.useByteOrderMarkEncoding`, and some minor auto-formatting
* some more auto-formatting changes
* addded some documentation to CSV escape function
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
---
docs/settings/reporting-settings.asciidoc | 24 ++++++++++++-------
.../data/common/exports/escape_value.ts | 11 +++++++++
2 files changed, 26 insertions(+), 9 deletions(-)
diff --git a/docs/settings/reporting-settings.asciidoc b/docs/settings/reporting-settings.asciidoc
index 9b5b5b37aa62d..3c1b86f771064 100644
--- a/docs/settings/reporting-settings.asciidoc
+++ b/docs/settings/reporting-settings.asciidoc
@@ -97,10 +97,10 @@ NOTE: Running multiple instances of {kib} in a cluster for load balancing of
reporting requires identical values for <> and, if
security is enabled, <>.
-`xpack.reporting.queue.pollInterval`::
+`xpack.reporting.queue.pollInterval`::
Specifies the {time-units}[time] that the reporting poller waits between polling the index for any pending Reporting jobs. Can be specified as number of milliseconds. Defaults to `3s`.
-[[xpack-reporting-q-timeout]] `xpack.reporting.queue.timeout` {ess-icon}::
+[[xpack-reporting-q-timeout]] `xpack.reporting.queue.timeout` {ess-icon}::
{time-units}[How long] each worker has to produce a report. If your machine is slow or under heavy load, you might need to increase this timeout. If a Reporting job execution goes over this time limit, the job is marked as a failure and no download will be available. Can be specified as number of milliseconds. Defaults to `2m`.
[float]
@@ -109,7 +109,7 @@ Specifies the {time-units}[time] that the reporting poller waits between polling
Reporting works by capturing screenshots from {kib}. The following settings control the capturing process.
-`xpack.reporting.capture.timeouts.openUrl` {ess-icon}::
+`xpack.reporting.capture.timeouts.openUrl` {ess-icon}::
Specify the {time-units}[time] to allow the Reporting browser to wait for the "Loading..." screen to dismiss and find the initial data for the page. If the time is exceeded, a screenshot is captured showing the current page, and the download link shows a warning message. Can be specified as number of milliseconds. Defaults to `1m`.
`xpack.reporting.capture.timeouts.waitForElements` {ess-icon}::
@@ -123,7 +123,7 @@ running a report job, Reporting will log the error and try to continue
capturing the page with a screenshot. As a result, a download will be
available, but there will likely be errors in the visualizations in the report.
-`xpack.reporting.capture.maxAttempts` {ess-icon}::
+`xpack.reporting.capture.maxAttempts` {ess-icon}::
If capturing a report fails for any reason, {kib} will re-attempt other reporting job, as many times as this setting. Defaults to `3`.
`xpack.reporting.capture.loadDelay`::
@@ -172,14 +172,14 @@ The rule objects are evaluated sequentially from the beginning to the end of the
-------------------------------------------------------
# Only allow requests to placeholder.com
xpack.reporting.capture.networkPolicy:
- rules: [ { allow: true, host: "placeholder.com" } ]
+ rules: [ { allow: true, host: "placeholder.com" } ]
-------------------------------------------------------
[source,yaml]
-------------------------------------------------------
-# Only allow requests to https://placeholder.com
+# Only allow requests to https://placeholder.com
xpack.reporting.capture.networkPolicy:
- rules: [ { allow: true, host: "placeholder.com", protocol: "https:" } ]
+ rules: [ { allow: true, host: "placeholder.com", protocol: "https:" } ]
-------------------------------------------------------
A final `allow` rule with no host or protocol allows all requests that are not explicitly denied:
@@ -235,12 +235,18 @@ Number of documents retrieved from {es} for each scroll iteration during a CSV e
Amount of {time-units}[time] allowed before {kib} cleans the scroll context during a CSV export. Defaults to `30s`.
`xpack.reporting.csv.checkForFormulas`::
-Enables a check that warns you when there's a potential formula involved in the output (=, -, +, and @ chars). See OWASP: https://www.owasp.org/index.php/CSV_Injection. Defaults to `true`.
+Enables a check that warns you when there's a potential formula included in the output (=, -, +, and @ chars). See OWASP: https://www.owasp.org/index.php/CSV_Injection. Defaults to `true`.
-`xpack.reporting.csv` `.enablePanelActionDownload`::
+`xpack.reporting.csv.escapeFormulaValues`::
+Escape formula values in cells with a `'`. See OWASP: https://www.owasp.org/index.php/CSV_Injection. Defaults to `true`.
+
+`xpack.reporting.csv.enablePanelActionDownload`::
Enables CSV export from a saved search on a dashboard. This action is available in the dashboard panel menu for the saved search.
NOTE: This setting exists for backwards compatibility, but is unused and hardcoded to `true`. CSV export from a saved search on a dashboard is enabled when Reporting is enabled.
+`xpack.reporting.csv.useByteOrderMarkEncoding`::
+Adds a byte order mark (`\ufeff`) at the beginning of the CSV file. Defaults to `false`.
+
[float]
[[reporting-advanced-settings]]
==== Security settings
diff --git a/src/plugins/data/common/exports/escape_value.ts b/src/plugins/data/common/exports/escape_value.ts
index 9277f792a4b86..393ce6043993a 100644
--- a/src/plugins/data/common/exports/escape_value.ts
+++ b/src/plugins/data/common/exports/escape_value.ts
@@ -10,6 +10,17 @@ import { cellHasFormulas } from './formula_checks';
type RawValue = string | object | null | undefined;
+/**
+ * Create a function that will escape CSV values like "=", "@" and "+" with a
+ * "'". This will also place CSV values in "" if contain non-alphanumeric chars.
+ *
+ * For example:
+ *
+ * Given: =1+1
+ * Returns: "'=1+1"
+ *
+ * See OWASP: https://www.owasp.org/index.php/CSV_Injection.
+ */
export function createEscapeValue(
quoteValues: boolean,
escapeFormulas: boolean
From 58d54b2ad4cf932d826a550b6cac503132a7de5c Mon Sep 17 00:00:00 2001
From: Alison Goryachev
Date: Mon, 31 Jan 2022 09:52:31 -0500
Subject: [PATCH 20/65] [Upgrade Assistant] Update copy around system indices
(#123946) (#124032)
---
.../public/doc_links/doc_links_service.ts | 1 +
.../translations/translations/ja-JP.json | 1 -
.../translations/translations/zh-CN.json | 1 -
.../es_deprecations/es_deprecations.tsx | 2 +-
.../migrate_system_indices.tsx | 32 ++++++++++++++++---
.../components/overview/overview.tsx | 1 +
6 files changed, 30 insertions(+), 8 deletions(-)
diff --git a/src/core/public/doc_links/doc_links_service.ts b/src/core/public/doc_links/doc_links_service.ts
index 186f6b337fbde..6ada73b3f5f74 100644
--- a/src/core/public/doc_links/doc_links_service.ts
+++ b/src/core/public/doc_links/doc_links_service.ts
@@ -239,6 +239,7 @@ export class DocLinksService {
asyncSearch: `${ELASTICSEARCH_DOCS}async-search-intro.html`,
dataStreams: `${ELASTICSEARCH_DOCS}data-streams.html`,
deprecationLogging: `${ELASTICSEARCH_DOCS}logging.html#deprecation-logging`,
+ hiddenIndices: `${ELASTICSEARCH_DOCS}multi-index.html#hidden`,
ilm: `${ELASTICSEARCH_DOCS}index-lifecycle-management.html`,
ilmForceMerge: `${ELASTICSEARCH_DOCS}ilm-forcemerge.html`,
ilmFreeze: `${ELASTICSEARCH_DOCS}ilm-freeze.html`,
diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json
index 8e47a12f9ba4a..a41026237bb58 100644
--- a/x-pack/plugins/translations/translations/ja-JP.json
+++ b/x-pack/plugins/translations/translations/ja-JP.json
@@ -27080,7 +27080,6 @@
"xpack.upgradeAssistant.overview.pageDescription": "次のバージョンのElasticをお待ちください。",
"xpack.upgradeAssistant.overview.pageTitle": "アップグレードアシスタント",
"xpack.upgradeAssistant.overview.snapshotRestoreLink": "スナップショットの作成",
- "xpack.upgradeAssistant.overview.systemIndices.body": "アップグレード前に、システム情報が格納されたインデックスを移行します。",
"xpack.upgradeAssistant.overview.systemIndices.errorLabel": "移行失敗",
"xpack.upgradeAssistant.overview.systemIndices.featureNameTableColumn": "機能",
"xpack.upgradeAssistant.overview.systemIndices.flyoutCloseButtonLabel": "閉じる",
diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json
index f2b19cf866e83..04dbb017f4a2e 100644
--- a/x-pack/plugins/translations/translations/zh-CN.json
+++ b/x-pack/plugins/translations/translations/zh-CN.json
@@ -27550,7 +27550,6 @@
"xpack.upgradeAssistant.overview.pageDescription": "准备使用下一版 Elastic!",
"xpack.upgradeAssistant.overview.pageTitle": "升级助手",
"xpack.upgradeAssistant.overview.snapshotRestoreLink": "创建快照",
- "xpack.upgradeAssistant.overview.systemIndices.body": "在升级之前迁移存储系统信息的索引。",
"xpack.upgradeAssistant.overview.systemIndices.errorLabel": "迁移失败",
"xpack.upgradeAssistant.overview.systemIndices.featureNameTableColumn": "功能",
"xpack.upgradeAssistant.overview.systemIndices.flyoutCloseButtonLabel": "关闭",
diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/es_deprecations.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/es_deprecations.tsx
index b74ed4275a17e..d4bbab5102d99 100644
--- a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/es_deprecations.tsx
+++ b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/es_deprecations.tsx
@@ -46,7 +46,7 @@ const i18nTexts = {
}),
pageDescription: i18n.translate('xpack.upgradeAssistant.esDeprecations.pageDescription', {
defaultMessage:
- 'Resolve all critical issues before upgrading. Before making changes, ensure you have a current snapshot of your cluster. Indices created before 7.0 must be reindexed or removed.',
+ 'Resolve all critical issues before upgrading. Before making changes, ensure you have a current snapshot of your cluster. Indices created before 7.0 must be reindexed or removed, including hidden indices such as those used to store Machine Learning data.',
}),
isLoading: i18n.translate('xpack.upgradeAssistant.esDeprecations.loadingText', {
defaultMessage: 'Loading deprecation issues…',
diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/overview/migrate_system_indices/migrate_system_indices.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/overview/migrate_system_indices/migrate_system_indices.tsx
index 2b4eb6ee1037f..8fade09bbcb47 100644
--- a/x-pack/plugins/upgrade_assistant/public/application/components/overview/migrate_system_indices/migrate_system_indices.tsx
+++ b/x-pack/plugins/upgrade_assistant/public/application/components/overview/migrate_system_indices/migrate_system_indices.tsx
@@ -19,9 +19,11 @@ import {
EuiFlexGroup,
EuiFlexItem,
EuiCode,
+ EuiLink,
} from '@elastic/eui';
import type { EuiStepProps } from '@elastic/eui/src/components/steps/step';
+import { DocLinksStart } from 'kibana/public';
import type { SystemIndicesMigrationFeature } from '../../../../../common/types';
import type { OverviewStepProps } from '../../types';
import { useMigrateSystemIndices } from './use_migrate_system_indices';
@@ -48,9 +50,24 @@ const i18nTexts = {
title: i18n.translate('xpack.upgradeAssistant.overview.systemIndices.title', {
defaultMessage: 'Migrate system indices',
}),
- bodyDescription: i18n.translate('xpack.upgradeAssistant.overview.systemIndices.body', {
- defaultMessage: 'Migrate the indices that store system information before you upgrade.',
- }),
+ bodyDescription: (docLink: string) => {
+ return (
+
+
+
+ ),
+ }}
+ />
+ );
+ },
startButtonLabel: i18n.translate(
'xpack.upgradeAssistant.overview.systemIndices.startButtonLabel',
{
@@ -214,10 +231,15 @@ const MigrateSystemIndicesStep: FunctionComponent = ({ setIsComplete }) =
);
};
+interface CustomProps {
+ docLinks: DocLinksStart;
+}
+
export const getMigrateSystemIndicesStep = ({
isComplete,
setIsComplete,
-}: OverviewStepProps): EuiStepProps => {
+ docLinks,
+}: OverviewStepProps & CustomProps): EuiStepProps => {
const status = isComplete ? 'complete' : 'incomplete';
return {
@@ -227,7 +249,7 @@ export const getMigrateSystemIndicesStep = ({
children: (
<>
- {i18nTexts.bodyDescription}
+ {i18nTexts.bodyDescription(docLinks.links.elasticsearch.hiddenIndices)}
diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/overview/overview.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/overview/overview.tsx
index db4ca7593642c..93dcc162aa6fe 100644
--- a/x-pack/plugins/upgrade_assistant/public/application/components/overview/overview.tsx
+++ b/x-pack/plugins/upgrade_assistant/public/application/components/overview/overview.tsx
@@ -107,6 +107,7 @@ export const Overview = withRouter(({ history }: RouteComponentProps) => {
setIsComplete: setCompletedStep.bind(null, 'backup'),
}),
getMigrateSystemIndicesStep({
+ docLinks,
isComplete: isStepComplete('migrate_system_indices'),
setIsComplete: setCompletedStep.bind(null, 'migrate_system_indices'),
}),
From 821f0710a6075383f95013898b13487e1430b7dd Mon Sep 17 00:00:00 2001
From: Alison Goryachev
Date: Mon, 31 Jan 2022 09:52:58 -0500
Subject: [PATCH 21/65] [Upgrade Assistant] Update upgrade guide doc links
(#123953) (#124029)
---
.../kibana-plugin-core-public.doclinksstart.links.md | 3 ++-
src/core/public/doc_links/doc_links_service.ts | 7 ++++---
src/core/public/public.api.md | 3 ++-
x-pack/plugins/translations/translations/ja-JP.json | 1 -
x-pack/plugins/translations/translations/zh-CN.json | 1 -
.../components/overview/upgrade_step/upgrade_step.tsx | 9 +++------
6 files changed, 11 insertions(+), 13 deletions(-)
diff --git a/docs/development/core/public/kibana-plugin-core-public.doclinksstart.links.md b/docs/development/core/public/kibana-plugin-core-public.doclinksstart.links.md
index 7864f2ca828db..7cf5a8a749302 100644
--- a/docs/development/core/public/kibana-plugin-core-public.doclinksstart.links.md
+++ b/docs/development/core/public/kibana-plugin-core-public.doclinksstart.links.md
@@ -11,7 +11,8 @@ readonly links: {
readonly settings: string;
readonly elasticStackGetStarted: string;
readonly upgrade: {
- readonly upgradingElasticStack: string;
+ readonly upgradingStackOnPrem: string;
+ readonly upgradingStackOnCloud: string;
};
readonly apm: {
readonly kibanaSettings: string;
diff --git a/src/core/public/doc_links/doc_links_service.ts b/src/core/public/doc_links/doc_links_service.ts
index 6ada73b3f5f74..205d4f4fe61fb 100644
--- a/src/core/public/doc_links/doc_links_service.ts
+++ b/src/core/public/doc_links/doc_links_service.ts
@@ -22,7 +22,6 @@ export class DocLinksService {
// Documentation for `main` branches is still published at a `master` URL.
const DOC_LINK_VERSION = kibanaBranch === 'main' ? 'master' : kibanaBranch;
const ELASTIC_WEBSITE_URL = 'https://www.elastic.co/';
- const STACK_DOCS = `${ELASTIC_WEBSITE_URL}guide/en/elastic-stack/${DOC_LINK_VERSION}/`;
const ELASTICSEARCH_DOCS = `${ELASTIC_WEBSITE_URL}guide/en/elasticsearch/reference/${DOC_LINK_VERSION}/`;
const KIBANA_DOCS = `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/`;
const FLEET_DOCS = `${ELASTIC_WEBSITE_URL}guide/en/fleet/${DOC_LINK_VERSION}/`;
@@ -41,7 +40,8 @@ export class DocLinksService {
settings: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/settings.html`,
elasticStackGetStarted: `${STACK_GETTING_STARTED}get-started-elastic-stack.html`,
upgrade: {
- upgradingElasticStack: `${STACK_DOCS}upgrading-elastic-stack.html`,
+ upgradingStackOnPrem: `${ELASTIC_WEBSITE_URL}guide/en/elastic-stack/8.0/upgrading-elastic-stack-on-prem.html`,
+ upgradingStackOnCloud: `${ELASTIC_WEBSITE_URL}guide/en/elastic-stack/8.0/upgrade-elastic-stack-for-elastic-cloud.html`,
},
apm: {
kibanaSettings: `${KIBANA_DOCS}apm-settings-in-kibana.html`,
@@ -613,7 +613,8 @@ export interface DocLinksStart {
readonly settings: string;
readonly elasticStackGetStarted: string;
readonly upgrade: {
- readonly upgradingElasticStack: string;
+ readonly upgradingStackOnPrem: string;
+ readonly upgradingStackOnCloud: string;
};
readonly apm: {
readonly kibanaSettings: string;
diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md
index a29b8d0b5cc68..4fb3e90e997d4 100644
--- a/src/core/public/public.api.md
+++ b/src/core/public/public.api.md
@@ -494,7 +494,8 @@ export interface DocLinksStart {
readonly settings: string;
readonly elasticStackGetStarted: string;
readonly upgrade: {
- readonly upgradingElasticStack: string;
+ readonly upgradingStackOnPrem: string;
+ readonly upgradingStackOnCloud: string;
};
readonly apm: {
readonly kibanaSettings: string;
diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json
index a41026237bb58..07fc674875ad1 100644
--- a/x-pack/plugins/translations/translations/ja-JP.json
+++ b/x-pack/plugins/translations/translations/ja-JP.json
@@ -27102,7 +27102,6 @@
"xpack.upgradeAssistant.overview.upgradeStepCloudLink": "クラウドでアップグレード",
"xpack.upgradeAssistant.overview.upgradeStepDescription": "重要な問題をすべて解決し、アプリケーションの準備を確認した後に、Elastic 8.xにアップグレードできます。アップグレードする前に、必ずもう一度データをバックアップしたことを確認してください。",
"xpack.upgradeAssistant.overview.upgradeStepDescriptionForCloud": "重要な問題をすべて解決し、アプリケーションの準備を確認した後に、Elastic 8.xにアップグレードできます。アップグレードする前に、必ずもう一度データをバックアップしたことを確認してください。Elastic Cloudでデプロイをアップグレードします。",
- "xpack.upgradeAssistant.overview.upgradeStepLink": "詳細",
"xpack.upgradeAssistant.overview.upgradeStepTitle": "Elastic 8.xへのアップグレード",
"xpack.upgradeAssistant.overview.verifyChanges.calloutBody": "変更した後、カウンターをリセットして監視を続け、廃止予定の機能を使用していないことを確認します。",
"xpack.upgradeAssistant.overview.verifyChanges.calloutTitle": "{previousCheck}以降{warningsCount, plural, =0 {0} other {{warningsCount}}}件の廃止予定{warningsCount, plural, other {件の問題}}",
diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json
index 04dbb017f4a2e..6d9b75e14d261 100644
--- a/x-pack/plugins/translations/translations/zh-CN.json
+++ b/x-pack/plugins/translations/translations/zh-CN.json
@@ -27572,7 +27572,6 @@
"xpack.upgradeAssistant.overview.upgradeStepCloudLink": "在 Cloud 上升级",
"xpack.upgradeAssistant.overview.upgradeStepDescription": "解决所有紧急问题并确认您的应用程序就绪后,便可以升级到 Elastic 8.x。在升级之前,请确保再次备份您的数据。",
"xpack.upgradeAssistant.overview.upgradeStepDescriptionForCloud": "解决所有紧急问题并确认您的应用程序就绪后,便可以升级到 Elastic 8.x。在升级之前,请确保再次备份您的数据。在 Elastic Cloud 上升级您的部署。",
- "xpack.upgradeAssistant.overview.upgradeStepLink": "了解详情",
"xpack.upgradeAssistant.overview.upgradeStepTitle": "升级到 Elastic 8.x",
"xpack.upgradeAssistant.overview.verifyChanges.calloutBody": "做出更改后,请重置计数器并继续监测,以确认您不再使用过时功能。",
"xpack.upgradeAssistant.overview.verifyChanges.calloutTitle": "自 {previousCheck} 以来出现 {warningsCount, plural, other {{warningsCount}}} 个弃用{warningsCount, plural, other {问题}} ",
diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/overview/upgrade_step/upgrade_step.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/overview/upgrade_step/upgrade_step.tsx
index 5025dccfaf3bf..3b2f6e9968fb3 100644
--- a/x-pack/plugins/upgrade_assistant/public/application/components/overview/upgrade_step/upgrade_step.tsx
+++ b/x-pack/plugins/upgrade_assistant/public/application/components/overview/upgrade_step/upgrade_step.tsx
@@ -35,9 +35,6 @@ const i18nTexts = {
"Once you've resolved all critical issues and verified that your applications are ready, you can upgrade to Elastic 8.x. Be sure to back up your data again before upgrading. Upgrade your deployment on Elastic Cloud.",
}
),
- upgradeStepLink: i18n.translate('xpack.upgradeAssistant.overview.upgradeStepLink', {
- defaultMessage: 'Learn more',
- }),
upgradeStepCloudLink: i18n.translate('xpack.upgradeAssistant.overview.upgradeStepCloudLink', {
defaultMessage: 'Upgrade on Cloud',
}),
@@ -110,7 +107,7 @@ const UpgradeStep = () => {
{
} else {
callToAction = (
- {i18nTexts.upgradeStepLink}
+ {i18nTexts.upgradeGuideLink}
);
}
From 6a707516f6e5c3e90f63003670a8cd348bb719f4 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Ester=20Mart=C3=AD=20Vilaseca?=
Date: Mon, 31 Jan 2022 15:55:45 +0100
Subject: [PATCH 22/65] [Unified Observability] Exploratory view style fixes
(#123843)
* Remove resize button and change show/hide button position
* change apply button size
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
---
.../exploratory_view/exploratory_view.tsx | 46 +++++++++----------
.../exploratory_view/views/view_actions.tsx | 1 -
2 files changed, 22 insertions(+), 25 deletions(-)
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.tsx
index 6ffb49560722a..62b54a3fec203 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.tsx
@@ -8,7 +8,14 @@
import { i18n } from '@kbn/i18n';
import React, { useEffect, useRef, useState } from 'react';
import styled from 'styled-components';
-import { EuiButtonEmpty, EuiResizableContainer, EuiTitle, EuiPanel } from '@elastic/eui';
+import {
+ EuiButtonEmpty,
+ EuiResizableContainer,
+ EuiTitle,
+ EuiPanel,
+ EuiFlexGroup,
+ EuiFlexItem,
+} from '@elastic/eui';
import { PanelDirection } from '@elastic/eui/src/components/resizable_container/types';
import { useKibana } from '../../../../../../../src/plugins/kibana_react/public';
import { ObservabilityPublicPluginsStart } from '../../../plugin';
@@ -108,6 +115,19 @@ export function ExploratoryView({
return (
<>
+
+
+ onChange('chartPanel')}
+ >
+ {hiddenPanel === 'chartPanel' ? SHOW_CHART_LABEL : HIDE_CHART_LABEL}
+
+
+
+
)}
-
+
- onChange('chartPanel')}
- />
-
(
-
- {isCollapsed ? SHOW_CHART_LABEL : HIDE_CHART_LABEL}
-
-))`
- &:focus,
- &:focus:enabled {
- background: none;
- }
- position: absolute;
- top: -30px;
- right: 0;
-`;
-
const HIDE_CHART_LABEL = i18n.translate('xpack.observability.overview.exploratoryView.hideChart', {
defaultMessage: 'Hide chart',
});
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/views/view_actions.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/views/view_actions.tsx
index 2b9e8a26b0c2a..c216c09f6c364 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/views/view_actions.tsx
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/views/view_actions.tsx
@@ -55,7 +55,6 @@ export function ViewActions({ onApply }: Props) {
onClick={() => applyChanges(onApply)}
isDisabled={noChanges}
fill
- size="s"
data-test-subj={'seriesChangesApplyButton'}
>
{i18n.translate('xpack.observability.expView.seriesBuilder.apply', {
From 9b20c4f035cda123616dadab61c7efce5e5e8ae5 Mon Sep 17 00:00:00 2001
From: Maja Grubic
Date: Mon, 31 Jan 2022 16:03:22 +0100
Subject: [PATCH 23/65] [Discover] Create data view from sidebar (#123391)
* [Discover] Create data view from sidebar
* Fix failing unit test
* Fix invalid import
* Addressing PR comments
* Add horizontal separator
* Design tweaks
* Update unit test
* Remove double declaration
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: Ryan Keairns
---
src/plugins/discover/kibana.json | 3 +-
.../components/layout/discover_layout.tsx | 11 +++-
...ver_index_pattern_management.test.tsx.snap | 3 +-
...discover_index_pattern_management.test.tsx | 14 +++-
.../discover_index_pattern_management.tsx | 38 +++++++++--
.../sidebar/discover_sidebar.test.tsx | 2 +
.../components/sidebar/discover_sidebar.tsx | 5 ++
.../discover_sidebar_responsive.test.tsx | 1 +
.../sidebar/discover_sidebar_responsive.tsx | 43 +++++++++++-
src/plugins/discover/public/build_services.ts | 3 +
src/plugins/discover/public/plugin.tsx | 2 +
src/plugins/discover/tsconfig.json | 3 +-
.../apps/discover/_data_view_editor.ts | 65 +++++++++++++++++++
test/functional/apps/discover/index.ts | 1 +
test/functional/page_objects/discover_page.ts | 13 ++++
15 files changed, 194 insertions(+), 13 deletions(-)
create mode 100644 test/functional/apps/discover/_data_view_editor.ts
diff --git a/src/plugins/discover/kibana.json b/src/plugins/discover/kibana.json
index d0ff54290256e..015cb6ddaf285 100644
--- a/src/plugins/discover/kibana.json
+++ b/src/plugins/discover/kibana.json
@@ -13,7 +13,8 @@
"navigation",
"uiActions",
"savedObjects",
- "dataViewFieldEditor"
+ "dataViewFieldEditor",
+ "dataViewEditor"
],
"optionalPlugins": ["home", "share", "usageCollection", "spaces"],
"requiredBundles": ["kibanaUtils", "home", "kibanaReact", "dataViews"],
diff --git a/src/plugins/discover/public/application/main/components/layout/discover_layout.tsx b/src/plugins/discover/public/application/main/components/layout/discover_layout.tsx
index 5601596a4d73b..b7af922a058cd 100644
--- a/src/plugins/discover/public/application/main/components/layout/discover_layout.tsx
+++ b/src/plugins/discover/public/application/main/components/layout/discover_layout.tsx
@@ -48,7 +48,7 @@ import {
import { FieldStatisticsTable } from '../field_stats_table';
import { VIEW_MODE } from '../../../../components/view_mode_toggle';
import { DOCUMENTS_VIEW_CLICK, FIELD_STATISTICS_VIEW_CLICK } from '../field_stats_table/constants';
-import { DataViewType } from '../../../../../../data_views/common';
+import { DataViewType, DataView } from '../../../../../../data_views/common';
/**
* Local storage key for sidebar persistence state
@@ -204,6 +204,14 @@ export function DiscoverLayout({
}, [isSidebarClosed, storage]);
const contentCentered = resultState === 'uninitialized' || resultState === 'none';
+ const onDataViewCreated = useCallback(
+ (dataView: DataView) => {
+ if (dataView.id) {
+ onChangeIndexPattern(dataView.id);
+ }
+ },
+ [onChangeIndexPattern]
+ );
return (
@@ -245,6 +253,7 @@ export function DiscoverLayout({
useNewFieldsApi={useNewFieldsApi}
onEditRuntimeField={onEditRuntimeField}
viewMode={viewMode}
+ onDataViewCreated={onDataViewCreated}
/>
diff --git a/src/plugins/discover/public/application/main/components/sidebar/__snapshots__/discover_index_pattern_management.test.tsx.snap b/src/plugins/discover/public/application/main/components/sidebar/__snapshots__/discover_index_pattern_management.test.tsx.snap
index 94aa55ff23853..059c247c38c2e 100644
--- a/src/plugins/discover/public/application/main/components/sidebar/__snapshots__/discover_index_pattern_management.test.tsx.snap
+++ b/src/plugins/discover/public/application/main/components/sidebar/__snapshots__/discover_index_pattern_management.test.tsx.snap
@@ -133,6 +133,7 @@ exports[`Discover DataView Management renders correctly 1`] = `
}
>
{
const indexPattern = stubLogstashIndexPattern;
const editField = jest.fn();
+ const createNewDataView = jest.fn();
const mountComponent = () => {
return mountWithIntl(
@@ -62,6 +63,7 @@ describe('Discover DataView Management', () => {
editField={editField}
selectedIndexPattern={indexPattern}
useNewFieldsApi={true}
+ createNewDataView={createNewDataView}
/>
);
@@ -81,7 +83,7 @@ describe('Discover DataView Management', () => {
button.simulate('click');
expect(component.find(EuiContextMenuPanel).length).toBe(1);
- expect(component.find(EuiContextMenuItem).length).toBe(2);
+ expect(component.find(EuiContextMenuItem).length).toBe(3);
});
test('click on an add button executes editField callback', () => {
@@ -103,4 +105,14 @@ describe('Discover DataView Management', () => {
manageButton.simulate('click');
expect(mockServices.core.application.navigateToApp).toHaveBeenCalled();
});
+
+ test('click on add dataView button executes createNewDataView callback', () => {
+ const component = mountComponent();
+ const button = findTestSubject(component, 'discoverIndexPatternActions');
+ button.simulate('click');
+
+ const manageButton = findTestSubject(component, 'dataview-create-new');
+ manageButton.simulate('click');
+ expect(createNewDataView).toHaveBeenCalled();
+ });
});
diff --git a/src/plugins/discover/public/application/main/components/sidebar/discover_index_pattern_management.tsx b/src/plugins/discover/public/application/main/components/sidebar/discover_index_pattern_management.tsx
index 0655357d55983..b62e6e15c55af 100644
--- a/src/plugins/discover/public/application/main/components/sidebar/discover_index_pattern_management.tsx
+++ b/src/plugins/discover/public/application/main/components/sidebar/discover_index_pattern_management.tsx
@@ -7,7 +7,13 @@
*/
import React, { useState } from 'react';
-import { EuiButtonIcon, EuiContextMenuItem, EuiContextMenuPanel, EuiPopover } from '@elastic/eui';
+import {
+ EuiButtonIcon,
+ EuiContextMenuItem,
+ EuiContextMenuPanel,
+ EuiHorizontalRule,
+ EuiPopover,
+} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { useDiscoverServices } from '../../../../utils/use_discover_services';
import { DataView } from '../../../../../../data/common';
@@ -26,11 +32,16 @@ export interface DiscoverIndexPatternManagementProps {
* @param fieldName
*/
editField: (fieldName?: string) => void;
+
+ /**
+ * Callback to execute on create new data action
+ */
+ createNewDataView: () => void;
}
export function DiscoverIndexPatternManagement(props: DiscoverIndexPatternManagementProps) {
const { dataViewFieldEditor, core } = useDiscoverServices();
- const { useNewFieldsApi, selectedIndexPattern, editField } = props;
+ const { useNewFieldsApi, selectedIndexPattern, editField, createNewDataView } = props;
const dataViewEditPermission = dataViewFieldEditor?.userPermissions.editIndexPattern();
const canEditDataViewField = !!dataViewEditPermission && useNewFieldsApi;
const [isAddIndexPatternFieldPopoverOpen, setIsAddIndexPatternFieldPopoverOpen] = useState(false);
@@ -45,7 +56,7 @@ export function DiscoverIndexPatternManagement(props: DiscoverIndexPatternManage
return (
{
setIsAddIndexPatternFieldPopoverOpen(false);
@@ -67,7 +78,8 @@ export function DiscoverIndexPatternManagement(props: DiscoverIndexPatternManage
}
>
{i18n.translate('discover.fieldChooser.indexPatterns.addFieldButton', {
- defaultMessage: 'Add field to data view',
+ defaultMessage: 'Add field',
})}
,
{i18n.translate('discover.fieldChooser.indexPatterns.manageFieldButton', {
- defaultMessage: 'Manage data view fields',
+ defaultMessage: 'Manage settings',
+ })}
+ ,
+ ,
+ {
+ setIsAddIndexPatternFieldPopoverOpen(false);
+ createNewDataView();
+ }}
+ >
+ {i18n.translate('discover.fieldChooser.dataViews.createNewDataView', {
+ defaultMessage: 'Create new data view',
})}
,
]}
diff --git a/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar.test.tsx b/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar.test.tsx
index e236d7e8a1b89..8a38b0b026bbb 100644
--- a/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar.test.tsx
+++ b/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar.test.tsx
@@ -63,6 +63,8 @@ function getCompProps(): DiscoverSidebarProps {
onEditRuntimeField: jest.fn(),
editField: jest.fn(),
viewMode: VIEW_MODE.DOCUMENT_LEVEL,
+ createNewDataView: jest.fn(),
+ onDataViewCreated: jest.fn(),
};
}
diff --git a/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar.tsx b/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar.tsx
index 087a5a6ae312b..6569348f99038 100644
--- a/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar.tsx
+++ b/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar.tsx
@@ -69,6 +69,8 @@ export interface DiscoverSidebarProps extends Omit void;
+ createNewDataView: () => void;
+
/**
* a statistics of the distribution of fields in the given hits
*/
@@ -104,6 +106,7 @@ export function DiscoverSidebarComponent({
closeFlyout,
editField,
viewMode,
+ createNewDataView,
}: DiscoverSidebarProps) {
const { uiSettings, dataViewFieldEditor } = useDiscoverServices();
const [fields, setFields] = useState(null);
@@ -299,6 +302,7 @@ export function DiscoverSidebarComponent({
selectedIndexPattern={selectedIndexPattern}
editField={editField}
useNewFieldsApi={useNewFieldsApi}
+ createNewDataView={createNewDataView}
/>
@@ -336,6 +340,7 @@ export function DiscoverSidebarComponent({
selectedIndexPattern={selectedIndexPattern}
useNewFieldsApi={useNewFieldsApi}
editField={editField}
+ createNewDataView={createNewDataView}
/>
diff --git a/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar_responsive.test.tsx b/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar_responsive.test.tsx
index 1915f6707f94d..fdf11075e6b48 100644
--- a/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar_responsive.test.tsx
+++ b/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar_responsive.test.tsx
@@ -104,6 +104,7 @@ function getCompProps(): DiscoverSidebarResponsiveProps {
trackUiMetric: jest.fn(),
onEditRuntimeField: jest.fn(),
viewMode: VIEW_MODE.DOCUMENT_LEVEL,
+ onDataViewCreated: jest.fn(),
};
}
diff --git a/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar_responsive.tsx b/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar_responsive.tsx
index abc59ff282863..88b43341afe7c 100644
--- a/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar_responsive.tsx
+++ b/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar_responsive.tsx
@@ -102,6 +102,10 @@ export interface DiscoverSidebarResponsiveProps {
* callback to execute on edit runtime field
*/
onEditRuntimeField: () => void;
+ /**
+ * callback to execute on create dataview
+ */
+ onDataViewCreated: (dataView: DataView) => void;
/**
* Discover view mode
*/
@@ -115,7 +119,13 @@ export interface DiscoverSidebarResponsiveProps {
*/
export function DiscoverSidebarResponsive(props: DiscoverSidebarResponsiveProps) {
const services = useDiscoverServices();
- const { selectedIndexPattern, onEditRuntimeField, useNewFieldsApi, onChangeIndexPattern } = props;
+ const {
+ selectedIndexPattern,
+ onEditRuntimeField,
+ useNewFieldsApi,
+ onChangeIndexPattern,
+ onDataViewCreated,
+ } = props;
const [fieldFilter, setFieldFilter] = useState(getDefaultFieldFilter());
const [isFlyoutVisible, setIsFlyoutVisible] = useState(false);
/**
@@ -146,12 +156,16 @@ export function DiscoverSidebarResponsive(props: DiscoverSidebarResponsiveProps)
}, [selectedIndexPattern]);
const closeFieldEditor = useRef<() => void | undefined>();
+ const closeDataViewEditor = useRef<() => void | undefined>();
useEffect(() => {
const cleanup = () => {
if (closeFieldEditor?.current) {
closeFieldEditor?.current();
}
+ if (closeDataViewEditor?.current) {
+ closeDataViewEditor?.current();
+ }
};
return () => {
// Make sure to close the editor when unmounting
@@ -163,11 +177,15 @@ export function DiscoverSidebarResponsive(props: DiscoverSidebarResponsiveProps)
closeFieldEditor.current = ref;
}, []);
+ const setDataViewEditorRef = useCallback((ref: () => void | undefined) => {
+ closeDataViewEditor.current = ref;
+ }, []);
+
const closeFlyout = useCallback(() => {
setIsFlyoutVisible(false);
}, []);
- const { dataViewFieldEditor } = services;
+ const { dataViewFieldEditor, dataViewEditor } = services;
const editField = useCallback(
(fieldName?: string) => {
@@ -203,6 +221,24 @@ export function DiscoverSidebarResponsive(props: DiscoverSidebarResponsiveProps)
]
);
+ const createNewDataView = useCallback(() => {
+ const indexPatternFieldEditPermission = dataViewEditor.userPermissions.editDataView;
+ if (!indexPatternFieldEditPermission) {
+ return;
+ }
+ const ref = dataViewEditor.openEditor({
+ onSave: async (dataView) => {
+ onDataViewCreated(dataView);
+ },
+ });
+ if (setDataViewEditorRef) {
+ setDataViewEditorRef(ref);
+ }
+ if (closeFlyout) {
+ closeFlyout();
+ }
+ }, [dataViewEditor, setDataViewEditorRef, closeFlyout, onDataViewCreated]);
+
if (!selectedIndexPattern) {
return null;
}
@@ -218,6 +254,7 @@ export function DiscoverSidebarResponsive(props: DiscoverSidebarResponsiveProps)
fieldCounts={fieldCounts.current}
setFieldFilter={setFieldFilter}
editField={editField}
+ createNewDataView={createNewDataView}
/>
)}
@@ -244,6 +281,7 @@ export function DiscoverSidebarResponsive(props: DiscoverSidebarResponsiveProps)
selectedIndexPattern={selectedIndexPattern}
editField={editField}
useNewFieldsApi={useNewFieldsApi}
+ createNewDataView={createNewDataView}
/>
@@ -307,6 +345,7 @@ export function DiscoverSidebarResponsive(props: DiscoverSidebarResponsiveProps)
setFieldEditorRef={setFieldEditorRef}
closeFlyout={closeFlyout}
editField={editField}
+ createNewDataView={createNewDataView}
/>
diff --git a/src/plugins/discover/public/build_services.ts b/src/plugins/discover/public/build_services.ts
index 393893432538b..f6492db6e8a42 100644
--- a/src/plugins/discover/public/build_services.ts
+++ b/src/plugins/discover/public/build_services.ts
@@ -40,6 +40,7 @@ import { FieldFormatsStart } from '../../field_formats/public';
import { EmbeddableStart } from '../../embeddable/public';
import type { SpacesApi } from '../../../../x-pack/plugins/spaces/public';
+import { DataViewEditorStart } from '../../../plugins/data_view_editor/public';
export interface HistoryLocationState {
referrer: string;
@@ -68,6 +69,7 @@ export interface DiscoverServices {
uiSettings: IUiSettingsClient;
trackUiMetric?: (metricType: UiCounterMetricType, eventName: string | string[]) => void;
dataViewFieldEditor: IndexPatternFieldEditorStart;
+ dataViewEditor: DataViewEditorStart;
http: HttpStart;
storage: Storage;
spaces?: SpacesApi;
@@ -109,5 +111,6 @@ export const buildServices = memoize(function (
dataViewFieldEditor: plugins.dataViewFieldEditor,
http: core.http,
spaces: plugins.spaces,
+ dataViewEditor: plugins.dataViewEditor,
};
});
diff --git a/src/plugins/discover/public/plugin.tsx b/src/plugins/discover/public/plugin.tsx
index 43c03a59b5b25..e55158b0dad5e 100644
--- a/src/plugins/discover/public/plugin.tsx
+++ b/src/plugins/discover/public/plugin.tsx
@@ -62,6 +62,7 @@ import type { SpacesPluginStart } from '../../../../x-pack/plugins/spaces/public
import { FieldFormatsStart } from '../../field_formats/public';
import { injectTruncateStyles } from './utils/truncate_styles';
import { DOC_TABLE_LEGACY, TRUNCATE_MAX_HEIGHT } from '../common';
+import { DataViewEditorStart } from '../../../plugins/data_view_editor/public';
import { useDiscoverServices } from './utils/use_discover_services';
declare module '../../share/public' {
@@ -176,6 +177,7 @@ export interface DiscoverSetupPlugins {
* @internal
*/
export interface DiscoverStartPlugins {
+ dataViewEditor: DataViewEditorStart;
uiActions: UiActionsStart;
embeddable: EmbeddableStart;
navigation: NavigationStart;
diff --git a/src/plugins/discover/tsconfig.json b/src/plugins/discover/tsconfig.json
index 4ff6f0598d7d8..6dad573a272fb 100644
--- a/src/plugins/discover/tsconfig.json
+++ b/src/plugins/discover/tsconfig.json
@@ -25,6 +25,7 @@
{ "path": "../data_view_field_editor/tsconfig.json"},
{ "path": "../field_formats/tsconfig.json" },
{ "path": "../data_views/tsconfig.json" },
- { "path": "../../../x-pack/plugins/spaces/tsconfig.json" }
+ { "path": "../../../x-pack/plugins/spaces/tsconfig.json" },
+ { "path": "../data_view_editor/tsconfig.json" }
]
}
diff --git a/test/functional/apps/discover/_data_view_editor.ts b/test/functional/apps/discover/_data_view_editor.ts
new file mode 100644
index 0000000000000..c67964fddf93b
--- /dev/null
+++ b/test/functional/apps/discover/_data_view_editor.ts
@@ -0,0 +1,65 @@
+/*
+ * 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 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import { FtrProviderContext } from './ftr_provider_context';
+
+export default function ({ getService, getPageObjects }: FtrProviderContext) {
+ const retry = getService('retry');
+ const testSubjects = getService('testSubjects');
+ const kibanaServer = getService('kibanaServer');
+ const esArchiver = getService('esArchiver');
+ const security = getService('security');
+ const PageObjects = getPageObjects(['common', 'discover', 'header', 'timePicker']);
+ const defaultSettings = {
+ defaultIndex: 'logstash-*',
+ };
+
+ const createDataView = async (dataViewName: string) => {
+ await PageObjects.discover.clickIndexPatternActions();
+ await PageObjects.discover.clickCreateNewDataView();
+ await testSubjects.setValue('createIndexPatternNameInput', dataViewName, {
+ clearWithKeyboard: true,
+ typeCharByChar: true,
+ });
+ await testSubjects.click('saveIndexPatternButton');
+ };
+
+ describe('discover integration with data view editor', function describeIndexTests() {
+ before(async function () {
+ await security.testUser.setRoles(['kibana_admin', 'test_logstash_reader']);
+ await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/logstash_functional');
+ await kibanaServer.savedObjects.clean({ types: ['saved-search', 'index-pattern'] });
+ await kibanaServer.importExport.load('test/functional/fixtures/kbn_archiver/discover');
+ await kibanaServer.uiSettings.replace(defaultSettings);
+ await PageObjects.timePicker.setDefaultAbsoluteRangeViaUiSettings();
+ await PageObjects.common.navigateToApp('discover');
+ });
+
+ after(async () => {
+ await security.testUser.restoreDefaults();
+ await kibanaServer.importExport.unload('test/functional/fixtures/kbn_archiver/discover');
+ await kibanaServer.savedObjects.clean({ types: ['saved-search', 'index-pattern'] });
+ });
+
+ it('allows creating a new data view', async function () {
+ const dataViewToCreate = 'logstash';
+ await createDataView(dataViewToCreate);
+ await PageObjects.header.waitUntilLoadingHasFinished();
+ await retry.waitForWithTimeout(
+ 'data view selector to include a newly created dataview',
+ 5000,
+ async () => {
+ const dataViewTitle = await PageObjects.discover.getCurrentlySelectedDataView();
+ // data view editor will add wildcard symbol by default
+ // so we need to include it in our original title when comparing
+ return dataViewTitle === `${dataViewToCreate}*`;
+ }
+ );
+ });
+ });
+}
diff --git a/test/functional/apps/discover/index.ts b/test/functional/apps/discover/index.ts
index b5eb160526876..d2b627c175fcc 100644
--- a/test/functional/apps/discover/index.ts
+++ b/test/functional/apps/discover/index.ts
@@ -54,6 +54,7 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) {
loadTestFile(require.resolve('./_search_on_page_load'));
loadTestFile(require.resolve('./_chart_hidden'));
loadTestFile(require.resolve('./_context_encoded_url_param'));
+ loadTestFile(require.resolve('./_data_view_editor'));
loadTestFile(require.resolve('./_empty_state'));
});
}
diff --git a/test/functional/page_objects/discover_page.ts b/test/functional/page_objects/discover_page.ts
index f9328e89cd19e..effacb30bdc89 100644
--- a/test/functional/page_objects/discover_page.ts
+++ b/test/functional/page_objects/discover_page.ts
@@ -363,6 +363,13 @@ export class DiscoverPageObject extends FtrService {
});
}
+ public async clickCreateNewDataView() {
+ await this.retry.try(async () => {
+ await this.testSubjects.click('dataview-create-new');
+ await this.find.byClassName('indexPatternEditor__form');
+ });
+ }
+
public async hasNoResults() {
return await this.testSubjects.exists('discoverNoResults');
}
@@ -598,4 +605,10 @@ export class DiscoverPageObject extends FtrService {
await this.testSubjects.existOrFail('dscFieldStatsEmbeddedContent');
});
}
+
+ public async getCurrentlySelectedDataView() {
+ await this.testSubjects.existOrFail('discover-sidebar');
+ const button = await this.testSubjects.find('indexPattern-switch-link');
+ return button.getAttribute('title');
+ }
}
From f68210d7f19fba8989288038c09068b88406509b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?David=20S=C3=A1nchez?=
Date: Mon, 31 Jan 2022 16:03:40 +0100
Subject: [PATCH 24/65] [Security Solution] [Endpoint] Address some host
isolation UX search issues (#124110)
* Address some issues in policy hosti isolation exceptions tab with loaders, wrong links and inconsistent statuses
* Keep all data when performing a search to avoid reseting the list while performing search action
* Removes unused import
---
.../host_isolation_exceptions/view/hooks.ts | 2 +-
.../host_isolation_exceptions_list.test.tsx | 9 +-
.../view/host_isolation_exceptions_list.tsx | 9 +-
.../components/assign_flyout.tsx | 2 +-
.../components/empty_unassigned.tsx | 28 ++---
.../components/empty_unexisting.tsx | 11 +-
.../components/list.test.tsx | 106 ++++++++++++------
.../components/list.tsx | 36 +++---
...y_host_isolation_exceptions_empty_hooks.ts | 65 +++++++++++
.../host_isolation_exceptions_tab.test.tsx | 16 +--
.../host_isolation_exceptions_tab.tsx | 39 ++-----
.../pages/policy/view/policy_hooks.ts | 18 +++
12 files changed, 216 insertions(+), 125 deletions(-)
create mode 100644 x-pack/plugins/security_solution/public/management/pages/policy/view/host_isolation_exceptions/components/use_policy_host_isolation_exceptions_empty_hooks.ts
diff --git a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/hooks.ts b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/hooks.ts
index 05422adda6e9f..7ab03f9eaa68f 100644
--- a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/hooks.ts
+++ b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/hooks.ts
@@ -120,7 +120,7 @@ export function useFetchHostIsolationExceptionsList({
filter: kql,
});
},
- { enabled }
+ { enabled, keepPreviousData: true }
);
}
diff --git a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/host_isolation_exceptions_list.test.tsx b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/host_isolation_exceptions_list.test.tsx
index 097fbe97fd908..57c8485c0eda6 100644
--- a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/host_isolation_exceptions_list.test.tsx
+++ b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/host_isolation_exceptions_list.test.tsx
@@ -170,9 +170,10 @@ describe('When on the host isolation exceptions page', () => {
await waitFor(() =>
expect(getHostIsolationExceptionItemsMock).toHaveBeenLastCalledWith({
http: mockedContext.coreStart.http,
- filter: undefined,
+ filter:
+ '(exception-list-agnostic.attributes.item_id:(*this*does*not*exists*) OR exception-list-agnostic.attributes.name:(*this*does*not*exists*) OR exception-list-agnostic.attributes.description:(*this*does*not*exists*) OR exception-list-agnostic.attributes.entries.value:(*this*does*not*exists*))',
page: 1,
- perPage: 1,
+ perPage: 10,
})
);
@@ -206,9 +207,9 @@ describe('When on the host isolation exceptions page', () => {
await waitFor(() =>
expect(getHostIsolationExceptionItemsMock).toHaveBeenLastCalledWith({
http: mockedContext.coreStart.http,
- filter: undefined,
+ filter: `((exception-list-agnostic.attributes.tags:"policy:${firstPolicy.id}"))`,
page: 1,
- perPage: 1,
+ perPage: 10,
})
);
diff --git a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/host_isolation_exceptions_list.tsx b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/host_isolation_exceptions_list.tsx
index 083c7bb58e340..b3dcc4a0fbc97 100644
--- a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/host_isolation_exceptions_list.tsx
+++ b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/host_isolation_exceptions_list.tsx
@@ -50,6 +50,7 @@ type HostIsolationExceptionPaginatedContent = PaginatedContentProps<
typeof ExceptionItem
>;
+/* eslint-disable complexity */
export const HostIsolationExceptionsList = () => {
const history = useHistory();
const privileges = useUserPrivileges().endpointPrivileges;
@@ -76,7 +77,7 @@ export const HostIsolationExceptionsList = () => {
const includedPoliciesParam = location.included_policies;
- const { isLoading, data, error, refetch } = useFetchHostIsolationExceptionsList({
+ const { isLoading, isRefetching, data, error, refetch } = useFetchHostIsolationExceptionsList({
filter: location.filter,
page: location.page_index,
perPage: location.page_size,
@@ -198,7 +199,9 @@ export const HostIsolationExceptionsList = () => {
[navigateCallback]
);
- if ((isLoading || isLoadingAll) && !hasDataToShow) {
+ const isSearchLoading = isLoading || isRefetching;
+
+ if ((isSearchLoading || isLoadingAll) && !hasDataToShow) {
return ;
}
@@ -278,7 +281,7 @@ export const HostIsolationExceptionsList = () => {
itemComponentProps={handleItemComponentProps}
onChange={handlePaginatedContentChange}
error={error?.message}
- loading={isLoading}
+ loading={isSearchLoading}
pagination={pagination}
contentClassName="host-isolation-exceptions-container"
data-test-subj="hostIsolationExceptionsContent"
diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/host_isolation_exceptions/components/assign_flyout.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/host_isolation_exceptions/components/assign_flyout.tsx
index 5b23504f61239..8e27fea173816 100644
--- a/x-pack/plugins/security_solution/public/management/pages/policy/view/host_isolation_exceptions/components/assign_flyout.tsx
+++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/host_isolation_exceptions/components/assign_flyout.tsx
@@ -277,7 +277,7 @@ export const PolicyHostIsolationExceptionsAssignFlyout = ({
data-test-subj="hostIsolationExceptions-assignable-list"
artifacts={exceptionsRequest.data}
selectedArtifactIds={selectedArtifactIds}
- isListLoading={exceptionsRequest.isLoading}
+ isListLoading={exceptionsRequest.isLoading || exceptionsRequest.isRefetching}
selectedArtifactsUpdated={handleSelectArtifact}
/>
diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/host_isolation_exceptions/components/empty_unassigned.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/host_isolation_exceptions/components/empty_unassigned.tsx
index 5c6fd49d9add5..40438338c5216 100644
--- a/x-pack/plugins/security_solution/public/management/pages/policy/view/host_isolation_exceptions/components/empty_unassigned.tsx
+++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/host_isolation_exceptions/components/empty_unassigned.tsx
@@ -7,27 +7,26 @@
import { EuiButton, EuiEmptyPrompt, EuiLink, EuiPageTemplate } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
-import React from 'react';
-import { useHistory } from 'react-router-dom';
-import { getPolicyHostIsolationExceptionsPath } from '../../../../../common/routing';
+import React, { useCallback } from 'react';
import { PolicyData } from '../../../../../../../common/endpoint/types';
+import { usePolicyDetailsHostIsolationExceptionsNavigateCallback } from '../../policy_hooks';
+import { useGetLinkTo } from './use_policy_host_isolation_exceptions_empty_hooks';
export const PolicyHostIsolationExceptionsEmptyUnassigned = ({
policy,
- toHostIsolationList,
}: {
policy: PolicyData;
- toHostIsolationList: string;
}) => {
- const history = useHistory();
-
- const onClickPrimaryButtonHandler = () =>
- history.push(
- getPolicyHostIsolationExceptionsPath(policy.id, {
- ...location,
+ const { onClickHandler, toRouteUrl } = useGetLinkTo(policy.id, policy.name);
+ const navigateCallback = usePolicyDetailsHostIsolationExceptionsNavigateCallback();
+ const onClickPrimaryButtonHandler = useCallback(
+ () =>
+ navigateCallback({
show: 'list',
- })
- );
+ }),
+ [navigateCallback]
+ );
+
return (
,
-
+ // eslint-disable-next-line @elastic/eui/href-or-on-click
+
{
+ const { onClickHandler, toRouteUrl } = useGetLinkTo(policy.id, policy.name);
+
return (
}
actions={
-
+ // eslint-disable-next-line @elastic/eui/href-or-on-click
+
{
let policyId: string;
- let render: (
- exceptions: FoundExceptionListItemSchema
- ) => ReturnType;
+ let render: () => ReturnType;
let renderResult: ReturnType;
let history: AppContextTestRender['history'];
let mockedContext: AppContextTestRender;
beforeEach(() => {
policyId = uuid.v4();
+ getHostIsolationExceptionItemsMock.mockClear();
useUserPrivilegesMock.mockReturnValue({
endpointPrivileges: {
canIsolateHost: true,
@@ -49,9 +50,9 @@ describe('Policy details host isolation exceptions tab', () => {
});
mockedContext = createAppRootMockRenderer();
({ history } = mockedContext);
- render = (exceptions: FoundExceptionListItemSchema) =>
+ render = () =>
(renderResult = mockedContext.render(
-
+
));
act(() => {
@@ -59,20 +60,22 @@ describe('Policy details host isolation exceptions tab', () => {
});
});
- it('should display a searchbar and count even with no exceptions', () => {
- render(emptyList);
+ it('should display a searchbar and count even with no exceptions', async () => {
+ getHostIsolationExceptionItemsMock.mockResolvedValue(emptyList);
+ render();
expect(
- renderResult.getByTestId('policyDetailsHostIsolationExceptionsSearchCount')
+ await renderResult.findByTestId('policyDetailsHostIsolationExceptionsSearchCount')
).toHaveTextContent('Showing 0 host isolation exceptions');
expect(renderResult.getByTestId('searchField')).toBeTruthy();
});
- it('should render the list of exceptions collapsed and expand it when clicked', () => {
+ it('should render the list of exceptions collapsed and expand it when clicked', async () => {
// render 3
- render(getFoundExceptionListItemSchemaMock(3));
- expect(renderResult.getAllByTestId('hostIsolationExceptions-collapsed-list-card')).toHaveLength(
- 3
- );
+ getHostIsolationExceptionItemsMock.mockResolvedValue(getFoundExceptionListItemSchemaMock(3));
+ render();
+ expect(
+ await renderResult.findAllByTestId('hostIsolationExceptions-collapsed-list-card')
+ ).toHaveLength(3);
expect(
renderResult.queryAllByTestId(
'hostIsolationExceptions-collapsed-list-card-criteriaConditions'
@@ -80,11 +83,12 @@ describe('Policy details host isolation exceptions tab', () => {
).toHaveLength(0);
});
- it('should expand an item when expand is clicked', () => {
- render(getFoundExceptionListItemSchemaMock(1));
- expect(renderResult.getAllByTestId('hostIsolationExceptions-collapsed-list-card')).toHaveLength(
- 1
- );
+ it('should expand an item when expand is clicked', async () => {
+ getHostIsolationExceptionItemsMock.mockResolvedValue(getFoundExceptionListItemSchemaMock(1));
+ render();
+ expect(
+ await renderResult.findAllByTestId('hostIsolationExceptions-collapsed-list-card')
+ ).toHaveLength(1);
userEvent.click(
renderResult.getByTestId('hostIsolationExceptions-collapsed-list-card-header-expandCollapse')
@@ -97,28 +101,46 @@ describe('Policy details host isolation exceptions tab', () => {
).toHaveLength(1);
});
- it('should change the address location when a filter is applied', () => {
- render(getFoundExceptionListItemSchemaMock(1));
- userEvent.type(renderResult.getByTestId('searchField'), 'search me{enter}');
+ it('should change the address location when a filter is applied', async () => {
+ getHostIsolationExceptionItemsMock.mockResolvedValue(getFoundExceptionListItemSchemaMock(1));
+ render();
+ userEvent.type(await renderResult.findByTestId('searchField'), 'search me{enter}');
expect(history.location.search).toBe('?filter=search%20me');
});
- it('should disable the "remove from policy" option to global exceptions', () => {
+ it('should apply a filter when requested from location search params', async () => {
+ history.push(getPolicyHostIsolationExceptionsPath(policyId, { filter: 'my filter' }));
+ getHostIsolationExceptionItemsMock.mockResolvedValue(() =>
+ getFoundExceptionListItemSchemaMock(4)
+ );
+ render();
+ expect(getHostIsolationExceptionItemsMock).toHaveBeenCalledWith({
+ filter: `((exception-list-agnostic.attributes.tags:"policy:${policyId}" OR exception-list-agnostic.attributes.tags:"policy:all")) AND ((exception-list-agnostic.attributes.item_id:(*my*filter*) OR exception-list-agnostic.attributes.name:(*my*filter*) OR exception-list-agnostic.attributes.description:(*my*filter*) OR exception-list-agnostic.attributes.entries.value:(*my*filter*)))`,
+ http: mockedContext.coreStart.http,
+ page: 1,
+ perPage: 10,
+ });
+ });
+
+ it('should disable the "remove from policy" option to global exceptions', async () => {
const testException = getExceptionListItemSchemaMock({ tags: ['policy:all'] });
const exceptions = {
...emptyList,
data: [testException],
total: 1,
};
- render(exceptions);
+ getHostIsolationExceptionItemsMock.mockResolvedValue(exceptions);
+ render();
// click the actions button
userEvent.click(
- renderResult.getByTestId('hostIsolationExceptions-collapsed-list-card-header-actions-button')
+ await renderResult.findByTestId(
+ 'hostIsolationExceptions-collapsed-list-card-header-actions-button'
+ )
);
expect(renderResult.getByTestId('remove-from-policy-action')).toBeDisabled();
});
- it('should enable the "remove from policy" option to policy-specific exceptions ', () => {
+ it('should enable the "remove from policy" option to policy-specific exceptions ', async () => {
const testException = getExceptionListItemSchemaMock({
tags: [`policy:${policyId}`, 'policy:1234', 'not-a-policy-tag'],
});
@@ -127,24 +149,30 @@ describe('Policy details host isolation exceptions tab', () => {
data: [testException],
total: 1,
};
- render(exceptions);
+ getHostIsolationExceptionItemsMock.mockResolvedValue(exceptions);
+ render();
// click the actions button
userEvent.click(
- renderResult.getByTestId('hostIsolationExceptions-collapsed-list-card-header-actions-button')
+ await renderResult.findByTestId(
+ 'hostIsolationExceptions-collapsed-list-card-header-actions-button'
+ )
);
expect(renderResult.getByTestId('remove-from-policy-action')).toBeEnabled();
});
- it('should enable the "view full details" action', () => {
- render(getFoundExceptionListItemSchemaMock(1));
+ it('should enable the "view full details" action', async () => {
+ getHostIsolationExceptionItemsMock.mockResolvedValue(getFoundExceptionListItemSchemaMock(1));
+ render();
// click the actions button
userEvent.click(
- renderResult.getByTestId('hostIsolationExceptions-collapsed-list-card-header-actions-button')
+ await renderResult.findByTestId(
+ 'hostIsolationExceptions-collapsed-list-card-header-actions-button'
+ )
);
expect(renderResult.queryByTestId('view-full-details-action')).toBeTruthy();
});
- it('should render the delete dialog when the "remove from policy" button is clicked', () => {
+ it('should render the delete dialog when the "remove from policy" button is clicked', async () => {
const testException = getExceptionListItemSchemaMock({
tags: [`policy:${policyId}`, 'policy:1234', 'not-a-policy-tag'],
});
@@ -153,10 +181,13 @@ describe('Policy details host isolation exceptions tab', () => {
data: [testException],
total: 1,
};
- render(exceptions);
+ getHostIsolationExceptionItemsMock.mockResolvedValue(exceptions);
+ render();
// click the actions button
userEvent.click(
- renderResult.getByTestId('hostIsolationExceptions-collapsed-list-card-header-actions-button')
+ await renderResult.findByTestId(
+ 'hostIsolationExceptions-collapsed-list-card-header-actions-button'
+ )
);
userEvent.click(renderResult.getByTestId('remove-from-policy-action'));
@@ -173,11 +204,12 @@ describe('Policy details host isolation exceptions tab', () => {
});
});
- it('should not display the delete action, do show the full details', () => {
- render(getFoundExceptionListItemSchemaMock(1));
+ it('should not display the delete action, do show the full details', async () => {
+ getHostIsolationExceptionItemsMock.mockResolvedValue(getFoundExceptionListItemSchemaMock(1));
+ render();
// click the actions button
userEvent.click(
- renderResult.getByTestId(
+ await renderResult.findByTestId(
'hostIsolationExceptions-collapsed-list-card-header-actions-button'
)
);
diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/host_isolation_exceptions/components/list.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/host_isolation_exceptions/components/list.tsx
index f8f18a2292c5d..feee5491da314 100644
--- a/x-pack/plugins/security_solution/public/management/pages/policy/view/host_isolation_exceptions/components/list.tsx
+++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/host_isolation_exceptions/components/list.tsx
@@ -7,10 +7,7 @@
import { EuiSpacer, EuiText } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
-import {
- ExceptionListItemSchema,
- FoundExceptionListItemSchema,
-} from '@kbn/securitysolution-io-ts-list-types';
+import { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types';
import React, { useCallback, useMemo, useState } from 'react';
import { useHistory } from 'react-router-dom';
import { useAppUrl } from '../../../../../../common/lib/kibana';
@@ -35,18 +32,14 @@ import { useGetEndpointSpecificPolicies } from '../../../../../services/policies
import { getCurrentArtifactsLocation } from '../../../store/policy_details/selectors';
import { usePolicyDetailsSelector } from '../../policy_hooks';
import { PolicyHostIsolationExceptionsDeleteModal } from './delete_modal';
+import { useFetchHostIsolationExceptionsList } from '../../../../host_isolation_exceptions/view/hooks';
-export const PolicyHostIsolationExceptionsList = ({
- exceptions,
- policyId,
-}: {
- exceptions: FoundExceptionListItemSchema;
- policyId: string;
-}) => {
+export const PolicyHostIsolationExceptionsList = ({ policyId }: { policyId: string }) => {
const history = useHistory();
const { getAppUrl } = useAppUrl();
const privileges = useUserPrivileges().endpointPrivileges;
+ const location = usePolicyDetailsSelector(getCurrentArtifactsLocation);
// load the list of policies>
const policiesRequest = useGetEndpointSpecificPolicies();
@@ -58,11 +51,18 @@ export const PolicyHostIsolationExceptionsList = ({
const [expandedItemsMap, setExpandedItemsMap] = useState
From a810fe6bc2ec74441b0f130cb9d161fb43ac23c8 Mon Sep 17 00:00:00 2001
From: Shahzad
Date: Mon, 31 Jan 2022 16:35:45 +0100
Subject: [PATCH 32/65] [Uptime Monitor Management] Update error conditions for
location fetch (#124111)
---
.../monitor_management/hooks/use_locations.test.tsx | 2 +-
.../monitor_management/hooks/use_locations.ts | 2 ++
.../public/pages/monitor_management/add_monitor.tsx | 4 ++--
.../lib/synthetics_service/service_api_client.ts | 9 ++-------
.../lib/synthetics_service/synthetics_service.ts | 13 +++++++++++++
.../synthetics_service/get_service_locations.ts | 9 +++++++--
6 files changed, 27 insertions(+), 12 deletions(-)
diff --git a/x-pack/plugins/uptime/public/components/monitor_management/hooks/use_locations.test.tsx b/x-pack/plugins/uptime/public/components/monitor_management/hooks/use_locations.test.tsx
index 73859e4de99d5..c1f2b741cb29c 100644
--- a/x-pack/plugins/uptime/public/components/monitor_management/hooks/use_locations.test.tsx
+++ b/x-pack/plugins/uptime/public/components/monitor_management/hooks/use_locations.test.tsx
@@ -55,6 +55,6 @@ describe('useExpViewTimeRange', function () {
wrapper: Wrapper,
});
- expect(result.current).toEqual({ loading, error });
+ expect(result.current).toEqual({ loading, error, locations: [] });
});
});
diff --git a/x-pack/plugins/uptime/public/components/monitor_management/hooks/use_locations.ts b/x-pack/plugins/uptime/public/components/monitor_management/hooks/use_locations.ts
index 1d0af00e232ea..a2df203136189 100644
--- a/x-pack/plugins/uptime/public/components/monitor_management/hooks/use_locations.ts
+++ b/x-pack/plugins/uptime/public/components/monitor_management/hooks/use_locations.ts
@@ -15,6 +15,7 @@ export function useLocations() {
const {
error: { serviceLocations: serviceLocationsError },
loading: { serviceLocations: serviceLocationsLoading },
+ locations,
} = useSelector(monitorManagementListSelector);
useEffect(() => {
@@ -22,6 +23,7 @@ export function useLocations() {
}, [dispatch]);
return {
+ locations,
error: serviceLocationsError,
loading: serviceLocationsLoading,
};
diff --git a/x-pack/plugins/uptime/public/pages/monitor_management/add_monitor.tsx b/x-pack/plugins/uptime/public/pages/monitor_management/add_monitor.tsx
index cc474b0654643..bc8737ccd4b35 100644
--- a/x-pack/plugins/uptime/public/pages/monitor_management/add_monitor.tsx
+++ b/x-pack/plugins/uptime/public/pages/monitor_management/add_monitor.tsx
@@ -19,13 +19,13 @@ export const AddMonitorPage: React.FC = () => {
useTrackPageview({ app: 'uptime', path: 'add-monitor' });
useTrackPageview({ app: 'uptime', path: 'add-monitor', delay: 15000 });
- const { error, loading } = useLocations();
+ const { error, loading, locations } = useLocations();
useMonitorManagementBreadcrumbs({ isAddMonitor: true });
return (
{
- this.locations = result.locations;
- });
}
getHttpsAgent() {
diff --git a/x-pack/plugins/uptime/server/lib/synthetics_service/synthetics_service.ts b/x-pack/plugins/uptime/server/lib/synthetics_service/synthetics_service.ts
index de29ee0466ac0..054bceec48571 100644
--- a/x-pack/plugins/uptime/server/lib/synthetics_service/synthetics_service.ts
+++ b/x-pack/plugins/uptime/server/lib/synthetics_service/synthetics_service.ts
@@ -26,9 +26,11 @@ import { formatMonitorConfig } from './formatters/format_configs';
import {
ConfigKey,
MonitorFields,
+ ServiceLocations,
SyntheticsMonitor,
SyntheticsMonitorWithId,
} from '../../../common/runtime_types';
+import { getServiceLocations } from './get_service_locations';
const SYNTHETICS_SERVICE_SYNC_MONITORS_TASK_TYPE =
'UPTIME:SyntheticsService:Sync-Saved-Monitor-Objects';
@@ -45,6 +47,8 @@ export class SyntheticsService {
private apiKey: SyntheticsServiceApiKey | undefined;
+ public locations: ServiceLocations;
+
constructor(logger: Logger, server: UptimeServerSetup, config: ServiceConfig) {
this.logger = logger;
this.server = server;
@@ -53,6 +57,8 @@ export class SyntheticsService {
this.apiClient = new ServiceAPIClient(logger, this.config, this.server.kibanaVersion);
this.esHosts = getEsHosts({ config: this.config, cloud: server.cloud });
+
+ this.locations = [];
}
public init() {
@@ -98,6 +104,13 @@ export class SyntheticsService {
async run() {
const { state } = taskInstance;
+ const { manifestUrl } = service.config;
+
+ getServiceLocations({ manifestUrl }).then((result) => {
+ service.locations = result.locations;
+ service.apiClient.locations = result.locations;
+ });
+
await service.pushConfigs();
return { state };
diff --git a/x-pack/plugins/uptime/server/rest_api/synthetics_service/get_service_locations.ts b/x-pack/plugins/uptime/server/rest_api/synthetics_service/get_service_locations.ts
index e785ec583d8ca..907b8a9ce3621 100644
--- a/x-pack/plugins/uptime/server/rest_api/synthetics_service/get_service_locations.ts
+++ b/x-pack/plugins/uptime/server/rest_api/synthetics_service/get_service_locations.ts
@@ -13,6 +13,11 @@ export const getServiceLocationsRoute: UMRestApiRouteFactory = () => ({
method: 'GET',
path: API_URLS.SERVICE_LOCATIONS,
validate: {},
- handler: async ({ server }): Promise =>
- getServiceLocations({ manifestUrl: server.config.service!.manifestUrl! }),
+ handler: async ({ server }): Promise => {
+ if (server.syntheticsService.locations.length > 0) {
+ return { locations: server.syntheticsService.locations };
+ }
+
+ return getServiceLocations({ manifestUrl: server.config.service!.manifestUrl! });
+ },
});
From 39de549049d223260c713d250867d355e0df83f4 Mon Sep 17 00:00:00 2001
From: Jonathan Buttner <56361221+jonathan-buttner@users.noreply.github.com>
Date: Mon, 31 Jan 2022 10:45:45 -0500
Subject: [PATCH 33/65] [Cases] Removing sub cases (#123006)
* Removing subcases from the backend
* making more progress
* Removing sub cases references
* Fixing tests
* Removing sub case class
* Fixing type errors
* First wave of fixes for integration tests
* Fixing integration tests and some types
* Fixing translations
* Fixing comments and todos
* Removing the collection keyword
* Updating readme and fixing type error
* Removing remainder of readme for case connector
* Fixing integration test type error
* Removing references to "sub case"
* Addressing additional feedback
* Removing styled table and fixing type errors
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
---
x-pack/plugins/cases/README.md | 121 +-
x-pack/plugins/cases/common/api/cases/case.ts | 54 +-
.../plugins/cases/common/api/cases/comment.ts | 22 +-
.../plugins/cases/common/api/cases/index.ts | 1 -
.../cases/common/api/cases/sub_case.ts | 112 --
.../common/api/cases/user_actions/common.ts | 13 +-
x-pack/plugins/cases/common/api/helpers.ts | 15 -
x-pack/plugins/cases/common/constants.ts | 18 +-
x-pack/plugins/cases/common/index.ts | 4 +-
x-pack/plugins/cases/common/ui/types.ts | 14 +-
x-pack/plugins/cases/package.json | 5 +-
.../public/common/navigation/paths.test.ts | 32 -
.../cases/public/common/navigation/paths.ts | 16 +-
.../cases/public/common/translations.ts | 6 -
.../components/add_comment/index.test.tsx | 1 -
.../public/components/add_comment/index.tsx | 5 +-
.../all_cases/all_cases_list.test.tsx | 101 +-
.../components/all_cases/all_cases_list.tsx | 23 +-
.../public/components/all_cases/columns.tsx | 55 +-
.../components/all_cases/expanded_row.tsx | 78 --
.../public/components/all_cases/helpers.ts | 45 -
.../all_cases_selector_modal.tsx | 6 +-
.../public/components/all_cases/table.tsx | 37 +-
.../components/all_cases/utility_bar.tsx | 5 +-
.../public/components/app/routes.test.tsx | 7 +-
.../cases/public/components/app/routes.tsx | 12 +-
.../public/components/bulk_actions/index.tsx | 8 +-
.../case_action_bar/actions.test.tsx | 2 +-
.../components/case_action_bar/actions.tsx | 4 +-
.../components/case_action_bar/index.tsx | 24 +-
.../case_view/case_view_page.test.tsx | 66 +-
.../components/case_view/case_view_page.tsx | 35 +-
.../components/case_view/index.test.tsx | 4 +-
.../public/components/case_view/index.tsx | 5 +-
.../public/components/case_view/types.ts | 1 -
.../configure_cases/closure_options.tsx | 1 -
.../connectors_dropdown.test.tsx | 21 -
.../configure_cases/connectors_dropdown.tsx | 7 -
.../configure_cases/translations.ts | 7 -
.../components/connector_selector/form.tsx | 3 -
.../connectors/case/alert_fields.tsx | 109 --
.../connectors/case/cases_dropdown.tsx | 74 -
.../connectors/case/existing_case.tsx | 82 --
.../components/connectors/case/index.ts | 42 -
.../connectors/case/translations.ts | 73 -
.../components/connectors/case/types.ts | 18 -
.../public/components/connectors/index.ts | 2 -
.../public/components/create/connector.tsx | 43 +-
.../cases/public/components/create/form.tsx | 25 +-
.../components/create/form_context.test.tsx | 1 -
.../public/components/create/form_context.tsx | 12 +-
.../cases/public/components/create/index.tsx | 12 +-
.../cases/public/components/create/mock.ts | 3 +-
.../components/edit_connector/index.tsx | 3 -
.../public/components/links/index.test.tsx | 6 +-
.../cases/public/components/links/index.tsx | 8 +-
.../components/recent_cases/recent_cases.tsx | 7 +-
.../create_case_modal.tsx | 9 +-
.../use_create_case_modal/index.tsx | 9 +-
.../components/user_actions/copy_link.tsx | 6 +-
.../components/user_actions/helpers.test.ts | 4 +-
.../components/user_actions/index.test.tsx | 5 +-
.../public/components/user_actions/index.tsx | 5 +-
.../use_user_actions_handler.test.tsx | 1 -
.../user_actions/use_user_actions_handler.tsx | 5 +-
x-pack/plugins/cases/public/containers/api.ts | 100 +-
.../plugins/cases/public/containers/mock.ts | 19 +-
.../containers/use_delete_cases.test.tsx | 7 +-
.../public/containers/use_delete_cases.tsx | 7 +-
.../cases/public/containers/use_get_case.tsx | 15 +-
.../containers/use_get_case_user_actions.tsx | 21 +-
.../cases/public/containers/use_get_cases.tsx | 1 -
.../containers/use_post_comment.test.tsx | 29 +-
.../public/containers/use_post_comment.tsx | 5 +-
.../containers/use_update_case.test.tsx | 24 +-
.../public/containers/use_update_case.tsx | 35 +-
.../containers/use_update_comment.test.tsx | 24 +-
.../public/containers/use_update_comment.tsx | 3 -
x-pack/plugins/cases/public/plugin.ts | 8 +-
x-pack/plugins/cases/public/types.ts | 6 +-
.../cases/server/client/attachments/add.ts | 256 +---
.../cases/server/client/attachments/delete.ts | 49 +-
.../cases/server/client/attachments/get.ts | 71 +-
.../cases/server/client/attachments/update.ts | 72 +-
.../cases/server/client/cases/create.ts | 20 +-
.../cases/server/client/cases/delete.ts | 58 +-
.../plugins/cases/server/client/cases/find.ts | 9 +-
.../plugins/cases/server/client/cases/get.ts | 66 +-
.../plugins/cases/server/client/cases/mock.ts | 12 -
.../plugins/cases/server/client/cases/push.ts | 20 +-
.../cases/server/client/cases/update.ts | 211 +--
.../cases/server/client/cases/utils.test.ts | 11 +-
.../cases/server/client/cases/utils.ts | 5 +-
x-pack/plugins/cases/server/client/client.ts | 16 -
.../server/client/metrics/actions/actions.ts | 1 -
.../server/client/metrics/alerts/count.ts | 1 -
.../server/client/metrics/lifespan.test.ts | 1 -
.../client/metrics/test_utils/lifespan.ts | 1 -
x-pack/plugins/cases/server/client/mocks.ts | 14 -
.../cases/server/client/stats/client.ts | 3 +-
.../cases/server/client/sub_cases/client.ts | 240 ----
.../cases/server/client/sub_cases/update.ts | 402 ------
.../cases/server/client/typedoc_interfaces.ts | 7 -
.../server/client/user_actions/client.ts | 4 -
.../server/client/user_actions/get.test.ts | 107 --
.../cases/server/client/user_actions/get.ts | 29 +-
.../plugins/cases/server/client/utils.test.ts | 5 +-
x-pack/plugins/cases/server/client/utils.ts | 147 +-
.../plugins/cases/server/common/constants.ts | 11 +-
.../server/common/models/commentable_case.ts | 169 +--
.../plugins/cases/server/common/utils.test.ts | 57 -
x-pack/plugins/cases/server/common/utils.ts | 108 +-
.../server/connectors/case/index.test.ts | 1190 -----------------
.../cases/server/connectors/case/index.ts | 192 ---
.../cases/server/connectors/case/schema.ts | 166 ---
.../server/connectors/case/translations.ts | 12 -
.../cases/server/connectors/case/types.ts | 45 -
.../server/connectors/case/validators.ts | 15 -
.../plugins/cases/server/connectors/index.ts | 64 -
.../plugins/cases/server/connectors/types.ts | 6 -
x-pack/plugins/cases/server/plugin.ts | 13 +-
.../api/__fixtures__/mock_saved_objects.ts | 14 +-
.../cases/server/routes/api/cases/get_case.ts | 8 +-
.../api/comments/delete_all_comments.ts | 8 +-
.../routes/api/comments/delete_comment.ts | 8 +-
.../routes/api/comments/get_all_comment.ts | 10 +-
.../routes/api/comments/patch_comment.ts | 8 +-
.../routes/api/comments/post_comment.ts | 18 +-
.../plugins/cases/server/routes/api/index.ts | 19 +-
.../routes/api/sub_case/delete_sub_cases.ts | 37 -
.../routes/api/sub_case/find_sub_cases.ts | 53 -
.../routes/api/sub_case/get_sub_case.ts | 46 -
.../routes/api/sub_case/patch_sub_cases.ts | 34 -
.../api/user_actions/get_all_user_actions.ts | 36 +-
.../cases/server/saved_object_types/cases.ts | 4 -
.../server/saved_object_types/comments.ts | 3 -
.../cases/server/saved_object_types/index.ts | 1 -
.../migrations/cases.test.ts | 23 +-
.../saved_object_types/migrations/cases.ts | 16 +-
.../migrations/comments.test.ts | 62 +-
.../saved_object_types/migrations/comments.ts | 34 +-
.../migrations/constants.ts | 14 +
.../saved_object_types/migrations/index.ts | 8 -
.../migrations/user_actions/alerts.ts | 8 +-
.../server/saved_object_types/sub_case.ts | 76 --
.../cases/server/scripts/sub_cases/README.md | 80 --
.../server/scripts/sub_cases/generator.js | 9 -
.../cases/server/scripts/sub_cases/index.ts | 240 ----
.../cases/server/services/alerts/index.ts | 4 +-
.../server/services/attachments/index.ts | 4 +-
.../cases/server/services/cases/index.test.ts | 2 -
.../cases/server/services/cases/index.ts | 599 +--------
.../server/services/cases/transform.test.ts | 2 -
x-pack/plugins/cases/server/services/mocks.ts | 13 -
.../cases/server/services/test_utils.ts | 2 -
.../services/user_actions/abstract_builder.ts | 21 +-
.../user_actions/builders/create_case.ts | 4 +-
.../services/user_actions/index.test.ts | 48 +-
.../server/services/user_actions/index.ts | 25 +-
.../server/services/user_actions/mocks.ts | 3 +-
.../server/services/user_actions/types.ts | 1 -
.../exploratory_view/hooks/use_add_to_case.ts | 8 +-
.../security_solution/common/constants.ts | 5 -
.../components/link_to/redirect_to_case.tsx | 22 +-
.../public/common/components/links/index.tsx | 9 +-
.../rules/rule_actions_field/index.test.tsx | 42 -
.../flyout/add_to_case_button/index.tsx | 4 +-
.../usage/detections/detections.mocks.ts | 1 -
.../server/usage/detections/types.ts | 1 -
.../timelines/public/hooks/use_add_to_case.ts | 7 +-
.../translations/translations/ja-JP.json | 12 -
.../translations/translations/zh-CN.json | 12 -
.../cases_api_integration/common/lib/mock.ts | 84 +-
.../cases_api_integration/common/lib/utils.ts | 168 +--
.../tests/common/cases/delete_cases.ts | 58 -
.../tests/common/cases/find_cases.ts | 199 +--
.../tests/common/cases/get_case.ts | 12 -
.../tests/common/cases/import_export.ts | 2 -
.../tests/common/cases/migrations.ts | 44 +-
.../tests/common/cases/patch_cases.ts | 106 --
.../tests/common/cases/post_case.ts | 7 -
.../tests/common/cases/resolve_case.ts | 12 -
.../tests/common/comments/delete_comment.ts | 89 --
.../tests/common/comments/find_comments.ts | 46 +-
.../tests/common/comments/get_all_comments.ts | 79 --
.../tests/common/comments/get_comment.ts | 24 -
.../tests/common/comments/migrations.ts | 35 +
.../tests/common/comments/patch_comment.ts | 107 +-
.../tests/common/comments/post_comment.ts | 76 +-
.../tests/common/connectors/case.ts | 1079 ---------------
.../security_and_spaces/tests/common/index.ts | 5 -
.../common/sub_cases/delete_sub_cases.ts | 112 --
.../tests/common/sub_cases/find_sub_cases.ts | 479 -------
.../tests/common/sub_cases/get_sub_case.ts | 119 --
.../tests/common/sub_cases/patch_sub_cases.ts | 531 --------
.../user_actions/get_all_user_actions.ts | 19 +-
.../tests/common/user_actions/migrations.ts | 31 -
.../tests/trial/cases/push_case.ts | 19 -
.../tests/common/comments/post_comment.ts | 1 -
.../cases/7.13.2/case_and_collection.json | 139 ++
200 files changed, 770 insertions(+), 10390 deletions(-)
delete mode 100644 x-pack/plugins/cases/common/api/cases/sub_case.ts
delete mode 100644 x-pack/plugins/cases/public/components/all_cases/expanded_row.tsx
delete mode 100644 x-pack/plugins/cases/public/components/all_cases/helpers.ts
delete mode 100644 x-pack/plugins/cases/public/components/connectors/case/alert_fields.tsx
delete mode 100644 x-pack/plugins/cases/public/components/connectors/case/cases_dropdown.tsx
delete mode 100644 x-pack/plugins/cases/public/components/connectors/case/existing_case.tsx
delete mode 100644 x-pack/plugins/cases/public/components/connectors/case/index.ts
delete mode 100644 x-pack/plugins/cases/public/components/connectors/case/translations.ts
delete mode 100644 x-pack/plugins/cases/public/components/connectors/case/types.ts
delete mode 100644 x-pack/plugins/cases/server/client/sub_cases/client.ts
delete mode 100644 x-pack/plugins/cases/server/client/sub_cases/update.ts
delete mode 100644 x-pack/plugins/cases/server/client/user_actions/get.test.ts
delete mode 100644 x-pack/plugins/cases/server/connectors/case/index.test.ts
delete mode 100644 x-pack/plugins/cases/server/connectors/case/index.ts
delete mode 100644 x-pack/plugins/cases/server/connectors/case/schema.ts
delete mode 100644 x-pack/plugins/cases/server/connectors/case/translations.ts
delete mode 100644 x-pack/plugins/cases/server/connectors/case/types.ts
delete mode 100644 x-pack/plugins/cases/server/connectors/case/validators.ts
delete mode 100644 x-pack/plugins/cases/server/routes/api/sub_case/delete_sub_cases.ts
delete mode 100644 x-pack/plugins/cases/server/routes/api/sub_case/find_sub_cases.ts
delete mode 100644 x-pack/plugins/cases/server/routes/api/sub_case/get_sub_case.ts
delete mode 100644 x-pack/plugins/cases/server/routes/api/sub_case/patch_sub_cases.ts
create mode 100644 x-pack/plugins/cases/server/saved_object_types/migrations/constants.ts
delete mode 100644 x-pack/plugins/cases/server/saved_object_types/sub_case.ts
delete mode 100644 x-pack/plugins/cases/server/scripts/sub_cases/README.md
delete mode 100644 x-pack/plugins/cases/server/scripts/sub_cases/generator.js
delete mode 100644 x-pack/plugins/cases/server/scripts/sub_cases/index.ts
delete mode 100644 x-pack/test/cases_api_integration/security_and_spaces/tests/common/connectors/case.ts
delete mode 100644 x-pack/test/cases_api_integration/security_and_spaces/tests/common/sub_cases/delete_sub_cases.ts
delete mode 100644 x-pack/test/cases_api_integration/security_and_spaces/tests/common/sub_cases/find_sub_cases.ts
delete mode 100644 x-pack/test/cases_api_integration/security_and_spaces/tests/common/sub_cases/get_sub_case.ts
delete mode 100644 x-pack/test/cases_api_integration/security_and_spaces/tests/common/sub_cases/patch_sub_cases.ts
create mode 100644 x-pack/test/functional/fixtures/kbn_archiver/cases/7.13.2/case_and_collection.json
diff --git a/x-pack/plugins/cases/README.md b/x-pack/plugins/cases/README.md
index 012a715dbb6cd..e43d3c8f47087 100644
--- a/x-pack/plugins/cases/README.md
+++ b/x-pack/plugins/cases/README.md
@@ -104,8 +104,8 @@ Arguments:
| owner | `string[];` owner ids of the cases |
| alertData? | `Omit;` alert data to post to case |
| hiddenStatuses? | `CaseStatuses[];` array of hidden statuses |
-| onRowClick | (theCase?: Case | SubCase) => void;
callback for row click, passing case in row |
-| updateCase? | (theCase: Case | SubCase) => void;
callback after case has been updated |
+| onRowClick | (theCase?: Case) => void;
callback for row click, passing case in row |
+| updateCase? | (theCase: Case) => void;
callback after case has been updated |
| onClose? | `() => void` called when the modal is closed without selecting a case |
UI component:
@@ -140,123 +140,6 @@ Arguments:
UI component:
![Recent Cases Component][recent-cases-img]
-## Case Action Type
-
-_**\*Feature in development, disabled by default**_
-
-See [Kibana Actions](https://github.com/elastic/kibana/tree/main/x-pack/plugins/actions) for more information.
-
-ID: `.case`
-
-The params properties are modelled after the arguments to the [Cases API](https://www.elastic.co/guide/en/security/master/cases-api-overview.html).
-
-### `config`
-
-This action has no `config` properties.
-
-### `secrets`
-
-This action type has no `secrets` properties.
-
-### `params`
-
-| Property | Description | Type |
-| --------------- | ------------------------------------------------------------------------- | ------ |
-| subAction | The sub action to perform. It can be `create`, `update`, and `addComment` | string |
-| subActionParams | The parameters of the sub action | object |
-
-#### `subActionParams (create)`
-
-| Property | Description | Type |
-| ----------- | --------------------------------------------------------------------- | ----------------------- |
-| tile | The case’s title. | string |
-| description | The case’s description. | string |
-| tags | String array containing words and phrases that help categorize cases. | string[] |
-| connector | Object containing the connector’s configuration. | [connector](#connector) |
-| settings | Object containing the case’s settings. | [settings](#settings) |
-
-#### `subActionParams (update)`
-
-| Property | Description | Type |
-| ----------- | ------------------------------------------------------------------------- | ----------------------- |
-| id | The ID of the case being updated. | string |
-| tile | The updated case title. | string |
-| description | The updated case description. | string |
-| tags | The updated case tags. | string |
-| connector | Object containing the connector’s configuration. | [connector](#connector) |
-| status | The updated case status, which can be: `open`, `in-progress` or `closed`. | string |
-| settings | Object containing the case’s settings. | [settings](#settings) |
-| version | The current case version. | string |
-
-#### `subActionParams (addComment)`
-
-| Property | Description | Type |
-| -------- | ------------------------ | ------ |
-| type | The type of the comment. | `user` |
-| comment | The comment. | string |
-
-#### `connector`
-
-| Property | Description | Type |
-| -------- | --------------------------------------------------------------------------------------------------------------------------------- | ----------------- |
-| id | ID of the connector used for pushing case updates to external systems. | string |
-| name | The connector name. | string |
-| type | The type of the connector. Must be one of these: `.servicenow`, `.servicenow-sir`, `.swimlane`, `jira`, `.resilient`, and `.none` | string |
-| fields | Object containing the connector’s fields. | [fields](#fields) |
-
-#### `fields`
-
-For ServiceNow ITSM connectors (`.servicenow`):
-
-| Property | Description | Type |
-| ----------- | ------------------------------ | ------ |
-| urgency | The urgency of the incident. | string |
-| severity | The severity of the incident. | string |
-| impact | The impact of the incident. | string |
-| category | The category in ServiceNow. | string |
-| subcategory | The subcategory in ServiceNow. | string |
-
-For ServiceNow SecOps connectors (`.servicenow-sir`):
-
-| Property | Description | Type |
-| ----------- | ----------------------------------------------------------------- | ------- |
-| category | The category in ServiceNow. | string |
-| destIp | Include all destination IPs from all alerts attached to the case. | boolean |
-| malwareHash | Include all malware hashes from all alerts attached to the case. | boolean |
-| malwareUrl | Include all malware URLs from all alerts attached to the case. | boolean |
-| priority | The priority of the incident. | string |
-| sourceIp | Include all sources IPs from all alerts attached to the case. | boolean |
-| subcategory | The subcategory in ServiceNow. | string |
-
-For Jira connectors (`.jira`):
-
-| Property | Description | Type |
-| --------- | -------------------------------------------------------------------- | ------ |
-| issueType | The issue type of the issue. | string |
-| priority | The priority of the issue. | string |
-| parent | The key of the parent issue (Valid when the issue type is Sub-task). | string |
-
-For IBM Resilient connectors (`.resilient`):
-
-| Property | Description | Type |
-| ------------ | ------------------------------- | -------- |
-| issueTypes | The issue types of the issue. | string[] |
-| severityCode | The severity code of the issue. | string |
-
-For Swimlane (`.swimlane`):
-
-| Property | Description | Type |
-| -------- | ------------------- | ------ |
-| caseId | The ID of the case. | string |
-
-Connectors of type (`.none`) should have the `fields` attribute set to `null`.
-
-#### `settings`
-
-| Property | Description | Type |
-| ---------- | ------------------------------ | ------- |
-| syncAlerts | Turn on or off alert synching. | boolean |
-
diff --git a/x-pack/plugins/cases/common/api/cases/case.ts b/x-pack/plugins/cases/common/api/cases/case.ts
index 3e9f59441208d..061a720bdf9c3 100644
--- a/x-pack/plugins/cases/common/api/cases/case.ts
+++ b/x-pack/plugins/cases/common/api/cases/case.ts
@@ -12,7 +12,6 @@ import { UserRT } from '../user';
import { CommentResponseRt } from './comment';
import { CasesStatusResponseRt, CaseStatusRt } from './status';
import { CaseConnectorRt } from '../connectors';
-import { SubCaseResponseRt } from './sub_case';
const BucketsAggs = rt.array(
rt.type({
@@ -36,18 +35,6 @@ export const CasesByAlertIdRt = rt.array(
})
);
-export enum CaseType {
- collection = 'collection',
- individual = 'individual',
-}
-
-/**
- * Exposing the field used to define the case type so that it can be used for filtering in saved object find queries.
- */
-export const caseTypeField = 'type';
-
-const CaseTypeRt = rt.union([rt.literal(CaseType.collection), rt.literal(CaseType.individual)]);
-
export const SettingsRt = rt.type({
syncAlerts: rt.boolean,
});
@@ -69,10 +56,6 @@ const CaseBasicRt = rt.type({
* The title of a case
*/
title: rt.string,
- /**
- * The type of a case (individual or collection)
- */
- [caseTypeField]: CaseTypeRt,
/**
* The external system that the case can be synced with
*/
@@ -122,7 +105,7 @@ export const CaseAttributesRt = rt.intersection([
}),
]);
-const CasePostRequestNoTypeRt = rt.type({
+export const CasePostRequestRt = rt.type({
/**
* Description of the case
*/
@@ -150,33 +133,7 @@ const CasePostRequestNoTypeRt = rt.type({
owner: rt.string,
});
-/**
- * This type is used for validating a create case request. It requires that the type field be defined.
- */
-export const CasesClientPostRequestRt = rt.type({
- ...CasePostRequestNoTypeRt.props,
- [caseTypeField]: CaseTypeRt,
-});
-
-/**
- * This type is not used for validation when decoding a request because intersection does not have props defined which
- * required for the excess function. Instead we use this as the type used by the UI. This allows the type field to be
- * optional and the server will handle setting it to a default value before validating that the request
- * has all the necessary fields. CasesClientPostRequestRt is used for validation.
- */
-export const CasePostRequestRt = rt.intersection([
- /**
- * The case type: an individual case (one without children) or a collection case (one with children)
- */
- rt.partial({ [caseTypeField]: CaseTypeRt }),
- CasePostRequestNoTypeRt,
-]);
-
export const CasesFindRequestRt = rt.partial({
- /**
- * Type of a case (individual, or collection)
- */
- type: CaseTypeRt,
/**
* Tags to filter by
*/
@@ -248,8 +205,6 @@ export const CaseResponseRt = rt.intersection([
version: rt.string,
}),
rt.partial({
- subCaseIds: rt.array(rt.string),
- subCases: rt.array(SubCaseResponseRt),
comments: rt.array(CommentResponseRt),
}),
]);
@@ -321,12 +276,7 @@ export const AllTagsFindRequestRt = rt.partial({
export const AllReportersFindRequestRt = AllTagsFindRequestRt;
export type CaseAttributes = rt.TypeOf;
-/**
- * This field differs from the CasePostRequest in that the post request's type field can be optional. This type requires
- * that the type field be defined. The CasePostRequest should be used in most places (the UI etc). This type is really
- * only necessary for validation.
- */
-export type CasesClientPostRequest = rt.TypeOf;
+
export type CasePostRequest = rt.TypeOf;
export type CaseResponse = rt.TypeOf;
export type CaseResolveResponse = rt.TypeOf;
diff --git a/x-pack/plugins/cases/common/api/cases/comment.ts b/x-pack/plugins/cases/common/api/cases/comment.ts
index 19ad15286db6a..900f90156d431 100644
--- a/x-pack/plugins/cases/common/api/cases/comment.ts
+++ b/x-pack/plugins/cases/common/api/cases/comment.ts
@@ -10,22 +10,7 @@ import { SavedObjectFindOptionsRt } from '../saved_object';
import { UserRT } from '../user';
-/**
- * this is used to differentiate between an alert attached to a top-level case and a group of alerts that should only
- * be attached to a sub case. The reason we need this is because an alert group comment will have references to both a case and
- * sub case when it is created. For us to be able to filter out alert groups in a top-level case we need a field to
- * use as a filter.
- */
-export enum AssociationType {
- case = 'case',
- subCase = 'sub_case',
-}
-
export const CommentAttributesBasicRt = rt.type({
- associationType: rt.union([
- rt.literal(AssociationType.case),
- rt.literal(AssociationType.subCase),
- ]),
created_at: rt.string,
created_by: UserRT,
owner: rt.string,
@@ -38,7 +23,6 @@ export const CommentAttributesBasicRt = rt.type({
export enum CommentType {
user = 'user',
alert = 'alert',
- generatedAlert = 'generated_alert',
actions = 'actions',
}
@@ -59,7 +43,7 @@ export const ContextTypeUserRt = rt.type({
* it matches this structure. User attached alerts do not need to be transformed.
*/
export const AlertCommentRequestRt = rt.type({
- type: rt.union([rt.literal(CommentType.generatedAlert), rt.literal(CommentType.alert)]),
+ type: rt.literal(CommentType.alert),
alertId: rt.union([rt.array(rt.string), rt.string]),
index: rt.union([rt.array(rt.string), rt.string]),
rule: rt.type({
@@ -167,10 +151,6 @@ export const AllCommentsResponseRt = rt.array(CommentResponseRt);
export const FindQueryParamsRt = rt.partial({
...SavedObjectFindOptionsRt.props,
- /**
- * If specified the attachments found will be associated to a sub case instead of a case object
- */
- subCaseId: rt.string,
});
export type FindQueryParams = rt.TypeOf;
diff --git a/x-pack/plugins/cases/common/api/cases/index.ts b/x-pack/plugins/cases/common/api/cases/index.ts
index ba8ad15d02e27..2bcad0c556e63 100644
--- a/x-pack/plugins/cases/common/api/cases/index.ts
+++ b/x-pack/plugins/cases/common/api/cases/index.ts
@@ -10,6 +10,5 @@ export * from './configure';
export * from './comment';
export * from './status';
export * from './user_actions';
-export * from './sub_case';
export * from './constants';
export * from './alerts';
diff --git a/x-pack/plugins/cases/common/api/cases/sub_case.ts b/x-pack/plugins/cases/common/api/cases/sub_case.ts
deleted file mode 100644
index 187778f4a4fab..0000000000000
--- a/x-pack/plugins/cases/common/api/cases/sub_case.ts
+++ /dev/null
@@ -1,112 +0,0 @@
-/*
- * 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 * as rt from 'io-ts';
-
-import { NumberFromString } from '../saved_object';
-import { UserRT } from '../user';
-import { CommentResponseRt } from './comment';
-import { CaseStatusRt, CasesStatusResponseRt } from './status';
-
-const SubCaseBasicRt = rt.type({
- /**
- * The status of the sub case (open, closed, in-progress)
- */
- status: CaseStatusRt,
-});
-
-export const SubCaseAttributesRt = rt.intersection([
- SubCaseBasicRt,
- rt.type({
- closed_at: rt.union([rt.string, rt.null]),
- closed_by: rt.union([UserRT, rt.null]),
- created_at: rt.string,
- created_by: rt.union([UserRT, rt.null]),
- updated_at: rt.union([rt.string, rt.null]),
- updated_by: rt.union([UserRT, rt.null]),
- owner: rt.string,
- }),
-]);
-
-export const SubCasesFindRequestRt = rt.partial({
- /**
- * The status of the sub case (open, closed, in-progress)
- */
- status: CaseStatusRt,
- /**
- * Operator to use for the `search` field
- */
- defaultSearchOperator: rt.union([rt.literal('AND'), rt.literal('OR')]),
- /**
- * The fields in the entity to return in the response
- */
- fields: rt.array(rt.string),
- /**
- * The page of objects to return
- */
- page: NumberFromString,
- /**
- * The number of objects to include in each page
- */
- perPage: NumberFromString,
- /**
- * An Elasticsearch simple_query_string
- */
- search: rt.string,
- /**
- * The fields to perform the simple_query_string parsed query against
- */
- searchFields: rt.array(rt.string),
- /**
- * The field to use for sorting the found objects.
- */
- sortField: rt.string,
- /**
- * The order to sort by
- */
- sortOrder: rt.union([rt.literal('desc'), rt.literal('asc')]),
- owner: rt.string,
-});
-
-export const SubCaseResponseRt = rt.intersection([
- SubCaseAttributesRt,
- rt.type({
- id: rt.string,
- totalComment: rt.number,
- totalAlerts: rt.number,
- version: rt.string,
- }),
- rt.partial({
- comments: rt.array(CommentResponseRt),
- }),
-]);
-
-export const SubCasesFindResponseRt = rt.intersection([
- rt.type({
- subCases: rt.array(SubCaseResponseRt),
- page: rt.number,
- per_page: rt.number,
- total: rt.number,
- }),
- CasesStatusResponseRt,
-]);
-
-export const SubCasePatchRequestRt = rt.intersection([
- rt.partial(SubCaseBasicRt.props),
- rt.type({ id: rt.string, version: rt.string }),
-]);
-
-export const SubCasesPatchRequestRt = rt.type({ subCases: rt.array(SubCasePatchRequestRt) });
-export const SubCasesResponseRt = rt.array(SubCaseResponseRt);
-
-export type SubCaseAttributes = rt.TypeOf;
-export type SubCaseResponse = rt.TypeOf;
-export type SubCasesResponse = rt.TypeOf;
-export type SubCasesFindResponse = rt.TypeOf;
-export type SubCasePatchRequest = rt.TypeOf;
-export type SubCasesPatchRequest = rt.TypeOf;
-export type SubCasesFindRequest = rt.TypeOf;
diff --git a/x-pack/plugins/cases/common/api/cases/user_actions/common.ts b/x-pack/plugins/cases/common/api/cases/user_actions/common.ts
index 8942d3c0ed926..a6d12d135c142 100644
--- a/x-pack/plugins/cases/common/api/cases/user_actions/common.ts
+++ b/x-pack/plugins/cases/common/api/cases/user_actions/common.ts
@@ -42,14 +42,11 @@ export const UserActionCommonAttributesRt = rt.type({
action: ActionsRt,
});
-export const CaseUserActionSavedObjectIdsRt = rt.intersection([
- rt.type({
- action_id: rt.string,
- case_id: rt.string,
- comment_id: rt.union([rt.string, rt.null]),
- }),
- rt.partial({ sub_case_id: rt.string }),
-]);
+export const CaseUserActionSavedObjectIdsRt = rt.type({
+ action_id: rt.string,
+ case_id: rt.string,
+ comment_id: rt.union([rt.string, rt.null]),
+});
export type UserActionWithAttributes = T & rt.TypeOf;
export type UserActionWithResponse = T & rt.TypeOf;
diff --git a/x-pack/plugins/cases/common/api/helpers.ts b/x-pack/plugins/cases/common/api/helpers.ts
index 2d5a7d1c33674..9aebb7d0ccd9d 100644
--- a/x-pack/plugins/cases/common/api/helpers.ts
+++ b/x-pack/plugins/cases/common/api/helpers.ts
@@ -11,10 +11,7 @@ import {
CASE_COMMENTS_URL,
CASE_USER_ACTIONS_URL,
CASE_COMMENT_DETAILS_URL,
- SUB_CASE_DETAILS_URL,
- SUB_CASES_URL,
CASE_PUSH_URL,
- SUB_CASE_USER_ACTIONS_URL,
CASE_CONFIGURE_DETAILS_URL,
CASE_ALERTS_URL,
} from '../constants';
@@ -27,14 +24,6 @@ export const getCaseDetailsMetricsUrl = (id: string): string => {
return CASE_METRICS_DETAILS_URL.replace('{case_id}', id);
};
-export const getSubCasesUrl = (caseID: string): string => {
- return SUB_CASES_URL.replace('{case_id}', caseID);
-};
-
-export const getSubCaseDetailsUrl = (caseID: string, subCaseId: string): string => {
- return SUB_CASE_DETAILS_URL.replace('{case_id}', caseID).replace('{sub_case_id}', subCaseId);
-};
-
export const getCaseCommentsUrl = (id: string): string => {
return CASE_COMMENTS_URL.replace('{case_id}', id);
};
@@ -47,10 +36,6 @@ export const getCaseUserActionUrl = (id: string): string => {
return CASE_USER_ACTIONS_URL.replace('{case_id}', id);
};
-export const getSubCaseUserActionUrl = (caseID: string, subCaseId: string): string => {
- return SUB_CASE_USER_ACTIONS_URL.replace('{case_id}', caseID).replace('{sub_case_id}', subCaseId);
-};
-
export const getCasePushUrl = (caseId: string, connectorId: string): string => {
return CASE_PUSH_URL.replace('{case_id}', caseId).replace('{connector_id}', connectorId);
};
diff --git a/x-pack/plugins/cases/common/constants.ts b/x-pack/plugins/cases/common/constants.ts
index 8e7d4934271ac..c57b40dbcf002 100644
--- a/x-pack/plugins/cases/common/constants.ts
+++ b/x-pack/plugins/cases/common/constants.ts
@@ -14,7 +14,6 @@ export const APP_ID = 'cases';
export const CASE_SAVED_OBJECT = 'cases';
export const CASE_CONNECTOR_MAPPINGS_SAVED_OBJECT = 'cases-connector-mappings';
-export const SUB_CASE_SAVED_OBJECT = 'cases-sub-case';
export const CASE_USER_ACTION_SAVED_OBJECT = 'cases-user-actions';
export const CASE_COMMENT_SAVED_OBJECT = 'cases-comments';
export const CASE_CONFIGURE_SAVED_OBJECT = 'cases-configure';
@@ -40,11 +39,6 @@ export const CASE_CONFIGURE_URL = `${CASES_URL}/configure`;
export const CASE_CONFIGURE_DETAILS_URL = `${CASES_URL}/configure/{configuration_id}`;
export const CASE_CONFIGURE_CONNECTORS_URL = `${CASE_CONFIGURE_URL}/connectors`;
-export const SUB_CASES_PATCH_DEL_URL = `${CASES_URL}/sub_cases`;
-export const SUB_CASES_URL = `${CASE_DETAILS_URL}/sub_cases`;
-export const SUB_CASE_DETAILS_URL = `${CASE_DETAILS_URL}/sub_cases/{sub_case_id}`;
-export const SUB_CASE_USER_ACTIONS_URL = `${SUB_CASE_DETAILS_URL}/user_actions`;
-
export const CASE_COMMENTS_URL = `${CASE_DETAILS_URL}/comments`;
export const CASE_COMMENT_DETAILS_URL = `${CASE_DETAILS_URL}/comments/{comment_id}`;
export const CASE_PUSH_URL = `${CASE_DETAILS_URL}/connector/{connector_id}/_push`;
@@ -77,8 +71,7 @@ export const SUPPORTED_CONNECTORS = [
/**
* Alerts
*/
-export const MAX_ALERTS_PER_SUB_CASE = 5000;
-export const MAX_GENERATED_ALERTS_PER_SUB_CASE = 50;
+export const MAX_ALERTS_PER_CASE = 5000;
export const SECURITY_SOLUTION_OWNER = 'securitySolution';
export const OBSERVABILITY_OWNER = 'observability';
@@ -94,15 +87,6 @@ export const OWNER_INFO = {
},
};
-/**
- * This flag governs enabling the case as a connector feature. It is disabled by default as the feature is not complete.
- */
-export const ENABLE_CASE_CONNECTOR = false;
-
-if (ENABLE_CASE_CONNECTOR) {
- SAVED_OBJECT_TYPES.push(SUB_CASE_SAVED_OBJECT);
-}
-
export const MAX_DOCS_PER_PAGE = 10000;
export const MAX_CONCURRENT_SEARCHES = 10;
diff --git a/x-pack/plugins/cases/common/index.ts b/x-pack/plugins/cases/common/index.ts
index 31f0d9daed4f0..79a5aa4eb7ddd 100644
--- a/x-pack/plugins/cases/common/index.ts
+++ b/x-pack/plugins/cases/common/index.ts
@@ -15,11 +15,11 @@
// For example, constants below could eventually be in a "kbn-cases-constants" instead.
// See: https://docs.elastic.dev/kibana-dev-docs/key-concepts/platform-intro#public-plugin-api
-export { CASES_URL, SECURITY_SOLUTION_OWNER, ENABLE_CASE_CONNECTOR } from './constants';
+export { CASES_URL, SECURITY_SOLUTION_OWNER } from './constants';
export { CommentType, CaseStatuses, getCasesFromAlertsUrl, throwErrors } from './api';
-export type { SubCase, Case, Ecs, CasesFeatures, CaseViewRefreshPropInterface } from './ui/types';
+export type { Case, Ecs, CasesFeatures, CaseViewRefreshPropInterface } from './ui/types';
export { StatusAll } from './ui/types';
diff --git a/x-pack/plugins/cases/common/ui/types.ts b/x-pack/plugins/cases/common/ui/types.ts
index cfa91a9c57cab..008d4b9245f63 100644
--- a/x-pack/plugins/cases/common/ui/types.ts
+++ b/x-pack/plugins/cases/common/ui/types.ts
@@ -6,12 +6,10 @@
*/
import {
- AssociationType,
CaseAttributes,
CaseConnector,
CasePatchRequest,
CaseStatuses,
- CaseType,
User,
ActionConnector,
CaseExternalServiceBasic,
@@ -83,20 +81,12 @@ interface BasicCase {
version: string;
}
-export interface SubCase extends BasicCase {
- associationType: AssociationType;
- caseParentId: string;
-}
-
export interface Case extends BasicCase {
connector: CaseConnector;
description: string;
externalService: CaseExternalService | null;
- subCases?: SubCase[] | null;
- subCaseIds: string[];
settings: CaseAttributes['settings'];
tags: string[];
- type: CaseType;
}
export interface ResolvedCase {
@@ -118,7 +108,6 @@ export interface FilterOptions {
tags: string[];
reporters: User[];
owner: string[];
- onlyCollectionType?: boolean;
}
export interface CasesStatus {
@@ -178,7 +167,6 @@ export interface ActionLicense {
export interface DeleteCase {
id: string;
- type: CaseType | null;
title: string;
}
@@ -195,7 +183,7 @@ export type UpdateKey = keyof Pick<
export interface UpdateByKey {
updateKey: UpdateKey;
updateValue: CasePatchRequest[UpdateKey];
- fetchCaseUserActions?: (caseId: string, caseConnectorId: string, subCaseId?: string) => void;
+ fetchCaseUserActions?: (caseId: string, caseConnectorId: string) => void;
updateCase?: (newCase: Case) => void;
caseData: Case;
onSuccess?: () => void;
diff --git a/x-pack/plugins/cases/package.json b/x-pack/plugins/cases/package.json
index bd9a547324ca6..a982c98768fd1 100644
--- a/x-pack/plugins/cases/package.json
+++ b/x-pack/plugins/cases/package.json
@@ -3,8 +3,5 @@
"name": "cases",
"version": "8.0.0",
"private": true,
- "license": "Elastic-License",
- "scripts": {
- "test:sub-cases": "node server/scripts/sub_cases/generator"
- }
+ "license": "Elastic-License"
}
diff --git a/x-pack/plugins/cases/public/common/navigation/paths.test.ts b/x-pack/plugins/cases/public/common/navigation/paths.test.ts
index 23fb870fb6cda..a3fa042042a2d 100644
--- a/x-pack/plugins/cases/public/common/navigation/paths.test.ts
+++ b/x-pack/plugins/cases/public/common/navigation/paths.test.ts
@@ -7,11 +7,9 @@
import {
getCreateCasePath,
- getSubCaseViewPath,
getCaseViewPath,
getCasesConfigurePath,
getCaseViewWithCommentPath,
- getSubCaseViewWithCommentPath,
generateCaseViewPath,
} from './paths';
@@ -34,51 +32,21 @@ describe('Paths', () => {
});
});
- describe('getSubCaseViewPath', () => {
- it('returns the correct path', () => {
- expect(getSubCaseViewPath('test')).toBe('test/:detailName/sub-cases/:subCaseId');
- });
- });
-
describe('getCaseViewWithCommentPath', () => {
it('returns the correct path', () => {
expect(getCaseViewWithCommentPath('test')).toBe('test/:detailName/:commentId');
});
});
- describe('getSubCaseViewWithCommentPath', () => {
- it('returns the correct path', () => {
- expect(getSubCaseViewWithCommentPath('test')).toBe(
- 'test/:detailName/sub-cases/:subCaseId/:commentId'
- );
- });
- });
-
describe('generateCaseViewPath', () => {
it('returns the correct path', () => {
expect(generateCaseViewPath({ detailName: 'my-case' })).toBe('/my-case');
});
- it('returns the correct path when subCaseId is provided', () => {
- expect(generateCaseViewPath({ detailName: 'my-case', subCaseId: 'my-sub-case' })).toBe(
- '/my-case/sub-cases/my-sub-case'
- );
- });
-
it('returns the correct path when commentId is provided', () => {
expect(generateCaseViewPath({ detailName: 'my-case', commentId: 'my-comment' })).toBe(
'/my-case/my-comment'
);
});
-
- it('returns the correct path when subCaseId and commentId is provided', () => {
- expect(
- generateCaseViewPath({
- detailName: 'my-case',
- subCaseId: 'my-sub-case',
- commentId: 'my-comment',
- })
- ).toBe('/my-case/sub-cases/my-sub-case/my-comment');
- });
});
});
diff --git a/x-pack/plugins/cases/public/common/navigation/paths.ts b/x-pack/plugins/cases/public/common/navigation/paths.ts
index 1cce1ca41ea77..1cd7a99630b85 100644
--- a/x-pack/plugins/cases/public/common/navigation/paths.ts
+++ b/x-pack/plugins/cases/public/common/navigation/paths.ts
@@ -10,38 +10,26 @@ import { generatePath } from 'react-router-dom';
export const DEFAULT_BASE_PATH = '/cases';
export interface CaseViewPathParams {
detailName: string;
- subCaseId?: string;
commentId?: string;
}
export const CASES_CREATE_PATH = '/create' as const;
export const CASES_CONFIGURE_PATH = '/configure' as const;
export const CASE_VIEW_PATH = '/:detailName' as const;
-export const SUB_CASE_VIEW_PATH = `${CASE_VIEW_PATH}/sub-cases/:subCaseId` as const;
export const CASE_VIEW_COMMENT_PATH = `${CASE_VIEW_PATH}/:commentId` as const;
-export const SUB_CASE_VIEW_COMMENT_PATH = `${SUB_CASE_VIEW_PATH}/:commentId` as const;
export const getCreateCasePath = (casesBasePath: string) => `${casesBasePath}${CASES_CREATE_PATH}`;
export const getCasesConfigurePath = (casesBasePath: string) =>
`${casesBasePath}${CASES_CONFIGURE_PATH}`;
export const getCaseViewPath = (casesBasePath: string) => `${casesBasePath}${CASE_VIEW_PATH}`;
-export const getSubCaseViewPath = (casesBasePath: string) =>
- `${casesBasePath}${SUB_CASE_VIEW_PATH}`;
export const getCaseViewWithCommentPath = (casesBasePath: string) =>
`${casesBasePath}${CASE_VIEW_COMMENT_PATH}`;
-export const getSubCaseViewWithCommentPath = (casesBasePath: string) =>
- `${casesBasePath}${SUB_CASE_VIEW_COMMENT_PATH}`;
export const generateCaseViewPath = (params: CaseViewPathParams): string => {
- const { subCaseId, commentId } = params;
+ const { commentId } = params;
// Cast for generatePath argument type constraint
const pathParams = params as unknown as { [paramName: string]: string };
- if (subCaseId && commentId) {
- return generatePath(SUB_CASE_VIEW_COMMENT_PATH, pathParams);
- }
- if (subCaseId) {
- return generatePath(SUB_CASE_VIEW_PATH, pathParams);
- }
+
if (commentId) {
return generatePath(CASE_VIEW_COMMENT_PATH, pathParams);
}
diff --git a/x-pack/plugins/cases/public/common/translations.ts b/x-pack/plugins/cases/public/common/translations.ts
index 47e7c461c6ffd..04075ac680627 100644
--- a/x-pack/plugins/cases/public/common/translations.ts
+++ b/x-pack/plugins/cases/public/common/translations.ts
@@ -241,12 +241,6 @@ export const ALERT_ADDED_TO_CASE = i18n.translate('xpack.cases.common.alertAdded
defaultMessage: 'added to case',
});
-export const SELECTABLE_MESSAGE_COLLECTIONS = i18n.translate(
- 'xpack.cases.common.allCases.table.selectableMessageCollections',
- {
- defaultMessage: 'Cases with sub-cases cannot be selected',
- }
-);
export const SELECT_CASE_TITLE = i18n.translate('xpack.cases.common.allCases.caseModal.title', {
defaultMessage: 'Select case',
});
diff --git a/x-pack/plugins/cases/public/components/add_comment/index.test.tsx b/x-pack/plugins/cases/public/components/add_comment/index.test.tsx
index f1167504628c4..f7535e6f52071 100644
--- a/x-pack/plugins/cases/public/components/add_comment/index.test.tsx
+++ b/x-pack/plugins/cases/public/components/add_comment/index.test.tsx
@@ -75,7 +75,6 @@ describe('AddComment ', () => {
expect(postComment).toBeCalledWith({
caseId: addCommentProps.caseId,
data: sampleData,
- subCaseId: undefined,
updateCase: onCommentPosted,
});
expect(wrapper.find(`[data-test-subj="add-comment"] textarea`).text()).toBe('');
diff --git a/x-pack/plugins/cases/public/components/add_comment/index.tsx b/x-pack/plugins/cases/public/components/add_comment/index.tsx
index 83bd187e7863a..dea692f7c71a1 100644
--- a/x-pack/plugins/cases/public/components/add_comment/index.tsx
+++ b/x-pack/plugins/cases/public/components/add_comment/index.tsx
@@ -52,7 +52,6 @@ export interface AddCommentProps {
onCommentPosted: (newCase: Case) => void;
showLoading?: boolean;
statusActionButton: JSX.Element | null;
- subCaseId?: string;
}
export const AddComment = React.memo(
@@ -66,7 +65,6 @@ export const AddComment = React.memo(
onCommentSaving,
showLoading = true,
statusActionButton,
- subCaseId,
},
ref
) => {
@@ -118,11 +116,10 @@ export const AddComment = React.memo(
caseId,
data: { ...data, type: CommentType.user, owner: owner[0] },
updateCase: onCommentPosted,
- subCaseId,
});
reset();
}
- }, [submit, onCommentSaving, postComment, caseId, owner, onCommentPosted, subCaseId, reset]);
+ }, [submit, onCommentSaving, postComment, caseId, owner, onCommentPosted, reset]);
/**
* Focus on the text area when a quote has been added.
diff --git a/x-pack/plugins/cases/public/components/all_cases/all_cases_list.test.tsx b/x-pack/plugins/cases/public/components/all_cases/all_cases_list.test.tsx
index 5897a757b5bdf..4f400bb52601b 100644
--- a/x-pack/plugins/cases/public/components/all_cases/all_cases_list.test.tsx
+++ b/x-pack/plugins/cases/public/components/all_cases/all_cases_list.test.tsx
@@ -14,15 +14,10 @@ import userEvent from '@testing-library/user-event';
import '../../common/mock/match_media';
import { TestProviders } from '../../common/mock';
-import {
- casesStatus,
- useGetCasesMockState,
- collectionCase,
- connectorsMock,
-} from '../../containers/mock';
+import { casesStatus, useGetCasesMockState, mockCase, connectorsMock } from '../../containers/mock';
import { StatusAll } from '../../../common/ui/types';
-import { CaseStatuses, CaseType } from '../../../common/api';
+import { CaseStatuses } from '../../../common/api';
import { SECURITY_SOLUTION_OWNER } from '../../../common/constants';
import { getEmptyTagValue } from '../empty_value';
import { useDeleteCases } from '../../containers/use_delete_cases';
@@ -207,7 +202,6 @@ describe('AllCasesListGeneric', () => {
createdAt: null,
createdBy: null,
status: null,
- subCases: null,
tags: null,
title: null,
totalComment: null,
@@ -256,43 +250,6 @@ describe('AllCasesListGeneric', () => {
});
});
- it.skip('should enable correct actions for sub cases', async () => {
- useGetCasesMock.mockReturnValue({
- ...defaultGetCases,
- data: {
- ...defaultGetCases.data,
- cases: [
- {
- ...defaultGetCases.data.cases[0],
- id: 'my-case-with-subcases',
- createdAt: null,
- createdBy: null,
- status: null,
- subCases: [
- {
- id: 'sub-case-id',
- },
- ],
- tags: null,
- title: null,
- totalComment: null,
- totalAlerts: null,
- type: CaseType.collection,
- },
- ],
- },
- });
- const wrapper = mount(
-
-
-
- );
-
- expect(wrapper.find('[data-test-subj="action-delete"]').first().props().disabled).toEqual(
- false
- );
- });
-
it('should tableHeaderSortButton AllCasesList', async () => {
const wrapper = mount(
@@ -339,7 +296,7 @@ describe('AllCasesListGeneric', () => {
useGetCasesMock.mockReturnValue({
...defaultGetCases,
filterOptions: { ...defaultGetCases.filterOptions, status: CaseStatuses.closed },
- selectedCases: [...useGetCasesMockState.data.cases, collectionCase],
+ selectedCases: [...useGetCasesMockState.data.cases, mockCase],
});
useDeleteCasesMock
@@ -372,11 +329,10 @@ describe('AllCasesListGeneric', () => {
expect(handleToggleModal).toBeCalled();
expect(handleOnDeleteConfirm.mock.calls[0][0]).toStrictEqual([
- ...useGetCasesMockState.data.cases.map(({ id, type, title }) => ({ id, type, title })),
+ ...useGetCasesMockState.data.cases.map(({ id, title }) => ({ id, title })),
{
- id: collectionCase.id,
- title: collectionCase.title,
- type: collectionCase.type,
+ id: mockCase.id,
+ title: mockCase.title,
},
]);
});
@@ -409,49 +365,6 @@ describe('AllCasesListGeneric', () => {
});
});
- it('Renders correct bulk actions for case collection when filter status is set to all - enable only bulk delete if any collection is selected', async () => {
- useGetCasesMock.mockReturnValue({
- ...defaultGetCases,
- filterOptions: { ...defaultGetCases.filterOptions, status: CaseStatuses.open },
- selectedCases: [
- ...useGetCasesMockState.data.cases,
- {
- ...useGetCasesMockState.data.cases[0],
- type: CaseType.collection,
- },
- ],
- });
-
- useDeleteCasesMock
- .mockReturnValueOnce({
- ...defaultDeleteCases,
- isDisplayConfirmDeleteModal: false,
- })
- .mockReturnValue({
- ...defaultDeleteCases,
- isDisplayConfirmDeleteModal: true,
- });
-
- const wrapper = mount(
-
-
-
- );
- wrapper.find('[data-test-subj="case-table-bulk-actions"] button').first().simulate('click');
- await waitFor(() => {
- expect(wrapper.find('[data-test-subj="cases-bulk-open-button"]').exists()).toEqual(false);
- expect(
- wrapper.find('[data-test-subj="cases-bulk-in-progress-button"]').first().props().disabled
- ).toEqual(true);
- expect(
- wrapper.find('[data-test-subj="cases-bulk-close-button"]').first().props().disabled
- ).toEqual(true);
- expect(
- wrapper.find('[data-test-subj="cases-bulk-delete-button"]').first().props().disabled
- ).toEqual(false);
- });
- });
-
it('Bulk close status update', async () => {
useGetCasesMock.mockReturnValue({
...defaultGetCases,
@@ -637,12 +550,10 @@ describe('AllCasesListGeneric', () => {
id: '1',
owner: SECURITY_SOLUTION_OWNER,
status: 'open',
- subCaseIds: [],
tags: ['coke', 'pepsi'],
title: 'Another horrible breach!!',
totalAlerts: 0,
totalComment: 0,
- type: CaseType.individual,
updatedAt: '2020-02-20T15:02:57.995Z',
updatedBy: {
email: 'leslie.knope@elastic.co',
diff --git a/x-pack/plugins/cases/public/components/all_cases/all_cases_list.tsx b/x-pack/plugins/cases/public/components/all_cases/all_cases_list.tsx
index bf88c5a7906bf..1ede1a2b736d3 100644
--- a/x-pack/plugins/cases/public/components/all_cases/all_cases_list.tsx
+++ b/x-pack/plugins/cases/public/components/all_cases/all_cases_list.tsx
@@ -9,23 +9,19 @@ import React, { useCallback, useMemo, useRef, useState } from 'react';
import { EuiProgress, EuiBasicTable, EuiTableSelectionType } from '@elastic/eui';
import { difference, head, isEmpty } from 'lodash/fp';
import styled, { css } from 'styled-components';
-import classnames from 'classnames';
import {
Case,
CaseStatusWithAllStatus,
FilterOptions,
SortFieldCase,
- SubCase,
} from '../../../common/ui/types';
-import { CaseStatuses, CaseType, CommentRequestAlertType, caseStatuses } from '../../../common/api';
-import { SELECTABLE_MESSAGE_COLLECTIONS } from '../../common/translations';
+import { CaseStatuses, CommentRequestAlertType, caseStatuses } from '../../../common/api';
import { useGetCases } from '../../containers/use_get_cases';
import { usePostComment } from '../../containers/use_post_comment';
import { useAvailableCasesOwners } from '../app/use_available_owners';
import { useCasesColumns } from './columns';
-import { getExpandedRowMap } from './expanded_row';
import { CasesTableFilters } from './table_filters';
import { EuiBasicTableOnChange } from './types';
@@ -53,7 +49,7 @@ export interface AllCasesListProps {
alertData?: Omit;
hiddenStatuses?: CaseStatusWithAllStatus[];
isSelectorView?: boolean;
- onRowClick?: (theCase?: Case | SubCase) => void;
+ onRowClick?: (theCase?: Case) => void;
updateCase?: (newCase: Case) => void;
doRefresh?: () => void;
}
@@ -102,7 +98,7 @@ export const AllCasesList = React.memo(
);
const filterRefetch = useRef<() => void>();
- const tableRef = useRef();
+ const tableRef = useRef(null);
const [isLoading, handleIsLoading] = useState(false);
const setFilterRefetch = useCallback(
@@ -190,16 +186,6 @@ export const AllCasesList = React.memo(
showSolutionColumn: !hasOwner && availableSolutions.length > 1,
});
- const itemIdToExpandedRowMap = useMemo(
- () =>
- getExpandedRowMap({
- columns,
- data: data.cases,
- onSubCaseClick: onRowClick,
- }),
- [data.cases, columns, onRowClick]
- );
-
const pagination = useMemo(
() => ({
pageIndex: queryParams.page - 1,
@@ -213,7 +199,6 @@ export const AllCasesList = React.memo(
const euiBasicTableSelectionProps = useMemo>(
() => ({
onSelectionChange: setSelectedCases,
- selectableMessage: (selectable) => (!selectable ? SELECTABLE_MESSAGE_COLLECTIONS : ''),
initialSelected: selectedCases,
}),
[selectedCases, setSelectedCases]
@@ -224,7 +209,6 @@ export const AllCasesList = React.memo(
const tableRowProps = useCallback(
(theCase: Case) => ({
'data-test-subj': `cases-table-row-${theCase.id}`,
- className: classnames({ isDisabled: theCase.type === CaseType.collection }),
}),
[]
);
@@ -263,7 +247,6 @@ export const AllCasesList = React.memo(
isCommentUpdating={isCommentUpdating}
isDataEmpty={isDataEmpty}
isSelectorView={isSelectorView}
- itemIdToExpandedRowMap={itemIdToExpandedRowMap}
onChange={tableOnChangeCallback}
pagination={pagination}
refreshCases={refreshCases}
diff --git a/x-pack/plugins/cases/public/components/all_cases/columns.tsx b/x-pack/plugins/cases/public/components/all_cases/columns.tsx
index d08f788c85311..9287d9db7e5b0 100644
--- a/x-pack/plugins/cases/public/components/all_cases/columns.tsx
+++ b/x-pack/plugins/cases/public/components/all_cases/columns.tsx
@@ -22,10 +22,9 @@ import {
import { RIGHT_ALIGNMENT } from '@elastic/eui/lib/services';
import styled from 'styled-components';
-import { Case, DeleteCase, SubCase } from '../../../common/ui/types';
+import { Case, DeleteCase } from '../../../common/ui/types';
import {
CaseStatuses,
- CaseType,
CommentType,
CommentRequestAlertType,
ActionConnector,
@@ -35,7 +34,6 @@ import { getEmptyTagValue } from '../empty_value';
import { FormattedRelativePreferenceDate } from '../formatted_date';
import { CaseDetailsLink } from '../links';
import * as i18n from './translations';
-import { getSubCasesStatusCountsBadges, isSubCase } from './helpers';
import { ALERTS } from '../../common/translations';
import { getActions } from './actions';
import { UpdateCase } from '../../containers/use_get_cases';
@@ -111,13 +109,12 @@ export const useCasesColumns = ({
const [deleteThisCase, setDeleteThisCase] = useState({
id: '',
title: '',
- type: null,
});
const toggleDeleteModal = useCallback(
(deleteCase: Case) => {
handleToggleModal();
- setDeleteThisCase({ id: deleteCase.id, title: deleteCase.title, type: deleteCase.type });
+ setDeleteThisCase({ id: deleteCase.id, title: deleteCase.title });
},
[handleToggleModal]
);
@@ -175,16 +172,12 @@ export const useCasesColumns = ({
return [
{
name: i18n.NAME,
- render: (theCase: Case | SubCase) => {
+ render: (theCase: Case) => {
if (theCase.id != null && theCase.title != null) {
const caseDetailsLinkComponent = isSelectorView ? (
) : (
-
+
);
@@ -350,32 +343,24 @@ export const useCasesColumns = ({
{
name: i18n.STATUS,
render: (theCase: Case) => {
- if (theCase?.subCases == null || theCase.subCases.length === 0) {
- if (theCase.status == null || theCase.type === CaseType.collection) {
- return getEmptyTagValue();
- }
- return (
- 0}
- onStatusChanged={(status) =>
- handleDispatchUpdate({
- updateKey: 'status',
- updateValue: status,
- caseId: theCase.id,
- version: theCase.version,
- })
- }
- />
- );
+ if (theCase.status === null || theCase.status === undefined) {
+ return getEmptyTagValue();
}
- const badges = getSubCasesStatusCountsBadges(theCase.subCases);
- return badges.map(({ color, count }, index) => (
-
- {count}
-
- ));
+ return (
+ 0}
+ onStatusChanged={(status) =>
+ handleDispatchUpdate({
+ updateKey: 'status',
+ updateValue: status,
+ caseId: theCase.id,
+ version: theCase.version,
+ })
+ }
+ />
+ );
},
},
]
diff --git a/x-pack/plugins/cases/public/components/all_cases/expanded_row.tsx b/x-pack/plugins/cases/public/components/all_cases/expanded_row.tsx
deleted file mode 100644
index 4719c2ce3db82..0000000000000
--- a/x-pack/plugins/cases/public/components/all_cases/expanded_row.tsx
+++ /dev/null
@@ -1,78 +0,0 @@
-/*
- * 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 React from 'react';
-import { EuiBasicTable } from '@elastic/eui';
-import styled from 'styled-components';
-import { Case, SubCase } from '../../containers/types';
-import { CasesColumns } from './columns';
-import { AssociationType } from '../../../common/api';
-
-type ExpandedRowMap = Record | {};
-
-// @ts-expect-error TS2769
-const BasicTable = styled(EuiBasicTable)`
- thead {
- display: none;
- }
-
- tbody {
- .euiTableCellContent {
- padding: 8px !important;
- }
- .euiTableRowCell {
- border: 0;
- }
- }
-`;
-BasicTable.displayName = 'BasicTable';
-
-export const getExpandedRowMap = ({
- data,
- columns,
- onSubCaseClick,
-}: {
- data: Case[] | null;
- columns: CasesColumns[];
- onSubCaseClick?: (theSubCase: SubCase) => void;
-}): ExpandedRowMap => {
- if (data == null) {
- return {};
- }
-
- const rowProps = (theSubCase: SubCase) => {
- return {
- ...(onSubCaseClick ? { onClick: () => onSubCaseClick(theSubCase) } : {}),
- className: 'subCase',
- };
- };
-
- return data.reduce((acc, curr) => {
- if (curr.subCases != null) {
- const subCases = curr.subCases.map((subCase, index) => ({
- ...subCase,
- caseParentId: curr.id,
- title: `${curr.title} ${index + 1}`,
- associationType: AssociationType.subCase,
- }));
- return {
- ...acc,
- [curr.id]: (
-
- ),
- };
- } else {
- return acc;
- }
- }, {});
-};
diff --git a/x-pack/plugins/cases/public/components/all_cases/helpers.ts b/x-pack/plugins/cases/public/components/all_cases/helpers.ts
deleted file mode 100644
index f84f19d3030ae..0000000000000
--- a/x-pack/plugins/cases/public/components/all_cases/helpers.ts
+++ /dev/null
@@ -1,45 +0,0 @@
-/*
- * 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 { filter } from 'lodash/fp';
-import { AssociationType, CaseStatuses, CaseType } from '../../../common/api';
-import { Case, SubCase } from '../../containers/types';
-import { statuses } from '../status';
-
-export const isSelectedCasesIncludeCollections = (selectedCases: Case[]) =>
- selectedCases.length > 0 &&
- selectedCases.some((caseObj: Case) => caseObj.type === CaseType.collection);
-
-export const isSubCase = (theCase: Case | SubCase): theCase is SubCase =>
- (theCase as SubCase).caseParentId !== undefined &&
- (theCase as SubCase).associationType === AssociationType.subCase;
-
-export const isCollection = (theCase: Case | SubCase | null | undefined) =>
- theCase != null && (theCase as Case).type === CaseType.collection;
-
-export const isIndividual = (theCase: Case | SubCase | null | undefined) =>
- theCase != null && (theCase as Case).type === CaseType.individual;
-
-export const getSubCasesStatusCountsBadges = (
- subCases: SubCase[]
-): Array<{ name: CaseStatuses; color: string; count: number }> => [
- {
- color: statuses[CaseStatuses.open].color,
- count: filter({ status: CaseStatuses.open }, subCases).length,
- name: CaseStatuses.open,
- },
- {
- color: statuses[CaseStatuses['in-progress']].color,
- count: filter({ status: CaseStatuses['in-progress'] }, subCases).length,
- name: CaseStatuses['in-progress'],
- },
- {
- color: statuses[CaseStatuses.closed].color,
- count: filter({ status: CaseStatuses.closed }, subCases).length,
- name: CaseStatuses.closed,
- },
-];
diff --git a/x-pack/plugins/cases/public/components/all_cases/selector_modal/all_cases_selector_modal.tsx b/x-pack/plugins/cases/public/components/all_cases/selector_modal/all_cases_selector_modal.tsx
index 5db6531d8e140..d63c56456b07e 100644
--- a/x-pack/plugins/cases/public/components/all_cases/selector_modal/all_cases_selector_modal.tsx
+++ b/x-pack/plugins/cases/public/components/all_cases/selector_modal/all_cases_selector_modal.tsx
@@ -15,14 +15,14 @@ import {
EuiModalHeaderTitle,
} from '@elastic/eui';
import styled from 'styled-components';
-import { Case, SubCase, CaseStatusWithAllStatus } from '../../../../common/ui/types';
+import { Case, CaseStatusWithAllStatus } from '../../../../common/ui/types';
import { CommentRequestAlertType } from '../../../../common/api';
import * as i18n from '../../../common/translations';
import { AllCasesList } from '../all_cases_list';
export interface AllCasesSelectorModalProps {
alertData?: Omit;
hiddenStatuses?: CaseStatusWithAllStatus[];
- onRowClick: (theCase?: Case | SubCase) => void;
+ onRowClick: (theCase?: Case) => void;
updateCase?: (newCase: Case) => void;
onClose?: () => void;
}
@@ -45,7 +45,7 @@ export const AllCasesSelectorModal = React.memo(
}, [onClose]);
const onClick = useCallback(
- (theCase?: Case | SubCase) => {
+ (theCase?: Case) => {
closeModal();
onRowClick(theCase);
},
diff --git a/x-pack/plugins/cases/public/components/all_cases/table.tsx b/x-pack/plugins/cases/public/components/all_cases/table.tsx
index 0d4a4cb6b38f1..2a2cf79e6f690 100644
--- a/x-pack/plugins/cases/public/components/all_cases/table.tsx
+++ b/x-pack/plugins/cases/public/components/all_cases/table.tsx
@@ -12,6 +12,7 @@ import {
EuiTableSelectionType,
EuiBasicTable,
EuiBasicTableProps,
+ Pagination,
} from '@elastic/eui';
import classnames from 'classnames';
import styled from 'styled-components';
@@ -23,7 +24,7 @@ import * as i18n from './translations';
import { useCreateCaseNavigation } from '../../common/navigation';
interface CasesTableProps {
- columns: EuiBasicTableProps['columns']; // CasesColumns[];
+ columns: EuiBasicTableProps['columns'];
data: AllCases;
filterOptions: FilterOptions;
goToCreateCase?: () => void;
@@ -32,44 +33,18 @@ interface CasesTableProps {
isCommentUpdating: boolean;
isDataEmpty: boolean;
isSelectorView?: boolean;
- itemIdToExpandedRowMap: EuiBasicTableProps['itemIdToExpandedRowMap'];
onChange: EuiBasicTableProps['onChange'];
- pagination: EuiBasicTableProps['pagination'];
+ pagination: Pagination;
refreshCases: (a?: boolean) => void;
selectedCases: Case[];
selection: EuiTableSelectionType;
showActions: boolean;
sorting: EuiBasicTableProps['sorting'];
- tableRef: MutableRefObject;
+ tableRef: MutableRefObject;
tableRowProps: EuiBasicTableProps['rowProps'];
userCanCrud: boolean;
}
-// @ts-expect-error TS2769
-const BasicTable = styled(EuiBasicTable)`
- ${({ theme }) => `
- .euiTableRow-isExpandedRow.euiTableRow-isSelectable .euiTableCellContent {
- padding: 8px 0 8px 32px;
- }
-
- &.isSelectorView .euiTableRow.isDisabled {
- cursor: not-allowed;
- background-color: ${theme.eui.euiTableHoverClickableColor};
- }
-
- &.isSelectorView .euiTableRow.euiTableRow-isExpandedRow .euiTableRowCell,
- &.isSelectorView .euiTableRow.euiTableRow-isExpandedRow:hover {
- background-color: transparent;
- }
-
- &.isSelectorView .euiTableRow.euiTableRow-isExpandedRow {
- .subCase:hover {
- background-color: ${theme.eui.euiTableHoverClickableColor};
- }
- }
- `}
-`;
-
const Div = styled.div`
margin-top: ${({ theme }) => theme.eui.paddingSizes.m};
`;
@@ -84,7 +59,6 @@ export const CasesTable: FunctionComponent = ({
isCommentUpdating,
isDataEmpty,
isSelectorView,
- itemIdToExpandedRowMap,
onChange,
pagination,
refreshCases,
@@ -123,14 +97,13 @@ export const CasesTable: FunctionComponent = ({
selectedCases={selectedCases}
refreshCases={refreshCases}
/>
- = ({
(cases: Case[]) => {
handleToggleModal();
- const convertToDeleteCases: DeleteCase[] = cases.map(({ id, title, type }) => ({
+ const convertToDeleteCases: DeleteCase[] = cases.map(({ id, title }) => ({
id,
title,
- type,
}));
setDeleteCases(convertToDeleteCases);
},
@@ -109,7 +107,6 @@ export const CasesTableUtilityBar: FunctionComponent = ({
deleteCasesAction: toggleBulkDeleteModal,
selectedCases,
updateCaseStatus: handleUpdateCaseStatus,
- includeCollections: isSelectedCasesIncludeCollections(selectedCases),
})}
/>
),
diff --git a/x-pack/plugins/cases/public/components/app/routes.test.tsx b/x-pack/plugins/cases/public/components/app/routes.test.tsx
index 69410327af1ef..0eb27479bc8a8 100644
--- a/x-pack/plugins/cases/public/components/app/routes.test.tsx
+++ b/x-pack/plugins/cases/public/components/app/routes.test.tsx
@@ -29,12 +29,7 @@ jest.mock('../configure_cases', () => ({
ConfigureCases: () => {'Configure cases'}
,
}));
-const getCaseViewPaths = () => [
- '/cases/test-id',
- '/cases/test-id/comment-id',
- '/cases/test-id/sub-cases/sub-case-id',
- '/cases/test-id/sub-cases/sub-case-id/comment-id',
-];
+const getCaseViewPaths = () => ['/cases/test-id', '/cases/test-id/comment-id'];
const renderWithRouter = (
initialEntries: MemoryRouterProps['initialEntries'] = ['/cases'],
diff --git a/x-pack/plugins/cases/public/components/app/routes.tsx b/x-pack/plugins/cases/public/components/app/routes.tsx
index ab4bf2ac51f19..6222c413a1167 100644
--- a/x-pack/plugins/cases/public/components/app/routes.tsx
+++ b/x-pack/plugins/cases/public/components/app/routes.tsx
@@ -18,8 +18,6 @@ import {
getCreateCasePath,
getCaseViewPath,
getCaseViewWithCommentPath,
- getSubCaseViewPath,
- getSubCaseViewWithCommentPath,
useAllCasesNavigation,
useCaseViewNavigation,
} from '../../common/navigation';
@@ -72,15 +70,7 @@ const CasesRoutesComponent: React.FC = ({
)}
-
+
void;
deleteCasesAction: (cases: Case[]) => void;
- includeCollections: boolean;
selectedCases: Case[];
updateCaseStatus: (status: string) => void;
}
@@ -27,7 +26,6 @@ export const getBulkItems = ({
caseStatus,
closePopover,
deleteCasesAction,
- includeCollections,
selectedCases,
updateCaseStatus,
}: GetBulkItems) => {
@@ -36,7 +34,7 @@ export const getBulkItems = ({
const openMenuItem = (
{
@@ -51,7 +49,7 @@ export const getBulkItems = ({
const inProgressMenuItem = (
{
@@ -66,7 +64,7 @@ export const getBulkItems = ({
const closeMenuItem = (
{
diff --git a/x-pack/plugins/cases/public/components/case_action_bar/actions.test.tsx b/x-pack/plugins/cases/public/components/case_action_bar/actions.test.tsx
index ed8e238db75e7..308f6bc86f41a 100644
--- a/x-pack/plugins/cases/public/components/case_action_bar/actions.test.tsx
+++ b/x-pack/plugins/cases/public/components/case_action_bar/actions.test.tsx
@@ -81,7 +81,7 @@ describe('CaseView actions', () => {
expect(wrapper.find('[data-test-subj="confirm-delete-case-modal"]').exists()).toBeTruthy();
wrapper.find('button[data-test-subj="confirmModalConfirmButton"]').simulate('click');
expect(handleOnDeleteConfirm.mock.calls[0][0]).toEqual([
- { id: basicCase.id, title: basicCase.title, type: 'individual' },
+ { id: basicCase.id, title: basicCase.title },
]);
});
diff --git a/x-pack/plugins/cases/public/components/case_action_bar/actions.tsx b/x-pack/plugins/cases/public/components/case_action_bar/actions.tsx
index a6db64b83bf09..3c78fa6f5fa4b 100644
--- a/x-pack/plugins/cases/public/components/case_action_bar/actions.tsx
+++ b/x-pack/plugins/cases/public/components/case_action_bar/actions.tsx
@@ -57,9 +57,7 @@ const ActionsComponent: React.FC = ({ caseData, currentExternal
caseTitle={caseData.title}
isModalVisible={isDisplayConfirmDeleteModal}
onCancel={handleToggleModal}
- onConfirm={handleOnDeleteConfirm.bind(null, [
- { id: caseData.id, title: caseData.title, type: caseData.type },
- ])}
+ onConfirm={handleOnDeleteConfirm.bind(null, [{ id: caseData.id, title: caseData.title }])}
/>
>
);
diff --git a/x-pack/plugins/cases/public/components/case_action_bar/index.tsx b/x-pack/plugins/cases/public/components/case_action_bar/index.tsx
index 26c7ed2699133..31d8b91d9432c 100644
--- a/x-pack/plugins/cases/public/components/case_action_bar/index.tsx
+++ b/x-pack/plugins/cases/public/components/case_action_bar/index.tsx
@@ -17,7 +17,7 @@ import {
EuiIconTip,
} from '@elastic/eui';
import { Case } from '../../../common/ui/types';
-import { CaseStatuses, CaseType } from '../../../common/api';
+import { CaseStatuses } from '../../../common/api';
import * as i18n from '../case_view/translations';
import { Actions } from './actions';
import { CaseService } from '../../containers/use_get_case_user_actions';
@@ -83,18 +83,16 @@ const CaseActionBarComponent: React.FC = ({
- {caseData.type !== CaseType.collection && (
-
- {i18n.STATUS}
-
-
-
-
- )}
+
+ {i18n.STATUS}
+
+
+
+
{!metricsFeatures.includes('lifespan') ? (
{title}
diff --git a/x-pack/plugins/cases/public/components/case_view/case_view_page.test.tsx b/x-pack/plugins/cases/public/components/case_view/case_view_page.test.tsx
index d67796b98d99a..e5564ee9429aa 100644
--- a/x-pack/plugins/cases/public/components/case_view/case_view_page.test.tsx
+++ b/x-pack/plugins/cases/public/components/case_view/case_view_page.test.tsx
@@ -26,7 +26,7 @@ import { useConnectors } from '../../containers/configure/use_connectors';
import { connectorsMock } from '../../containers/configure/mock';
import { usePostPushToService } from '../../containers/use_post_push_to_service';
import { useGetCaseMetrics } from '../../containers/use_get_case_metrics';
-import { CaseType, ConnectorTypes } from '../../../common/api';
+import { ConnectorTypes } from '../../../common/api';
import { caseViewProps, caseData } from './index.test';
jest.mock('../../containers/use_update_case');
@@ -576,68 +576,4 @@ describe('CaseViewPage', () => {
});
});
});
-
- describe('Collections', () => {
- it('it does not allow the user to update the status', async () => {
- const wrapper = mount(
-
-
-
- );
-
- await waitFor(() => {
- expect(wrapper.find('[data-test-subj="case-action-bar-wrapper"]').exists()).toBe(true);
- expect(wrapper.find('button[data-test-subj="case-view-status"]').exists()).toBe(false);
- expect(wrapper.find('[data-test-subj="user-actions"]').exists()).toBe(true);
- expect(
- wrapper.find('button[data-test-subj="case-view-status-action-button"]').exists()
- ).toBe(false);
- });
- });
-
- it('it shows the push button when has data to push', async () => {
- useGetCaseUserActionsMock.mockImplementation(() => ({
- ...defaultUseGetCaseUserActions,
- hasDataToPush: true,
- }));
-
- const wrapper = mount(
-
-
-
- );
-
- await waitFor(() => {
- expect(wrapper.find('[data-test-subj="has-data-to-push-button"]').exists()).toBe(true);
- });
- });
-
- it('it does not show the horizontal rule when does NOT has data to push', async () => {
- useGetCaseUserActionsMock.mockImplementation(() => ({
- ...defaultUseGetCaseUserActions,
- hasDataToPush: false,
- }));
-
- const wrapper = mount(
-
-
-
- );
-
- await waitFor(() => {
- expect(
- wrapper.find('[data-test-subj="case-view-bottom-actions-horizontal-rule"]').exists()
- ).toBe(false);
- });
- });
- });
});
diff --git a/x-pack/plugins/cases/public/components/case_view/case_view_page.tsx b/x-pack/plugins/cases/public/components/case_view/case_view_page.tsx
index e58a10d22481f..c0dc4fa4d95e9 100644
--- a/x-pack/plugins/cases/public/components/case_view/case_view_page.tsx
+++ b/x-pack/plugins/cases/public/components/case_view/case_view_page.tsx
@@ -8,7 +8,7 @@
import React, { useCallback, useEffect, useMemo, useState, useRef } from 'react';
import { EuiFlexGroup, EuiFlexItem, EuiLoadingContent, EuiSpacer } from '@elastic/eui';
-import { CaseStatuses, CaseAttributes, CaseType, CaseConnector } from '../../../common/api';
+import { CaseStatuses, CaseAttributes, CaseConnector } from '../../../common/api';
import { Case, UpdateKey, UpdateByKey } from '../../../common/ui';
import { EditableTitle } from '../header_page/editable_title';
import { TagList } from '../tag_list';
@@ -38,19 +38,13 @@ import { useCasesFeatures } from '../cases_context/use_cases_features';
const useOnUpdateField = ({
caseData,
caseId,
- subCaseId,
handleUpdateField,
}: {
caseData: Case;
caseId: string;
- subCaseId?: string;
handleUpdateField: (newCase: Case, updateKey: UpdateKey) => void;
}) => {
- const {
- isLoading,
- updateKey: loadingKey,
- updateCaseProperty,
- } = useUpdateCase({ caseId, subCaseId });
+ const { isLoading, updateKey: loadingKey, updateCaseProperty } = useUpdateCase({ caseId });
const onUpdateField = useCallback(
({ key, value, onSuccess, onError }: OnUpdateFields) => {
@@ -117,7 +111,6 @@ export const CaseViewPage = React.memo(
actionsNavigation,
ruleDetailsNavigation,
showAlertDetails,
- subCaseId,
updateCase,
useFetchAlertData,
refreshRef,
@@ -138,11 +131,11 @@ export const CaseViewPage = React.memo(
hasDataToPush,
isLoading: isLoadingUserActions,
participants,
- } = useGetCaseUserActions(caseId, caseData.connector.id, subCaseId);
+ } = useGetCaseUserActions(caseId, caseData.connector.id);
const refetchCaseUserActions = useCallback(() => {
- fetchCaseUserActions(caseId, caseData.connector.id, subCaseId);
- }, [caseId, fetchCaseUserActions, subCaseId, caseData]);
+ fetchCaseUserActions(caseId, caseData.connector.id);
+ }, [caseId, fetchCaseUserActions, caseData]);
const {
metrics,
@@ -159,24 +152,23 @@ export const CaseViewPage = React.memo(
const handleUpdateCase = useCallback(
(newCase: Case) => {
updateCase(newCase);
- fetchCaseUserActions(caseId, newCase.connector.id, subCaseId);
+ fetchCaseUserActions(caseId, newCase.connector.id);
fetchCaseMetrics();
},
- [updateCase, fetchCaseUserActions, caseId, subCaseId, fetchCaseMetrics]
+ [updateCase, fetchCaseUserActions, caseId, fetchCaseMetrics]
);
const handleUpdateField = useCallback(
(newCase: Case, updateKey: UpdateKey) => {
updateCase({ ...newCase, comments: caseData.comments });
- fetchCaseUserActions(caseId, newCase.connector.id, subCaseId);
+ fetchCaseUserActions(caseId, newCase.connector.id);
fetchCaseMetrics();
},
- [updateCase, caseData, fetchCaseUserActions, caseId, subCaseId, fetchCaseMetrics]
+ [updateCase, caseData, fetchCaseUserActions, caseId, fetchCaseMetrics]
);
const { onUpdateField, isLoading, loadingKey } = useOnUpdateField({
caseId,
- subCaseId,
caseData,
handleUpdateField,
});
@@ -274,9 +266,9 @@ export const CaseViewPage = React.memo(
const emailContent = useMemo(
() => ({
subject: i18n.EMAIL_SUBJECT(caseData.title),
- body: i18n.EMAIL_BODY(getCaseViewUrl({ detailName: caseId, subCaseId })),
+ body: i18n.EMAIL_BODY(getCaseViewUrl({ detailName: caseId })),
}),
- [caseData.title, getCaseViewUrl, caseId, subCaseId]
+ [caseData.title, getCaseViewUrl, caseId]
);
useEffect(() => {
@@ -368,7 +360,7 @@ export const CaseViewPage = React.memo(
timelineUi?.renderInvestigateInTimelineActionComponent
}
statusActionButton={
- caseData.type !== CaseType.collection && userCanCrud ? (
+ userCanCrud ? (
(
connectorName={connectorName}
connectors={connectors}
hasDataToPush={hasDataToPush && userCanCrud}
- hideConnectorServiceNowSir={
- subCaseId != null || caseData.type === CaseType.collection
- }
isLoading={isLoadingConnectors || (isLoading && loadingKey === 'connector')}
isValidConnector={isLoadingConnectors ? true : isValidConnector}
onSubmit={onSubmitConnector}
diff --git a/x-pack/plugins/cases/public/components/case_view/index.test.tsx b/x-pack/plugins/cases/public/components/case_view/index.test.tsx
index aeffbd3240cab..4f112b09d0ea2 100644
--- a/x-pack/plugins/cases/public/components/case_view/index.test.tsx
+++ b/x-pack/plugins/cases/public/components/case_view/index.test.tsx
@@ -263,7 +263,7 @@ describe('CaseView', () => {
);
wrapper.find('[data-test-subj="case-refresh"]').first().simulate('click');
await waitFor(() => {
- expect(fetchCaseUserActions).toBeCalledWith(caseData.id, 'resilient-2', undefined);
+ expect(fetchCaseUserActions).toBeCalledWith(caseData.id, 'resilient-2');
expect(fetchCaseMetrics).toBeCalled();
expect(fetchCase).toBeCalled();
});
@@ -303,7 +303,7 @@ describe('CaseView', () => {
it('should refresh actions and comments', async () => {
refreshRef!.current!.refreshCase();
await waitFor(() => {
- expect(fetchCaseUserActions).toBeCalledWith('basic-case-id', 'resilient-2', undefined);
+ expect(fetchCaseUserActions).toBeCalledWith('basic-case-id', 'resilient-2');
expect(fetchCaseMetrics).toBeCalledWith(true);
expect(fetchCase).toBeCalledWith(true);
});
diff --git a/x-pack/plugins/cases/public/components/case_view/index.tsx b/x-pack/plugins/cases/public/components/case_view/index.tsx
index afb1666d0f370..25c578e8e3be2 100644
--- a/x-pack/plugins/cases/public/components/case_view/index.tsx
+++ b/x-pack/plugins/cases/public/components/case_view/index.tsx
@@ -42,10 +42,10 @@ export const CaseView = React.memo(
refreshRef,
}: CaseViewProps) => {
const { spaces: spacesApi } = useKibana().services;
- const { detailName: caseId, subCaseId } = useCaseViewParams();
+ const { detailName: caseId } = useCaseViewParams();
const { basePath } = useCasesContext();
const { data, resolveOutcome, resolveAliasId, isLoading, isError, fetchCase, updateCase } =
- useGetCase(caseId, subCaseId);
+ useGetCase(caseId);
useEffect(() => {
if (spacesApi && resolveOutcome === 'aliasMatch' && resolveAliasId != null) {
@@ -91,7 +91,6 @@ export const CaseView = React.memo(
actionsNavigation={actionsNavigation}
ruleDetailsNavigation={ruleDetailsNavigation}
showAlertDetails={showAlertDetails}
- subCaseId={subCaseId}
updateCase={updateCase}
useFetchAlertData={useFetchAlertData}
refreshRef={refreshRef}
diff --git a/x-pack/plugins/cases/public/components/case_view/types.ts b/x-pack/plugins/cases/public/components/case_view/types.ts
index 525bbcd225cb2..69d05918e182f 100644
--- a/x-pack/plugins/cases/public/components/case_view/types.ts
+++ b/x-pack/plugins/cases/public/components/case_view/types.ts
@@ -29,7 +29,6 @@ export interface CaseViewProps extends CaseViewBaseProps {
export interface CaseViewPageProps extends CaseViewBaseProps {
caseId: string;
- subCaseId?: string;
fetchCase: UseGetCase['fetchCase'];
caseData: Case;
updateCase: (newCase: Case) => void;
diff --git a/x-pack/plugins/cases/public/components/configure_cases/closure_options.tsx b/x-pack/plugins/cases/public/components/configure_cases/closure_options.tsx
index 2bcb137d348ad..1681f2cf94497 100644
--- a/x-pack/plugins/cases/public/components/configure_cases/closure_options.tsx
+++ b/x-pack/plugins/cases/public/components/configure_cases/closure_options.tsx
@@ -29,7 +29,6 @@ const ClosureOptionsComponent: React.FC = ({
description={
<>
{i18n.CASE_CLOSURE_OPTIONS_DESC}
- {i18n.CASE_CLOSURE_OPTIONS_SUB_CASES}
>
}
data-test-subj="case-closure-options-form-group"
diff --git a/x-pack/plugins/cases/public/components/configure_cases/connectors_dropdown.test.tsx b/x-pack/plugins/cases/public/components/configure_cases/connectors_dropdown.test.tsx
index 03ed3d6512638..127c6c30febfb 100644
--- a/x-pack/plugins/cases/public/components/configure_cases/connectors_dropdown.test.tsx
+++ b/x-pack/plugins/cases/public/components/configure_cases/connectors_dropdown.test.tsx
@@ -245,27 +245,6 @@ describe('ConnectorsDropdown', () => {
).toBeTruthy();
});
- test('if the props hideConnectorServiceNowSir is true, the connector should not be part of the list of options ', () => {
- const newWrapper = mount(
- ,
- {
- wrappingComponent: TestProviders,
- }
- );
- const selectProps = newWrapper.find(EuiSuperSelect).props();
- const options = selectProps.options as Array<{ 'data-test-subj': string }>;
- expect(
- options.some((o) => o['data-test-subj'] === 'dropdown-connector-servicenow-1')
- ).toBeTruthy();
- expect(
- options.some((o) => o['data-test-subj'] === 'dropdown-connector-servicenow-sir')
- ).toBeFalsy();
- });
-
test('it does not throw when accessing the icon if the connector type is not registered', () => {
expect(() =>
mount(
diff --git a/x-pack/plugins/cases/public/components/configure_cases/connectors_dropdown.tsx b/x-pack/plugins/cases/public/components/configure_cases/connectors_dropdown.tsx
index 9082fc572324e..cf8d86eb1700a 100644
--- a/x-pack/plugins/cases/public/components/configure_cases/connectors_dropdown.tsx
+++ b/x-pack/plugins/cases/public/components/configure_cases/connectors_dropdown.tsx
@@ -9,7 +9,6 @@ import React, { useMemo } from 'react';
import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiIconTip, EuiSuperSelect } from '@elastic/eui';
import styled from 'styled-components';
-import { ConnectorTypes } from '../../../common/api';
import { ActionConnector } from '../../containers/configure/types';
import * as i18n from './translations';
import { useKibana } from '../../common/lib/kibana';
@@ -23,7 +22,6 @@ export interface Props {
onChange: (id: string) => void;
selectedConnector: string;
appendAddConnectorButton?: boolean;
- hideConnectorServiceNowSir?: boolean;
}
const ICON_SIZE = 'm';
@@ -70,16 +68,11 @@ const ConnectorsDropdownComponent: React.FC = ({
onChange,
selectedConnector,
appendAddConnectorButton = false,
- hideConnectorServiceNowSir = false,
}) => {
const { triggersActionsUi } = useKibana().services;
const connectorsAsOptions = useMemo(() => {
const connectorsFormatted = connectors.reduce(
(acc, connector) => {
- if (hideConnectorServiceNowSir && connector.actionTypeId === ConnectorTypes.serviceNowSIR) {
- return acc;
- }
-
return [
...acc,
{
diff --git a/x-pack/plugins/cases/public/components/configure_cases/translations.ts b/x-pack/plugins/cases/public/components/configure_cases/translations.ts
index 8b682064a0274..ba457905590ab 100644
--- a/x-pack/plugins/cases/public/components/configure_cases/translations.ts
+++ b/x-pack/plugins/cases/public/components/configure_cases/translations.ts
@@ -50,13 +50,6 @@ export const CASE_CLOSURE_OPTIONS_DESC = i18n.translate(
}
);
-export const CASE_CLOSURE_OPTIONS_SUB_CASES = i18n.translate(
- 'xpack.cases.configureCases.caseClosureOptionsSubCases',
- {
- defaultMessage: 'Automatic closure of sub-cases is not supported.',
- }
-);
-
export const CASE_CLOSURE_OPTIONS_LABEL = i18n.translate(
'xpack.cases.configureCases.caseClosureOptionsLabel',
{
diff --git a/x-pack/plugins/cases/public/components/connector_selector/form.tsx b/x-pack/plugins/cases/public/components/connector_selector/form.tsx
index 83733ea7e97dd..c863d4862f31a 100644
--- a/x-pack/plugins/cases/public/components/connector_selector/form.tsx
+++ b/x-pack/plugins/cases/public/components/connector_selector/form.tsx
@@ -23,7 +23,6 @@ interface ConnectorSelectorProps {
isEdit: boolean;
isLoading: boolean;
handleChange?: (newValue: string) => void;
- hideConnectorServiceNowSir?: boolean;
}
const EuiFormRowWrapper = styled(EuiFormRow)`
@@ -41,7 +40,6 @@ export const ConnectorSelector = ({
isEdit = true,
isLoading = false,
handleChange,
- hideConnectorServiceNowSir = false,
}: ConnectorSelectorProps) => {
const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage(field);
const onChange = useCallback(
@@ -68,7 +66,6 @@ export const ConnectorSelector = ({
`
- padding: ${theme.eui?.euiSizeS ?? '8px'} ${theme.eui?.euiSizeL ?? '24px'} ${
- theme.eui?.euiSizeL ?? '24px'
- } ${theme.eui?.euiSizeL ?? '24px'};
- `}
-`;
-
-const defaultAlertComment = {
- type: CommentType.generatedAlert,
- alerts: `[{{#context.alerts}}{"_id": "{{_id}}", "_index": "{{_index}}", "ruleId": "{{signal.rule.id}}", "ruleName": "{{signal.rule.name}}"}__SEPARATOR__{{/context.alerts}}]`,
-};
-
-const CaseParamsFields: React.FunctionComponent> = ({
- actionParams,
- editAction,
- index,
- actionConnector,
-}) => {
- const { caseId = null, comment = defaultAlertComment } = actionParams.subActionParams ?? {};
-
- const [selectedCase, setSelectedCase] = useState(null);
-
- const editSubActionProperty = useCallback(
- (key: string, value: unknown) => {
- const newProps = { ...actionParams.subActionParams, [key]: value };
- editAction('subActionParams', newProps, index);
- },
- // edit action causes re-renders
- // eslint-disable-next-line react-hooks/exhaustive-deps
- [actionParams.subActionParams, index]
- );
-
- const onCaseChanged = useCallback(
- (id: string) => {
- setSelectedCase(id);
- editSubActionProperty('caseId', id);
- },
- [editSubActionProperty]
- );
-
- useEffect(() => {
- if (!actionParams.subAction) {
- editAction('subAction', 'addComment', index);
- }
-
- if (!actionParams.subActionParams?.caseId) {
- editSubActionProperty('caseId', caseId);
- }
-
- if (!actionParams.subActionParams?.comment) {
- editSubActionProperty('comment', comment);
- }
-
- if (caseId != null) {
- setSelectedCase((prevCaseId) => (prevCaseId !== caseId ? caseId : prevCaseId));
- }
-
- // editAction creates an infinity loop.
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [
- actionConnector,
- index,
- actionParams.subActionParams?.caseId,
- actionParams.subActionParams?.comment,
- caseId,
- comment,
- actionParams.subAction,
- ]);
-
- /**
- * TODO: When the case connector is enabled we need to figure out
- * how we can pass the owner to this component
- */
- return (
-
-
-
-
- {i18n.CASE_CONNECTOR_CALL_OUT_MSG}
-
-
- );
-};
-CaseParamsFields.displayName = 'CaseParamsFields';
-
-// eslint-disable-next-line import/no-default-export
-export { CaseParamsFields as default };
diff --git a/x-pack/plugins/cases/public/components/connectors/case/cases_dropdown.tsx b/x-pack/plugins/cases/public/components/connectors/case/cases_dropdown.tsx
deleted file mode 100644
index c26fa04df6843..0000000000000
--- a/x-pack/plugins/cases/public/components/connectors/case/cases_dropdown.tsx
+++ /dev/null
@@ -1,74 +0,0 @@
-/*
- * 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 { EuiFormRow, EuiSuperSelect, EuiSuperSelectOption } from '@elastic/eui';
-import React, { memo, useMemo, useCallback } from 'react';
-import { Case } from '../../../containers/types';
-
-import * as i18n from './translations';
-
-interface CaseDropdownProps {
- isLoading: boolean;
- cases: Case[];
- selectedCase?: string;
- onCaseChanged: (id: string) => void;
-}
-
-export const ADD_CASE_BUTTON_ID = 'add-case';
-
-const addNewCase = {
- value: ADD_CASE_BUTTON_ID,
- inputDisplay: (
-
- {i18n.CASE_CONNECTOR_ADD_NEW_CASE}
-
- ),
- 'data-test-subj': 'dropdown-connector-add-connector',
-};
-
-const CasesDropdownComponent: React.FC = ({
- isLoading,
- cases,
- selectedCase,
- onCaseChanged,
-}) => {
- const caseOptions: Array> = useMemo(
- () =>
- cases.reduce>>(
- (acc, theCase) => [
- ...acc,
- {
- value: theCase.id,
- inputDisplay: {theCase.title},
- 'data-test-subj': `case-connector-cases-dropdown-${theCase.id}`,
- },
- ],
- []
- ),
- [cases]
- );
-
- const options = useMemo(() => [...caseOptions, addNewCase], [caseOptions]);
- const onChange = useCallback((id: string) => onCaseChanged(id), [onCaseChanged]);
-
- return (
-
-
-
- );
-};
-CasesDropdownComponent.displayName = 'CasesDropdown';
-
-export const CasesDropdown = memo(CasesDropdownComponent);
diff --git a/x-pack/plugins/cases/public/components/connectors/case/existing_case.tsx b/x-pack/plugins/cases/public/components/connectors/case/existing_case.tsx
deleted file mode 100644
index 7472bc6b6047e..0000000000000
--- a/x-pack/plugins/cases/public/components/connectors/case/existing_case.tsx
+++ /dev/null
@@ -1,82 +0,0 @@
-/*
- * 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 React, { memo, useMemo, useCallback } from 'react';
-import {
- useGetCases,
- DEFAULT_QUERY_PARAMS,
- DEFAULT_FILTER_OPTIONS,
-} from '../../../containers/use_get_cases';
-import { useCreateCaseModal } from '../../use_create_case_modal';
-import { CasesDropdown, ADD_CASE_BUTTON_ID } from './cases_dropdown';
-
-interface ExistingCaseProps {
- selectedCase: string | null;
- onCaseChanged: (id: string) => void;
-}
-
-const ExistingCaseComponent: React.FC = ({ onCaseChanged, selectedCase }) => {
- const {
- data: cases,
- loading: isLoadingCases,
- refetchCases,
- } = useGetCases({
- initialQueryParams: DEFAULT_QUERY_PARAMS,
- initialFilterOptions: {
- ...DEFAULT_FILTER_OPTIONS,
- onlyCollectionType: true,
- },
- });
-
- const onCaseCreated = useCallback(
- (newCase) => {
- refetchCases();
- onCaseChanged(newCase.id);
- },
- [onCaseChanged, refetchCases]
- );
-
- const { modal, openModal } = useCreateCaseModal({
- onCaseCreated,
- // FUTURE DEVELOPER
- // We are making the assumption that this component is only used in rules creation
- // that's why we want to hide ServiceNow SIR
- hideConnectorServiceNowSir: true,
- });
-
- const onChange = useCallback(
- (id: string) => {
- if (id === ADD_CASE_BUTTON_ID) {
- openModal();
- return;
- }
-
- onCaseChanged(id);
- },
- [onCaseChanged, openModal]
- );
-
- const isCasesLoading = useMemo(
- () => isLoadingCases.includes('cases') || isLoadingCases.includes('caseUpdate'),
- [isLoadingCases]
- );
-
- return (
- <>
-
- {modal}
- >
- );
-};
-ExistingCaseComponent.displayName = 'ExistingCase';
-
-export const ExistingCase = memo(ExistingCaseComponent);
diff --git a/x-pack/plugins/cases/public/components/connectors/case/index.ts b/x-pack/plugins/cases/public/components/connectors/case/index.ts
deleted file mode 100644
index 8e6680cd65387..0000000000000
--- a/x-pack/plugins/cases/public/components/connectors/case/index.ts
+++ /dev/null
@@ -1,42 +0,0 @@
-/*
- * 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 { lazy } from 'react';
-
-// eslint-disable-next-line @kbn/eslint/no-restricted-paths
-import { ActionTypeModel } from '../../../../../triggers_actions_ui/public/types';
-import { CaseActionParams } from './types';
-import * as i18n from './translations';
-
-interface ValidationResult {
- errors: {
- caseId: string[];
- };
-}
-
-const validateParams = (actionParams: CaseActionParams) => {
- const validationResult: ValidationResult = { errors: { caseId: [] } };
-
- if (actionParams.subActionParams && !actionParams.subActionParams.caseId) {
- validationResult.errors.caseId.push(i18n.CASE_CONNECTOR_CASE_REQUIRED);
- }
-
- return Promise.resolve(validationResult);
-};
-
-export function getActionType(): ActionTypeModel {
- return {
- id: '.case',
- iconClass: 'securityAnalyticsApp',
- selectMessage: i18n.CASE_CONNECTOR_DESC,
- actionTypeTitle: i18n.CASE_CONNECTOR_TITLE,
- validateConnector: () => Promise.resolve({ config: { errors: {} }, secrets: { errors: {} } }),
- validateParams,
- actionConnectorFields: null,
- actionParamsFields: lazy(() => import('./alert_fields')),
- };
-}
diff --git a/x-pack/plugins/cases/public/components/connectors/case/translations.ts b/x-pack/plugins/cases/public/components/connectors/case/translations.ts
deleted file mode 100644
index 0d4e1d2f0bd63..0000000000000
--- a/x-pack/plugins/cases/public/components/connectors/case/translations.ts
+++ /dev/null
@@ -1,73 +0,0 @@
-/*
- * 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 { i18n } from '@kbn/i18n';
-
-export * from '../../../common/translations';
-
-export const CASE_CONNECTOR_DESC = i18n.translate(
- 'xpack.cases.components.connectors.cases.selectMessageText',
- {
- defaultMessage: 'Create or update a case.',
- }
-);
-
-export const CASE_CONNECTOR_TITLE = i18n.translate(
- 'xpack.cases.components.connectors.cases.actionTypeTitle',
- {
- defaultMessage: 'Cases',
- }
-);
-export const CASE_CONNECTOR_CASES_DROPDOWN_ROW_LABEL = i18n.translate(
- 'xpack.cases.components.connectors.cases.casesDropdownRowLabel',
- {
- defaultMessage: 'Case allowing sub-cases',
- }
-);
-
-export const CASE_CONNECTOR_CASES_OPTION_EXISTING_CASE = i18n.translate(
- 'xpack.cases.components.connectors.cases.optionAddToExistingCase',
- {
- defaultMessage: 'Add to existing case',
- }
-);
-
-export const CASE_CONNECTOR_CASE_REQUIRED = i18n.translate(
- 'xpack.cases.components.connectors.cases.caseRequired',
- {
- defaultMessage: 'You must select a case.',
- }
-);
-
-export const CASE_CONNECTOR_CALL_OUT_TITLE = i18n.translate(
- 'xpack.cases.components.connectors.cases.callOutTitle',
- {
- defaultMessage: 'Generated alerts will be attached to sub-cases',
- }
-);
-
-export const CASE_CONNECTOR_CALL_OUT_MSG = i18n.translate(
- 'xpack.cases.components.connectors.cases.callOutMsg',
- {
- defaultMessage:
- 'A case can contain multiple sub-cases to allow grouping of generated alerts. Sub-cases will give more granular control over the status of these generated alerts and prevents having too many alerts attached to one case.',
- }
-);
-
-export const CASE_CONNECTOR_ADD_NEW_CASE = i18n.translate(
- 'xpack.cases.components.connectors.cases.addNewCaseOption',
- {
- defaultMessage: 'Add new case',
- }
-);
-
-export const CREATE_CASE = i18n.translate(
- 'xpack.cases.components.connectors.cases.createCaseLabel',
- {
- defaultMessage: 'Create case',
- }
-);
diff --git a/x-pack/plugins/cases/public/components/connectors/case/types.ts b/x-pack/plugins/cases/public/components/connectors/case/types.ts
deleted file mode 100644
index aec9e09ea198c..0000000000000
--- a/x-pack/plugins/cases/public/components/connectors/case/types.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-/*
- * 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.
- */
-
-export interface CaseActionParams {
- subAction: string;
- subActionParams: {
- caseId: string;
- comment: {
- alertId: string;
- index: string;
- type: 'alert';
- };
- };
-}
diff --git a/x-pack/plugins/cases/public/components/connectors/index.ts b/x-pack/plugins/cases/public/components/connectors/index.ts
index 0d5e33a818d3a..03d18976c40fd 100644
--- a/x-pack/plugins/cases/public/components/connectors/index.ts
+++ b/x-pack/plugins/cases/public/components/connectors/index.ts
@@ -19,8 +19,6 @@ import {
SwimlaneFieldsType,
} from '../../../common/api';
-export { getActionType as getCaseConnectorUi } from './case';
-
export * from './types';
interface GetCaseConnectorsReturn {
diff --git a/x-pack/plugins/cases/public/components/create/connector.tsx b/x-pack/plugins/cases/public/components/create/connector.tsx
index 47cc3ea91a868..3b479927ee069 100644
--- a/x-pack/plugins/cases/public/components/create/connector.tsx
+++ b/x-pack/plugins/cases/public/components/create/connector.tsx
@@ -8,7 +8,7 @@
import React, { memo, useCallback, useMemo, useEffect } from 'react';
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
-import { ConnectorTypes, ActionConnector } from '../../../common/api';
+import { ActionConnector } from '../../../common/api';
import {
UseField,
useFormData,
@@ -26,7 +26,6 @@ interface Props {
connectors: ActionConnector[];
isLoading: boolean;
isLoadingConnectors: boolean;
- hideConnectorServiceNowSir?: boolean;
}
interface ConnectorsFieldProps {
@@ -34,27 +33,13 @@ interface ConnectorsFieldProps {
field: FieldHook;
isEdit: boolean;
setErrors: (errors: boolean) => void;
- hideConnectorServiceNowSir?: boolean;
}
-const ConnectorFields = ({
- connectors,
- isEdit,
- field,
- setErrors,
- hideConnectorServiceNowSir = false,
-}: ConnectorsFieldProps) => {
+const ConnectorFields = ({ connectors, isEdit, field, setErrors }: ConnectorsFieldProps) => {
const [{ connectorId }] = useFormData({ watch: ['connectorId'] });
const { setValue } = field;
- let connector = getConnectorById(connectorId, connectors) ?? null;
+ const connector = getConnectorById(connectorId, connectors) ?? null;
- if (
- connector &&
- hideConnectorServiceNowSir &&
- connector.actionTypeId === ConnectorTypes.serviceNowSIR
- ) {
- connector = null;
- }
return (
= ({
- connectors,
- hideConnectorServiceNowSir = false,
- isLoading,
- isLoadingConnectors,
-}) => {
+const ConnectorComponent: React.FC = ({ connectors, isLoading, isLoadingConnectors }) => {
const { getFields, setFieldValue } = useFormContext();
const { connector: configurationConnector } = useCaseConfigure();
@@ -81,21 +61,10 @@ const ConnectorComponent: React.FC = ({
}, [getFields]);
const defaultConnectorId = useMemo(() => {
- if (
- hideConnectorServiceNowSir &&
- configurationConnector.type === ConnectorTypes.serviceNowSIR
- ) {
- return 'none';
- }
return connectors.some((connector) => connector.id === configurationConnector.id)
? configurationConnector.id
: 'none';
- }, [
- configurationConnector.id,
- configurationConnector.type,
- connectors,
- hideConnectorServiceNowSir,
- ]);
+ }, [configurationConnector.id, connectors]);
useEffect(
() => setFieldValue('connectorId', defaultConnectorId),
@@ -118,7 +87,6 @@ const ConnectorComponent: React.FC = ({
componentProps={{
connectors,
handleChange: handleConnectorChange,
- hideConnectorServiceNowSir,
dataTestSubj: 'caseConnectors',
disabled: isLoading || isLoadingConnectors,
idAria: 'caseConnectors',
@@ -132,7 +100,6 @@ const ConnectorComponent: React.FC = ({
component={ConnectorFields}
componentProps={{
connectors,
- hideConnectorServiceNowSir,
isEdit: true,
}}
/>
diff --git a/x-pack/plugins/cases/public/components/create/form.tsx b/x-pack/plugins/cases/public/components/create/form.tsx
index a1784397a21a8..506bfdd974b47 100644
--- a/x-pack/plugins/cases/public/components/create/form.tsx
+++ b/x-pack/plugins/cases/public/components/create/form.tsx
@@ -23,7 +23,7 @@ import { Tags } from './tags';
import { Connector } from './connector';
import * as i18n from './translations';
import { SyncAlertsToggle } from './sync_alerts_toggle';
-import { ActionConnector, CaseType } from '../../../common/api';
+import { ActionConnector } from '../../../common/api';
import { Case } from '../../containers/types';
import { CasesTimelineIntegration, CasesTimelineIntegrationProvider } from '../timeline_context';
import { InsertTimeline } from '../insert_timeline';
@@ -55,21 +55,18 @@ const MySpinner = styled(EuiLoadingSpinner)`
export interface CreateCaseFormFieldsProps {
connectors: ActionConnector[];
isLoadingConnectors: boolean;
- hideConnectorServiceNowSir: boolean;
withSteps: boolean;
}
-export interface CreateCaseFormProps
- extends Pick, 'hideConnectorServiceNowSir' | 'withSteps'> {
+export interface CreateCaseFormProps extends Pick, 'withSteps'> {
onCancel: () => void;
onSuccess: (theCase: Case) => Promise;
afterCaseCreated?: (theCase: Case, postComment: UsePostComment['postComment']) => Promise;
- caseType?: CaseType;
timelineIntegration?: CasesTimelineIntegration;
}
const empty: ActionConnector[] = [];
export const CreateCaseFormFields: React.FC = React.memo(
- ({ connectors, isLoadingConnectors, hideConnectorServiceNowSir, withSteps }) => {
+ ({ connectors, isLoadingConnectors, withSteps }) => {
const { isSubmitting } = useFormContext();
const { isSyncAlertsEnabled } = useCasesFeatures();
@@ -122,14 +119,13 @@ export const CreateCaseFormFields: React.FC = React.m
),
}),
- [connectors, hideConnectorServiceNowSir, isLoadingConnectors, isSubmitting]
+ [connectors, isLoadingConnectors, isSubmitting]
);
const allSteps = useMemo(
@@ -161,21 +157,12 @@ export const CreateCaseFormFields: React.FC = React.m
CreateCaseFormFields.displayName = 'CreateCaseFormFields';
export const CreateCaseForm: React.FC = React.memo(
- ({
- hideConnectorServiceNowSir = false,
- withSteps = true,
- afterCaseCreated,
- caseType,
- onCancel,
- onSuccess,
- timelineIntegration,
- }) => (
+ ({ withSteps = true, afterCaseCreated, onCancel, onSuccess, timelineIntegration }) => (
-
+
diff --git a/x-pack/plugins/cases/public/components/create/form_context.test.tsx b/x-pack/plugins/cases/public/components/create/form_context.test.tsx
index 6e406386b48ef..a07aa55d1a57f 100644
--- a/x-pack/plugins/cases/public/components/create/form_context.test.tsx
+++ b/x-pack/plugins/cases/public/components/create/form_context.test.tsx
@@ -81,7 +81,6 @@ const defaultCreateCaseForm: CreateCaseFormFieldsProps = {
isLoadingConnectors: false,
connectors: [],
withSteps: true,
- hideConnectorServiceNowSir: false,
};
const defaultPostPushToService = {
diff --git a/x-pack/plugins/cases/public/components/create/form_context.tsx b/x-pack/plugins/cases/public/components/create/form_context.tsx
index 34700c37db71e..dca67cac7e688 100644
--- a/x-pack/plugins/cases/public/components/create/form_context.tsx
+++ b/x-pack/plugins/cases/public/components/create/form_context.tsx
@@ -14,7 +14,7 @@ import { usePostPushToService } from '../../containers/use_post_push_to_service'
import { useConnectors } from '../../containers/configure/use_connectors';
import { Case } from '../../containers/types';
-import { CaseType, NONE_CONNECTOR_ID } from '../../../common/api';
+import { NONE_CONNECTOR_ID } from '../../../common/api';
import { UsePostComment, usePostComment } from '../../containers/use_post_comment';
import { useCasesContext } from '../cases_context/use_cases_context';
import { useCasesFeatures } from '../cases_context/use_cases_features';
@@ -32,17 +32,11 @@ const initialCaseValue: FormProps = {
interface Props {
afterCaseCreated?: (theCase: Case, postComment: UsePostComment['postComment']) => Promise;
- caseType?: CaseType;
children?: JSX.Element | JSX.Element[];
onSuccess?: (theCase: Case) => Promise;
}
-export const FormContext: React.FC = ({
- afterCaseCreated,
- caseType = CaseType.individual,
- children,
- onSuccess,
-}) => {
+export const FormContext: React.FC = ({ afterCaseCreated, children, onSuccess }) => {
const { connectors, loading: isLoadingConnectors } = useConnectors();
const { owner } = useCasesContext();
const { isSyncAlertsEnabled } = useCasesFeatures();
@@ -70,7 +64,6 @@ export const FormContext: React.FC = ({
const updatedCase = await postCase({
...userFormData,
- type: caseType,
connector: connectorToUpdate,
settings: { syncAlerts },
owner: selectedOwner ?? owner[0],
@@ -96,7 +89,6 @@ export const FormContext: React.FC = ({
isSyncAlertsEnabled,
connectors,
postCase,
- caseType,
owner,
afterCaseCreated,
onSuccess,
diff --git a/x-pack/plugins/cases/public/components/create/index.tsx b/x-pack/plugins/cases/public/components/create/index.tsx
index aea7c08829128..eb35f78b87f5f 100644
--- a/x-pack/plugins/cases/public/components/create/index.tsx
+++ b/x-pack/plugins/cases/public/components/create/index.tsx
@@ -17,15 +17,7 @@ import { CasesDeepLinkId } from '../../common/navigation';
export const CommonUseField = getUseField({ component: Field });
export const CreateCase = React.memo(
- ({
- afterCaseCreated,
- caseType,
- hideConnectorServiceNowSir,
- onCancel,
- onSuccess,
- timelineIntegration,
- withSteps,
- }) => {
+ ({ afterCaseCreated, onCancel, onSuccess, timelineIntegration, withSteps }) => {
useCasesBreadcrumbs(CasesDeepLinkId.casesCreate);
return (
@@ -37,8 +29,6 @@ export const CreateCase = React.memo(
/>
{
});
test('it calls navigateToCaseViewClick on click', () => {
- render();
+ render();
userEvent.click(screen.getByText('test detail name'));
expect(navigateToCaseView).toHaveBeenCalledWith({
detailName: props.detailName,
- subCaseId: 'sub-case-id',
});
});
test('it set the href correctly', () => {
- render();
+ render();
expect(getCaseViewUrl).toHaveBeenCalledWith({
detailName: props.detailName,
- subCaseId: 'sub-case-id',
});
expect(screen.getByRole('link')).toHaveAttribute('href', '/cases/test');
});
diff --git a/x-pack/plugins/cases/public/components/links/index.tsx b/x-pack/plugins/cases/public/components/links/index.tsx
index d7c2c18eff9ab..db8a4df2a88ca 100644
--- a/x-pack/plugins/cases/public/components/links/index.tsx
+++ b/x-pack/plugins/cases/public/components/links/index.tsx
@@ -39,7 +39,6 @@ export const LinkAnchor: React.FC = ({ children, ...props }) => (
export interface CaseDetailsLinkProps {
children?: React.ReactNode;
detailName: string;
- subCaseId?: string;
title?: string;
}
@@ -48,22 +47,21 @@ export interface CaseDetailsLinkProps {
const CaseDetailsLinkComponent: React.FC = ({
children,
detailName,
- subCaseId,
title,
}) => {
const { getCaseViewUrl, navigateToCaseView } = useCaseViewNavigation();
const navigateToCaseViewClick = useCallback(
(ev) => {
ev.preventDefault();
- navigateToCaseView({ detailName, subCaseId });
+ navigateToCaseView({ detailName });
},
- [navigateToCaseView, detailName, subCaseId]
+ [navigateToCaseView, detailName]
);
return (
diff --git a/x-pack/plugins/cases/public/components/recent_cases/recent_cases.tsx b/x-pack/plugins/cases/public/components/recent_cases/recent_cases.tsx
index 0f9111e5acd1d..231bf666d625e 100644
--- a/x-pack/plugins/cases/public/components/recent_cases/recent_cases.tsx
+++ b/x-pack/plugins/cases/public/components/recent_cases/recent_cases.tsx
@@ -16,7 +16,6 @@ import { useGetCases } from '../../containers/use_get_cases';
import { CaseDetailsLink } from '../links';
import { LoadingPlaceholders } from './loading_placeholders';
import { NoCases } from './no_cases';
-import { isSubCase } from '../all_cases/helpers';
import { MarkdownRenderer } from '../markdown_editor';
import { FilterOptions } from '../../containers/types';
import { TruncatedText } from '../truncated_text';
@@ -67,11 +66,7 @@ export const RecentCasesComp = ({ filterOptions, maxCasesToShow }: RecentCasesPr
-
+
diff --git a/x-pack/plugins/cases/public/components/use_create_case_modal/create_case_modal.tsx b/x-pack/plugins/cases/public/components/use_create_case_modal/create_case_modal.tsx
index 6c844a660b801..f7450b2dd31ef 100644
--- a/x-pack/plugins/cases/public/components/use_create_case_modal/create_case_modal.tsx
+++ b/x-pack/plugins/cases/public/components/use_create_case_modal/create_case_modal.tsx
@@ -13,14 +13,12 @@ import * as i18n from '../../common/translations';
import { CreateCase } from '../create';
export interface CreateCaseModalProps {
- hideConnectorServiceNowSir?: boolean;
isModalOpen: boolean;
onCloseCaseModal: () => void;
onSuccess: (theCase: Case) => Promise;
}
const CreateModalComponent: React.FC = ({
- hideConnectorServiceNowSir,
isModalOpen,
onCloseCaseModal,
onSuccess,
@@ -31,12 +29,7 @@ const CreateModalComponent: React.FC = ({
{i18n.CREATE_CASE_TITLE}
-
+
) : null;
diff --git a/x-pack/plugins/cases/public/components/use_create_case_modal/index.tsx b/x-pack/plugins/cases/public/components/use_create_case_modal/index.tsx
index afae43b462a5b..1654bfe86a620 100644
--- a/x-pack/plugins/cases/public/components/use_create_case_modal/index.tsx
+++ b/x-pack/plugins/cases/public/components/use_create_case_modal/index.tsx
@@ -11,7 +11,6 @@ import { CreateCaseModal } from './create_case_modal';
export interface UseCreateCaseModalProps {
onCaseCreated: (theCase: Case) => void;
- hideConnectorServiceNowSir?: boolean;
}
export interface UseCreateCaseModalReturnedValues {
modal: JSX.Element;
@@ -20,10 +19,7 @@ export interface UseCreateCaseModalReturnedValues {
openModal: () => void;
}
-export const useCreateCaseModal = ({
- onCaseCreated,
- hideConnectorServiceNowSir = false,
-}: UseCreateCaseModalProps) => {
+export const useCreateCaseModal = ({ onCaseCreated }: UseCreateCaseModalProps) => {
const [isModalOpen, setIsModalOpen] = useState(false);
const closeModal = useCallback(() => setIsModalOpen(false), []);
const openModal = useCallback(() => setIsModalOpen(true), []);
@@ -39,7 +35,6 @@ export const useCreateCaseModal = ({
() => ({
modal: (
{
const { getCaseViewUrl } = useCaseViewNavigation();
- const { detailName, subCaseId } = useCaseViewParams();
+ const { detailName } = useCaseViewParams();
const handleAnchorLink = useCallback(() => {
- copy(getCaseViewUrl({ detailName, subCaseId, commentId }, true));
- }, [detailName, subCaseId, commentId, getCaseViewUrl]);
+ copy(getCaseViewUrl({ detailName, commentId }, true));
+ }, [detailName, commentId, getCaseViewUrl]);
return (
{i18n.COPY_REFERENCE_LINK}}>
diff --git a/x-pack/plugins/cases/public/components/user_actions/helpers.test.ts b/x-pack/plugins/cases/public/components/user_actions/helpers.test.ts
index dd75ed8c6fcd8..80b68450f9edd 100644
--- a/x-pack/plugins/cases/public/components/user_actions/helpers.test.ts
+++ b/x-pack/plugins/cases/public/components/user_actions/helpers.test.ts
@@ -5,14 +5,13 @@
* 2.0.
*/
-import { AssociationType, CommentType } from '../../../common/api';
+import { CommentType } from '../../../common/api';
import { SECURITY_SOLUTION_OWNER } from '../../../common/constants';
import { Comment } from '../../containers/types';
import { isUserActionTypeSupported, getManualAlertIdsWithNoRuleId } from './helpers';
const comments: Comment[] = [
{
- associationType: AssociationType.case,
type: CommentType.alert,
alertId: 'alert-id-1',
index: 'alert-index-1',
@@ -31,7 +30,6 @@ const comments: Comment[] = [
owner: SECURITY_SOLUTION_OWNER,
},
{
- associationType: AssociationType.case,
type: CommentType.alert,
alertId: 'alert-id-2',
index: 'alert-index-2',
diff --git a/x-pack/plugins/cases/public/components/user_actions/index.test.tsx b/x-pack/plugins/cases/public/components/user_actions/index.test.tsx
index 67e9b4505ae62..2426e74f3e7b6 100644
--- a/x-pack/plugins/cases/public/components/user_actions/index.test.tsx
+++ b/x-pack/plugins/cases/public/components/user_actions/index.test.tsx
@@ -73,9 +73,7 @@ describe(`UserActions`, () => {
patchComment,
});
- jest
- .spyOn(routeData, 'useParams')
- .mockReturnValue({ detailName: 'case-id', subCaseId: 'sub-case-id' });
+ jest.spyOn(routeData, 'useParams').mockReturnValue({ detailName: 'case-id' });
});
it('Loading spinner when user actions loading and displays fullName/username', () => {
@@ -285,7 +283,6 @@ describe(`UserActions`, () => {
expect(patchComment).toBeCalledWith({
commentUpdate: sampleData.content,
caseId: 'case-id',
- subCaseId: 'sub-case-id',
commentId: props.data.comments[0].id,
fetchUserActions,
updateCase,
diff --git a/x-pack/plugins/cases/public/components/user_actions/index.tsx b/x-pack/plugins/cases/public/components/user_actions/index.tsx
index 084d1c548f903..33394017a4698 100644
--- a/x-pack/plugins/cases/public/components/user_actions/index.tsx
+++ b/x-pack/plugins/cases/public/components/user_actions/index.tsx
@@ -89,13 +89,12 @@ export const UserActions = React.memo(
onRuleDetailsClick,
onShowAlertDetails,
onUpdateField,
- renderInvestigateInTimelineActionComponent,
statusActionButton,
updateCase,
useFetchAlertData,
userCanCrud,
}: UserActionTreeProps) => {
- const { detailName: caseId, subCaseId, commentId } = useCaseViewParams();
+ const { detailName: caseId, commentId } = useCaseViewParams();
const [initLoading, setInitLoading] = useState(true);
const currentUser = useCurrentUser();
@@ -126,7 +125,6 @@ export const UserActions = React.memo(
onCommentSaving={handleManageMarkdownEditId.bind(null, NEW_COMMENT_ID)}
showLoading={false}
statusActionButton={statusActionButton}
- subCaseId={subCaseId}
/>
),
[
@@ -135,7 +133,6 @@ export const UserActions = React.memo(
handleUpdate,
handleManageMarkdownEditId,
statusActionButton,
- subCaseId,
commentRefs,
]
);
diff --git a/x-pack/plugins/cases/public/components/user_actions/use_user_actions_handler.test.tsx b/x-pack/plugins/cases/public/components/user_actions/use_user_actions_handler.test.tsx
index 574109b438167..a90cac5b15d56 100644
--- a/x-pack/plugins/cases/public/components/user_actions/use_user_actions_handler.test.tsx
+++ b/x-pack/plugins/cases/public/components/user_actions/use_user_actions_handler.test.tsx
@@ -85,7 +85,6 @@ describe('useUserActionsHandler', () => {
commentId: 'test-id',
commentUpdate: 'a comment',
fetchUserActions,
- subCaseId: undefined,
updateCase,
version: 'test-version',
});
diff --git a/x-pack/plugins/cases/public/components/user_actions/use_user_actions_handler.tsx b/x-pack/plugins/cases/public/components/user_actions/use_user_actions_handler.tsx
index b9943a8960392..61f5329908c04 100644
--- a/x-pack/plugins/cases/public/components/user_actions/use_user_actions_handler.tsx
+++ b/x-pack/plugins/cases/public/components/user_actions/use_user_actions_handler.tsx
@@ -43,7 +43,7 @@ export const useUserActionsHandler = ({
fetchUserActions,
updateCase,
}: UseUserActionsHandlerArgs): UseUserActionsHandler => {
- const { detailName: caseId, subCaseId } = useCaseViewParams();
+ const { detailName: caseId } = useCaseViewParams();
const { clearDraftComment, draftComment, hasIncomingLensState, openLensModal } =
useLensDraftComment();
const handlerTimeoutId = useRef(0);
@@ -75,10 +75,9 @@ export const useUserActionsHandler = ({
fetchUserActions,
version,
updateCase,
- subCaseId,
});
},
- [caseId, fetchUserActions, patchComment, subCaseId, updateCase]
+ [caseId, fetchUserActions, patchComment, updateCase]
);
const handleOutlineComment = useCallback(
diff --git a/x-pack/plugins/cases/public/containers/api.ts b/x-pack/plugins/cases/public/containers/api.ts
index 6ca920fd5a8fe..9d250e2ae6e0c 100644
--- a/x-pack/plugins/cases/public/containers/api.ts
+++ b/x-pack/plugins/cases/public/containers/api.ts
@@ -5,7 +5,7 @@
* 2.0.
*/
-import { assign, omit } from 'lodash';
+import { omit } from 'lodash';
import { StatusAll, ResolvedCase } from '../../common/ui/types';
import {
@@ -16,7 +16,6 @@ import {
CasesFindResponse,
CasesResponse,
CasesStatusResponse,
- CaseType,
CaseUserActionsResponse,
CommentRequest,
CommentType,
@@ -25,11 +24,6 @@ import {
getCaseDetailsMetricsUrl,
getCasePushUrl,
getCaseUserActionUrl,
- getSubCaseDetailsUrl,
- getSubCaseUserActionUrl,
- SubCasePatchRequest,
- SubCaseResponse,
- SubCasesResponse,
User,
CaseMetricsResponse,
} from '../../common/api';
@@ -38,8 +32,6 @@ import {
CASE_STATUS_URL,
CASE_TAGS_URL,
CASES_URL,
- SUB_CASE_DETAILS_URL,
- SUB_CASES_PATCH_DEL_URL,
} from '../../common/constants';
import { getAllConnectorTypesUrl } from '../../common/utils/connectors_api';
@@ -104,34 +96,6 @@ export const resolveCase = async (
return convertToCamelCase(decodeCaseResolveResponse(response));
};
-export const getSubCase = async (
- caseId: string,
- subCaseId: string,
- includeComments: boolean = true,
- signal: AbortSignal
-): Promise => {
- const [caseResponse, subCaseResponse] = await Promise.all([
- KibanaServices.get().http.fetch(getCaseDetailsUrl(caseId), {
- method: 'GET',
- query: {
- includeComments: false,
- },
- signal,
- }),
- KibanaServices.get().http.fetch(getSubCaseDetailsUrl(caseId, subCaseId), {
- method: 'GET',
- query: {
- includeComments,
- },
- signal,
- }),
- ]);
- const response = assign(caseResponse, subCaseResponse);
- const subCaseIndex = response.subCaseIds?.findIndex((scId) => scId === response.id) ?? -1;
- response.title = `${response.title}${subCaseIndex >= 0 ? ` ${subCaseIndex + 1}` : ''}`;
- return convertToCamelCase(decodeCaseResponse(response));
-};
-
export const getCasesStatus = async (
signal: AbortSignal,
owner: string[]
@@ -192,24 +156,8 @@ export const getCaseUserActions = async (
return convertArrayToCamelCase(decodeCaseUserActionsResponse(response)) as CaseUserActions[];
};
-export const getSubCaseUserActions = async (
- caseId: string,
- subCaseId: string,
- signal: AbortSignal
-): Promise => {
- const response = await KibanaServices.get().http.fetch(
- getSubCaseUserActionUrl(caseId, subCaseId),
- {
- method: 'GET',
- signal,
- }
- );
- return convertArrayToCamelCase(decodeCaseUserActionsResponse(response)) as CaseUserActions[];
-};
-
export const getCases = async ({
filterOptions = {
- onlyCollectionType: false,
search: '',
reporters: [],
status: StatusAll,
@@ -229,7 +177,6 @@ export const getCases = async ({
tags: filterOptions.tags,
status: filterOptions.status,
...(filterOptions.search.length > 0 ? { search: filterOptions.search } : {}),
- ...(filterOptions.onlyCollectionType ? { type: CaseType.collection } : {}),
...(filterOptions.owner.length > 0 ? { owner: filterOptions.owner } : {}),
...queryParams,
};
@@ -267,35 +214,6 @@ export const patchCase = async (
return convertToCamelCase(decodeCasesResponse(response));
};
-export const patchSubCase = async (
- caseId: string,
- subCaseId: string,
- updatedSubCase: Pick,
- version: string,
- signal: AbortSignal
-): Promise => {
- const subCaseResponse = await KibanaServices.get().http.fetch(
- SUB_CASE_DETAILS_URL,
- {
- method: 'PATCH',
- body: JSON.stringify({ cases: [{ ...updatedSubCase, id: caseId, version }] }),
- signal,
- }
- );
- const caseResponse = await KibanaServices.get().http.fetch(
- getCaseDetailsUrl(caseId),
- {
- method: 'GET',
- query: {
- includeComments: false,
- },
- signal,
- }
- );
- const response = subCaseResponse.map((subCaseResp) => assign(caseResponse, subCaseResp));
- return convertToCamelCase(decodeCasesResponse(response));
-};
-
export const patchCasesStatus = async (
cases: BulkUpdateStatus[],
signal: AbortSignal
@@ -311,15 +229,13 @@ export const patchCasesStatus = async (
export const postComment = async (
newComment: CommentRequest,
caseId: string,
- signal: AbortSignal,
- subCaseId?: string
+ signal: AbortSignal
): Promise => {
const response = await KibanaServices.get().http.fetch(
`${CASES_URL}/${caseId}/comments`,
{
method: 'POST',
body: JSON.stringify(newComment),
- ...(subCaseId ? { query: { subCaseId } } : {}),
signal,
}
);
@@ -333,7 +249,6 @@ export const patchComment = async ({
version,
signal,
owner,
- subCaseId,
}: {
caseId: string;
commentId: string;
@@ -341,7 +256,6 @@ export const patchComment = async ({
version: string;
signal: AbortSignal;
owner: string;
- subCaseId?: string;
}): Promise => {
const response = await KibanaServices.get().http.fetch(getCaseCommentsUrl(caseId), {
method: 'PATCH',
@@ -352,7 +266,6 @@ export const patchComment = async ({
version,
owner,
}),
- ...(subCaseId ? { query: { subCaseId } } : {}),
signal,
});
return convertToCamelCase(decodeCaseResponse(response));
@@ -367,15 +280,6 @@ export const deleteCases = async (caseIds: string[], signal: AbortSignal): Promi
return response;
};
-export const deleteSubCases = async (caseIds: string[], signal: AbortSignal): Promise => {
- const response = await KibanaServices.get().http.fetch(SUB_CASES_PATCH_DEL_URL, {
- method: 'DELETE',
- query: { ids: JSON.stringify(caseIds) },
- signal,
- });
- return response;
-};
-
export const pushCase = async (
caseId: string,
connectorId: string,
diff --git a/x-pack/plugins/cases/public/containers/mock.ts b/x-pack/plugins/cases/public/containers/mock.ts
index c0fb9fd9f0138..0d2aff225dfa6 100644
--- a/x-pack/plugins/cases/public/containers/mock.ts
+++ b/x-pack/plugins/cases/public/containers/mock.ts
@@ -11,14 +11,12 @@ import type { ResolvedCase, CaseMetrics, CaseMetricsFeature } from '../../common
import {
Actions,
ActionTypes,
- AssociationType,
CaseConnector,
CaseResponse,
CasesFindResponse,
CasesResponse,
CasesStatusResponse,
CaseStatuses,
- CaseType,
CaseUserActionResponse,
CaseUserActionsResponse,
CommentResponse,
@@ -38,7 +36,6 @@ export { connectorsMock } from './configure/mock';
export const basicCaseId = 'basic-case-id';
export const caseWithAlertsId = 'case-with-alerts-id';
export const caseWithAlertsSyncOffId = 'case-with-alerts-syncoff-id';
-export const basicSubCaseId = 'basic-sub-case-id';
const basicCommentId = 'basic-comment-id';
const basicCreatedAt = '2020-02-19T23:06:33.798Z';
const basicUpdatedAt = '2020-02-20T15:02:57.995Z';
@@ -54,7 +51,6 @@ export const elasticUser = {
export const tags: string[] = ['coke', 'pepsi'];
export const basicComment: Comment = {
- associationType: AssociationType.case,
comment: 'Solve this fast!',
type: CommentType.user,
id: basicCommentId,
@@ -70,7 +66,6 @@ export const basicComment: Comment = {
export const alertComment: Comment = {
alertId: 'alert-id-1',
- associationType: AssociationType.case,
index: 'alert-index-1',
type: CommentType.alert,
id: 'alert-comment-id',
@@ -102,7 +97,6 @@ export const hostIsolationComment: () => Comment = () => {
],
type: 'isolate',
},
- associationType: AssociationType.case,
createdAt: basicCreatedAt,
createdBy: elasticUser,
owner: SECURITY_SOLUTION_OWNER,
@@ -128,7 +122,6 @@ export const hostReleaseComment: () => Comment = () => {
],
type: 'unisolate',
},
- associationType: AssociationType.case,
createdAt: basicCreatedAt,
createdBy: elasticUser,
owner: SECURITY_SOLUTION_OWNER,
@@ -141,7 +134,6 @@ export const hostReleaseComment: () => Comment = () => {
};
export const basicCase: Case = {
- type: CaseType.individual,
owner: SECURITY_SOLUTION_OWNER,
closedAt: null,
closedBy: null,
@@ -168,7 +160,6 @@ export const basicCase: Case = {
settings: {
syncAlerts: true,
},
- subCaseIds: [],
};
export const caseWithAlerts = {
@@ -234,12 +225,11 @@ export const basicCaseMetrics: CaseMetrics = {
},
};
-export const collectionCase: Case = {
- type: CaseType.collection,
+export const mockCase: Case = {
owner: SECURITY_SOLUTION_OWNER,
closedAt: null,
closedBy: null,
- id: 'collection-id',
+ id: 'mock-id',
comments: [basicComment],
createdAt: basicCreatedAt,
createdBy: elasticUser,
@@ -253,7 +243,7 @@ export const collectionCase: Case = {
externalService: null,
status: CaseStatuses.open,
tags,
- title: 'Another horrible breach in a collection!!',
+ title: 'Another horrible breach!!',
totalComment: 1,
totalAlerts: 0,
updatedAt: basicUpdatedAt,
@@ -262,8 +252,6 @@ export const collectionCase: Case = {
settings: {
syncAlerts: true,
},
- subCases: [],
- subCaseIds: [],
};
export const basicCasePost: Case = {
@@ -367,7 +355,6 @@ export const elasticUserSnake = {
};
export const basicCommentSnake: CommentResponse = {
- associationType: AssociationType.case,
comment: 'Solve this fast!',
type: CommentType.user,
id: basicCommentId,
diff --git a/x-pack/plugins/cases/public/containers/use_delete_cases.test.tsx b/x-pack/plugins/cases/public/containers/use_delete_cases.test.tsx
index 307dc0941e398..88f6db42144f6 100644
--- a/x-pack/plugins/cases/public/containers/use_delete_cases.test.tsx
+++ b/x-pack/plugins/cases/public/containers/use_delete_cases.test.tsx
@@ -7,7 +7,6 @@
import { renderHook, act } from '@testing-library/react-hooks';
-import { CaseType } from '../../common/api';
import { useDeleteCases, UseDeleteCase } from './use_delete_cases';
import * as api from './api';
@@ -17,9 +16,9 @@ jest.mock('../common/lib/kibana');
describe('useDeleteCases', () => {
const abortCtrl = new AbortController();
const deleteObj = [
- { id: '1', type: CaseType.individual, title: 'case 1' },
- { id: '2', type: CaseType.individual, title: 'case 2' },
- { id: '3', type: CaseType.individual, title: 'case 3' },
+ { id: '1', title: 'case 1' },
+ { id: '2', title: 'case 2' },
+ { id: '3', title: 'case 3' },
];
const deleteArr = ['1', '2', '3'];
it('init', async () => {
diff --git a/x-pack/plugins/cases/public/containers/use_delete_cases.tsx b/x-pack/plugins/cases/public/containers/use_delete_cases.tsx
index 153be6126f967..7ccec4436ec0b 100644
--- a/x-pack/plugins/cases/public/containers/use_delete_cases.tsx
+++ b/x-pack/plugins/cases/public/containers/use_delete_cases.tsx
@@ -7,7 +7,7 @@
import { useCallback, useReducer, useRef, useEffect } from 'react';
import * as i18n from './translations';
-import { deleteCases, deleteSubCases } from './api';
+import { deleteCases } from './api';
import { DeleteCase } from './types';
import { useToasts } from '../common/lib/kibana';
@@ -85,11 +85,8 @@ export const useDeleteCases = (): UseDeleteCase => {
dispatch({ type: 'FETCH_INIT' });
const caseIds = cases.map((theCase) => theCase.id);
- // We don't allow user batch delete sub cases on UI at the moment.
- if (cases[0].type != null || cases.length > 1) {
+ if (cases.length > 0) {
await deleteCases(caseIds, abortCtrlRef.current.signal);
- } else {
- await deleteSubCases(caseIds, abortCtrlRef.current.signal);
}
if (!isCancelledRef.current) {
diff --git a/x-pack/plugins/cases/public/containers/use_get_case.tsx b/x-pack/plugins/cases/public/containers/use_get_case.tsx
index 52610981a227c..c7d2a79a53256 100644
--- a/x-pack/plugins/cases/public/containers/use_get_case.tsx
+++ b/x-pack/plugins/cases/public/containers/use_get_case.tsx
@@ -10,7 +10,7 @@ import { useEffect, useReducer, useCallback, useRef } from 'react';
import { Case, ResolvedCase } from './types';
import * as i18n from './translations';
import { useToasts } from '../common/lib/kibana';
-import { resolveCase, getSubCase } from './api';
+import { resolveCase } from './api';
interface CaseState {
data: Case | null;
@@ -71,7 +71,7 @@ export interface UseGetCase extends CaseState {
updateCase: (newCase: Case) => void;
}
-export const useGetCase = (caseId: string, subCaseId?: string): UseGetCase => {
+export const useGetCase = (caseId: string): UseGetCase => {
const [state, dispatch] = useReducer(dataFetchReducer, {
isLoading: false,
isError: false,
@@ -94,12 +94,7 @@ export const useGetCase = (caseId: string, subCaseId?: string): UseGetCase => {
abortCtrlRef.current = new AbortController();
dispatch({ type: 'FETCH_INIT', payload: { silent } });
- const response: ResolvedCase = subCaseId
- ? {
- case: await getSubCase(caseId, subCaseId, true, abortCtrlRef.current.signal),
- outcome: 'exactMatch', // sub-cases are not resolved, forced to exactMatch always
- }
- : await resolveCase(caseId, true, abortCtrlRef.current.signal);
+ const response: ResolvedCase = await resolveCase(caseId, true, abortCtrlRef.current.signal);
if (!isCancelledRef.current) {
dispatch({ type: 'FETCH_SUCCESS', payload: response });
@@ -116,7 +111,7 @@ export const useGetCase = (caseId: string, subCaseId?: string): UseGetCase => {
}
}
},
- [caseId, subCaseId, toasts]
+ [caseId, toasts]
);
useEffect(() => {
@@ -127,6 +122,6 @@ export const useGetCase = (caseId: string, subCaseId?: string): UseGetCase => {
abortCtrlRef.current.abort();
};
// eslint-disable-next-line react-hooks/exhaustive-deps
- }, [caseId, subCaseId]);
+ }, [caseId]);
return { ...state, fetchCase: callFetch, updateCase };
};
diff --git a/x-pack/plugins/cases/public/containers/use_get_case_user_actions.tsx b/x-pack/plugins/cases/public/containers/use_get_case_user_actions.tsx
index fc73a898aa8e7..e5f3006ee924f 100644
--- a/x-pack/plugins/cases/public/containers/use_get_case_user_actions.tsx
+++ b/x-pack/plugins/cases/public/containers/use_get_case_user_actions.tsx
@@ -11,7 +11,7 @@ import deepEqual from 'fast-deep-equal';
import { ElasticUser, CaseUserActions, CaseExternalService } from '../../common/ui/types';
import { ActionTypes, CaseConnector, NONE_CONNECTOR_ID } from '../../common/api';
-import { getCaseUserActions, getSubCaseUserActions } from './api';
+import { getCaseUserActions } from './api';
import * as i18n from './translations';
import { useToasts } from '../common/lib/kibana';
import {
@@ -50,11 +50,7 @@ export const initialData: CaseUserActionsState = {
};
export interface UseGetCaseUserActions extends CaseUserActionsState {
- fetchCaseUserActions: (
- caseId: string,
- caseConnectorId: string,
- subCaseId?: string
- ) => Promise;
+ fetchCaseUserActions: (caseId: string, caseConnectorId: string) => Promise;
}
const groupConnectorFields = (
@@ -234,8 +230,7 @@ export const getPushedInfo = (
export const useGetCaseUserActions = (
caseId: string,
- caseConnectorId: string,
- subCaseId?: string
+ caseConnectorId: string
): UseGetCaseUserActions => {
const [caseUserActionsState, setCaseUserActionsState] =
useState(initialData);
@@ -244,7 +239,7 @@ export const useGetCaseUserActions = (
const toasts = useToasts();
const fetchCaseUserActions = useCallback(
- async (thisCaseId: string, thisCaseConnectorId: string, thisSubCaseId?: string) => {
+ async (thisCaseId: string, thisCaseConnectorId: string) => {
try {
isCancelledRef.current = false;
abortCtrlRef.current.abort();
@@ -254,9 +249,7 @@ export const useGetCaseUserActions = (
isLoading: true,
});
- const response = await (thisSubCaseId
- ? getSubCaseUserActions(thisCaseId, thisSubCaseId, abortCtrlRef.current.signal)
- : getCaseUserActions(thisCaseId, abortCtrlRef.current.signal));
+ const response = await getCaseUserActions(thisCaseId, abortCtrlRef.current.signal);
if (!isCancelledRef.current) {
// Attention Future developer
@@ -302,7 +295,7 @@ export const useGetCaseUserActions = (
useEffect(() => {
if (!isEmpty(caseId)) {
- fetchCaseUserActions(caseId, caseConnectorId, subCaseId);
+ fetchCaseUserActions(caseId, caseConnectorId);
}
return () => {
@@ -310,6 +303,6 @@ export const useGetCaseUserActions = (
abortCtrlRef.current.abort();
};
// eslint-disable-next-line react-hooks/exhaustive-deps
- }, [caseId, subCaseId]);
+ }, [caseId]);
return { ...caseUserActionsState, fetchCaseUserActions };
};
diff --git a/x-pack/plugins/cases/public/containers/use_get_cases.tsx b/x-pack/plugins/cases/public/containers/use_get_cases.tsx
index 3b144620b6352..283cdfdf39aa4 100644
--- a/x-pack/plugins/cases/public/containers/use_get_cases.tsx
+++ b/x-pack/plugins/cases/public/containers/use_get_cases.tsx
@@ -104,7 +104,6 @@ export const DEFAULT_FILTER_OPTIONS: FilterOptions = {
reporters: [],
status: StatusAll,
tags: [],
- onlyCollectionType: false,
owner: [],
};
diff --git a/x-pack/plugins/cases/public/containers/use_post_comment.test.tsx b/x-pack/plugins/cases/public/containers/use_post_comment.test.tsx
index dd9d73cff9bae..cd76738b70307 100644
--- a/x-pack/plugins/cases/public/containers/use_post_comment.test.tsx
+++ b/x-pack/plugins/cases/public/containers/use_post_comment.test.tsx
@@ -10,7 +10,7 @@ import { renderHook, act } from '@testing-library/react-hooks';
import { CommentType } from '../../common/api';
import { SECURITY_SOLUTION_OWNER } from '../../common/constants';
import { usePostComment, UsePostComment } from './use_post_comment';
-import { basicCaseId, basicSubCaseId } from './mock';
+import { basicCaseId } from './mock';
import * as api from './api';
jest.mock('./api');
@@ -58,32 +58,7 @@ describe('usePostComment', () => {
updateCase: updateCaseCallback,
});
await waitForNextUpdate();
- expect(spyOnPostCase).toBeCalledWith(samplePost, basicCaseId, abortCtrl.signal, undefined);
- });
- });
-
- it('calls postComment with correct arguments - sub case', async () => {
- const spyOnPostCase = jest.spyOn(api, 'postComment');
-
- await act(async () => {
- const { result, waitForNextUpdate } = renderHook(() =>
- usePostComment()
- );
- await waitForNextUpdate();
-
- result.current.postComment({
- caseId: basicCaseId,
- data: samplePost,
- updateCase: updateCaseCallback,
- subCaseId: basicSubCaseId,
- });
- await waitForNextUpdate();
- expect(spyOnPostCase).toBeCalledWith(
- samplePost,
- basicCaseId,
- abortCtrl.signal,
- basicSubCaseId
- );
+ expect(spyOnPostCase).toBeCalledWith(samplePost, basicCaseId, abortCtrl.signal);
});
});
diff --git a/x-pack/plugins/cases/public/containers/use_post_comment.tsx b/x-pack/plugins/cases/public/containers/use_post_comment.tsx
index d796c5035ff9d..67db681166d95 100644
--- a/x-pack/plugins/cases/public/containers/use_post_comment.tsx
+++ b/x-pack/plugins/cases/public/containers/use_post_comment.tsx
@@ -45,7 +45,6 @@ export interface PostComment {
caseId: string;
data: CommentRequest;
updateCase?: (newCase: Case) => void;
- subCaseId?: string;
}
export interface UsePostComment extends NewCommentState {
postComment: (args: PostComment) => Promise;
@@ -61,14 +60,14 @@ export const usePostComment = (): UsePostComment => {
const abortCtrlRef = useRef(new AbortController());
const postMyComment = useCallback(
- async ({ caseId, data, updateCase, subCaseId }: PostComment) => {
+ async ({ caseId, data, updateCase }: PostComment) => {
try {
isCancelledRef.current = false;
abortCtrlRef.current.abort();
abortCtrlRef.current = new AbortController();
dispatch({ type: 'FETCH_INIT' });
- const response = await postComment(data, caseId, abortCtrlRef.current.signal, subCaseId);
+ const response = await postComment(data, caseId, abortCtrlRef.current.signal);
if (!isCancelledRef.current) {
dispatch({ type: 'FETCH_SUCCESS' });
diff --git a/x-pack/plugins/cases/public/containers/use_update_case.test.tsx b/x-pack/plugins/cases/public/containers/use_update_case.test.tsx
index 8d7cfc56b195c..52ab9d3a3ebc5 100644
--- a/x-pack/plugins/cases/public/containers/use_update_case.test.tsx
+++ b/x-pack/plugins/cases/public/containers/use_update_case.test.tsx
@@ -7,7 +7,7 @@
import { renderHook, act } from '@testing-library/react-hooks';
import { useUpdateCase, UseUpdateCase } from './use_update_case';
-import { basicCase, basicSubCaseId } from './mock';
+import { basicCase } from './mock';
import * as api from './api';
import { UpdateKey } from './types';
@@ -85,27 +85,7 @@ describe('useUpdateCase', () => {
isError: false,
updateCaseProperty: result.current.updateCaseProperty,
});
- expect(fetchCaseUserActions).toBeCalledWith(basicCase.id, 'none', undefined);
- expect(updateCase).toBeCalledWith(basicCase);
- expect(onSuccess).toHaveBeenCalled();
- });
- });
-
- it('patch sub case', async () => {
- await act(async () => {
- const { result, waitForNextUpdate } = renderHook(() =>
- useUpdateCase({ caseId: basicCase.id, subCaseId: basicSubCaseId })
- );
- await waitForNextUpdate();
- result.current.updateCaseProperty(sampleUpdate);
- await waitForNextUpdate();
- expect(result.current).toEqual({
- updateKey: null,
- isLoading: false,
- isError: false,
- updateCaseProperty: result.current.updateCaseProperty,
- });
- expect(fetchCaseUserActions).toBeCalledWith(basicCase.id, 'none', basicSubCaseId);
+ expect(fetchCaseUserActions).toBeCalledWith(basicCase.id, 'none');
expect(updateCase).toBeCalledWith(basicCase);
expect(onSuccess).toHaveBeenCalled();
});
diff --git a/x-pack/plugins/cases/public/containers/use_update_case.tsx b/x-pack/plugins/cases/public/containers/use_update_case.tsx
index 42e861d300341..eedaf1fca7a8e 100644
--- a/x-pack/plugins/cases/public/containers/use_update_case.tsx
+++ b/x-pack/plugins/cases/public/containers/use_update_case.tsx
@@ -8,9 +8,8 @@
import { useReducer, useCallback, useRef, useEffect } from 'react';
import { useToasts } from '../common/lib/kibana';
-import { patchCase, patchSubCase } from './api';
+import { patchCase } from './api';
import { UpdateKey, UpdateByKey } from '../../common/ui/types';
-import { CaseStatuses } from '../../common/api';
import * as i18n from './translations';
import { createUpdateSuccessToaster } from './utils';
@@ -57,13 +56,7 @@ const dataFetchReducer = (state: NewCaseState, action: Action): NewCaseState =>
export interface UseUpdateCase extends NewCaseState {
updateCaseProperty: (updates: UpdateByKey) => void;
}
-export const useUpdateCase = ({
- caseId,
- subCaseId,
-}: {
- caseId: string;
- subCaseId?: string;
-}): UseUpdateCase => {
+export const useUpdateCase = ({ caseId }: { caseId: string }): UseUpdateCase => {
const [state, dispatch] = useReducer(dataFetchReducer, {
isLoading: false,
isError: false,
@@ -89,24 +82,16 @@ export const useUpdateCase = ({
abortCtrlRef.current = new AbortController();
dispatch({ type: 'FETCH_INIT', payload: updateKey });
- const response = await (updateKey === 'status' && subCaseId
- ? patchSubCase(
- caseId,
- subCaseId,
- { status: updateValue as CaseStatuses },
- caseData.version,
- abortCtrlRef.current.signal
- )
- : patchCase(
- caseId,
- { [updateKey]: updateValue },
- caseData.version,
- abortCtrlRef.current.signal
- ));
+ const response = await patchCase(
+ caseId,
+ { [updateKey]: updateValue },
+ caseData.version,
+ abortCtrlRef.current.signal
+ );
if (!isCancelledRef.current) {
if (fetchCaseUserActions != null) {
- fetchCaseUserActions(caseId, response[0].connector.id, subCaseId);
+ fetchCaseUserActions(caseId, response[0].connector.id);
}
if (updateCase != null) {
updateCase(response[0]);
@@ -136,7 +121,7 @@ export const useUpdateCase = ({
}
},
// eslint-disable-next-line react-hooks/exhaustive-deps
- [caseId, subCaseId]
+ [caseId]
);
useEffect(
diff --git a/x-pack/plugins/cases/public/containers/use_update_comment.test.tsx b/x-pack/plugins/cases/public/containers/use_update_comment.test.tsx
index 836ec10e608a6..9e5358a5f8b5e 100644
--- a/x-pack/plugins/cases/public/containers/use_update_comment.test.tsx
+++ b/x-pack/plugins/cases/public/containers/use_update_comment.test.tsx
@@ -8,7 +8,7 @@
import React from 'react';
import { renderHook, act } from '@testing-library/react-hooks';
import { useUpdateComment, UseUpdateComment } from './use_update_comment';
-import { basicCase, basicCaseCommentPatch, basicSubCaseId } from './mock';
+import { basicCase, basicCaseCommentPatch } from './mock';
import * as api from './api';
import { TestProviders } from '../common/mock';
import { SECURITY_SOLUTION_OWNER } from '../../common/constants';
@@ -67,28 +67,6 @@ describe('useUpdateComment', () => {
version: basicCase.comments[0].version,
signal: abortCtrl.signal,
owner: SECURITY_SOLUTION_OWNER,
- subCaseId: undefined,
- });
- });
- });
-
- it('calls patchComment with correct arguments - sub case', async () => {
- const spyOnPatchComment = jest.spyOn(api, 'patchComment');
-
- await act(async () => {
- const { result, waitForNextUpdate } = renderHookUseUpdateComment();
- await waitForNextUpdate();
-
- result.current.patchComment({ ...sampleUpdate, subCaseId: basicSubCaseId });
- await waitForNextUpdate();
- expect(spyOnPatchComment).toBeCalledWith({
- caseId: basicCase.id,
- commentId: basicCase.comments[0].id,
- commentUpdate: 'updated comment',
- version: basicCase.comments[0].version,
- signal: abortCtrl.signal,
- owner: SECURITY_SOLUTION_OWNER,
- subCaseId: basicSubCaseId,
});
});
});
diff --git a/x-pack/plugins/cases/public/containers/use_update_comment.tsx b/x-pack/plugins/cases/public/containers/use_update_comment.tsx
index cc287d4dee863..1a457237d2e64 100644
--- a/x-pack/plugins/cases/public/containers/use_update_comment.tsx
+++ b/x-pack/plugins/cases/public/containers/use_update_comment.tsx
@@ -56,7 +56,6 @@ interface UpdateComment {
commentId: string;
commentUpdate: string;
fetchUserActions: () => void;
- subCaseId?: string;
updateCase: (newCase: Case) => void;
version: string;
}
@@ -83,7 +82,6 @@ export const useUpdateComment = (): UseUpdateComment => {
commentId,
commentUpdate,
fetchUserActions,
- subCaseId,
updateCase,
version,
}: UpdateComment) => {
@@ -99,7 +97,6 @@ export const useUpdateComment = (): UseUpdateComment => {
commentUpdate,
version,
signal: abortCtrlRef.current.signal,
- subCaseId,
owner,
});
diff --git a/x-pack/plugins/cases/public/plugin.ts b/x-pack/plugins/cases/public/plugin.ts
index f38b2d12e2ad4..70882560edb77 100644
--- a/x-pack/plugins/cases/public/plugin.ts
+++ b/x-pack/plugins/cases/public/plugin.ts
@@ -8,7 +8,6 @@
import { CoreSetup, CoreStart, Plugin, PluginInitializerContext } from 'src/core/public';
import { CasesUiStart, SetupPlugins, StartPlugins } from './types';
import { KibanaServices } from './common/lib/kibana';
-import { getCaseConnectorUi } from './components/connectors';
import {
getCasesLazy,
getRecentCasesLazy,
@@ -17,7 +16,6 @@ import {
canUseCases,
} from './methods';
import { CasesUiConfigType } from '../common/ui/types';
-import { ENABLE_CASE_CONNECTOR } from '../common/constants';
/**
* @public
@@ -29,11 +27,7 @@ export class CasesUiPlugin implements Plugin();
diff --git a/x-pack/plugins/cases/public/types.ts b/x-pack/plugins/cases/public/types.ts
index cb2e570b58e13..5d53b46b8c014 100644
--- a/x-pack/plugins/cases/public/types.ts
+++ b/x-pack/plugins/cases/public/types.ts
@@ -10,10 +10,7 @@ import { ReactElement } from 'react';
import type { LensPublicStart } from '../../lens/public';
import type { SecurityPluginSetup } from '../../security/public';
-import type {
- TriggersAndActionsUIPublicPluginSetup as TriggersActionsSetup,
- TriggersAndActionsUIPublicPluginStart as TriggersActionsStart,
-} from '../../triggers_actions_ui/public';
+import type { TriggersAndActionsUIPublicPluginStart as TriggersActionsStart } from '../../triggers_actions_ui/public';
import type { DataPublicPluginStart } from '../../../../src/plugins/data/public';
import type { EmbeddableStart } from '../../../../src/plugins/embeddable/public';
import type { SpacesPluginStart } from '../../spaces/public';
@@ -29,7 +26,6 @@ import type {
export interface SetupPlugins {
security: SecurityPluginSetup;
- triggersActionsUi: TriggersActionsSetup;
}
export interface StartPlugins {
diff --git a/x-pack/plugins/cases/server/client/attachments/add.ts b/x-pack/plugins/cases/server/client/attachments/add.ts
index b443d0c8bfa7b..ca03381681796 100644
--- a/x-pack/plugins/cases/server/client/attachments/add.ts
+++ b/x-pack/plugins/cases/server/client/attachments/add.ts
@@ -10,9 +10,7 @@ import { pipe } from 'fp-ts/lib/pipeable';
import { fold } from 'fp-ts/lib/Either';
import { identity } from 'fp-ts/lib/function';
-import { nodeBuilder } from '@kbn/es-query';
import {
- SavedObject,
SavedObjectsClientContract,
Logger,
SavedObjectsUtils,
@@ -22,201 +20,24 @@ import { LensServerPluginSetup } from '../../../../lens/server';
import {
Actions,
ActionTypes,
- AlertCommentRequestRt,
CaseResponse,
- CaseStatuses,
- CaseType,
CommentRequest,
CommentRequestRt,
CommentType,
- SubCaseAttributes,
throwErrors,
User,
} from '../../../common/api';
-import {
- CASE_COMMENT_SAVED_OBJECT,
- ENABLE_CASE_CONNECTOR,
- MAX_GENERATED_ALERTS_PER_SUB_CASE,
-} from '../../../common/constants';
-import { AttachmentService, CasesService, CaseUserActionService } from '../../services';
+import { AttachmentService, CasesService } from '../../services';
import { CommentableCase } from '../../common/models';
import { createCaseError } from '../../common/error';
-import { createAlertUpdateRequest, isCommentRequestTypeGenAlert } from '../../common/utils';
+import { createAlertUpdateRequest } from '../../common/utils';
import { CasesClientArgs, CasesClientInternal } from '..';
import { decodeCommentRequest } from '../utils';
import { Operations } from '../../authorization';
-async function getSubCase({
- caseService,
- unsecuredSavedObjectsClient,
- caseId,
- createdAt,
- userActionService,
- user,
-}: {
- caseService: CasesService;
- unsecuredSavedObjectsClient: SavedObjectsClientContract;
- caseId: string;
- createdAt: string;
- userActionService: CaseUserActionService;
- user: User;
-}): Promise> {
- const mostRecentSubCase = await caseService.getMostRecentSubCase(
- unsecuredSavedObjectsClient,
- caseId
- );
- if (mostRecentSubCase && mostRecentSubCase.attributes.status !== CaseStatuses.closed) {
- const subCaseAlertsAttachement = await caseService.getAllSubCaseComments({
- unsecuredSavedObjectsClient,
- id: mostRecentSubCase.id,
- options: {
- fields: [],
- filter: nodeBuilder.is(
- `${CASE_COMMENT_SAVED_OBJECT}.attributes.type`,
- CommentType.generatedAlert
- ),
- page: 1,
- perPage: 1,
- },
- });
-
- if (subCaseAlertsAttachement.total <= MAX_GENERATED_ALERTS_PER_SUB_CASE) {
- return mostRecentSubCase;
- }
- }
-
- const newSubCase = await caseService.createSubCase({
- unsecuredSavedObjectsClient,
- createdAt,
- caseId,
- createdBy: user,
- });
-
- return newSubCase;
-}
-
-const addGeneratedAlerts = async (
- { caseId, comment }: AddArgs,
- clientArgs: CasesClientArgs,
- casesClientInternal: CasesClientInternal
-): Promise => {
- const {
- unsecuredSavedObjectsClient,
- attachmentService,
- caseService,
- userActionService,
- logger,
- lensEmbeddableFactory,
- authorization,
- alertsService,
- user,
- } = clientArgs;
-
- const query = pipe(
- AlertCommentRequestRt.decode(comment),
- fold(throwErrors(Boom.badRequest), identity)
- );
-
- decodeCommentRequest(comment);
-
- // This function only supports adding generated alerts
- if (comment.type !== CommentType.generatedAlert) {
- throw Boom.internal('Attempting to add a non generated alert in the wrong context');
- }
-
- try {
- const createdDate = new Date().toISOString();
- const savedObjectID = SavedObjectsUtils.generateId();
-
- await authorization.ensureAuthorized({
- entities: [{ owner: comment.owner, id: savedObjectID }],
- operation: Operations.createComment,
- });
-
- const caseInfo = await caseService.getCase({
- unsecuredSavedObjectsClient,
- id: caseId,
- });
-
- if (
- query.type === CommentType.generatedAlert &&
- caseInfo.attributes.type !== CaseType.collection
- ) {
- throw Boom.badRequest('Sub case style alert comment cannot be added to an individual case');
- }
-
- const userDetails: User = {
- username: caseInfo.attributes.created_by?.username,
- full_name: caseInfo.attributes.created_by?.full_name,
- email: caseInfo.attributes.created_by?.email,
- };
-
- const subCase = await getSubCase({
- caseService,
- unsecuredSavedObjectsClient,
- caseId,
- createdAt: createdDate,
- userActionService,
- user: userDetails,
- });
-
- const commentableCase = new CommentableCase({
- logger,
- collection: caseInfo,
- subCase,
- unsecuredSavedObjectsClient,
- caseService,
- attachmentService,
- lensEmbeddableFactory,
- });
-
- const { comment: newComment, commentableCase: updatedCase } =
- await commentableCase.createComment({
- createdDate,
- user: userDetails,
- commentReq: query,
- id: savedObjectID,
- });
-
- if (
- (newComment.attributes.type === CommentType.alert ||
- newComment.attributes.type === CommentType.generatedAlert) &&
- caseInfo.attributes.settings.syncAlerts
- ) {
- const alertsToUpdate = createAlertUpdateRequest({
- comment: query,
- status: subCase.attributes.status,
- });
- await alertsService.updateAlertsStatus(alertsToUpdate);
- }
-
- await userActionService.createUserAction({
- type: ActionTypes.comment,
- action: Actions.create,
- unsecuredSavedObjectsClient,
- caseId: updatedCase.caseId,
- subCaseId: updatedCase.subCaseId,
- payload: {
- attachment: query,
- },
- attachmentId: newComment.id,
- user,
- owner: newComment.attributes.owner,
- });
-
- return updatedCase.encode();
- } catch (error) {
- throw createCaseError({
- message: `Failed while adding a generated alert to case id: ${caseId} error: ${error}`,
- error,
- logger,
- });
- }
-};
-
-async function getCombinedCase({
+async function createCommentableCase({
caseService,
attachmentService,
unsecuredSavedObjectsClient,
@@ -231,53 +52,19 @@ async function getCombinedCase({
logger: Logger;
lensEmbeddableFactory: LensServerPluginSetup['lensEmbeddableFactory'];
}): Promise {
- const [casePromise, subCasePromise] = await Promise.allSettled([
- caseService.getCase({
- unsecuredSavedObjectsClient,
- id,
- }),
- ...(ENABLE_CASE_CONNECTOR
- ? [
- caseService.getSubCase({
- unsecuredSavedObjectsClient,
- id,
- }),
- ]
- : [Promise.reject(new Error('case connector feature is disabled'))]),
- ]);
-
- if (subCasePromise.status === 'fulfilled') {
- if (subCasePromise.value.references.length > 0) {
- const caseValue = await caseService.getCase({
- unsecuredSavedObjectsClient,
- id: subCasePromise.value.references[0].id,
- });
- return new CommentableCase({
- logger,
- collection: caseValue,
- subCase: subCasePromise.value,
- caseService,
- attachmentService,
- unsecuredSavedObjectsClient,
- lensEmbeddableFactory,
- });
- } else {
- throw Boom.badRequest('Sub case found without reference to collection');
- }
- }
+ const caseInfo = await caseService.getCase({
+ unsecuredSavedObjectsClient,
+ id,
+ });
- if (casePromise.status === 'rejected') {
- throw casePromise.reason;
- } else {
- return new CommentableCase({
- logger,
- collection: casePromise.value,
- caseService,
- attachmentService,
- unsecuredSavedObjectsClient,
- lensEmbeddableFactory,
- });
- }
+ return new CommentableCase({
+ logger,
+ caseInfo,
+ caseService,
+ attachmentService,
+ unsecuredSavedObjectsClient,
+ lensEmbeddableFactory,
+ });
}
/**
@@ -322,16 +109,6 @@ export const addComment = async (
alertsService,
} = clientArgs;
- if (isCommentRequestTypeGenAlert(comment)) {
- if (!ENABLE_CASE_CONNECTOR) {
- throw Boom.badRequest(
- 'Attempting to add a generated alert when case connector feature is disabled'
- );
- }
-
- return addGeneratedAlerts(addArgs, clientArgs, casesClientInternal);
- }
-
decodeCommentRequest(comment);
try {
const savedObjectID = SavedObjectsUtils.generateId();
@@ -343,7 +120,7 @@ export const addComment = async (
const createdDate = new Date().toISOString();
- const combinedCase = await getCombinedCase({
+ const combinedCase = await createCommentableCase({
caseService,
attachmentService,
unsecuredSavedObjectsClient,
@@ -381,7 +158,6 @@ export const addComment = async (
action: Actions.create,
unsecuredSavedObjectsClient,
caseId,
- subCaseId: updatedCase.subCaseId,
attachmentId: newComment.id,
payload: {
attachment: query,
diff --git a/x-pack/plugins/cases/server/client/attachments/delete.ts b/x-pack/plugins/cases/server/client/attachments/delete.ts
index 66394dd3cb7f5..9d048162da7eb 100644
--- a/x-pack/plugins/cases/server/client/attachments/delete.ts
+++ b/x-pack/plugins/cases/server/client/attachments/delete.ts
@@ -9,33 +9,24 @@ import Boom from '@hapi/boom';
import pMap from 'p-map';
import { SavedObject } from 'kibana/public';
-import { Actions, ActionTypes, AssociationType, CommentAttributes } from '../../../common/api';
-import {
- CASE_SAVED_OBJECT,
- MAX_CONCURRENT_SEARCHES,
- SUB_CASE_SAVED_OBJECT,
-} from '../../../common/constants';
+import { Actions, ActionTypes, CommentAttributes } from '../../../common/api';
+import { CASE_SAVED_OBJECT, MAX_CONCURRENT_SEARCHES } from '../../../common/constants';
import { CasesClientArgs } from '../types';
import { createCaseError } from '../../common/error';
-import { checkEnabledCaseConnectorOrThrow } from '../../common/utils';
import { Operations } from '../../authorization';
/**
- * Parameters for deleting all comments of a case or sub case.
+ * Parameters for deleting all comments of a case.
*/
export interface DeleteAllArgs {
/**
* The case ID to delete all attachments for
*/
caseID: string;
- /**
- * If specified the caseID will be ignored and this value will be used to find a sub case for deleting all the attachments
- */
- subCaseID?: string;
}
/**
- * Parameters for deleting a single attachment of a case or sub case.
+ * Parameters for deleting a single attachment of a case.
*/
export interface DeleteArgs {
/**
@@ -46,19 +37,15 @@ export interface DeleteArgs {
* The attachment ID to delete
*/
attachmentID: string;
- /**
- * If specified the caseID will be ignored and this value will be used to find a sub case for deleting the attachment
- */
- subCaseID?: string;
}
/**
- * Delete all comments for a case or sub case.
+ * Delete all comments for a case.
*
* @ignore
*/
export async function deleteAll(
- { caseID, subCaseID }: DeleteAllArgs,
+ { caseID }: DeleteAllArgs,
clientArgs: CasesClientArgs
): Promise {
const {
@@ -72,17 +59,13 @@ export async function deleteAll(
} = clientArgs;
try {
- checkEnabledCaseConnectorOrThrow(subCaseID);
-
- const id = subCaseID ?? caseID;
- const comments = await caseService.getCommentsByAssociation({
+ const comments = await caseService.getAllCaseComments({
unsecuredSavedObjectsClient,
- id,
- associationType: subCaseID ? AssociationType.subCase : AssociationType.case,
+ id: caseID,
});
if (comments.total <= 0) {
- throw Boom.notFound(`No comments found for ${id}.`);
+ throw Boom.notFound(`No comments found for ${caseID}.`);
}
await authorization.ensureAuthorized({
@@ -107,7 +90,6 @@ export async function deleteAll(
await userActionService.bulkCreateAttachmentDeletion({
unsecuredSavedObjectsClient,
caseId: caseID,
- subCaseId: subCaseID,
attachments: comments.saved_objects.map((comment) => ({
id: comment.id,
owner: comment.attributes.owner,
@@ -117,7 +99,7 @@ export async function deleteAll(
});
} catch (error) {
throw createCaseError({
- message: `Failed to delete all comments case id: ${caseID} sub case id: ${subCaseID}: ${error}`,
+ message: `Failed to delete all comments case id: ${caseID}: ${error}`,
error,
logger,
});
@@ -130,7 +112,7 @@ export async function deleteAll(
* @ignore
*/
export async function deleteComment(
- { caseID, attachmentID, subCaseID }: DeleteArgs,
+ { caseID, attachmentID }: DeleteArgs,
clientArgs: CasesClientArgs
) {
const {
@@ -143,8 +125,6 @@ export async function deleteComment(
} = clientArgs;
try {
- checkEnabledCaseConnectorOrThrow(subCaseID);
-
const myComment = await attachmentService.get({
unsecuredSavedObjectsClient,
attachmentId: attachmentID,
@@ -159,8 +139,8 @@ export async function deleteComment(
operation: Operations.deleteComment,
});
- const type = subCaseID ? SUB_CASE_SAVED_OBJECT : CASE_SAVED_OBJECT;
- const id = subCaseID ?? caseID;
+ const type = CASE_SAVED_OBJECT;
+ const id = caseID;
const caseRef = myComment.references.find((c) => c.type === type);
if (caseRef == null || (caseRef != null && caseRef.id !== id)) {
@@ -177,7 +157,6 @@ export async function deleteComment(
action: Actions.delete,
unsecuredSavedObjectsClient,
caseId: id,
- subCaseId: subCaseID,
attachmentId: attachmentID,
payload: { attachment: { ...myComment.attributes } },
user,
@@ -185,7 +164,7 @@ export async function deleteComment(
});
} catch (error) {
throw createCaseError({
- message: `Failed to delete comment: ${caseID} comment id: ${attachmentID} sub case id: ${subCaseID}: ${error}`,
+ message: `Failed to delete comment: ${caseID} comment id: ${attachmentID}: ${error}`,
error,
logger,
});
diff --git a/x-pack/plugins/cases/server/client/attachments/get.ts b/x-pack/plugins/cases/server/client/attachments/get.ts
index 264b3d9f11b85..b9ac0f2eea000 100644
--- a/x-pack/plugins/cases/server/client/attachments/get.ts
+++ b/x-pack/plugins/cases/server/client/attachments/get.ts
@@ -4,25 +4,20 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
-import Boom from '@hapi/boom';
-import { SavedObject, SavedObjectsFindResponse } from 'kibana/server';
+import { SavedObject } from 'kibana/server';
import {
AlertResponse,
AllCommentsResponse,
AllCommentsResponseRt,
- AssociationType,
AttributesTypeAlerts,
- CommentAttributes,
CommentResponse,
CommentResponseRt,
CommentsResponse,
CommentsResponseRt,
FindQueryParams,
} from '../../../common/api';
-import { ENABLE_CASE_CONNECTOR } from '../../../common/constants';
import {
- checkEnabledCaseConnectorOrThrow,
defaultSortField,
transformComments,
flattenCommentSavedObject,
@@ -59,14 +54,6 @@ export interface GetAllArgs {
* The case ID to retrieve all attachments for
*/
caseID: string;
- /**
- * Optionally include the attachments associated with a sub case
- */
- includeSubCaseComments?: boolean;
- /**
- * If included the case ID will be ignored and the attachments will be retrieved from the specified ID of the sub case
- */
- subCaseID?: string;
}
export interface GetArgs {
@@ -122,7 +109,6 @@ export const getAllAlertsAttachToCase = async (
const theCase = await casesClient.cases.get({
id: caseId,
includeComments: false,
- includeSubCaseComments: false,
});
const { filter: authorizationFilter, ensureSavedObjectsAreAuthorized } =
@@ -163,13 +149,10 @@ export async function find(
const { unsecuredSavedObjectsClient, caseService, logger, authorization } = clientArgs;
try {
- checkEnabledCaseConnectorOrThrow(queryParams?.subCaseId);
-
const { filter: authorizationFilter, ensureSavedObjectsAreAuthorized } =
await authorization.getAuthorizationFilter(Operations.findComments);
- const id = queryParams?.subCaseId ?? caseID;
- const associationType = queryParams?.subCaseId ? AssociationType.subCase : AssociationType.case;
+ const id = caseID;
const { filter, ...queryWithoutFilter } = queryParams ?? {};
// if the fields property was defined, make sure we include the 'owner' field in the response
@@ -194,7 +177,6 @@ export async function find(
...queryWithoutFilter,
fields,
},
- associationType,
}
: {
caseService,
@@ -206,10 +188,9 @@ export async function find(
sortField: 'created_at',
filter: combinedFilter,
},
- associationType,
};
- const theComments = await caseService.getCommentsByAssociation(args);
+ const theComments = await caseService.getAllCaseComments(args);
ensureSavedObjectsAreAuthorized(
theComments.saved_objects.map((comment) => ({
@@ -261,53 +242,29 @@ export async function get(
}
/**
- * Retrieves all the attachments for a case. The `includeSubCaseComments` can be used to include the sub case comments for
- * collections. If the entity is a sub case, pass in the subCaseID.
+ * Retrieves all the attachments for a case.
*
* @ignore
*/
export async function getAll(
- { caseID, includeSubCaseComments, subCaseID }: GetAllArgs,
+ { caseID }: GetAllArgs,
clientArgs: CasesClientArgs
): Promise {
const { unsecuredSavedObjectsClient, caseService, logger, authorization } = clientArgs;
try {
- let comments: SavedObjectsFindResponse;
-
- if (
- !ENABLE_CASE_CONNECTOR &&
- (subCaseID !== undefined || includeSubCaseComments !== undefined)
- ) {
- throw Boom.badRequest(
- 'The sub case id and include sub case comments fields are not supported when the case connector feature is disabled'
- );
- }
-
const { filter, ensureSavedObjectsAreAuthorized } = await authorization.getAuthorizationFilter(
Operations.getAllComments
);
- if (subCaseID) {
- comments = await caseService.getAllSubCaseComments({
- unsecuredSavedObjectsClient,
- id: subCaseID,
- options: {
- filter,
- sortField: defaultSortField,
- },
- });
- } else {
- comments = await caseService.getAllCaseComments({
- unsecuredSavedObjectsClient,
- id: caseID,
- includeSubCaseComments,
- options: {
- filter,
- sortField: defaultSortField,
- },
- });
- }
+ const comments = await caseService.getAllCaseComments({
+ unsecuredSavedObjectsClient,
+ id: caseID,
+ options: {
+ filter,
+ sortField: defaultSortField,
+ },
+ });
ensureSavedObjectsAreAuthorized(
comments.saved_objects.map((comment) => ({ id: comment.id, owner: comment.attributes.owner }))
@@ -316,7 +273,7 @@ export async function getAll(
return AllCommentsResponseRt.encode(flattenCommentSavedObjects(comments.saved_objects));
} catch (error) {
throw createCaseError({
- message: `Failed to get all comments case id: ${caseID} include sub case comments: ${includeSubCaseComments} sub case id: ${subCaseID}: ${error}`,
+ message: `Failed to get all comments case id: ${caseID}: ${error}`,
error,
logger,
});
diff --git a/x-pack/plugins/cases/server/client/attachments/update.ts b/x-pack/plugins/cases/server/client/attachments/update.ts
index bfa1d70e55636..1928057f17edf 100644
--- a/x-pack/plugins/cases/server/client/attachments/update.ts
+++ b/x-pack/plugins/cases/server/client/attachments/update.ts
@@ -11,9 +11,8 @@ import { SavedObjectsClientContract, Logger } from 'kibana/server';
import { LensServerPluginSetup } from '../../../../lens/server';
import { CommentableCase } from '../../common/models';
import { createCaseError } from '../../common/error';
-import { checkEnabledCaseConnectorOrThrow } from '../../common/utils';
import { Actions, ActionTypes, CaseResponse, CommentPatchRequest } from '../../../common/api';
-import { CASE_SAVED_OBJECT, SUB_CASE_SAVED_OBJECT } from '../../../common/constants';
+import { CASE_SAVED_OBJECT } from '../../../common/constants';
import { AttachmentService, CasesService } from '../../services';
import { CasesClientArgs } from '..';
import { decodeCommentRequest } from '../utils';
@@ -31,10 +30,6 @@ export interface UpdateArgs {
* The full attachment request with the fields updated with appropriate values
*/
updateRequest: CommentPatchRequest;
- /**
- * The ID of a sub case, if specified a sub case will be searched for to perform the attachment update instead of on a case
- */
- subCaseID?: string;
}
interface CombinedCaseParams {
@@ -44,52 +39,29 @@ interface CombinedCaseParams {
caseID: string;
logger: Logger;
lensEmbeddableFactory: LensServerPluginSetup['lensEmbeddableFactory'];
- subCaseId?: string;
}
-async function getCommentableCase({
+async function createCommentableCase({
attachmentService,
caseService,
unsecuredSavedObjectsClient,
caseID,
- subCaseId,
logger,
lensEmbeddableFactory,
}: CombinedCaseParams) {
- if (subCaseId) {
- const [caseInfo, subCase] = await Promise.all([
- caseService.getCase({
- unsecuredSavedObjectsClient,
- id: caseID,
- }),
- caseService.getSubCase({
- unsecuredSavedObjectsClient,
- id: subCaseId,
- }),
- ]);
- return new CommentableCase({
- attachmentService,
- caseService,
- collection: caseInfo,
- subCase,
- unsecuredSavedObjectsClient,
- logger,
- lensEmbeddableFactory,
- });
- } else {
- const caseInfo = await caseService.getCase({
- unsecuredSavedObjectsClient,
- id: caseID,
- });
- return new CommentableCase({
- attachmentService,
- caseService,
- collection: caseInfo,
- unsecuredSavedObjectsClient,
- logger,
- lensEmbeddableFactory,
- });
- }
+ const caseInfo = await caseService.getCase({
+ unsecuredSavedObjectsClient,
+ id: caseID,
+ });
+
+ return new CommentableCase({
+ attachmentService,
+ caseService,
+ caseInfo,
+ unsecuredSavedObjectsClient,
+ logger,
+ lensEmbeddableFactory,
+ });
}
/**
@@ -98,7 +70,7 @@ async function getCommentableCase({
* @ignore
*/
export async function update(
- { caseID, subCaseID, updateRequest: queryParams }: UpdateArgs,
+ { caseID, updateRequest: queryParams }: UpdateArgs,
clientArgs: CasesClientArgs
): Promise {
const {
@@ -113,8 +85,6 @@ export async function update(
} = clientArgs;
try {
- checkEnabledCaseConnectorOrThrow(subCaseID);
-
const {
id: queryCommentId,
version: queryCommentVersion,
@@ -123,12 +93,11 @@ export async function update(
decodeCommentRequest(queryRestAttributes);
- const commentableCase = await getCommentableCase({
+ const commentableCase = await createCommentableCase({
attachmentService,
caseService,
unsecuredSavedObjectsClient,
caseID,
- subCaseId: subCaseID,
logger,
lensEmbeddableFactory,
});
@@ -155,9 +124,7 @@ export async function update(
throw Boom.badRequest(`You cannot change the owner of the comment.`);
}
- const saveObjType = subCaseID ? SUB_CASE_SAVED_OBJECT : CASE_SAVED_OBJECT;
-
- const caseRef = myComment.references.find((c) => c.type === saveObjType);
+ const caseRef = myComment.references.find((c) => c.type === CASE_SAVED_OBJECT);
if (caseRef == null || (caseRef != null && caseRef.id !== commentableCase.id)) {
throw Boom.notFound(
`This comment ${queryCommentId} does not exist in ${commentableCase.id}).`
@@ -183,7 +150,6 @@ export async function update(
action: Actions.update,
unsecuredSavedObjectsClient,
caseId: caseID,
- subCaseId: subCaseID,
attachmentId: updatedComment.id,
payload: { attachment: queryRestAttributes },
user,
@@ -193,7 +159,7 @@ export async function update(
return await updatedCase.encode();
} catch (error) {
throw createCaseError({
- message: `Failed to patch comment case id: ${caseID} sub case id: ${subCaseID}: ${error}`,
+ message: `Failed to patch comment case id: ${caseID}: ${error}`,
error,
logger,
});
diff --git a/x-pack/plugins/cases/server/client/cases/create.ts b/x-pack/plugins/cases/server/client/cases/create.ts
index 0a82d900719e0..0101227e265a5 100644
--- a/x-pack/plugins/cases/server/client/cases/create.ts
+++ b/x-pack/plugins/cases/server/client/cases/create.ts
@@ -17,12 +17,11 @@ import {
excess,
CaseResponseRt,
CaseResponse,
- CasesClientPostRequestRt,
CasePostRequest,
- CaseType,
ActionTypes,
+ CasePostRequestRt,
} from '../../../common/api';
-import { ENABLE_CASE_CONNECTOR, MAX_TITLE_LENGTH } from '../../../common/constants';
+import { MAX_TITLE_LENGTH } from '../../../common/constants';
import { Operations } from '../../authorization';
import { createCaseError } from '../../common/error';
@@ -47,20 +46,9 @@ export const create = async (
authorization: auth,
} = clientArgs;
- // default to an individual case if the type is not defined.
- const { type = CaseType.individual, ...nonTypeCaseFields } = data;
-
- if (!ENABLE_CASE_CONNECTOR && type === CaseType.collection) {
- throw Boom.badRequest(
- 'Case type cannot be collection when the case connector feature is disabled'
- );
- }
-
const query = pipe(
- // decode with the defaulted type field
- excess(CasesClientPostRequestRt).decode({
- type,
- ...nonTypeCaseFields,
+ excess(CasePostRequestRt).decode({
+ ...data,
}),
fold(throwErrors(Boom.badRequest), identity)
);
diff --git a/x-pack/plugins/cases/server/client/cases/delete.ts b/x-pack/plugins/cases/server/client/cases/delete.ts
index 182fa78387707..d1c12219f2ef2 100644
--- a/x-pack/plugins/cases/server/client/cases/delete.ts
+++ b/x-pack/plugins/cases/server/client/cases/delete.ts
@@ -7,56 +7,13 @@
import pMap from 'p-map';
import { Boom } from '@hapi/boom';
-import { SavedObject, SavedObjectsClientContract, SavedObjectsFindResponse } from 'kibana/server';
-import { CommentAttributes, SubCaseAttributes } from '../../../common/api';
-import { ENABLE_CASE_CONNECTOR, MAX_CONCURRENT_SEARCHES } from '../../../common/constants';
+import { SavedObjectsFindResponse } from 'kibana/server';
+import { CommentAttributes } from '../../../common/api';
+import { MAX_CONCURRENT_SEARCHES } from '../../../common/constants';
import { CasesClientArgs } from '..';
import { createCaseError } from '../../common/error';
-import { AttachmentService, CasesService } from '../../services';
import { Operations, OwnerEntity } from '../../authorization';
-async function deleteSubCases({
- attachmentService,
- caseService,
- unsecuredSavedObjectsClient,
- caseIds,
-}: {
- attachmentService: AttachmentService;
- caseService: CasesService;
- unsecuredSavedObjectsClient: SavedObjectsClientContract;
- caseIds: string[];
-}) {
- const subCasesForCaseIds = await caseService.findSubCasesByCaseId({
- unsecuredSavedObjectsClient,
- ids: caseIds,
- });
-
- const subCaseIDs = subCasesForCaseIds.saved_objects.map((subCase) => subCase.id);
- const commentsForSubCases = await caseService.getAllSubCaseComments({
- unsecuredSavedObjectsClient,
- id: subCaseIDs,
- });
-
- const commentMapper = (commentSO: SavedObject) =>
- attachmentService.delete({ unsecuredSavedObjectsClient, attachmentId: commentSO.id });
-
- const subCasesMapper = (subCaseSO: SavedObject) =>
- caseService.deleteSubCase(unsecuredSavedObjectsClient, subCaseSO.id);
-
- /**
- * This shouldn't actually delete anything because
- * all the comments should be deleted when comments are deleted
- * per case ID. We also ensure that we don't too many concurrent deletions running.
- */
- await pMap(commentsForSubCases.saved_objects, commentMapper, {
- concurrency: MAX_CONCURRENT_SEARCHES,
- });
-
- await pMap(subCasesForCaseIds.saved_objects, subCasesMapper, {
- concurrency: MAX_CONCURRENT_SEARCHES,
- });
-}
-
/**
* Deletes the specified cases and their attachments.
*
@@ -134,15 +91,6 @@ export async function deleteCases(ids: string[], clientArgs: CasesClientArgs): P
concurrency: MAX_CONCURRENT_SEARCHES,
});
- if (ENABLE_CASE_CONNECTOR) {
- await deleteSubCases({
- attachmentService,
- caseService,
- unsecuredSavedObjectsClient,
- caseIds: ids,
- });
- }
-
await userActionService.bulkCreateCaseDeletion({
unsecuredSavedObjectsClient,
cases: cases.saved_objects.map((caseInfo) => ({
diff --git a/x-pack/plugins/cases/server/client/cases/find.ts b/x-pack/plugins/cases/server/client/cases/find.ts
index 4257dfce6d5e3..3e4bc47231d12 100644
--- a/x-pack/plugins/cases/server/client/cases/find.ts
+++ b/x-pack/plugins/cases/server/client/cases/find.ts
@@ -28,7 +28,7 @@ import { Operations } from '../../authorization';
import { CasesClientArgs } from '..';
/**
- * Retrieves a case and optionally its comments and sub case comments.
+ * Retrieves a case and optionally its comments.
*
* @ignore
*/
@@ -52,7 +52,6 @@ export const find = async (
reporters: queryParams.reporters,
sortByField: queryParams.sortField,
status: queryParams.status,
- caseType: queryParams.type,
owner: queryParams.owner,
};
@@ -61,7 +60,7 @@ export const find = async (
unsecuredSavedObjectsClient,
caseOptions: {
...queryParams,
- ...caseQueries.case,
+ ...caseQueries,
searchFields:
queryParams.searchFields != null
? Array.isArray(queryParams.searchFields)
@@ -70,7 +69,6 @@ export const find = async (
: queryParams.searchFields,
fields: includeFieldsRequiredForAuthentication(queryParams.fields),
},
- subCaseOptions: caseQueries.subCase,
});
ensureSavedObjectsAreAuthorized([...cases.casesMap.values()]);
@@ -81,8 +79,7 @@ export const find = async (
const statusQuery = constructQueryOptions({ ...queryArgs, status, authorizationFilter });
return caseService.findCaseStatusStats({
unsecuredSavedObjectsClient,
- caseOptions: statusQuery.case,
- subCaseOptions: statusQuery.subCase,
+ caseOptions: statusQuery,
ensureSavedObjectsAreAuthorized,
});
}),
diff --git a/x-pack/plugins/cases/server/client/cases/get.ts b/x-pack/plugins/cases/server/client/cases/get.ts
index 0d0bcc7f78270..72d5ca2708d28 100644
--- a/x-pack/plugins/cases/server/client/cases/get.ts
+++ b/x-pack/plugins/cases/server/client/cases/get.ts
@@ -28,7 +28,6 @@ import {
CasesByAlertIdRt,
CaseAttributes,
} from '../../../common/api';
-import { ENABLE_CASE_CONNECTOR } from '../../../common/constants';
import { createCaseError } from '../../common/error';
import { countAlertsForID, flattenCaseSavedObject } from '../../common/utils';
import { CasesClientArgs } from '..';
@@ -147,52 +146,24 @@ export interface GetParams {
* Whether to include the attachments for a case in the response
*/
includeComments?: boolean;
- /**
- * Whether to include the attachments for all children of a case in the response
- */
- includeSubCaseComments?: boolean;
}
/**
- * Retrieves a case and optionally its comments and sub case comments.
+ * Retrieves a case and optionally its comments.
*
* @ignore
*/
export const get = async (
- { id, includeComments, includeSubCaseComments }: GetParams,
+ { id, includeComments }: GetParams,
clientArgs: CasesClientArgs
): Promise => {
const { unsecuredSavedObjectsClient, caseService, logger, authorization } = clientArgs;
try {
- if (!ENABLE_CASE_CONNECTOR && includeSubCaseComments) {
- throw Boom.badRequest(
- 'The `includeSubCaseComments` is not supported when the case connector feature is disabled'
- );
- }
-
- let theCase: SavedObject;
- let subCaseIds: string[] = [];
- if (ENABLE_CASE_CONNECTOR) {
- const [caseInfo, subCasesForCaseId] = await Promise.all([
- caseService.getCase({
- unsecuredSavedObjectsClient,
- id,
- }),
- caseService.findSubCasesByCaseId({
- unsecuredSavedObjectsClient,
- ids: [id],
- }),
- ]);
-
- theCase = caseInfo;
- subCaseIds = subCasesForCaseId.saved_objects.map((so) => so.id);
- } else {
- theCase = await caseService.getCase({
- unsecuredSavedObjectsClient,
- id,
- });
- }
+ const theCase: SavedObject = await caseService.getCase({
+ unsecuredSavedObjectsClient,
+ id,
+ });
await authorization.ensureAuthorized({
operation: Operations.getCase,
@@ -203,7 +174,6 @@ export const get = async (
return CaseResponseRt.encode(
flattenCaseSavedObject({
savedObject: theCase,
- subCaseIds,
})
);
}
@@ -215,14 +185,12 @@ export const get = async (
sortField: 'created_at',
sortOrder: 'asc',
},
- includeSubCaseComments: ENABLE_CASE_CONNECTOR && includeSubCaseComments,
});
return CaseResponseRt.encode(
flattenCaseSavedObject({
savedObject: theCase,
comments: theComments.saved_objects,
- subCaseIds,
totalComment: theComments.total,
totalAlerts: countAlertsForID({ comments: theComments, id }),
})
@@ -233,23 +201,17 @@ export const get = async (
};
/**
- * Retrieves a case resolving its ID and optionally loading its comments and sub case comments.
+ * Retrieves a case resolving its ID and optionally loading its comments.
*
* @experimental
*/
export const resolve = async (
- { id, includeComments, includeSubCaseComments }: GetParams,
+ { id, includeComments }: GetParams,
clientArgs: CasesClientArgs
): Promise => {
const { unsecuredSavedObjectsClient, caseService, logger, authorization } = clientArgs;
try {
- if (!ENABLE_CASE_CONNECTOR && includeSubCaseComments) {
- throw Boom.badRequest(
- 'The `includeSubCaseComments` is not supported when the case connector feature is disabled'
- );
- }
-
const {
saved_object: resolvedSavedObject,
...resolveData
@@ -268,21 +230,11 @@ export const resolve = async (
],
});
- let subCaseIds: string[] = [];
- if (ENABLE_CASE_CONNECTOR) {
- const subCasesForCaseId = await caseService.findSubCasesByCaseId({
- unsecuredSavedObjectsClient,
- ids: [resolvedSavedObject.id],
- });
- subCaseIds = subCasesForCaseId.saved_objects.map((so) => so.id);
- }
-
if (!includeComments) {
return CaseResolveResponseRt.encode({
...resolveData,
case: flattenCaseSavedObject({
savedObject: resolvedSavedObject,
- subCaseIds,
}),
});
}
@@ -294,14 +246,12 @@ export const resolve = async (
sortField: 'created_at',
sortOrder: 'asc',
},
- includeSubCaseComments: ENABLE_CASE_CONNECTOR && includeSubCaseComments,
});
return CaseResolveResponseRt.encode({
...resolveData,
case: flattenCaseSavedObject({
savedObject: resolvedSavedObject,
- subCaseIds,
comments: theComments.saved_objects,
totalComment: theComments.total,
totalAlerts: countAlertsForID({ comments: theComments, id: resolvedSavedObject.id }),
diff --git a/x-pack/plugins/cases/server/client/cases/mock.ts b/x-pack/plugins/cases/server/client/cases/mock.ts
index 63170ad0957fa..69a5f2d3a587b 100644
--- a/x-pack/plugins/cases/server/client/cases/mock.ts
+++ b/x-pack/plugins/cases/server/client/cases/mock.ts
@@ -10,7 +10,6 @@ import {
CommentType,
ConnectorMappingsAttributes,
CaseUserActionsResponse,
- AssociationType,
CommentResponseAlertsType,
ConnectorTypes,
Actions,
@@ -32,7 +31,6 @@ const entity = {
};
export const comment: CommentResponse = {
- associationType: AssociationType.case,
id: 'mock-comment-1',
comment: 'Wow, good luck catching that bad meanie!',
type: CommentType.user as const,
@@ -55,7 +53,6 @@ export const comment: CommentResponse = {
};
export const isolateCommentActions: CommentResponse = {
- associationType: AssociationType.case,
id: 'mock-action-comment-1',
comment: 'Isolating this for investigation',
type: CommentType.actions as const,
@@ -87,7 +84,6 @@ export const isolateCommentActions: CommentResponse = {
};
export const releaseCommentActions: CommentResponse = {
- associationType: AssociationType.case,
id: 'mock-action-comment-1',
comment: 'Releasing this for investigation',
type: CommentType.actions as const,
@@ -119,7 +115,6 @@ export const releaseCommentActions: CommentResponse = {
};
export const isolateCommentActionsMultipleTargets: CommentResponse = {
- associationType: AssociationType.case,
id: 'mock-action-comment-1',
comment: 'Isolating this for investigation',
type: CommentType.actions as const,
@@ -155,7 +150,6 @@ export const isolateCommentActionsMultipleTargets: CommentResponse = {
};
export const commentAlert: CommentResponse = {
- associationType: AssociationType.case,
id: 'mock-comment-1',
alertId: 'alert-id-1',
index: 'alert-index-1',
@@ -191,12 +185,6 @@ export const commentAlertMultipleIds: CommentResponseAlertsType = {
owner: SECURITY_SOLUTION_OWNER,
};
-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/cases/server/client/cases/push.ts b/x-pack/plugins/cases/server/client/cases/push.ts
index 8775d89f22802..e68c67951d571 100644
--- a/x-pack/plugins/cases/server/client/cases/push.ts
+++ b/x-pack/plugins/cases/server/client/cases/push.ts
@@ -6,7 +6,7 @@
*/
import Boom from '@hapi/boom';
-import { SavedObjectsFindResponse, SavedObject } from 'kibana/server';
+import { SavedObjectsFindResponse } from 'kibana/server';
import {
ActionConnector,
@@ -14,12 +14,9 @@ import {
CaseResponse,
CaseStatuses,
ExternalServiceResponse,
- CaseType,
CasesConfigureAttributes,
- CaseAttributes,
ActionTypes,
} from '../../../common/api';
-import { ENABLE_CASE_CONNECTOR } from '../../../common/constants';
import { createIncident, getCommentContextFromAttributes } from './utils';
import { createCaseError } from '../../common/error';
@@ -30,18 +27,14 @@ import { casesConnectors } from '../../connectors';
import { getAlerts } from '../alerts/get';
/**
- * Returns true if the case should be closed based on the configuration settings and whether the case
- * is a collection. Collections are not closable because we aren't allowing their status to be changed.
- * In the future we could allow push to close all the sub cases of a collection but that's not currently supported.
+ * Returns true if the case should be closed based on the configuration settings.
*/
function shouldCloseByPush(
- configureSettings: SavedObjectsFindResponse,
- caseInfo: SavedObject
+ configureSettings: SavedObjectsFindResponse
): boolean {
return (
configureSettings.total > 0 &&
- configureSettings.saved_objects[0].attributes.closure_type === 'close-by-pushing' &&
- caseInfo.attributes.type !== CaseType.collection
+ configureSettings.saved_objects[0].attributes.closure_type === 'close-by-pushing'
);
}
@@ -88,7 +81,6 @@ export const push = async (
casesClient.cases.get({
id: caseId,
includeComments: true,
- includeSubCaseComments: ENABLE_CASE_CONNECTOR,
}),
actionsClient.get({ id: connectorId }),
casesClient.userActions.getAll({ caseId }),
@@ -99,7 +91,6 @@ export const push = async (
operation: Operations.pushCase,
});
- // We need to change the logic when we support subcases
if (theCase?.status === CaseStatuses.closed) {
throw Boom.conflict(
`The ${theCase.title} case is closed. Pushing a closed case is not allowed.`
@@ -163,7 +154,6 @@ export const push = async (
page: 1,
perPage: theCase?.totalComment ?? 0,
},
- includeSubCaseComments: ENABLE_CASE_CONNECTOR,
}),
]);
@@ -182,7 +172,7 @@ export const push = async (
external_url: externalServiceResponse.url,
};
- const shouldMarkAsClosed = shouldCloseByPush(myCaseConfigure, myCase);
+ const shouldMarkAsClosed = shouldCloseByPush(myCaseConfigure);
const [updatedCase, updatedComments] = await Promise.all([
caseService.patchCase({
diff --git a/x-pack/plugins/cases/server/client/cases/update.ts b/x-pack/plugins/cases/server/client/cases/update.ts
index 23db7f36634ce..fa8319d37efd8 100644
--- a/x-pack/plugins/cases/server/client/cases/update.ts
+++ b/x-pack/plugins/cases/server/client/cases/update.ts
@@ -5,7 +5,6 @@
* 2.0.
*/
-import pMap from 'p-map';
import Boom from '@hapi/boom';
import { pipe } from 'fp-ts/lib/pipeable';
import { fold } from 'fp-ts/lib/Either';
@@ -21,14 +20,12 @@ import {
import { nodeBuilder } from '@kbn/es-query';
import {
- AssociationType,
CasePatchRequest,
CasesPatchRequest,
CasesPatchRequestRt,
CasesResponse,
CasesResponseRt,
CaseStatuses,
- CaseType,
CommentAttributes,
CommentType,
excess,
@@ -38,9 +35,6 @@ import {
import {
CASE_COMMENT_SAVED_OBJECT,
CASE_SAVED_OBJECT,
- ENABLE_CASE_CONNECTOR,
- MAX_CONCURRENT_SEARCHES,
- SUB_CASE_SAVED_OBJECT,
MAX_TITLE_LENGTH,
} from '../../../common/constants';
@@ -51,62 +45,12 @@ import { createCaseError } from '../../common/error';
import {
createAlertUpdateRequest,
flattenCaseSavedObject,
- isCommentRequestTypeAlertOrGenAlert,
+ isCommentRequestTypeAlert,
} from '../../common/utils';
import { UpdateAlertRequest } from '../alerts/types';
import { CasesClientArgs } from '..';
import { Operations, OwnerEntity } from '../../authorization';
-/**
- * Throws an error if any of the requests attempt to update a collection style cases' status field.
- */
-function throwIfUpdateStatusOfCollection(requests: UpdateRequestWithOriginalCase[]) {
- const requestsUpdatingStatusOfCollection = requests.filter(
- ({ updateReq, originalCase }) =>
- updateReq.status !== undefined && originalCase.attributes.type === CaseType.collection
- );
-
- if (requestsUpdatingStatusOfCollection.length > 0) {
- const ids = requestsUpdatingStatusOfCollection.map(({ updateReq }) => updateReq.id);
- throw Boom.badRequest(
- `Updating the status of a collection is not allowed ids: [${ids.join(', ')}]`
- );
- }
-}
-
-/**
- * Throws an error if any of the requests attempt to update a collection style case to an individual one.
- */
-function throwIfUpdateTypeCollectionToIndividual(requests: UpdateRequestWithOriginalCase[]) {
- const requestsUpdatingTypeCollectionToInd = requests.filter(
- ({ updateReq, originalCase }) =>
- updateReq.type === CaseType.individual && originalCase.attributes.type === CaseType.collection
- );
-
- if (requestsUpdatingTypeCollectionToInd.length > 0) {
- const ids = requestsUpdatingTypeCollectionToInd.map(({ updateReq }) => updateReq.id);
- throw Boom.badRequest(
- `Converting a collection to an individual case is not allowed ids: [${ids.join(', ')}]`
- );
- }
-}
-
-/**
- * Throws an error if any of the requests attempt to update the type of a case.
- */
-function throwIfUpdateType(requests: UpdateRequestWithOriginalCase[]) {
- const requestsUpdatingType = requests.filter(({ updateReq }) => updateReq.type !== undefined);
-
- if (requestsUpdatingType.length > 0) {
- const ids = requestsUpdatingType.map(({ updateReq }) => updateReq.id);
- throw Boom.badRequest(
- `Updating the type of a case when sub cases are disabled is not allowed ids: [${ids.join(
- ', '
- )}]`
- );
- }
-}
-
/**
* Throws an error if any of the requests attempt to update the owner of a case.
*/
@@ -119,64 +63,6 @@ function throwIfUpdateOwner(requests: UpdateRequestWithOriginalCase[]) {
}
}
-/**
- * Throws an error if any of the requests attempt to update an individual style cases' type field to a collection
- * when alerts are attached to the case.
- */
-async function throwIfInvalidUpdateOfTypeWithAlerts({
- requests,
- caseService,
- unsecuredSavedObjectsClient,
-}: {
- requests: UpdateRequestWithOriginalCase[];
- caseService: CasesService;
- unsecuredSavedObjectsClient: SavedObjectsClientContract;
-}) {
- const getAlertsForID = async ({ updateReq }: UpdateRequestWithOriginalCase) => {
- const alerts = await caseService.getAllCaseComments({
- unsecuredSavedObjectsClient,
- id: updateReq.id,
- options: {
- fields: [],
- // there should never be generated alerts attached to an individual case but we'll check anyway
- filter: nodeBuilder.or([
- nodeBuilder.is(`${CASE_COMMENT_SAVED_OBJECT}.attributes.type`, CommentType.alert),
- nodeBuilder.is(
- `${CASE_COMMENT_SAVED_OBJECT}.attributes.type`,
- CommentType.generatedAlert
- ),
- ]),
- page: 1,
- perPage: 1,
- },
- });
-
- return { id: updateReq.id, alerts };
- };
-
- const requestsUpdatingTypeField = requests.filter(
- ({ updateReq }) => updateReq.type === CaseType.collection
- );
- const getAlertsMapper = async (caseToUpdate: UpdateRequestWithOriginalCase) =>
- getAlertsForID(caseToUpdate);
- // Ensuring we don't too many concurrent get running.
- const casesAlertTotals = await pMap(requestsUpdatingTypeField, getAlertsMapper, {
- concurrency: MAX_CONCURRENT_SEARCHES,
- });
-
- // grab the cases that have at least one alert comment attached to them
- const typeUpdateWithAlerts = casesAlertTotals.filter((caseInfo) => caseInfo.alerts.total > 0);
-
- if (typeUpdateWithAlerts.length > 0) {
- const ids = typeUpdateWithAlerts.map((req) => req.id);
- throw Boom.badRequest(
- `Converting a case to a collection is not allowed when it has alert comments, ids: [${ids.join(
- ', '
- )}]`
- );
- }
-}
-
/**
* Throws an error if any of the requests updates a title and the length is over MAX_TITLE_LENGTH.
*/
@@ -200,7 +86,7 @@ function throwIfTitleIsInvalid(requests: UpdateRequestWithOriginalCase[]) {
*/
function getID(
comment: SavedObject,
- type: typeof CASE_SAVED_OBJECT | typeof SUB_CASE_SAVED_OBJECT
+ type: typeof CASE_SAVED_OBJECT
): string | undefined {
return comment.references.find((ref) => ref.type === type)?.id;
}
@@ -223,80 +109,29 @@ async function getAlertComments({
return caseService.getAllCaseComments({
unsecuredSavedObjectsClient,
id: idsOfCasesToSync,
- includeSubCaseComments: true,
options: {
- filter: nodeBuilder.or([
- nodeBuilder.is(`${CASE_COMMENT_SAVED_OBJECT}.attributes.type`, CommentType.alert),
- nodeBuilder.is(`${CASE_COMMENT_SAVED_OBJECT}.attributes.type`, CommentType.generatedAlert),
- ]),
+ filter: nodeBuilder.is(`${CASE_COMMENT_SAVED_OBJECT}.attributes.type`, CommentType.alert),
},
});
}
/**
- * Returns a map of sub case IDs to their status. This uses a group of alert comments to determine which sub cases should
- * be retrieved. This is based on whether the comment is associated to a sub case.
- */
-async function getSubCasesToStatus({
- totalAlerts,
- caseService,
- unsecuredSavedObjectsClient,
-}: {
- totalAlerts: SavedObjectsFindResponse;
- caseService: CasesService;
- unsecuredSavedObjectsClient: SavedObjectsClientContract;
-}): Promise