>, to avoid gaps in detection.
+[float]
+==== Action variables
+
+When the ES query alert condition is met, the following variables are available to use inside each action:
+
+`context.title`:: A preconstructed title for the alert. Example: `alert term match alert query matched`.
+`context.message`:: A preconstructed message for the alert. Example: +
+`alert 'term match alert' is active:` +
+`- Value: 42` +
+`- Conditions Met: count greater than 4 over 5m` +
+`- Timestamp: 2020-01-01T00:00:00.000Z`
+
+`context.group`:: The name of the action group associated with the condition. Example: `query matched`.
+`context.date`:: The date, in ISO format, that the alert met the condition. Example: `2020-01-01T00:00:00.000Z`.
+`context.value`:: The value of the alert that met the condition.
+`context.conditions`:: A description of the condition. Example: `count greater than 4`.
+`context.hits`:: The most recent ES documents that matched the query. Using the https://mustache.github.io/[Mustache] template array syntax, you can iterate over these hits to get values from the ES documents into your actions.
+
+[role="screenshot"]
+image::images/alert-types-es-query-example-action-variable.png[Iterate over hits using Mustache template syntax]
+
[float]
==== Testing your query
diff --git a/docs/user/alerting/stack-alerts/index-threshold.asciidoc b/docs/user/alerting/stack-alerts/index-threshold.asciidoc
index 424320aea3adc..6b45f69401c4a 100644
--- a/docs/user/alerting/stack-alerts/index-threshold.asciidoc
+++ b/docs/user/alerting/stack-alerts/index-threshold.asciidoc
@@ -31,6 +31,23 @@ If data is available and all clauses have been defined, a preview chart will ren
[role="screenshot"]
image::user/alerting/images/alert-types-index-threshold-preview.png[Five clauses define the condition to detect]
+[float]
+==== Action variables
+
+When the index threshold alert condition is met, the following variables are available to use inside each action:
+
+`context.title`:: A preconstructed title for the alert. Example: `alert kibana sites - high egress met threshold`.
+`context.message`:: A preconstructed message for the alert. Example: +
+`alert 'kibana sites - high egress' is active for group 'threshold met':` +
+`- Value: 42` +
+`- Conditions Met: count greater than 4 over 5m` +
+`- Timestamp: 2020-01-01T00:00:00.000Z`
+
+`context.group`:: The name of the action group associated with the threshold condition. Example: `threshold met`.
+`context.date`:: The date, in ISO format, that the alert met the threshold condition. Example: `2020-01-01T00:00:00.000Z`.
+`context.value`:: The value for the alert that met the threshold condition.
+`context.conditions`:: A description of the threshold condition. Example: `count greater than 4`
+
[float]
==== Example
diff --git a/package.json b/package.json
index 3c628de532019..9cf6f97fcafbf 100644
--- a/package.json
+++ b/package.json
@@ -74,11 +74,11 @@
"**/cross-fetch/node-fetch": "^2.6.1",
"**/deepmerge": "^4.2.2",
"**/fast-deep-equal": "^3.1.1",
- "**/graphql-toolkit/lodash": "^4.17.15",
+ "**/graphql-toolkit/lodash": "^4.17.21",
"**/hoist-non-react-statics": "^3.3.2",
"**/isomorphic-fetch/node-fetch": "^2.6.1",
"**/istanbul-instrumenter-loader/schema-utils": "1.0.0",
- "**/load-grunt-config/lodash": "^4.17.20",
+ "**/load-grunt-config/lodash": "^4.17.21",
"**/minimist": "^1.2.5",
"**/node-jose/node-forge": "^0.10.0",
"**/prismjs": "1.22.0",
@@ -233,7 +233,7 @@
"json-stringify-safe": "5.0.1",
"jsonwebtoken": "^8.5.1",
"load-json-file": "^6.2.0",
- "lodash": "^4.17.20",
+ "lodash": "^4.17.21",
"lru-cache": "^4.1.5",
"markdown-it": "^10.0.0",
"md5": "^2.1.0",
@@ -390,7 +390,6 @@
"@storybook/addon-essentials": "^6.0.26",
"@storybook/addon-knobs": "^6.0.26",
"@storybook/addon-storyshots": "^6.0.26",
- "@storybook/addons": "^6.0.16",
"@storybook/components": "^6.0.26",
"@storybook/core": "^6.0.26",
"@storybook/core-events": "^6.0.26",
diff --git a/src/dev/ci_setup/setup.sh b/src/dev/ci_setup/setup.sh
index c4559029e5607..f9c1e67c0540d 100755
--- a/src/dev/ci_setup/setup.sh
+++ b/src/dev/ci_setup/setup.sh
@@ -21,6 +21,10 @@ cp "src/dev/ci_setup/.bazelrc-ci" "$HOME/.bazelrc";
echo "# Appended by src/dev/ci_setup/setup.sh" >> "$HOME/.bazelrc"
echo "build --remote_header=x-buildbuddy-api-key=$KIBANA_BUILDBUDDY_CI_API_KEY" >> "$HOME/.bazelrc"
+if [[ "$BUILD_TS_REFS_CACHE_ENABLE" != "true" ]]; then
+ export BUILD_TS_REFS_CACHE_ENABLE=false
+fi
+
###
### install dependencies
###
diff --git a/src/dev/typescript/build_ts_refs_cli.ts b/src/dev/typescript/build_ts_refs_cli.ts
index fc8911a251773..a073e58623278 100644
--- a/src/dev/typescript/build_ts_refs_cli.ts
+++ b/src/dev/typescript/build_ts_refs_cli.ts
@@ -23,7 +23,7 @@ export async function runBuildRefsCli() {
async ({ log, flags }) => {
const outDirs = getOutputsDeep(REF_CONFIG_PATHS);
- const cacheEnabled = process.env.BUILD_TS_REFS_CACHE_ENABLE === 'true' || !!flags.cache;
+ const cacheEnabled = process.env.BUILD_TS_REFS_CACHE_ENABLE !== 'false' && !!flags.cache;
const doCapture = process.env.BUILD_TS_REFS_CACHE_CAPTURE === 'true';
const doClean = !!flags.clean || doCapture;
const doInitCache = cacheEnabled && !doClean;
@@ -62,6 +62,9 @@ export async function runBuildRefsCli() {
description: 'Build TypeScript projects',
flags: {
boolean: ['clean', 'cache'],
+ default: {
+ cache: true,
+ },
},
log: {
defaultLevel: 'debug',
diff --git a/src/dev/typescript/ref_output_cache/ref_output_cache.ts b/src/dev/typescript/ref_output_cache/ref_output_cache.ts
index 342470ce0c6e3..6f51243e47555 100644
--- a/src/dev/typescript/ref_output_cache/ref_output_cache.ts
+++ b/src/dev/typescript/ref_output_cache/ref_output_cache.ts
@@ -132,7 +132,7 @@ export class RefOutputCache {
this.log.debug(`[${relative}] clearing outDir and replacing with cache`);
await del(outDir);
await unzip(Path.resolve(tmpDir, cacheName), outDir);
- await Fs.writeFile(Path.resolve(outDir, OUTDIR_MERGE_BASE_FILENAME), archive.sha);
+ await Fs.writeFile(Path.resolve(outDir, OUTDIR_MERGE_BASE_FILENAME), this.mergeBase);
});
}
diff --git a/x-pack/plugins/case/server/client/cases/mock.ts b/x-pack/plugins/case/server/client/cases/mock.ts
index 809c4ad1ea1bd..490519187f49e 100644
--- a/x-pack/plugins/case/server/client/cases/mock.ts
+++ b/x-pack/plugins/case/server/client/cases/mock.ts
@@ -11,6 +11,7 @@ import {
ConnectorMappingsAttributes,
CaseUserActionsResponse,
AssociationType,
+ CommentResponseAlertsType,
} from '../../../common/api';
import { BasicParams } from './types';
@@ -76,6 +77,20 @@ export const commentAlert: CommentResponse = {
version: 'WzEsMV0=',
};
+export const commentAlertMultipleIds: CommentResponseAlertsType = {
+ ...commentAlert,
+ id: 'mock-comment-2',
+ alertId: ['alert-id-1', 'alert-id-2'],
+ index: 'alert-index-1',
+ type: CommentType.alert as const,
+};
+
+export const commentGeneratedAlert: CommentResponseAlertsType = {
+ ...commentAlertMultipleIds,
+ id: 'mock-comment-3',
+ type: CommentType.generatedAlert as const,
+};
+
export const defaultPipes = ['informationCreated'];
export const basicParams: BasicParams = {
description: 'a description',
diff --git a/x-pack/plugins/case/server/client/cases/types.ts b/x-pack/plugins/case/server/client/cases/types.ts
index f1d56e7132bd1..2dd2caf9fe73a 100644
--- a/x-pack/plugins/case/server/client/cases/types.ts
+++ b/x-pack/plugins/case/server/client/cases/types.ts
@@ -72,7 +72,7 @@ export interface TransformFieldsArgs {
export interface ExternalServiceComment {
comment: string;
- commentId: string;
+ commentId?: string;
}
export interface MapIncident {
diff --git a/x-pack/plugins/case/server/client/cases/utils.test.ts b/x-pack/plugins/case/server/client/cases/utils.test.ts
index 361d0fb561afd..44e7a682aa7ed 100644
--- a/x-pack/plugins/case/server/client/cases/utils.test.ts
+++ b/x-pack/plugins/case/server/client/cases/utils.test.ts
@@ -17,6 +17,8 @@ import {
basicParams,
userActions,
commentAlert,
+ commentAlertMultipleIds,
+ commentGeneratedAlert,
} from './mock';
import {
@@ -48,7 +50,7 @@ describe('utils', () => {
{
actionType: 'overwrite',
key: 'short_description',
- pipes: ['informationCreated'],
+ pipes: [],
value: 'a title',
},
{
@@ -71,7 +73,7 @@ describe('utils', () => {
{
actionType: 'overwrite',
key: 'short_description',
- pipes: ['myTestPipe'],
+ pipes: [],
value: 'a title',
},
{
@@ -98,7 +100,7 @@ describe('utils', () => {
});
expect(res).toEqual({
- short_description: 'a title (created at 2020-03-13T08:34:53.450Z by Elastic User)',
+ short_description: 'a title',
description: 'a description (created at 2020-03-13T08:34:53.450Z by Elastic User)',
});
});
@@ -122,13 +124,13 @@ describe('utils', () => {
},
fields,
currentIncident: {
- short_description: 'first title (created at 2020-03-13T08:34:53.450Z by Elastic User)',
+ short_description: 'first title',
description: 'first description (created at 2020-03-13T08:34:53.450Z by Elastic User)',
},
});
expect(res).toEqual({
- short_description: 'a title (updated at 2020-03-15T08:34:53.450Z by Another User)',
+ short_description: 'a title',
description:
'first description (created at 2020-03-13T08:34:53.450Z by Elastic User) \r\na description (updated at 2020-03-15T08:34:53.450Z by Another User)',
});
@@ -168,7 +170,7 @@ describe('utils', () => {
});
expect(res).toEqual({
- short_description: 'a title (created at 2020-03-13T08:34:53.450Z by elastic)',
+ short_description: 'a title',
description: 'a description (created at 2020-03-13T08:34:53.450Z by elastic)',
});
});
@@ -190,7 +192,7 @@ describe('utils', () => {
});
expect(res).toEqual({
- short_description: 'a title (updated at 2020-03-15T08:34:53.450Z by anotherUser)',
+ short_description: 'a title',
description: 'a description (updated at 2020-03-15T08:34:53.450Z by anotherUser)',
});
});
@@ -448,8 +450,7 @@ describe('utils', () => {
labels: ['defacement'],
issueType: null,
parent: null,
- short_description:
- 'Super Bad Security Issue (created at 2019-11-25T21:54:48.952Z by elastic)',
+ short_description: 'Super Bad Security Issue',
description:
'This is a brand new case of a bad meanie defacing data (created at 2019-11-25T21:54:48.952Z by elastic)',
externalId: null,
@@ -504,7 +505,7 @@ describe('utils', () => {
expect(res.comments).toEqual([]);
});
- it('it creates comments of type alert correctly', async () => {
+ it('it adds the total alert comments correctly', async () => {
const res = await createIncident({
actionsClient: actionsMock,
theCase: {
@@ -512,7 +513,9 @@ describe('utils', () => {
comments: [
{ ...commentObj, id: 'comment-user-1' },
{ ...commentAlert, id: 'comment-alert-1' },
- { ...commentAlert, id: 'comment-alert-2' },
+ {
+ ...commentAlertMultipleIds,
+ },
],
},
// Remove second push
@@ -536,14 +539,36 @@ describe('utils', () => {
commentId: 'comment-user-1',
},
{
- comment:
- 'Alert with ids alert-id-1 added to case (added at 2019-11-25T21:55:00.177Z by elastic)',
- commentId: 'comment-alert-1',
+ comment: 'Elastic Security Alerts attached to the case: 3',
},
+ ]);
+ });
+
+ it('it removes alerts correctly', async () => {
+ const res = await createIncident({
+ actionsClient: actionsMock,
+ theCase: {
+ ...theCase,
+ comments: [
+ { ...commentObj, id: 'comment-user-1' },
+ commentAlertMultipleIds,
+ commentGeneratedAlert,
+ ],
+ },
+ userActions,
+ connector,
+ mappings,
+ alerts: [],
+ });
+
+ expect(res.comments).toEqual([
{
comment:
- 'Alert with ids alert-id-1 added to case (added at 2019-11-25T21:55:00.177Z by elastic)',
- commentId: 'comment-alert-2',
+ 'Wow, good luck catching that bad meanie! (added at 2019-11-25T21:55:00.177Z by elastic)',
+ commentId: 'comment-user-1',
+ },
+ {
+ comment: 'Elastic Security Alerts attached to the case: 4',
},
]);
});
@@ -578,8 +603,7 @@ describe('utils', () => {
description:
'fun description \r\nThis is a brand new case of a bad meanie defacing data (updated at 2019-11-25T21:54:48.952Z by elastic)',
externalId: 'external-id',
- short_description:
- 'Super Bad Security Issue (updated at 2019-11-25T21:54:48.952Z by elastic)',
+ short_description: 'Super Bad Security Issue',
},
comments: [],
});
diff --git a/x-pack/plugins/case/server/client/cases/utils.ts b/x-pack/plugins/case/server/client/cases/utils.ts
index fda4142bf77c7..a5013d9b93982 100644
--- a/x-pack/plugins/case/server/client/cases/utils.ts
+++ b/x-pack/plugins/case/server/client/cases/utils.ts
@@ -40,6 +40,15 @@ import {
} from './types';
import { getAlertIds } from '../../routes/api/utils';
+interface CreateIncidentArgs {
+ actionsClient: ActionsClient;
+ theCase: CaseResponse;
+ userActions: CaseUserActionsResponse;
+ connector: ActionConnector;
+ mappings: ConnectorMappingsAttributes[];
+ alerts: CaseClientGetAlertsResponse;
+}
+
export const getLatestPushInfo = (
connectorId: string,
userActions: CaseUserActionsResponse
@@ -75,14 +84,13 @@ const getCommentContent = (comment: CommentResponse): string => {
return '';
};
-interface CreateIncidentArgs {
- actionsClient: ActionsClient;
- theCase: CaseResponse;
- userActions: CaseUserActionsResponse;
- connector: ActionConnector;
- mappings: ConnectorMappingsAttributes[];
- alerts: CaseClientGetAlertsResponse;
-}
+const countAlerts = (comments: CaseResponse['comments']): number =>
+ comments?.reduce((total, comment) => {
+ if (comment.type === CommentType.alert || comment.type === CommentType.generatedAlert) {
+ return total + (Array.isArray(comment.alertId) ? comment.alertId.length : 1);
+ }
+ return total;
+ }, 0) ?? 0;
export const createIncident = async ({
actionsClient,
@@ -152,22 +160,34 @@ export const createIncident = async ({
userActions
.slice(latestPushInfo?.index ?? 0)
.filter(
- (action, index) =>
- Array.isArray(action.action_field) && action.action_field[0] === 'comment'
+ (action) => Array.isArray(action.action_field) && action.action_field[0] === 'comment'
)
.map((action) => action.comment_id)
);
- const commentsToBeUpdated = caseComments?.filter((comment) =>
- commentsIdsToBeUpdated.has(comment.id)
+
+ const commentsToBeUpdated = caseComments?.filter(
+ (comment) =>
+ // We push only user's comments
+ comment.type === CommentType.user && commentsIdsToBeUpdated.has(comment.id)
);
+ const totalAlerts = countAlerts(caseComments);
+
let comments: ExternalServiceComment[] = [];
+
if (commentsToBeUpdated && Array.isArray(commentsToBeUpdated) && commentsToBeUpdated.length > 0) {
const commentsMapping = mappings.find((m) => m.source === 'comments');
if (commentsMapping?.action_type !== 'nothing') {
comments = transformComments(commentsToBeUpdated, ['informationAdded']);
}
}
+
+ if (totalAlerts > 0) {
+ comments.push({
+ comment: `Elastic Security Alerts attached to the case: ${totalAlerts}`,
+ });
+ }
+
return { incident, comments };
};
@@ -247,7 +267,13 @@ export const prepareFieldsForTransformation = ({
key: mapping.target,
value: params[mapping.source] ?? '',
actionType: mapping.action_type,
- pipes: mapping.action_type === 'append' ? [...defaultPipes, 'append'] : defaultPipes,
+ pipes:
+ // Do not transform titles
+ mapping.source !== 'title'
+ ? mapping.action_type === 'append'
+ ? [...defaultPipes, 'append']
+ : defaultPipes
+ : [],
},
]
: acc,
diff --git a/x-pack/plugins/case/server/routes/api/cases/push_case.test.ts b/x-pack/plugins/case/server/routes/api/cases/push_case.test.ts
index bf398d1ffcf40..c8501130493ba 100644
--- a/x-pack/plugins/case/server/routes/api/cases/push_case.test.ts
+++ b/x-pack/plugins/case/server/routes/api/cases/push_case.test.ts
@@ -170,7 +170,7 @@ describe('Push case', () => {
parent: null,
priority: 'High',
labels: ['LOLBins'],
- summary: 'Another bad one (created at 2019-11-25T22:32:17.947Z by elastic)',
+ summary: 'Another bad one',
description:
'Oh no, a bad meanie going LOLBins all over the place! (created at 2019-11-25T22:32:17.947Z by elastic)',
externalId: null,
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item_content/boosts/boost_item.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/boost_item.tsx
similarity index 73%
rename from x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item_content/boosts/boost_item.tsx
rename to x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/boost_item.tsx
index e5a76bc586b80..641628c32659c 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item_content/boosts/boost_item.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/boost_item.tsx
@@ -9,26 +9,29 @@ import React, { useMemo } from 'react';
import { EuiFlexItem, EuiAccordion, EuiFlexGroup, EuiHideFor } from '@elastic/eui';
-import { BoostIcon } from '../../../boost_icon';
-import { BOOST_TYPE_TO_DISPLAY_MAP } from '../../../constants';
-import { Boost } from '../../../types';
-import { ValueBadge } from '../../value_badge';
+import { BoostIcon } from '../boost_icon';
+import { BOOST_TYPE_TO_DISPLAY_MAP } from '../constants';
+import { Boost } from '../types';
+import { ValueBadge } from '../value_badge';
+import { BoostItemContent } from './boost_item_content';
import { getBoostSummary } from './get_boost_summary';
interface Props {
boost: Boost;
id: string;
+ index: number;
+ name: string;
}
-export const BoostItem: React.FC = ({ id, boost }) => {
+export const BoostItem: React.FC = ({ id, boost, index, name }) => {
const summary = useMemo(() => getBoostSummary(boost), [boost]);
return (
@@ -48,6 +51,8 @@ export const BoostItem: React.FC = ({ id, boost }) => {
}
paddingSize="s"
- />
+ >
+
+
);
};
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/boost_item_content/boost_item_content.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/boost_item_content/boost_item_content.test.tsx
new file mode 100644
index 0000000000000..3296155fdce5d
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/boost_item_content/boost_item_content.test.tsx
@@ -0,0 +1,99 @@
+/*
+ * 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 { setMockActions } from '../../../../../__mocks__/kea.mock';
+
+import React from 'react';
+
+import { shallow } from 'enzyme';
+
+import { EuiButton, EuiRange } from '@elastic/eui';
+
+import { BoostType } from '../../types';
+
+import { BoostItemContent } from './boost_item_content';
+import { FunctionalBoostForm } from './functional_boost_form';
+import { ProximityBoostForm } from './proximity_boost_form';
+import { ValueBoostForm } from './value_boost_form';
+
+describe('BoostItemContent', () => {
+ const actions = {
+ updateBoostFactor: jest.fn(),
+ deleteBoost: jest.fn(),
+ };
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ setMockActions(actions);
+ });
+
+ it('renders a value boost form if the provided boost is "value" boost', () => {
+ const boost = {
+ factor: 2,
+ type: 'value' as BoostType,
+ };
+
+ const wrapper = shallow();
+
+ expect(wrapper.find(ValueBoostForm).exists()).toBe(true);
+ expect(wrapper.find(FunctionalBoostForm).exists()).toBe(false);
+ expect(wrapper.find(ProximityBoostForm).exists()).toBe(false);
+ });
+
+ it('renders a functional boost form if the provided boost is "functional" boost', () => {
+ const boost = {
+ factor: 10,
+ type: 'functional' as BoostType,
+ };
+
+ const wrapper = shallow();
+
+ expect(wrapper.find(ValueBoostForm).exists()).toBe(false);
+ expect(wrapper.find(FunctionalBoostForm).exists()).toBe(true);
+ expect(wrapper.find(ProximityBoostForm).exists()).toBe(false);
+ });
+
+ it('renders a proximity boost form if the provided boost is "proximity" boost', () => {
+ const boost = {
+ factor: 8,
+ type: 'proximity' as BoostType,
+ };
+
+ const wrapper = shallow();
+
+ expect(wrapper.find(ValueBoostForm).exists()).toBe(false);
+ expect(wrapper.find(FunctionalBoostForm).exists()).toBe(false);
+ expect(wrapper.find(ProximityBoostForm).exists()).toBe(true);
+ });
+
+ it("renders an impact slider that can be used to update the boost's 'factor'", () => {
+ const boost = {
+ factor: 8,
+ type: 'proximity' as BoostType,
+ };
+
+ const wrapper = shallow();
+ const impactSlider = wrapper.find(EuiRange);
+ expect(impactSlider.prop('value')).toBe(8);
+
+ impactSlider.simulate('change', { target: { value: '2' } });
+
+ expect(actions.updateBoostFactor).toHaveBeenCalledWith('foo', 3, 2);
+ });
+
+ it("will delete the current boost if the 'Delete Boost' button is clicked", () => {
+ const boost = {
+ factor: 8,
+ type: 'proximity' as BoostType,
+ };
+
+ const wrapper = shallow();
+ wrapper.find(EuiButton).simulate('click');
+
+ expect(actions.deleteBoost).toHaveBeenCalledWith('foo', 3);
+ });
+});
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/boost_item_content/boost_item_content.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/boost_item_content/boost_item_content.tsx
new file mode 100644
index 0000000000000..7a19564543c81
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/boost_item_content/boost_item_content.tsx
@@ -0,0 +1,83 @@
+/*
+ * 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 { useActions } from 'kea';
+
+import { EuiButton, EuiFormRow, EuiPanel, EuiRange, EuiSpacer } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+
+import { RelevanceTuningLogic } from '../..';
+import { Boost, BoostType } from '../../types';
+
+import { FunctionalBoostForm } from './functional_boost_form';
+import { ProximityBoostForm } from './proximity_boost_form';
+import { ValueBoostForm } from './value_boost_form';
+
+interface Props {
+ boost: Boost;
+ index: number;
+ name: string;
+}
+
+export const BoostItemContent: React.FC = ({ boost, index, name }) => {
+ const { deleteBoost, updateBoostFactor } = useActions(RelevanceTuningLogic);
+ const { type } = boost;
+
+ const getBoostForm = () => {
+ switch (type) {
+ case BoostType.Value:
+ return ;
+ case BoostType.Functional:
+ return ;
+ case BoostType.Proximity:
+ return ;
+ }
+ };
+
+ return (
+
+ {getBoostForm()}
+
+
+
+ updateBoostFactor(
+ name,
+ index,
+ parseFloat((e as React.ChangeEvent).target.value)
+ )
+ }
+ showInput
+ compressed
+ fullWidth
+ />
+
+ deleteBoost(name, index)}>
+ {i18n.translate(
+ 'xpack.enterpriseSearch.appSearch.engine.relevanceTuning.boosts.deleteBoostButtonLabel',
+ {
+ defaultMessage: 'Delete Boost',
+ }
+ )}
+
+
+ );
+};
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/boost_item_content/functional_boost_form.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/boost_item_content/functional_boost_form.test.tsx
new file mode 100644
index 0000000000000..11a224a71d7f8
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/boost_item_content/functional_boost_form.test.tsx
@@ -0,0 +1,68 @@
+/*
+ * 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 { setMockActions } from '../../../../../__mocks__/kea.mock';
+
+import React from 'react';
+
+import { shallow, ShallowWrapper } from 'enzyme';
+
+import { EuiSelect } from '@elastic/eui';
+
+import { Boost, BoostOperation, BoostType, FunctionalBoostFunction } from '../../types';
+
+import { FunctionalBoostForm } from './functional_boost_form';
+
+describe('FunctionalBoostForm', () => {
+ const boost: Boost = {
+ factor: 2,
+ type: 'functional' as BoostType,
+ function: 'logarithmic' as FunctionalBoostFunction,
+ operation: 'multiply' as BoostOperation,
+ };
+
+ const actions = {
+ updateBoostSelectOption: jest.fn(),
+ };
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ setMockActions(actions);
+ });
+
+ const functionSelect = (wrapper: ShallowWrapper) => wrapper.find(EuiSelect).at(0);
+ const operationSelect = (wrapper: ShallowWrapper) => wrapper.find(EuiSelect).at(1);
+
+ it('renders select boxes with values from the provided boost selected', () => {
+ const wrapper = shallow();
+ expect(functionSelect(wrapper).prop('value')).toEqual('logarithmic');
+ expect(operationSelect(wrapper).prop('value')).toEqual('multiply');
+ });
+
+ it('will update state when a user makes a selection', () => {
+ const wrapper = shallow();
+
+ functionSelect(wrapper).simulate('change', {
+ target: {
+ value: 'exponential',
+ },
+ });
+ expect(actions.updateBoostSelectOption).toHaveBeenCalledWith(
+ 'foo',
+ 3,
+ 'function',
+ 'exponential'
+ );
+
+ operationSelect(wrapper).simulate('change', {
+ target: {
+ value: 'add',
+ },
+ });
+ expect(actions.updateBoostSelectOption).toHaveBeenCalledWith('foo', 3, 'operation', 'add');
+ });
+});
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/boost_item_content/functional_boost_form.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/boost_item_content/functional_boost_form.tsx
new file mode 100644
index 0000000000000..d677fe5cbc069
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/boost_item_content/functional_boost_form.tsx
@@ -0,0 +1,89 @@
+/*
+ * 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 { useActions } from 'kea';
+
+import { EuiFormRow, EuiSelect } from '@elastic/eui';
+
+import { i18n } from '@kbn/i18n';
+
+import { RelevanceTuningLogic } from '../..';
+import {
+ BOOST_OPERATION_DISPLAY_MAP,
+ FUNCTIONAL_BOOST_FUNCTION_DISPLAY_MAP,
+} from '../../constants';
+import {
+ Boost,
+ BoostFunction,
+ BoostOperation,
+ BoostType,
+ FunctionalBoostFunction,
+} from '../../types';
+
+interface Props {
+ boost: Boost;
+ index: number;
+ name: string;
+}
+
+const functionOptions = Object.values(FunctionalBoostFunction).map((boostFunction) => ({
+ value: boostFunction,
+ text: FUNCTIONAL_BOOST_FUNCTION_DISPLAY_MAP[boostFunction as FunctionalBoostFunction],
+}));
+
+const operationOptions = Object.values(BoostOperation).map((boostOperation) => ({
+ value: boostOperation,
+ text: BOOST_OPERATION_DISPLAY_MAP[boostOperation as BoostOperation],
+}));
+
+export const FunctionalBoostForm: React.FC = ({ boost, index, name }) => {
+ const { updateBoostSelectOption } = useActions(RelevanceTuningLogic);
+ return (
+ <>
+
+
+ updateBoostSelectOption(name, index, 'function', e.target.value as BoostFunction)
+ }
+ fullWidth
+ />
+
+
+
+ updateBoostSelectOption(name, index, 'operation', e.target.value as BoostOperation)
+ }
+ fullWidth
+ />
+
+ >
+ );
+};
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/boost_item_content/index.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/boost_item_content/index.ts
new file mode 100644
index 0000000000000..1a13c486ca523
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/boost_item_content/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 { BoostItemContent } from './boost_item_content';
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/boost_item_content/proximity_boost_form.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/boost_item_content/proximity_boost_form.test.tsx
new file mode 100644
index 0000000000000..6abbcc3d98862
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/boost_item_content/proximity_boost_form.test.tsx
@@ -0,0 +1,102 @@
+/*
+ * 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 { setMockActions } from '../../../../../__mocks__/kea.mock';
+
+import React from 'react';
+
+import { shallow, ShallowWrapper } from 'enzyme';
+
+import { EuiFieldText, EuiSelect } from '@elastic/eui';
+
+import { Boost, BoostType, ProximityBoostFunction } from '../../types';
+
+import { ProximityBoostForm } from './proximity_boost_form';
+
+describe('ProximityBoostForm', () => {
+ const boost: Boost = {
+ factor: 2,
+ type: 'proximity' as BoostType,
+ function: 'linear' as ProximityBoostFunction,
+ center: '2',
+ };
+
+ const actions = {
+ updateBoostSelectOption: jest.fn(),
+ updateBoostCenter: jest.fn(),
+ };
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ setMockActions(actions);
+ });
+
+ const functionSelect = (wrapper: ShallowWrapper) => wrapper.find(EuiSelect);
+ const centerInput = (wrapper: ShallowWrapper) => wrapper.find(EuiFieldText);
+
+ it('renders input with values from the provided boost', () => {
+ const wrapper = shallow();
+ expect(functionSelect(wrapper).prop('value')).toEqual('linear');
+ expect(centerInput(wrapper).prop('defaultValue')).toEqual('2');
+ });
+
+ describe('various boost values', () => {
+ const renderWithBoostValues = (boostValues: {
+ center?: Boost['center'];
+ function?: Boost['function'];
+ }) => {
+ return shallow(
+
+ );
+ };
+
+ it('will set the center value as a string if the value is a number', () => {
+ const wrapper = renderWithBoostValues({ center: 0 });
+ expect(centerInput(wrapper).prop('defaultValue')).toEqual('0');
+ });
+
+ it('will set the center value as an empty string if the value is undefined', () => {
+ const wrapper = renderWithBoostValues({ center: undefined });
+ expect(centerInput(wrapper).prop('defaultValue')).toEqual('');
+ });
+
+ it('will set the function to Guaussian if it is not already set', () => {
+ const wrapper = renderWithBoostValues({ function: undefined });
+ expect(functionSelect(wrapper).prop('value')).toEqual('gaussian');
+ });
+ });
+
+ it('will update state when a user enters input', () => {
+ const wrapper = shallow();
+
+ functionSelect(wrapper).simulate('change', {
+ target: {
+ value: 'exponential',
+ },
+ });
+ expect(actions.updateBoostSelectOption).toHaveBeenCalledWith(
+ 'foo',
+ 3,
+ 'function',
+ 'exponential'
+ );
+
+ centerInput(wrapper).simulate('change', {
+ target: {
+ value: '5',
+ },
+ });
+ expect(actions.updateBoostCenter).toHaveBeenCalledWith('foo', 3, '5');
+ });
+});
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/boost_item_content/proximity_boost_form.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/boost_item_content/proximity_boost_form.tsx
new file mode 100644
index 0000000000000..f01f060bfcee6
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/boost_item_content/proximity_boost_form.tsx
@@ -0,0 +1,79 @@
+/*
+ * 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 { useActions } from 'kea';
+
+import { EuiFieldText, EuiFormRow, EuiSelect } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+
+import { RelevanceTuningLogic } from '../..';
+import { PROXIMITY_BOOST_FUNCTION_DISPLAY_MAP } from '../../constants';
+import { Boost, BoostType, ProximityBoostFunction } from '../../types';
+
+interface Props {
+ boost: Boost;
+ index: number;
+ name: string;
+}
+
+export const ProximityBoostForm: React.FC = ({ boost, index, name }) => {
+ const { updateBoostSelectOption, updateBoostCenter } = useActions(RelevanceTuningLogic);
+
+ const currentBoostCenter = boost.center !== undefined ? boost.center.toString() : '';
+ const currentBoostFunction = boost.function || ProximityBoostFunction.Gaussian;
+
+ const functionOptions = Object.values(ProximityBoostFunction).map((boostFunction) => ({
+ value: boostFunction,
+ text: PROXIMITY_BOOST_FUNCTION_DISPLAY_MAP[boostFunction as ProximityBoostFunction],
+ }));
+
+ return (
+ <>
+
+
+ updateBoostSelectOption(
+ name,
+ index,
+ 'function',
+ e.target.value as ProximityBoostFunction
+ )
+ }
+ fullWidth
+ />
+
+
+ updateBoostCenter(name, index, e.target.value)}
+ fullWidth
+ />
+
+ >
+ );
+};
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/boost_item_content/value_boost_form.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/boost_item_content/value_boost_form.test.tsx
new file mode 100644
index 0000000000000..447ca8e178349
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/boost_item_content/value_boost_form.test.tsx
@@ -0,0 +1,88 @@
+/*
+ * 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 { setMockActions } from '../../../../../__mocks__/kea.mock';
+
+import React from 'react';
+
+import { shallow, ShallowWrapper } from 'enzyme';
+
+import { EuiButton, EuiButtonIcon, EuiFieldText } from '@elastic/eui';
+
+import { Boost, BoostType } from '../../types';
+
+import { ValueBoostForm } from './value_boost_form';
+
+describe('ValueBoostForm', () => {
+ const boost: Boost = {
+ factor: 2,
+ type: 'value' as BoostType,
+ value: ['bar', '', 'baz'],
+ };
+
+ const actions = {
+ removeBoostValue: jest.fn(),
+ updateBoostValue: jest.fn(),
+ addBoostValue: jest.fn(),
+ };
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ setMockActions(actions);
+ });
+
+ const valueInput = (wrapper: ShallowWrapper, index: number) =>
+ wrapper.find(EuiFieldText).at(index);
+ const removeButton = (wrapper: ShallowWrapper, index: number) =>
+ wrapper.find(EuiButtonIcon).at(index);
+ const addButton = (wrapper: ShallowWrapper) => wrapper.find(EuiButton);
+
+ it('renders a text input for each value from the boost', () => {
+ const wrapper = shallow();
+ expect(valueInput(wrapper, 0).prop('value')).toEqual('bar');
+ expect(valueInput(wrapper, 1).prop('value')).toEqual('');
+ expect(valueInput(wrapper, 2).prop('value')).toEqual('baz');
+ });
+
+ it('renders a single empty text box if the boost has no value', () => {
+ const wrapper = shallow(
+
+ );
+ expect(valueInput(wrapper, 0).prop('value')).toEqual('');
+ });
+
+ it('updates the corresponding value in state whenever a user changes the value in a text input', () => {
+ const wrapper = shallow();
+
+ valueInput(wrapper, 2).simulate('change', { target: { value: 'new value' } });
+
+ expect(actions.updateBoostValue).toHaveBeenCalledWith('foo', 3, 2, 'new value');
+ });
+
+ it('deletes a boost value when the Remove Value button is clicked', () => {
+ const wrapper = shallow();
+
+ removeButton(wrapper, 2).simulate('click');
+
+ expect(actions.removeBoostValue).toHaveBeenCalledWith('foo', 3, 2);
+ });
+
+ it('adds a new boost value when the Add Value is button clicked', () => {
+ const wrapper = shallow();
+
+ addButton(wrapper).simulate('click');
+
+ expect(actions.addBoostValue).toHaveBeenCalledWith('foo', 3);
+ });
+});
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/boost_item_content/value_boost_form.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/boost_item_content/value_boost_form.tsx
new file mode 100644
index 0000000000000..15d19a9741d0a
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/boost_item_content/value_boost_form.tsx
@@ -0,0 +1,79 @@
+/*
+ * 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 { useActions } from 'kea';
+
+import {
+ EuiButton,
+ EuiButtonIcon,
+ EuiFieldText,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiSpacer,
+} from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+
+import { RelevanceTuningLogic } from '../..';
+import { Boost } from '../../types';
+
+interface Props {
+ boost: Boost;
+ index: number;
+ name: string;
+}
+
+export const ValueBoostForm: React.FC = ({ boost, index, name }) => {
+ const { updateBoostValue, removeBoostValue, addBoostValue } = useActions(RelevanceTuningLogic);
+ const values = boost.value || [''];
+
+ return (
+ <>
+ {values.map((value, valueIndex) => (
+
+
+ updateBoostValue(name, index, valueIndex, e.target.value)}
+ aria-label={i18n.translate(
+ 'xpack.enterpriseSearch.appSearch.engine.relevanceTuning.boosts.value.valueNameAriaLabel',
+ {
+ defaultMessage: 'Value name',
+ }
+ )}
+ autoFocus
+ />
+
+
+ removeBoostValue(name, index, valueIndex)}
+ aria-label={i18n.translate(
+ 'xpack.enterpriseSearch.appSearch.engine.relevanceTuning.boosts.value.removeValueAriaLabel',
+ {
+ defaultMessage: 'Remove value',
+ }
+ )}
+ />
+
+
+ ))}
+
+ addBoostValue(name, index)}>
+ {i18n.translate(
+ 'xpack.enterpriseSearch.appSearch.engine.relevanceTuning.boosts.value.addValueButtonLabel',
+ {
+ defaultMessage: 'Add Value',
+ }
+ )}
+
+ >
+ );
+};
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item_content/boosts/boosts.scss b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/boosts.scss
similarity index 94%
rename from x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item_content/boosts/boosts.scss
rename to x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/boosts.scss
index 53b3c233301b0..0e9b2b1035b36 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item_content/boosts/boosts.scss
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/boosts.scss
@@ -3,7 +3,7 @@
min-width: $euiSizeXXL * 4;
}
- &__itemContent {
+ &__itemButton {
width: 100%;
}
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item_content/boosts/boosts.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/boosts.test.tsx
similarity index 65%
rename from x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item_content/boosts/boosts.test.tsx
rename to x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/boosts.test.tsx
index b313e16c0bda1..75c22d2ae9473 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item_content/boosts/boosts.test.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/boosts.test.tsx
@@ -5,7 +5,7 @@
* 2.0.
*/
-import { setMockActions } from '../../../../../../__mocks__/kea.mock';
+import { setMockActions } from '../../../../__mocks__/kea.mock';
import React from 'react';
@@ -13,8 +13,11 @@ import { shallow } from 'enzyme';
import { EuiSuperSelect } from '@elastic/eui';
-import { SchemaTypes } from '../../../../../../shared/types';
+import { SchemaTypes } from '../../../../shared/types';
+import { BoostType } from '../types';
+
+import { BoostItem } from './boost_item';
import { Boosts } from './boosts';
describe('Boosts', () => {
@@ -68,4 +71,33 @@ describe('Boosts', () => {
expect(actions.addBoost).toHaveBeenCalledWith('foo', 'functional');
});
+
+ it('will render a list of boosts', () => {
+ const boost1 = {
+ factor: 2,
+ type: 'value' as BoostType,
+ };
+ const boost2 = {
+ factor: 10,
+ type: 'functional' as BoostType,
+ };
+ const boost3 = {
+ factor: 8,
+ type: 'proximity' as BoostType,
+ };
+
+ const wrapper = shallow(
+
+ );
+
+ const boostItems = wrapper.find(BoostItem);
+ expect(boostItems.at(0).prop('boost')).toEqual(boost1);
+ expect(boostItems.at(1).prop('boost')).toEqual(boost2);
+ expect(boostItems.at(2).prop('boost')).toEqual(boost3);
+ });
});
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item_content/boosts/boosts.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/boosts.tsx
similarity index 86%
rename from x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item_content/boosts/boosts.tsx
rename to x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/boosts.tsx
index 1ad27346d2630..d6d43ea7beab0 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item_content/boosts/boosts.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/boosts.tsx
@@ -13,13 +13,13 @@ import { EuiFlexGroup, EuiFlexItem, EuiPanel, EuiTitle, EuiSuperSelect } from '@
import { i18n } from '@kbn/i18n';
-import { TEXT } from '../../../../../../shared/constants/field_types';
-import { SchemaTypes } from '../../../../../../shared/types';
+import { TEXT } from '../../../../shared/constants/field_types';
+import { SchemaTypes } from '../../../../shared/types';
-import { BoostIcon } from '../../../boost_icon';
-import { FUNCTIONAL_DISPLAY, PROXIMITY_DISPLAY, VALUE_DISPLAY } from '../../../constants';
-import { RelevanceTuningLogic } from '../../../relevance_tuning_logic';
-import { Boost, BoostType } from '../../../types';
+import { BoostIcon } from '../boost_icon';
+import { FUNCTIONAL_DISPLAY, PROXIMITY_DISPLAY, VALUE_DISPLAY } from '../constants';
+import { RelevanceTuningLogic } from '../relevance_tuning_logic';
+import { Boost, BoostType } from '../types';
import { BoostItem } from './boost_item';
@@ -111,7 +111,13 @@ export const Boosts: React.FC = ({ name, type, boosts = [] }) => {
{boosts.map((boost, index) => (
-
+
))}
);
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item_content/boosts/get_boost_summary.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/get_boost_summary.test.ts
similarity index 80%
rename from x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item_content/boosts/get_boost_summary.test.ts
rename to x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/get_boost_summary.test.ts
index f6852569213a6..4d78fe8f06739 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item_content/boosts/get_boost_summary.test.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/get_boost_summary.test.ts
@@ -5,7 +5,7 @@
* 2.0.
*/
-import { Boost, BoostFunction, BoostType, BoostOperation } from '../../../types';
+import { Boost, BoostFunction, BoostType, BoostOperation, FunctionalBoostFunction } from '../types';
import { getBoostSummary } from './get_boost_summary';
@@ -29,6 +29,15 @@ describe('getBoostSummary', () => {
})
).toEqual('');
});
+
+ it('filters out empty values', () => {
+ expect(
+ getBoostSummary({
+ ...boost,
+ value: [' ', '', 'foo', '', 'bar'],
+ })
+ ).toEqual('foo,bar');
+ });
});
describe('when the boost type is "proximity"', () => {
@@ -55,18 +64,20 @@ describe('getBoostSummary', () => {
describe('when the boost type is "functional"', () => {
const boost: Boost = {
type: BoostType.Functional,
- function: BoostFunction.Gaussian,
+ function: FunctionalBoostFunction.Logarithmic,
operation: BoostOperation.Add,
factor: 5,
};
it('creates a summary that is name of the function and operation', () => {
- expect(getBoostSummary(boost)).toEqual('gaussian add');
+ expect(getBoostSummary(boost)).toEqual('logarithmic add');
});
it('prints empty if function or operation is missing', () => {
expect(getBoostSummary({ ...boost, function: undefined })).toEqual(BoostOperation.Add);
- expect(getBoostSummary({ ...boost, operation: undefined })).toEqual(BoostFunction.Gaussian);
+ expect(getBoostSummary({ ...boost, operation: undefined })).toEqual(
+ FunctionalBoostFunction.Logarithmic
+ );
expect(getBoostSummary({ ...boost, function: undefined, operation: undefined })).toEqual('');
});
});
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item_content/boosts/get_boost_summary.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/get_boost_summary.ts
similarity index 80%
rename from x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item_content/boosts/get_boost_summary.ts
rename to x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/get_boost_summary.ts
index f3922ebb0fffe..71b1a6136cf65 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item_content/boosts/get_boost_summary.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/get_boost_summary.ts
@@ -5,11 +5,11 @@
* 2.0.
*/
-import { Boost, BoostType } from '../../../types';
+import { Boost, BoostType } from '../types';
export const getBoostSummary = (boost: Boost): string => {
if (boost.type === BoostType.Value) {
- return !boost.value ? '' : boost.value.join(',');
+ return !boost.value ? '' : boost.value.filter((v) => v.trim() !== '').join(',');
} else if (boost.type === BoostType.Proximity) {
return boost.function || '';
} else {
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item_content/boosts/index.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/index.ts
similarity index 100%
rename from x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item_content/boosts/index.ts
rename to x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/index.ts
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/constants.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/constants.ts
index 9fdbb8e979b31..8131a6a3a57c6 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/constants.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/constants.ts
@@ -7,7 +7,12 @@
import { i18n } from '@kbn/i18n';
-import { BoostType } from './types';
+import {
+ BoostOperation,
+ BoostType,
+ FunctionalBoostFunction,
+ ProximityBoostFunction,
+} from './types';
export const FIELD_FILTER_CUTOFF = 10;
@@ -59,6 +64,7 @@ export const VALUE_DISPLAY = i18n.translate(
defaultMessage: 'Value',
}
);
+
export const BOOST_TYPE_TO_DISPLAY_MAP = {
[BoostType.Proximity]: PROXIMITY_DISPLAY,
[BoostType.Functional]: FUNCTIONAL_DISPLAY,
@@ -70,3 +76,62 @@ export const BOOST_TYPE_TO_ICON_MAP = {
[BoostType.Functional]: 'tokenFunction',
[BoostType.Proximity]: 'tokenGeo',
};
+
+export const ADD_DISPLAY = i18n.translate(
+ 'xpack.enterpriseSearch.appSearch.engine.relevanceTuning.boosts.addOperationDropDownOptionLabel',
+ {
+ defaultMessage: 'Add',
+ }
+);
+
+export const MULTIPLY_DISPLAY = i18n.translate(
+ 'xpack.enterpriseSearch.appSearch.engine.relevanceTuning.boosts.multiplyOperationDropDownOptionLabel',
+ {
+ defaultMessage: 'Multiply',
+ }
+);
+
+export const BOOST_OPERATION_DISPLAY_MAP = {
+ [BoostOperation.Add]: ADD_DISPLAY,
+ [BoostOperation.Multiply]: MULTIPLY_DISPLAY,
+};
+
+export const LOGARITHMIC_DISPLAY = i18n.translate(
+ 'xpack.enterpriseSearch.appSearch.engine.relevanceTuning.boosts.logarithmicBoostFunctionDropDownOptionLabel',
+ {
+ defaultMessage: 'Logarithmic',
+ }
+);
+
+export const GAUSSIAN_DISPLAY = i18n.translate(
+ 'xpack.enterpriseSearch.appSearch.engine.relevanceTuning.boosts.gaussianFunctionDropDownOptionLabel',
+ {
+ defaultMessage: 'Gaussian',
+ }
+);
+
+export const EXPONENTIAL_DISPLAY = i18n.translate(
+ 'xpack.enterpriseSearch.appSearch.engine.relevanceTuning.boosts.exponentialFunctionDropDownOptionLabel',
+ {
+ defaultMessage: 'Exponential',
+ }
+);
+
+export const LINEAR_DISPLAY = i18n.translate(
+ 'xpack.enterpriseSearch.appSearch.engine.relevanceTuning.boosts.linearFunctionDropDownOptionLabel',
+ {
+ defaultMessage: 'Linear',
+ }
+);
+
+export const PROXIMITY_BOOST_FUNCTION_DISPLAY_MAP = {
+ [ProximityBoostFunction.Gaussian]: GAUSSIAN_DISPLAY,
+ [ProximityBoostFunction.Exponential]: EXPONENTIAL_DISPLAY,
+ [ProximityBoostFunction.Linear]: LINEAR_DISPLAY,
+};
+
+export const FUNCTIONAL_BOOST_FUNCTION_DISPLAY_MAP = {
+ [FunctionalBoostFunction.Logarithmic]: LOGARITHMIC_DISPLAY,
+ [FunctionalBoostFunction.Exponential]: EXPONENTIAL_DISPLAY,
+ [FunctionalBoostFunction.Linear]: LINEAR_DISPLAY,
+};
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_form.scss b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_form.scss
index 749fca6f79811..9795564da04d5 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_form.scss
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_form.scss
@@ -17,4 +17,10 @@
}
}
}
+
+ .relevanceTuningAccordionItem {
+ border: none;
+ border-top: $euiBorderThin;
+ border-radius: 0;
+ }
}
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item.test.tsx
index 6043e7ae65b26..674bb91929a76 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item.test.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item.test.tsx
@@ -13,9 +13,9 @@ import { SchemaTypes } from '../../../../shared/types';
import { BoostIcon } from '../boost_icon';
import { Boost, BoostType, SearchField } from '../types';
+import { ValueBadge } from '../value_badge';
import { RelevanceTuningItem } from './relevance_tuning_item';
-import { ValueBadge } from './value_badge';
describe('RelevanceTuningItem', () => {
const props = {
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item.tsx
index 38cec4825cfe7..f7f4c64622fa6 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item.tsx
@@ -13,8 +13,7 @@ import { SchemaTypes } from '../../../../shared/types';
import { BoostIcon } from '../boost_icon';
import { Boost, SearchField } from '../types';
-
-import { ValueBadge } from './value_badge';
+import { ValueBadge } from '../value_badge';
interface Props {
name: string;
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item_content/relevance_tuning_item_content.scss b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item_content/relevance_tuning_item_content.scss
deleted file mode 100644
index 63718a95551fa..0000000000000
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item_content/relevance_tuning_item_content.scss
+++ /dev/null
@@ -1,6 +0,0 @@
-.relevanceTuningForm {
- &__itemContent {
- border: none;
- border-top: $euiBorderThin;
- }
-}
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item_content/relevance_tuning_item_content.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item_content/relevance_tuning_item_content.tsx
index 29ab559485d77..e780a4de07252 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item_content/relevance_tuning_item_content.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item_content/relevance_tuning_item_content.tsx
@@ -11,14 +11,12 @@ import { EuiPanel } from '@elastic/eui';
import { SchemaTypes } from '../../../../../shared/types';
+import { Boosts } from '../../boosts';
import { Boost, SearchField } from '../../types';
-import { Boosts } from './boosts';
import { TextSearchToggle } from './text_search_toggle';
import { WeightSlider } from './weight_slider';
-import './relevance_tuning_item_content.scss';
-
interface Props {
name: string;
type: SchemaTypes;
@@ -29,7 +27,7 @@ interface Props {
export const RelevanceTuningItemContent: React.FC = ({ name, type, boosts, field }) => {
return (
<>
-
+
{field && }
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_logic.test.ts
index a7ee6f9755fc4..8ce07dc699cbb 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_logic.test.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_logic.test.ts
@@ -9,7 +9,7 @@ import { LogicMounter, mockFlashMessageHelpers, mockHttpValues } from '../../../
import { nextTick } from '@kbn/test/jest';
-import { Boost, BoostFunction, BoostOperation, BoostType } from './types';
+import { Boost, BoostOperation, BoostType, FunctionalBoostFunction } from './types';
import { RelevanceTuningLogic } from './';
@@ -1053,14 +1053,14 @@ describe('RelevanceTuningLogic', () => {
'foo',
1,
'function',
- BoostFunction.Exponential
+ FunctionalBoostFunction.Exponential
);
expect(RelevanceTuningLogic.actions.setSearchSettings).toHaveBeenCalledWith(
searchSettingsWithBoost({
factor: 1,
type: BoostType.Functional,
- function: BoostFunction.Exponential,
+ function: FunctionalBoostFunction.Exponential,
})
);
});
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/types.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/types.ts
index 95bd33aac5b9f..16da5868da681 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/types.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/types.ts
@@ -11,15 +11,23 @@ export enum BoostType {
Proximity = 'proximity',
}
-export enum BoostFunction {
+export enum FunctionalBoostFunction {
+ Logarithmic = 'logarithmic',
+ Exponential = 'exponential',
+ Linear = 'linear',
+}
+
+export enum ProximityBoostFunction {
Gaussian = 'gaussian',
Exponential = 'exponential',
Linear = 'linear',
}
+export type BoostFunction = FunctionalBoostFunction | ProximityBoostFunction;
+
export enum BoostOperation {
Add = 'add',
- Multiple = 'multiply',
+ Multiply = 'multiply',
}
export interface BaseBoost {
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/value_badge.scss b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/value_badge.scss
similarity index 100%
rename from x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/value_badge.scss
rename to x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/value_badge.scss
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/value_badge.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/value_badge.tsx
similarity index 100%
rename from x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/value_badge.tsx
rename to x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/value_badge.tsx
diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx
index 59b64de369745..1d75e873f9b18 100644
--- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx
+++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx
@@ -187,272 +187,275 @@ export function LayerPanel(
]);
return (
-
-
-
-
-
-
-
- {layerDatasource && (
-
- {
- const newState =
- typeof updater === 'function' ? updater(layerDatasourceState) : updater;
- // Look for removed columns
- const nextPublicAPI = layerDatasource.getPublicAPI({
- state: newState,
- layerId,
- });
- const nextTable = new Set(
- nextPublicAPI.getTableSpec().map(({ columnId }) => columnId)
- );
- const removed = datasourcePublicAPI
- .getTableSpec()
- .map(({ columnId }) => columnId)
- .filter((columnId) => !nextTable.has(columnId));
- let nextVisState = props.visualizationState;
- removed.forEach((columnId) => {
- nextVisState = activeVisualization.removeDimension({
- layerId,
- columnId,
- prevState: nextVisState,
- });
- });
-
- props.updateAll(datasourceId, newState, nextVisState);
- },
+ <>
+
+
+
+
+
- )}
-
-
-
-
- {groups.map((group, groupIndex) => {
- const isMissing = !isEmptyLayer && group.required && group.accessors.length === 0;
- return (
- {group.groupLabel}}
- labelType="legend"
- key={group.groupId}
- isInvalid={isMissing}
- error={
- isMissing ? (
-
- {i18n.translate('xpack.lens.editorFrame.requiredDimensionWarningLabel', {
- defaultMessage: 'Required dimension',
- })}
-
- ) : (
- []
- )
- }
- >
- <>
-
- {group.accessors.map((accessorConfig, accessorIndex) => {
- const { columnId } = accessorConfig;
- return (
-
-
- {
- setActiveDimension({
- isNew: false,
- activeGroup: group,
- activeId: id,
- });
- }}
- onRemoveClick={(id: string) => {
- trackUiEvent('indexpattern_dimension_removed');
- props.updateAll(
- datasourceId,
- layerDatasource.removeColumn({
- layerId,
- columnId: id,
- prevState: layerDatasourceState,
- }),
- activeVisualization.removeDimension({
- layerId,
- columnId: id,
- prevState: props.visualizationState,
- })
- );
- removeButtonRef(id);
- }}
- >
-
-
-
-
- );
- })}
-
- {group.supportsMoreColumns ? (
- {
- setActiveDimension({
- activeGroup: group,
- activeId: id,
- isNew: true,
- });
- }}
- onDrop={onDrop}
- />
- ) : null}
- >
-
- );
- })}
- {
- if (layerDatasource.updateStateOnCloseDimension) {
- const newState = layerDatasource.updateStateOnCloseDimension({
- state: layerDatasourceState,
- layerId,
- columnId: activeId!,
- });
- if (newState) {
- props.updateDatasource(datasourceId, newState);
- }
- }
- setActiveDimension(initialActiveDimensionState);
- }}
- panel={
- <>
- {activeGroup && activeId && (
+ {layerDatasource && (
+
{
- if (shouldReplaceDimension || shouldRemoveDimension) {
- props.updateAll(
- datasourceId,
- newState,
- shouldRemoveDimension
- ? activeVisualization.removeDimension({
- layerId,
- columnId: activeId,
- prevState: props.visualizationState,
- })
- : activeVisualization.setDimension({
- layerId,
- groupId: activeGroup.groupId,
- columnId: activeId,
- prevState: props.visualizationState,
- })
- );
- } else {
- props.updateDatasource(datasourceId, newState);
- }
- setActiveDimension({
- ...activeDimension,
- isNew: false,
+ layerId,
+ state: layerDatasourceState,
+ activeData: props.framePublicAPI.activeData,
+ setState: (updater: unknown) => {
+ const newState =
+ typeof updater === 'function' ? updater(layerDatasourceState) : updater;
+ // Look for removed columns
+ const nextPublicAPI = layerDatasource.getPublicAPI({
+ state: newState,
+ layerId,
+ });
+ const nextTable = new Set(
+ nextPublicAPI.getTableSpec().map(({ columnId }) => columnId)
+ );
+ const removed = datasourcePublicAPI
+ .getTableSpec()
+ .map(({ columnId }) => columnId)
+ .filter((columnId) => !nextTable.has(columnId));
+ let nextVisState = props.visualizationState;
+ removed.forEach((columnId) => {
+ nextVisState = activeVisualization.removeDimension({
+ layerId,
+ columnId,
+ prevState: nextVisState,
+ });
});
+
+ props.updateAll(datasourceId, newState, nextVisState);
},
}}
/>
- )}
- {activeGroup &&
- activeId &&
- !activeDimension.isNew &&
- activeVisualization.renderDimensionEditor &&
- activeGroup?.enableDimensionEditor && (
-
-
+ )}
+
+
+
+
+ {groups.map((group, groupIndex) => {
+ const isMissing = !isEmptyLayer && group.required && group.accessors.length === 0;
+ return (
+ {group.groupLabel}
}
+ labelType="legend"
+ key={group.groupId}
+ isInvalid={isMissing}
+ error={
+ isMissing ? (
+
+ {i18n.translate('xpack.lens.editorFrame.requiredDimensionWarningLabel', {
+ defaultMessage: 'Required dimension',
+ })}
+
+ ) : (
+ []
+ )
+ }
+ >
+ <>
+
+ {group.accessors.map((accessorConfig, accessorIndex) => {
+ const { columnId } = accessorConfig;
+
+ return (
+
+
+ {
+ setActiveDimension({
+ isNew: false,
+ activeGroup: group,
+ activeId: id,
+ });
+ }}
+ onRemoveClick={(id: string) => {
+ trackUiEvent('indexpattern_dimension_removed');
+ props.updateAll(
+ datasourceId,
+ layerDatasource.removeColumn({
+ layerId,
+ columnId: id,
+ prevState: layerDatasourceState,
+ }),
+ activeVisualization.removeDimension({
+ layerId,
+ columnId: id,
+ prevState: props.visualizationState,
+ })
+ );
+ removeButtonRef(id);
+ }}
+ >
+
+
+
+
+ );
+ })}
+
+ {group.supportsMoreColumns ? (
+ {
+ setActiveDimension({
+ activeGroup: group,
+ activeId: id,
+ isNew: true,
+ });
}}
+ onDrop={onDrop}
/>
-
- )}
- >
- }
- />
+ ) : null}
+ >
+
+ );
+ })}
-
+
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+ {
+ if (layerDatasource.updateStateOnCloseDimension) {
+ const newState = layerDatasource.updateStateOnCloseDimension({
+ state: layerDatasourceState,
+ layerId,
+ columnId: activeId!,
+ });
+ if (newState) {
+ props.updateDatasource(datasourceId, newState);
+ }
+ }
+ setActiveDimension(initialActiveDimensionState);
+ }}
+ panel={
+ <>
+ {activeGroup && activeId && (
+ {
+ if (shouldReplaceDimension || shouldRemoveDimension) {
+ props.updateAll(
+ datasourceId,
+ newState,
+ shouldRemoveDimension
+ ? activeVisualization.removeDimension({
+ layerId,
+ columnId: activeId,
+ prevState: props.visualizationState,
+ })
+ : activeVisualization.setDimension({
+ layerId,
+ groupId: activeGroup.groupId,
+ columnId: activeId,
+ prevState: props.visualizationState,
+ })
+ );
+ } else {
+ props.updateDatasource(datasourceId, newState);
+ }
+ setActiveDimension({
+ ...activeDimension,
+ isNew: false,
+ });
+ },
+ }}
+ />
+ )}
+ {activeGroup &&
+ activeId &&
+ !activeDimension.isNew &&
+ activeVisualization.renderDimensionEditor &&
+ activeGroup?.enableDimensionEditor && (
+
+
+
+ )}
+ >
+ }
+ />
+ >
);
}
diff --git a/x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix.tsx b/x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix.tsx
index 740d127e1b08d..344464bfe9590 100644
--- a/x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix.tsx
+++ b/x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix.tsx
@@ -14,6 +14,7 @@ import {
EuiFlexGroup,
EuiFlexItem,
EuiFormRow,
+ EuiIconTip,
EuiSelect,
EuiSpacer,
EuiSwitch,
@@ -57,6 +58,24 @@ const TOGGLE_OFF = i18n.translate('xpack.ml.splom.toggleOff', {
const sampleSizeOptions = [100, 1000, 10000].map((d) => ({ value: d, text: '' + d }));
+interface OptionLabelWithIconTipProps {
+ label: string;
+ tooltip: string;
+}
+
+const OptionLabelWithIconTip: FC = ({ label, tooltip }) => (
+ <>
+ {label}
+
+ >
+);
+
export interface ScatterplotMatrixProps {
fields: string[];
index: string;
@@ -252,9 +271,16 @@ export const ScatterplotMatrix: FC = ({
+ }
display="rowCompressed"
fullWidth
>
@@ -276,9 +302,16 @@ export const ScatterplotMatrix: FC = ({
+ }
display="rowCompressed"
fullWidth
>
@@ -292,9 +325,17 @@ export const ScatterplotMatrix: FC = ({
+ }
display="rowCompressed"
fullWidth
>
@@ -310,9 +351,16 @@ export const ScatterplotMatrix: FC = ({
{resultsField !== undefined && legendType === undefined && (
+ }
display="rowCompressed"
fullWidth
>
diff --git a/x-pack/plugins/security_solution/public/cases/components/connectors/servicenow/translations.ts b/x-pack/plugins/security_solution/public/cases/components/connectors/servicenow/translations.ts
index 0867dc41eeb78..77c263385df0a 100644
--- a/x-pack/plugins/security_solution/public/cases/components/connectors/servicenow/translations.ts
+++ b/x-pack/plugins/security_solution/public/cases/components/connectors/servicenow/translations.ts
@@ -87,7 +87,7 @@ export const PRIORITY = i18n.translate(
export const ALERT_FIELDS_LABEL = i18n.translate(
'xpack.securitySolution.components.connectors.serviceNow.alertFieldsTitle',
{
- defaultMessage: 'Fields associated with alerts',
+ defaultMessage: 'Select Observables to push',
}
);
diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_delete/delete_action_modal.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_delete/delete_action_modal.tsx
index d82f0769c8b74..fb846d041bd17 100644
--- a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_delete/delete_action_modal.tsx
+++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_delete/delete_action_modal.tsx
@@ -123,6 +123,7 @@ export const DeleteActionModal: FC = ({
return (
= ({ closeModal, items, startAndC
return (
= ({
}) => {
return (
<>
-
+
-
+
{stepName}
-
+
= ({
-
+
= (item) => {
- return {item.name};
+ return (
+
+ {item.name}
+
+ );
};
interface Props {
diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/legend.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/legend.tsx
index c746a5cc63a9b..9a66b586d1d56 100644
--- a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/legend.tsx
+++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/legend.tsx
@@ -9,18 +9,25 @@ import React from 'react';
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { IWaterfallContext } from '../context/waterfall_chart';
import { WaterfallChartProps } from './waterfall_chart';
+import { euiStyled } from '../../../../../../../../../src/plugins/kibana_react/common';
interface LegendProps {
items: Required['legendItems'];
render: Required['renderLegendItem'];
}
+const StyledFlexItem = euiStyled(EuiFlexItem)`
+ margin-right: ${(props) => props.theme.eui.paddingSizes.m};
+ max-width: 7%;
+ min-width: 160px;
+`;
+
export const Legend: React.FC = ({ items, render }) => {
return (
-
- {items.map((item, index) => {
- return {render(item, index)};
- })}
+
+ {items.map((item, index) => (
+ {render(item, index)}
+ ))}
);
};
diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/waterfall_chart.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/waterfall_chart.tsx
index 59990b29db5db..119c907f76ca1 100644
--- a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/waterfall_chart.tsx
+++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/waterfall_chart.tsx
@@ -120,8 +120,12 @@ export const WaterfallChart = ({
-
-
+
+
{shouldRenderSidebar && }
) {
@@ -188,6 +191,40 @@ export function defineRoutes(core: CoreSetup) {
}
);
+ router.put(
+ {
+ path: '/api/alerts_fixture/{id}/reset_task_status',
+ validate: {
+ params: schema.object({
+ id: schema.string(),
+ }),
+ body: schema.object({
+ status: schema.string(),
+ }),
+ },
+ },
+ async (
+ context: RequestHandlerContext,
+ req: KibanaRequest,
+ res: KibanaResponseFactory
+ ): Promise> => {
+ const { id } = req.params;
+ const { status } = req.body;
+
+ const [{ savedObjects }] = await core.getStartServices();
+ const savedObjectsWithTasksAndAlerts = await savedObjects.getScopedClient(req, {
+ includedHiddenTypes: ['task', 'alert'],
+ });
+ const alert = await savedObjectsWithTasksAndAlerts.get('alert', id);
+ const result = await savedObjectsWithTasksAndAlerts.update(
+ 'task',
+ alert.attributes.scheduledTaskId!,
+ { status }
+ );
+ return res.ok({ body: result });
+ }
+ );
+
router.get(
{
path: '/api/alerts_fixture/api_keys_pending_invalidation',
diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/index.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/index.ts
index c1f65fab3669e..e8cc8ea699e17 100644
--- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/index.ts
+++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/index.ts
@@ -11,8 +11,7 @@ import { setupSpacesAndUsers, tearDown } from '..';
// eslint-disable-next-line import/no-default-export
export default function alertingTests({ loadTestFile, getService }: FtrProviderContext) {
describe('Alerts', () => {
- // FLAKY: https://github.com/elastic/kibana/issues/86952
- describe.skip('legacy alerts', () => {
+ describe('legacy alerts', () => {
before(async () => {
await setupSpacesAndUsers(getService);
});
diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/rbac_legacy.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/rbac_legacy.ts
index ef5914965ddce..3db3565374740 100644
--- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/rbac_legacy.ts
+++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/rbac_legacy.ts
@@ -77,6 +77,7 @@ export default function alertTests({ getService }: FtrProviderContext) {
case 'space_1_all at space1':
case 'superuser at space1':
case 'space_1_all_with_restricted_fixture at space1':
+ await resetTaskStatus(migratedAlertId);
await ensureLegacyAlertHasBeenMigrated(migratedAlertId);
await updateMigratedAlertToUseApiKeyOfCurrentUser(migratedAlertId);
@@ -92,6 +93,7 @@ export default function alertTests({ getService }: FtrProviderContext) {
await ensureAlertIsRunning();
break;
case 'global_read at space1':
+ await resetTaskStatus(migratedAlertId);
await ensureLegacyAlertHasBeenMigrated(migratedAlertId);
await updateMigratedAlertToUseApiKeyOfCurrentUser(migratedAlertId);
@@ -115,6 +117,7 @@ export default function alertTests({ getService }: FtrProviderContext) {
});
break;
case 'space_1_all_alerts_none_actions at space1':
+ await resetTaskStatus(migratedAlertId);
await ensureLegacyAlertHasBeenMigrated(migratedAlertId);
await updateMigratedAlertToUseApiKeyOfCurrentUser(migratedAlertId);
@@ -140,6 +143,21 @@ export default function alertTests({ getService }: FtrProviderContext) {
throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`);
}
+ async function resetTaskStatus(alertId: string) {
+ // occasionally when the task manager starts running while the alert saved objects
+ // are mid-migration, the task will fail and set its status to "failed". this prevents
+ // the alert from running ever again and downstream tasks that depend on successful alert
+ // execution will fail. this ensures the task status is set to "idle" so the
+ // task manager will continue claiming and executing it.
+ await supertest
+ .put(`${getUrlPrefix(space.id)}/api/alerts_fixture/${alertId}/reset_task_status`)
+ .set('kbn-xsrf', 'foo')
+ .send({
+ status: 'idle',
+ })
+ .expect(200);
+ }
+
async function ensureLegacyAlertHasBeenMigrated(alertId: string) {
const getResponse = await supertestWithoutAuth
.get(`${getUrlPrefix(space.id)}/api/alerts/alert/${alertId}`)
diff --git a/x-pack/test/functional/apps/transform/cloning.ts b/x-pack/test/functional/apps/transform/cloning.ts
index d804f0ef14cf8..665c126e00a01 100644
--- a/x-pack/test/functional/apps/transform/cloning.ts
+++ b/x-pack/test/functional/apps/transform/cloning.ts
@@ -159,7 +159,7 @@ export default function ({ getService }: FtrProviderContext) {
await transform.table.filterWithSearchString(testData.originalConfig.id, 1);
await transform.testExecution.logTestStep('should show the actions popover');
- await transform.table.assertTransformRowActions(false);
+ await transform.table.assertTransformRowActions(testData.originalConfig.id, false);
await transform.testExecution.logTestStep('should display the define pivot step');
await transform.table.clickTransformRowAction('Clone');
diff --git a/x-pack/test/functional/apps/transform/deleting.ts b/x-pack/test/functional/apps/transform/deleting.ts
new file mode 100644
index 0000000000000..bdba06454c5c2
--- /dev/null
+++ b/x-pack/test/functional/apps/transform/deleting.ts
@@ -0,0 +1,136 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { TRANSFORM_STATE } from '../../../../plugins/transform/common/constants';
+
+import { FtrProviderContext } from '../../ftr_provider_context';
+import { getLatestTransformConfig, getPivotTransformConfig } from './index';
+
+export default function ({ getService }: FtrProviderContext) {
+ const esArchiver = getService('esArchiver');
+ const transform = getService('transform');
+
+ describe('deleting', function () {
+ const PREFIX = 'deleting';
+
+ const testDataList = [
+ {
+ suiteTitle: 'batch transform with pivot configuration',
+ originalConfig: getPivotTransformConfig(PREFIX, false),
+ expected: {
+ row: {
+ status: TRANSFORM_STATE.STOPPED,
+ mode: 'batch',
+ progress: 100,
+ },
+ },
+ },
+ {
+ suiteTitle: 'continuous transform with pivot configuration',
+ originalConfig: getPivotTransformConfig(PREFIX, true),
+ expected: {
+ row: {
+ status: TRANSFORM_STATE.STOPPED,
+ mode: 'continuous',
+ progress: undefined,
+ },
+ },
+ },
+ {
+ suiteTitle: 'batch transform with latest configuration',
+ originalConfig: getLatestTransformConfig(PREFIX),
+ transformDescription: 'updated description',
+ transformDocsPerSecond: '1000',
+ transformFrequency: '10m',
+ expected: {
+ messageText: 'updated transform.',
+ row: {
+ status: TRANSFORM_STATE.STOPPED,
+ mode: 'batch',
+ progress: 100,
+ },
+ },
+ },
+ ];
+
+ before(async () => {
+ await esArchiver.loadIfNeeded('ml/ecommerce');
+ await transform.testResources.createIndexPatternIfNeeded('ft_ecommerce', 'order_date');
+
+ for (const testData of testDataList) {
+ await transform.api.createAndRunTransform(
+ testData.originalConfig.id,
+ testData.originalConfig
+ );
+ }
+
+ await transform.testResources.setKibanaTimeZoneToUTC();
+ await transform.securityUI.loginAsTransformPowerUser();
+ });
+
+ after(async () => {
+ for (const testData of testDataList) {
+ await transform.testResources.deleteIndexPatternByTitle(testData.originalConfig.dest.index);
+ await transform.api.deleteIndices(testData.originalConfig.dest.index);
+ }
+ await transform.api.cleanTransformIndices();
+ });
+
+ for (const testData of testDataList) {
+ describe(`${testData.suiteTitle}`, function () {
+ it('delete transform', async () => {
+ await transform.testExecution.logTestStep('should load the home page');
+ await transform.navigation.navigateTo();
+ await transform.management.assertTransformListPageExists();
+
+ await transform.testExecution.logTestStep('should display the transforms table');
+ await transform.management.assertTransformsTableExists();
+
+ if (testData.expected.row.mode === 'continuous') {
+ await transform.testExecution.logTestStep('should have the delete action disabled');
+ await transform.table.assertTransformRowActionEnabled(
+ testData.originalConfig.id,
+ 'Delete',
+ false
+ );
+
+ await transform.testExecution.logTestStep('should stop the transform');
+ await transform.table.clickTransformRowActionWithRetry(
+ testData.originalConfig.id,
+ 'Stop'
+ );
+ }
+
+ await transform.testExecution.logTestStep('should display the stopped transform');
+ await transform.table.assertTransformRowFields(testData.originalConfig.id, {
+ id: testData.originalConfig.id,
+ description: testData.originalConfig.description,
+ status: testData.expected.row.status,
+ mode: testData.expected.row.mode,
+ progress: testData.expected.row.progress,
+ });
+
+ await transform.testExecution.logTestStep('should show the delete modal');
+ await transform.table.assertTransformRowActionEnabled(
+ testData.originalConfig.id,
+ 'Delete',
+ true
+ );
+ await transform.table.clickTransformRowActionWithRetry(
+ testData.originalConfig.id,
+ 'Delete'
+ );
+ await transform.table.assertTransformDeleteModalExists();
+
+ await transform.testExecution.logTestStep('should delete the transform');
+ await transform.table.confirmDeleteTransform();
+ await transform.table.assertTransformRowNotExists(testData.originalConfig.id);
+ });
+ });
+ }
+ });
+}
diff --git a/x-pack/test/functional/apps/transform/editing.ts b/x-pack/test/functional/apps/transform/editing.ts
index 71a7cf02df1fd..1f0bb058bdc38 100644
--- a/x-pack/test/functional/apps/transform/editing.ts
+++ b/x-pack/test/functional/apps/transform/editing.ts
@@ -109,7 +109,7 @@ export default function ({ getService }: FtrProviderContext) {
await transform.table.filterWithSearchString(testData.originalConfig.id, 1);
await transform.testExecution.logTestStep('should show the actions popover');
- await transform.table.assertTransformRowActions(false);
+ await transform.table.assertTransformRowActions(testData.originalConfig.id, false);
await transform.testExecution.logTestStep('should show the edit flyout');
await transform.table.clickTransformRowAction('Edit');
diff --git a/x-pack/test/functional/apps/transform/index.ts b/x-pack/test/functional/apps/transform/index.ts
index 63d8d0b51bc8c..1440f0a3f9a09 100644
--- a/x-pack/test/functional/apps/transform/index.ts
+++ b/x-pack/test/functional/apps/transform/index.ts
@@ -6,7 +6,10 @@
*/
import { FtrProviderContext } from '../../ftr_provider_context';
-import { TransformLatestConfig } from '../../../../plugins/transform/common/types/transform';
+import {
+ TransformLatestConfig,
+ TransformPivotConfig,
+} from '../../../../plugins/transform/common/types/transform';
export default function ({ getService, loadTestFile }: FtrProviderContext) {
const esArchiver = getService('esArchiver');
@@ -41,6 +44,8 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) {
loadTestFile(require.resolve('./cloning'));
loadTestFile(require.resolve('./editing'));
loadTestFile(require.resolve('./feature_controls'));
+ loadTestFile(require.resolve('./deleting'));
+ loadTestFile(require.resolve('./starting'));
});
}
export interface ComboboxOption {
@@ -80,20 +85,46 @@ export function isLatestTransformTestData(arg: any): arg is LatestTransformTestD
return arg.type === 'latest';
}
-export function getLatestTransformConfig(): TransformLatestConfig {
+export function getPivotTransformConfig(
+ prefix: string,
+ continuous?: boolean
+): TransformPivotConfig {
const timestamp = Date.now();
return {
- id: `ec_cloning_2_${timestamp}`,
+ id: `ec_${prefix}_pivot_${timestamp}_${continuous ? 'cont' : 'batch'}`,
+ source: { index: ['ft_ecommerce'] },
+ pivot: {
+ group_by: { category: { terms: { field: 'category.keyword' } } },
+ aggregations: { 'products.base_price.avg': { avg: { field: 'products.base_price' } } },
+ },
+ description: `ecommerce ${
+ continuous ? 'continuous' : 'batch'
+ } transform with avg(products.base_price) grouped by terms(category.keyword)`,
+ dest: { index: `user-ec_2_${timestamp}` },
+ ...(continuous ? { sync: { time: { field: 'order_date', delay: '60s' } } } : {}),
+ };
+}
+
+export function getLatestTransformConfig(
+ prefix: string,
+ continuous?: boolean
+): TransformLatestConfig {
+ const timestamp = Date.now();
+ return {
+ id: `ec_${prefix}_latest_${timestamp}_${continuous ? 'cont' : 'batch'}`,
source: { index: ['ft_ecommerce'] },
latest: {
unique_key: ['category.keyword'],
sort: 'order_date',
},
- description: 'ecommerce batch transform with category unique key and sorted by order date',
+ description: `ecommerce ${
+ continuous ? 'continuous' : 'batch'
+ } transform with category unique key and sorted by order date`,
frequency: '3s',
settings: {
max_page_search_size: 250,
},
dest: { index: `user-ec_3_${timestamp}` },
+ ...(continuous ? { sync: { time: { field: 'order_date', delay: '60s' } } } : {}),
};
}
diff --git a/x-pack/test/functional/apps/transform/starting.ts b/x-pack/test/functional/apps/transform/starting.ts
new file mode 100644
index 0000000000000..4b0b6f8dade66
--- /dev/null
+++ b/x-pack/test/functional/apps/transform/starting.ts
@@ -0,0 +1,106 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { FtrProviderContext } from '../../ftr_provider_context';
+import { TRANSFORM_STATE } from '../../../../plugins/transform/common/constants';
+import { getLatestTransformConfig, getPivotTransformConfig } from './index';
+
+export default function ({ getService }: FtrProviderContext) {
+ const esArchiver = getService('esArchiver');
+ const transform = getService('transform');
+
+ describe('starting', function () {
+ const PREFIX = 'starting';
+ const testDataList = [
+ {
+ suiteTitle: 'batch transform with pivot configuration',
+ originalConfig: getPivotTransformConfig(PREFIX, false),
+ mode: 'batch',
+ },
+ {
+ suiteTitle: 'continuous transform with pivot configuration',
+ originalConfig: getPivotTransformConfig(PREFIX, true),
+ mode: 'continuous',
+ },
+ {
+ suiteTitle: 'batch transform with latest configuration',
+ originalConfig: getLatestTransformConfig(PREFIX, false),
+ mode: 'batch',
+ },
+ {
+ suiteTitle: 'continuous transform with latest configuration',
+ originalConfig: getLatestTransformConfig(PREFIX, true),
+ mode: 'continuous',
+ },
+ ];
+
+ before(async () => {
+ await esArchiver.loadIfNeeded('ml/ecommerce');
+ await transform.testResources.createIndexPatternIfNeeded('ft_ecommerce', 'order_date');
+
+ for (const testData of testDataList) {
+ await transform.api.createTransform(testData.originalConfig.id, testData.originalConfig);
+ }
+ await transform.testResources.setKibanaTimeZoneToUTC();
+ await transform.securityUI.loginAsTransformPowerUser();
+ });
+
+ after(async () => {
+ for (const testData of testDataList) {
+ await transform.testResources.deleteIndexPatternByTitle(testData.originalConfig.dest.index);
+ await transform.api.deleteIndices(testData.originalConfig.dest.index);
+ }
+
+ await transform.api.cleanTransformIndices();
+ });
+
+ for (const testData of testDataList) {
+ const transformId = testData.originalConfig.id;
+
+ describe(`${testData.suiteTitle}`, function () {
+ it('start transform', async () => {
+ await transform.testExecution.logTestStep('should load the home page');
+ await transform.navigation.navigateTo();
+ await transform.management.assertTransformListPageExists();
+
+ await transform.testExecution.logTestStep('should display the transforms table');
+ await transform.management.assertTransformsTableExists();
+
+ await transform.testExecution.logTestStep(
+ 'should display the original transform in the transform list'
+ );
+ await transform.table.filterWithSearchString(transformId, 1);
+
+ await transform.testExecution.logTestStep('should start the transform');
+ await transform.table.assertTransformRowActionEnabled(transformId, 'Start', true);
+ await transform.table.clickTransformRowActionWithRetry(transformId, 'Start');
+ await transform.table.confirmStartTransform();
+ await transform.table.clearSearchString(testDataList.length);
+
+ if (testData.mode === 'continuous') {
+ await transform.testExecution.logTestStep('should display the started transform');
+ await transform.table.assertTransformRowStatusNotEql(
+ testData.originalConfig.id,
+ TRANSFORM_STATE.STOPPED
+ );
+ } else {
+ await transform.table.assertTransformRowProgressGreaterThan(transformId, 0);
+ }
+
+ await transform.table.assertTransformRowStatusNotEql(
+ testData.originalConfig.id,
+ TRANSFORM_STATE.FAILED
+ );
+ await transform.table.assertTransformRowStatusNotEql(
+ testData.originalConfig.id,
+ TRANSFORM_STATE.ABORTING
+ );
+ });
+ });
+ }
+ });
+}
diff --git a/x-pack/test/functional/services/transform/management.ts b/x-pack/test/functional/services/transform/management.ts
index fdfd1d1d9b40f..807c3d49e344c 100644
--- a/x-pack/test/functional/services/transform/management.ts
+++ b/x-pack/test/functional/services/transform/management.ts
@@ -5,8 +5,11 @@
* 2.0.
*/
+import { ProvidedType } from '@kbn/test/types/ftr';
import { FtrProviderContext } from '../../ftr_provider_context';
+export type TransformManagement = ProvidedType;
+
export function TransformManagementProvider({ getService }: FtrProviderContext) {
const testSubjects = getService('testSubjects');
diff --git a/x-pack/test/functional/services/transform/transform_table.ts b/x-pack/test/functional/services/transform/transform_table.ts
index 72626580e9461..ce2625677e479 100644
--- a/x-pack/test/functional/services/transform/transform_table.ts
+++ b/x-pack/test/functional/services/transform/transform_table.ts
@@ -12,6 +12,7 @@ import { FtrProviderContext } from '../../ftr_provider_context';
export function TransformTableProvider({ getService }: FtrProviderContext) {
const retry = getService('retry');
const testSubjects = getService('testSubjects');
+ const browser = getService('browser');
return new (class TransformTable {
public async parseTransformTable() {
@@ -129,21 +130,63 @@ export function TransformTableProvider({ getService }: FtrProviderContext) {
const filteredRows = rows.filter((row) => row.id === filter);
expect(filteredRows).to.have.length(
expectedRowCount,
- `Filtered DFA job table should have ${expectedRowCount} row(s) for filter '${filter}' (got matching items '${filteredRows}')`
+ `Filtered Transform table should have ${expectedRowCount} row(s) for filter '${filter}' (got matching items '${filteredRows}')`
);
}
- public async assertTransformRowFields(transformId: string, expectedRow: object) {
+ public async clearSearchString(expectedRowCount: number = 1) {
+ await this.waitForTransformsToLoad();
+ const tableListContainer = await testSubjects.find('transformListTableContainer');
+ const searchBarInput = await tableListContainer.findByClassName('euiFieldSearch');
+ await searchBarInput.clearValueWithKeyboard();
const rows = await this.parseTransformTable();
- const transformRow = rows.filter((row) => row.id === transformId)[0];
- expect(transformRow).to.eql(
- expectedRow,
- `Expected transform row to be '${JSON.stringify(expectedRow)}' (got '${JSON.stringify(
- transformRow
- )}')`
+ expect(rows).to.have.length(
+ expectedRowCount,
+ `Transform table should have ${expectedRowCount} row(s) after clearing search' (got '${rows.length}')`
);
}
+ public async assertTransformRowFields(transformId: string, expectedRow: object) {
+ await retry.tryForTime(30 * 1000, async () => {
+ await this.refreshTransformList();
+ const rows = await this.parseTransformTable();
+ const transformRow = rows.filter((row) => row.id === transformId)[0];
+ expect(transformRow).to.eql(
+ expectedRow,
+ `Expected transform row to be '${JSON.stringify(expectedRow)}' (got '${JSON.stringify(
+ transformRow
+ )}')`
+ );
+ });
+ }
+
+ public async assertTransformRowProgressGreaterThan(
+ transformId: string,
+ expectedProgress: number
+ ) {
+ await retry.tryForTime(30 * 1000, async () => {
+ await this.refreshTransformList();
+ const rows = await this.parseTransformTable();
+ const transformRow = rows.filter((row) => row.id === transformId)[0];
+ expect(transformRow.progress).to.greaterThan(
+ 0,
+ `Expected transform row progress to be greater than '${expectedProgress}' (got '${transformRow.progress}')`
+ );
+ });
+ }
+
+ public async assertTransformRowStatusNotEql(transformId: string, status: string) {
+ await retry.tryForTime(30 * 1000, async () => {
+ await this.refreshTransformList();
+ const rows = await this.parseTransformTable();
+ const transformRow = rows.filter((row) => row.id === transformId)[0];
+ expect(transformRow.status).to.not.eql(
+ status,
+ `Expected transform row status to not be '${status}' (got '${transformRow.status}')`
+ );
+ });
+ }
+
public async assertTransformExpandedRow() {
await testSubjects.click('transformListRowDetailsToggle');
@@ -185,8 +228,13 @@ export function TransformTableProvider({ getService }: FtrProviderContext) {
});
}
- public async assertTransformRowActions(isTransformRunning = false) {
- await testSubjects.click('euiCollapsedItemActionsButton');
+ public rowSelector(transformId: string, subSelector?: string) {
+ const row = `~transformListTable > ~row-${transformId}`;
+ return !subSelector ? row : `${row} > ${subSelector}`;
+ }
+
+ public async assertTransformRowActions(transformId: string, isTransformRunning = false) {
+ await testSubjects.click(this.rowSelector(transformId, 'euiCollapsedItemActionsButton'));
await testSubjects.existOrFail('transformActionClone');
await testSubjects.existOrFail('transformActionDelete');
@@ -201,6 +249,42 @@ export function TransformTableProvider({ getService }: FtrProviderContext) {
}
}
+ public async assertTransformRowActionEnabled(
+ transformId: string,
+ action: 'Delete' | 'Start' | 'Stop' | 'Clone' | 'Edit',
+ expectedValue: boolean
+ ) {
+ const selector = `transformAction${action}`;
+ await retry.tryForTime(60 * 1000, async () => {
+ await this.refreshTransformList();
+
+ await browser.pressKeys(browser.keys.ESCAPE);
+ await testSubjects.click(this.rowSelector(transformId, 'euiCollapsedItemActionsButton'));
+
+ await testSubjects.existOrFail(selector);
+ const isEnabled = await testSubjects.isEnabled(selector);
+ expect(isEnabled).to.eql(
+ expectedValue,
+ `Expected '${action}' button to be '${expectedValue ? 'enabled' : 'disabled'}' (got '${
+ isEnabled ? 'enabled' : 'disabled'
+ }')`
+ );
+ });
+ }
+
+ public async clickTransformRowActionWithRetry(
+ transformId: string,
+ action: 'Delete' | 'Start' | 'Stop' | 'Clone' | 'Edit'
+ ) {
+ await retry.tryForTime(30 * 1000, async () => {
+ await browser.pressKeys(browser.keys.ESCAPE);
+ await testSubjects.click(this.rowSelector(transformId, 'euiCollapsedItemActionsButton'));
+ await testSubjects.existOrFail(`transformAction${action}`);
+ await testSubjects.click(`transformAction${action}`);
+ await testSubjects.missingOrFail(`transformAction${action}`);
+ });
+ }
+
public async clickTransformRowAction(action: string) {
await testSubjects.click(`transformAction${action}`);
}
@@ -214,5 +298,53 @@ export function TransformTableProvider({ getService }: FtrProviderContext) {
await this.waitForTransformsExpandedRowPreviewTabToLoad();
await this.assertEuiDataGridColumnValues('transformPivotPreview', column, values);
}
+
+ public async assertTransformDeleteModalExists() {
+ await testSubjects.existOrFail('transformDeleteModal', { timeout: 60 * 1000 });
+ }
+
+ public async assertTransformDeleteModalNotExists() {
+ await testSubjects.missingOrFail('transformDeleteModal', { timeout: 60 * 1000 });
+ }
+
+ public async assertTransformStartModalExists() {
+ await testSubjects.existOrFail('transformStartModal', { timeout: 60 * 1000 });
+ }
+
+ public async assertTransformStartModalNotExists() {
+ await testSubjects.missingOrFail('transformStartModal', { timeout: 60 * 1000 });
+ }
+
+ public async confirmDeleteTransform() {
+ await retry.tryForTime(30 * 1000, async () => {
+ await this.assertTransformDeleteModalExists();
+ await testSubjects.click('transformDeleteModal > confirmModalConfirmButton');
+ await this.assertTransformDeleteModalNotExists();
+ });
+ }
+
+ public async assertTransformRowNotExists(transformId: string) {
+ await retry.tryForTime(30 * 1000, async () => {
+ // If after deletion, and there's no transform left
+ const noTransformsFoundMessageExists = await testSubjects.exists(
+ 'transformNoTransformsFound'
+ );
+
+ if (noTransformsFoundMessageExists) {
+ return true;
+ } else {
+ // Checks that the tranform was deleted
+ await this.filterWithSearchString(transformId, 0);
+ }
+ });
+ }
+
+ public async confirmStartTransform() {
+ await retry.tryForTime(30 * 1000, async () => {
+ await this.assertTransformStartModalExists();
+ await testSubjects.click('transformStartModal > confirmModalConfirmButton');
+ await this.assertTransformStartModalNotExists();
+ });
+ }
})();
}
diff --git a/yarn.lock b/yarn.lock
index bd95cec520a4c..6cd37aa545577 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -20000,10 +20000,10 @@ lodash.uniq@4.5.0, lodash.uniq@^4.5.0:
resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773"
integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=
-lodash@4.17.11, lodash@4.17.15, lodash@>4.17.4, lodash@^4, lodash@^4.0.0, lodash@^4.0.1, lodash@^4.10.0, lodash@^4.15.0, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.4, lodash@^4.2.0, lodash@~4.17.10, lodash@~4.17.15, lodash@~4.17.19, lodash@~4.17.20:
- version "4.17.20"
- resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52"
- integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==
+lodash@4.17.11, lodash@4.17.15, lodash@>4.17.4, lodash@^4, lodash@^4.0.0, lodash@^4.0.1, lodash@^4.10.0, lodash@^4.15.0, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.21, lodash@^4.17.4, lodash@^4.2.0, lodash@~4.17.10, lodash@~4.17.15, lodash@~4.17.19, lodash@~4.17.20:
+ version "4.17.21"
+ resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
+ integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
log-ok@^0.1.1:
version "0.1.1"