From 858befef44d10db1ad388b10e48cad84991c8355 Mon Sep 17 00:00:00 2001
From: Dario Gieselaar
Date: Mon, 9 Nov 2020 13:46:46 +0100
Subject: [PATCH 1/8] [APM] Expose APM event client as part of plugin contract
(#82724)
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
---
.../create_apm_event_client/index.ts | 21 +++++----
.../apm/server/lib/helpers/setup_request.ts | 3 +-
x-pack/plugins/apm/server/plugin.ts | 47 +++++++++++++++++--
3 files changed, 57 insertions(+), 14 deletions(-)
diff --git a/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/index.ts b/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/index.ts
index 2bfd3c94ed34c..9020cb1b9953a 100644
--- a/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/index.ts
+++ b/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/index.ts
@@ -7,14 +7,16 @@
import { ValuesType } from 'utility-types';
import { APMBaseDoc } from '../../../../../typings/es_schemas/raw/apm_base_doc';
import { APMError } from '../../../../../typings/es_schemas/ui/apm_error';
-import { KibanaRequest } from '../../../../../../../../src/core/server';
+import {
+ KibanaRequest,
+ LegacyScopedClusterClient,
+} from '../../../../../../../../src/core/server';
import { ProcessorEvent } from '../../../../../common/processor_event';
import {
ESSearchRequest,
ESSearchResponse,
} from '../../../../../typings/elasticsearch';
import { ApmIndicesConfig } from '../../../settings/apm_indices/get_apm_indices';
-import { APMRequestHandlerContext } from '../../../../routes/typings';
import { addFilterToExcludeLegacyData } from './add_filter_to_exclude_legacy_data';
import { callClientWithDebug } from '../call_client_with_debug';
import { Transaction } from '../../../../../typings/es_schemas/ui/transaction';
@@ -51,20 +53,23 @@ type TypedSearchResponse<
export type APMEventClient = ReturnType;
export function createApmEventClient({
- context,
+ esClient,
+ debug,
request,
indices,
options: { includeFrozen } = { includeFrozen: false },
}: {
- context: APMRequestHandlerContext;
+ esClient: Pick<
+ LegacyScopedClusterClient,
+ 'callAsInternalUser' | 'callAsCurrentUser'
+ >;
+ debug: boolean;
request: KibanaRequest;
indices: ApmIndicesConfig;
options: {
includeFrozen: boolean;
};
}) {
- const client = context.core.elasticsearch.legacy.client;
-
return {
search(
params: TParams,
@@ -77,14 +82,14 @@ export function createApmEventClient({
: withProcessorEventFilter;
return callClientWithDebug({
- apiCaller: client.callAsCurrentUser,
+ apiCaller: esClient.callAsCurrentUser,
operationName: 'search',
params: {
...withPossibleLegacyDataFilter,
ignore_throttled: !includeFrozen,
},
request,
- debug: context.params.query._debug,
+ debug,
});
},
};
diff --git a/x-pack/plugins/apm/server/lib/helpers/setup_request.ts b/x-pack/plugins/apm/server/lib/helpers/setup_request.ts
index 5e75535c678b3..363c4128137e0 100644
--- a/x-pack/plugins/apm/server/lib/helpers/setup_request.ts
+++ b/x-pack/plugins/apm/server/lib/helpers/setup_request.ts
@@ -88,7 +88,8 @@ export async function setupRequest(
const coreSetupRequest = {
indices,
apmEventClient: createApmEventClient({
- context,
+ esClient: context.core.elasticsearch.legacy.client,
+ debug: context.params.query._debug,
request,
indices,
options: { includeFrozen },
diff --git a/x-pack/plugins/apm/server/plugin.ts b/x-pack/plugins/apm/server/plugin.ts
index d3341b6c1b163..44269b1775953 100644
--- a/x-pack/plugins/apm/server/plugin.ts
+++ b/x-pack/plugins/apm/server/plugin.ts
@@ -10,14 +10,17 @@ import { map, take } from 'rxjs/operators';
import {
CoreSetup,
CoreStart,
+ KibanaRequest,
Logger,
Plugin,
PluginInitializerContext,
+ RequestHandlerContext,
} from 'src/core/server';
import { APMConfig, APMXPackConfig, mergeConfigs } from '.';
import { APMOSSPluginSetup } from '../../../../src/plugins/apm_oss/server';
import { HomeServerPluginSetup } from '../../../../src/plugins/home/server';
import { UsageCollectionSetup } from '../../../../src/plugins/usage_collection/server';
+import { UI_SETTINGS } from '../../../../src/plugins/data/common';
import { ActionsPlugin } from '../../actions/server';
import { AlertingPlugin } from '../../alerts/server';
import { CloudSetup } from '../../cloud/server';
@@ -30,6 +33,7 @@ import { TaskManagerSetupContract } from '../../task_manager/server';
import { APM_FEATURE, registerFeaturesUsage } from './feature';
import { registerApmAlerts } from './lib/alerts/register_apm_alerts';
import { createApmTelemetry } from './lib/apm_telemetry';
+import { createApmEventClient } from './lib/helpers/create_es_client/create_apm_event_client';
import { getInternalSavedObjectsClient } from './lib/helpers/get_internal_saved_objects_client';
import { createApmAgentConfigurationIndex } from './lib/settings/agent_configuration/create_agent_config_index';
import { getApmIndices } from './lib/settings/apm_indices/get_apm_indices';
@@ -42,6 +46,11 @@ import { uiSettings } from './ui_settings';
export interface APMPluginSetup {
config$: Observable;
getApmIndices: () => ReturnType;
+ createApmEventClient: (params: {
+ debug?: boolean;
+ request: KibanaRequest;
+ context: RequestHandlerContext;
+ }) => Promise>;
}
export class APMPlugin implements Plugin {
@@ -141,13 +150,41 @@ export class APMPlugin implements Plugin {
},
});
+ const boundGetApmIndices = async () =>
+ getApmIndices({
+ savedObjectsClient: await getInternalSavedObjectsClient(core),
+ config: await mergedConfig$.pipe(take(1)).toPromise(),
+ });
+
return {
config$: mergedConfig$,
- getApmIndices: async () =>
- getApmIndices({
- savedObjectsClient: await getInternalSavedObjectsClient(core),
- config: await mergedConfig$.pipe(take(1)).toPromise(),
- }),
+ getApmIndices: boundGetApmIndices,
+ createApmEventClient: async ({
+ request,
+ context,
+ debug,
+ }: {
+ debug?: boolean;
+ request: KibanaRequest;
+ context: RequestHandlerContext;
+ }) => {
+ const [indices, includeFrozen] = await Promise.all([
+ boundGetApmIndices(),
+ context.core.uiSettings.client.get(UI_SETTINGS.SEARCH_INCLUDE_FROZEN),
+ ]);
+
+ const esClient = context.core.elasticsearch.legacy.client;
+
+ return createApmEventClient({
+ debug: debug ?? false,
+ esClient,
+ request,
+ indices,
+ options: {
+ includeFrozen,
+ },
+ });
+ },
};
}
From 3c525d7341ebe683e6ed8827927c5b0c18e97631 Mon Sep 17 00:00:00 2001
From: Gidi Meir Morris
Date: Mon, 9 Nov 2020 12:56:56 +0000
Subject: [PATCH 2/8] [Alerting] adds an Run When field in the alert flyout to
assign the action to an Action Group (#82472)
Adds a `RunsWhen` field to actions in the Alerts Flyout when creating / editing an Alert which allows the user to assign specific actions to a certain Action Groups
---
.../public/alert_types/astros.tsx | 6 +-
.../server/alert_types/always_firing.ts | 18 +-
x-pack/plugins/triggers_actions_ui/README.md | 21 +-
.../lib/check_action_type_enabled.scss | 12 +-
.../action_form.test.tsx | 89 ++-
.../action_connector_form/action_form.tsx | 637 +++++-------------
.../action_type_form.tsx | 339 ++++++++++
.../connector_add_inline.tsx | 153 +++++
.../connector_add_modal.test.tsx | 3 +-
.../connector_add_modal.tsx | 13 +-
.../sections/alert_form/alert_form.tsx | 51 +-
.../alert_create_flyout.ts | 57 ++
.../fixtures/plugins/alerts/server/plugin.ts | 1 +
13 files changed, 846 insertions(+), 554 deletions(-)
create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_type_form.tsx
create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_inline.tsx
diff --git a/x-pack/examples/alerting_example/public/alert_types/astros.tsx b/x-pack/examples/alerting_example/public/alert_types/astros.tsx
index 73c7dfea1263b..54f989b93e22f 100644
--- a/x-pack/examples/alerting_example/public/alert_types/astros.tsx
+++ b/x-pack/examples/alerting_example/public/alert_types/astros.tsx
@@ -127,9 +127,9 @@ export const PeopleinSpaceExpression: React.FunctionComponent
- errs.map((e) => (
-
+ Object.entries(errors).map(([field, errs]: [string, string[]], fieldIndex) =>
+ errs.map((e, index) => (
+
{field}: `: ${errs}`
))
diff --git a/x-pack/examples/alerting_example/server/alert_types/always_firing.ts b/x-pack/examples/alerting_example/server/alert_types/always_firing.ts
index bb1cb0d97689b..d02406a23045e 100644
--- a/x-pack/examples/alerting_example/server/alert_types/always_firing.ts
+++ b/x-pack/examples/alerting_example/server/alert_types/always_firing.ts
@@ -5,25 +5,31 @@
*/
import uuid from 'uuid';
-import { range } from 'lodash';
+import { range, random } from 'lodash';
import { AlertType } from '../../../../plugins/alerts/server';
import { DEFAULT_INSTANCES_TO_GENERATE, ALERTING_EXAMPLE_APP_ID } from '../../common/constants';
+const ACTION_GROUPS = [
+ { id: 'small', name: 'small' },
+ { id: 'medium', name: 'medium' },
+ { id: 'large', name: 'large' },
+];
+
export const alertType: AlertType = {
id: 'example.always-firing',
name: 'Always firing',
- actionGroups: [{ id: 'default', name: 'default' }],
- defaultActionGroupId: 'default',
+ actionGroups: ACTION_GROUPS,
+ defaultActionGroupId: 'small',
async executor({ services, params: { instances = DEFAULT_INSTANCES_TO_GENERATE }, state }) {
const count = (state.count ?? 0) + 1;
range(instances)
- .map(() => ({ id: uuid.v4() }))
- .forEach((instance: { id: string }) => {
+ .map(() => ({ id: uuid.v4(), tshirtSize: ACTION_GROUPS[random(0, 2)].id! }))
+ .forEach((instance: { id: string; tshirtSize: string }) => {
services
.alertInstanceFactory(instance.id)
.replaceState({ triggerdOnCycle: count })
- .scheduleActions('default');
+ .scheduleActions(instance.tshirtSize);
});
return {
diff --git a/x-pack/plugins/triggers_actions_ui/README.md b/x-pack/plugins/triggers_actions_ui/README.md
index aabb9899cb343..32e157255c0cc 100644
--- a/x-pack/plugins/triggers_actions_ui/README.md
+++ b/x-pack/plugins/triggers_actions_ui/README.md
@@ -1319,19 +1319,19 @@ ActionForm Props definition:
interface ActionAccordionFormProps {
actions: AlertAction[];
defaultActionGroupId: string;
+ actionGroups?: ActionGroup[];
setActionIdByIndex: (id: string, index: number) => void;
+ setActionGroupIdByIndex?: (group: string, index: number) => void;
setAlertProperty: (actions: AlertAction[]) => void;
setActionParamsProperty: (key: string, value: any, index: number) => void;
http: HttpSetup;
- actionTypeRegistry: TypeRegistry;
- toastNotifications: Pick<
- ToastsApi,
- 'get$' | 'add' | 'remove' | 'addSuccess' | 'addWarning' | 'addDanger' | 'addError'
- >;
+ actionTypeRegistry: ActionTypeRegistryContract;
+ toastNotifications: ToastsSetup;
+ docLinks: DocLinksStart;
actionTypes?: ActionType[];
messageVariables?: ActionVariable[];
defaultActionMessage?: string;
- consumer: string;
+ capabilities: ApplicationStart['capabilities'];
}
```
@@ -1339,17 +1339,20 @@ interface ActionAccordionFormProps {
|Property|Description|
|---|---|
|actions|List of actions comes from alert.actions property.|
-|defaultActionGroupId|Default action group id to which each new action will belong to.|
+|defaultActionGroupId|Default action group id to which each new action will belong by default.|
+|actionGroups|Optional. List of action groups to which new action can be assigned. The RunWhen field is only displayed when these action groups are specified|
|setActionIdByIndex|Function for changing action 'id' by the proper index in alert.actions array.|
+|setActionGroupIdByIndex|Function for changing action 'group' by the proper index in alert.actions array.|
|setAlertProperty|Function for changing alert property 'actions'. Used when deleting action from the array to reset it.|
|setActionParamsProperty|Function for changing action key/value property by index in alert.actions array.|
|http|HttpSetup needed for executing API calls.|
|actionTypeRegistry|Registry for action types.|
-|toastNotifications|Toast messages.|
+|toastNotifications|Toast messages Plugin Setup Contract.|
+|docLinks|Documentation links Plugin Start Contract.|
|actionTypes|Optional property, which allowes to define a list of available actions specific for a current plugin.|
|actionTypes|Optional property, which allowes to define a list of variables for action 'message' property.|
|defaultActionMessage|Optional property, which allowes to define a message value for action with 'message' property.|
-|consumer|Name of the plugin that creates an action.|
+|capabilities|Kibana core's Capabilities ApplicationStart['capabilities'].|
AlertsContextProvider value options:
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/check_action_type_enabled.scss b/x-pack/plugins/triggers_actions_ui/public/application/lib/check_action_type_enabled.scss
index 24dbb865742d8..bb622829e997a 100644
--- a/x-pack/plugins/triggers_actions_ui/public/application/lib/check_action_type_enabled.scss
+++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/check_action_type_enabled.scss
@@ -3,9 +3,15 @@
}
.actAccordionActionForm {
- .euiCard {
- box-shadow: none;
- }
+ background-color: $euiColorLightestShade;
+}
+
+.actAccordionActionForm .euiCard {
+ box-shadow: none;
+}
+
+.actAccordionActionForm__button {
+ padding: $euiSizeM;
}
.actConnectorsListGrid {
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.test.tsx
index 7c718e8248e41..94452e70e6bfa 100644
--- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.test.tsx
+++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.test.tsx
@@ -6,7 +6,6 @@
import React, { Fragment, lazy } from 'react';
import { mountWithIntl, nextTick } from 'test_utils/enzyme_helpers';
import { coreMock } from '../../../../../../../src/core/public/mocks';
-import { ReactWrapper } from 'enzyme';
import { act } from 'react-dom/test-utils';
import { actionTypeRegistryMock } from '../../action_type_registry.mock';
import { ValidationResult, Alert, AlertAction } from '../../../types';
@@ -112,8 +111,6 @@ describe('action_form', () => {
};
describe('action_form in alert', () => {
- let wrapper: ReactWrapper;
-
async function setup(customActions?: AlertAction[]) {
const { loadAllActions } = jest.requireMock('../../lib/action_connector_api');
loadAllActions.mockResolvedValueOnce([
@@ -217,7 +214,7 @@ describe('action_form', () => {
mutedInstanceIds: [],
} as unknown) as Alert;
- wrapper = mountWithIntl(
+ const wrapper = mountWithIntl(
{
setActionIdByIndex={(id: string, index: number) => {
initialAlert.actions[index].id = id;
}}
+ actionGroups={[{ id: 'default', name: 'Default' }]}
+ setActionGroupIdByIndex={(group: string, index: number) => {
+ initialAlert.actions[index].group = group;
+ }}
setAlertProperty={(_updatedActions: AlertAction[]) => {}}
setActionParamsProperty={(key: string, value: any, index: number) =>
(initialAlert.actions[index] = { ...initialAlert.actions[index], [key]: value })
@@ -297,10 +298,12 @@ describe('action_form', () => {
await nextTick();
wrapper.update();
});
+
+ return wrapper;
}
it('renders available action cards', async () => {
- await setup();
+ const wrapper = await setup();
const actionOption = wrapper.find(
`[data-test-subj="${actionType.id}-ActionTypeSelectOption"]`
);
@@ -314,7 +317,7 @@ describe('action_form', () => {
});
it('does not render action types disabled by config', async () => {
- await setup();
+ const wrapper = await setup();
const actionOption = wrapper.find(
'[data-test-subj="disabled-by-config-ActionTypeSelectOption"]'
);
@@ -322,52 +325,72 @@ describe('action_form', () => {
});
it('render action types which is preconfigured only (disabled by config and with preconfigured connectors)', async () => {
- await setup();
+ const wrapper = await setup();
const actionOption = wrapper.find('[data-test-subj="preconfigured-ActionTypeSelectOption"]');
expect(actionOption.exists()).toBeTruthy();
});
+ it('renders available action groups for the selected action type', async () => {
+ const wrapper = await setup();
+ const actionOption = wrapper.find(
+ `[data-test-subj="${actionType.id}-ActionTypeSelectOption"]`
+ );
+ actionOption.first().simulate('click');
+ const actionGroupsSelect = wrapper.find(
+ `[data-test-subj="addNewActionConnectorActionGroup-0"]`
+ );
+ expect((actionGroupsSelect.first().props() as any).options).toMatchInlineSnapshot(`
+ Array [
+ Object {
+ "data-test-subj": "addNewActionConnectorActionGroup-0-option-default",
+ "inputDisplay": "Default",
+ "value": "default",
+ },
+ ]
+ `);
+ });
+
it('renders available connectors for the selected action type', async () => {
- await setup();
+ const wrapper = await setup();
const actionOption = wrapper.find(
`[data-test-subj="${actionType.id}-ActionTypeSelectOption"]`
);
actionOption.first().simulate('click');
const combobox = wrapper.find(`[data-test-subj="selectActionConnector-${actionType.id}"]`);
expect((combobox.first().props() as any).options).toMatchInlineSnapshot(`
- Array [
- Object {
- "id": "test",
- "key": "test",
- "label": "Test connector ",
- },
- Object {
- "id": "test2",
- "key": "test2",
- "label": "Test connector 2 (preconfigured)",
- },
- ]
- `);
+ Array [
+ Object {
+ "id": "test",
+ "key": "test",
+ "label": "Test connector ",
+ },
+ Object {
+ "id": "test2",
+ "key": "test2",
+ "label": "Test connector 2 (preconfigured)",
+ },
+ ]
+ `);
});
it('renders only preconfigured connectors for the selected preconfigured action type', async () => {
- await setup();
+ const wrapper = await setup();
const actionOption = wrapper.find('[data-test-subj="preconfigured-ActionTypeSelectOption"]');
actionOption.first().simulate('click');
const combobox = wrapper.find('[data-test-subj="selectActionConnector-preconfigured"]');
expect((combobox.first().props() as any).options).toMatchInlineSnapshot(`
- Array [
- Object {
- "id": "test3",
- "key": "test3",
- "label": "Preconfigured Only (preconfigured)",
- },
- ]
- `);
+ Array [
+ Object {
+ "id": "test3",
+ "key": "test3",
+ "label": "Preconfigured Only (preconfigured)",
+ },
+ ]
+ `);
});
it('does not render "Add connector" button for preconfigured only action type', async () => {
- await setup();
+ const wrapper = await setup();
const actionOption = wrapper.find('[data-test-subj="preconfigured-ActionTypeSelectOption"]');
actionOption.first().simulate('click');
const preconfigPannel = wrapper.find('[data-test-subj="alertActionAccordion-default"]');
@@ -378,7 +401,7 @@ describe('action_form', () => {
});
it('renders action types disabled by license', async () => {
- await setup();
+ const wrapper = await setup();
const actionOption = wrapper.find(
'[data-test-subj="disabled-by-license-ActionTypeSelectOption"]'
);
@@ -391,7 +414,7 @@ describe('action_form', () => {
});
it(`shouldn't render action types without params component`, async () => {
- await setup();
+ const wrapper = await setup();
const actionOption = wrapper.find(
`[data-test-subj="${actionTypeWithoutParams.id}-ActionTypeSelectOption"]`
);
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.tsx
index 74432157f5659..3a7341afe3e07 100644
--- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.tsx
+++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.tsx
@@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import React, { Fragment, Suspense, useState, useEffect } from 'react';
+import React, { Fragment, useState, useEffect, useCallback } from 'react';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import {
@@ -14,25 +14,13 @@ import {
EuiIcon,
EuiTitle,
EuiSpacer,
- EuiFormRow,
- EuiComboBox,
EuiKeyPadMenuItem,
- EuiAccordion,
- EuiButtonIcon,
- EuiEmptyPrompt,
- EuiButtonEmpty,
EuiToolTip,
- EuiIconTip,
EuiLink,
- EuiCallOut,
- EuiHorizontalRule,
- EuiText,
- EuiLoadingSpinner,
} from '@elastic/eui';
import { HttpSetup, ToastsSetup, ApplicationStart, DocLinksStart } from 'kibana/public';
import { loadActionTypes, loadAllActions as loadConnectors } from '../../lib/action_connector_api';
import {
- IErrorObject,
ActionTypeModel,
ActionTypeRegistryContract,
AlertAction,
@@ -43,15 +31,19 @@ import {
} from '../../../types';
import { SectionLoading } from '../../components/section_loading';
import { ConnectorAddModal } from './connector_add_modal';
+import { ActionTypeForm, ActionTypeFormProps } from './action_type_form';
+import { AddConnectorInline } from './connector_add_inline';
import { actionTypeCompare } from '../../lib/action_type_compare';
import { checkActionFormActionTypeEnabled } from '../../lib/check_action_type_enabled';
import { VIEW_LICENSE_OPTIONS_LINK, DEFAULT_HIDDEN_ACTION_TYPES } from '../../../common/constants';
-import { hasSaveActionsCapability } from '../../lib/capabilities';
+import { ActionGroup } from '../../../../../alerts/common';
-interface ActionAccordionFormProps {
+export interface ActionAccordionFormProps {
actions: AlertAction[];
defaultActionGroupId: string;
+ actionGroups?: ActionGroup[];
setActionIdByIndex: (id: string, index: number) => void;
+ setActionGroupIdByIndex?: (group: string, index: number) => void;
setAlertProperty: (actions: AlertAction[]) => void;
setActionParamsProperty: (key: string, value: any, index: number) => void;
http: HttpSetup;
@@ -74,7 +66,9 @@ interface ActiveActionConnectorState {
export const ActionForm = ({
actions,
defaultActionGroupId,
+ actionGroups,
setActionIdByIndex,
+ setActionGroupIdByIndex,
setAlertProperty,
setActionParamsProperty,
http,
@@ -88,8 +82,6 @@ export const ActionForm = ({
capabilities,
docLinks,
}: ActionAccordionFormProps) => {
- const canSave = hasSaveActionsCapability(capabilities);
-
const [addModalVisible, setAddModalVisibility] = useState(false);
const [activeActionItem, setActiveActionItem] = useState(
undefined
@@ -101,6 +93,10 @@ export const ActionForm = ({
const [actionTypesIndex, setActionTypesIndex] = useState(undefined);
const [emptyActionsIds, setEmptyActionsIds] = useState([]);
+ const closeAddConnectorModal = useCallback(() => setAddModalVisibility(false), [
+ setAddModalVisibility,
+ ]);
+
// load action types
useEffect(() => {
(async () => {
@@ -183,359 +179,6 @@ export const ActionForm = ({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [actions, connectors]);
- const preconfiguredMessage = i18n.translate(
- 'xpack.triggersActionsUI.sections.actionForm.preconfiguredTitleMessage',
- {
- defaultMessage: '(preconfigured)',
- }
- );
-
- const getSelectedOptions = (actionItemId: string) => {
- const selectedConnector = connectors.find((connector) => connector.id === actionItemId);
- if (
- !selectedConnector ||
- // if selected connector is not preconfigured and action type is for preconfiguration only,
- // do not show regular connectors of this type
- (actionTypesIndex &&
- !actionTypesIndex[selectedConnector.actionTypeId].enabledInConfig &&
- !selectedConnector.isPreconfigured)
- ) {
- return [];
- }
- const optionTitle = `${selectedConnector.name} ${
- selectedConnector.isPreconfigured ? preconfiguredMessage : ''
- }`;
- return [
- {
- label: optionTitle,
- value: optionTitle,
- id: actionItemId,
- 'data-test-subj': 'itemActionConnector',
- },
- ];
- };
-
- const getActionTypeForm = (
- actionItem: AlertAction,
- actionConnector: ActionConnector,
- actionParamsErrors: {
- errors: IErrorObject;
- },
- index: number
- ) => {
- if (!actionTypesIndex) {
- return null;
- }
-
- const actionType = actionTypesIndex[actionItem.actionTypeId];
-
- const optionsList = connectors
- .filter(
- (connectorItem) =>
- connectorItem.actionTypeId === actionItem.actionTypeId &&
- // include only enabled by config connectors or preconfigured
- (actionType.enabledInConfig || connectorItem.isPreconfigured)
- )
- .map(({ name, id, isPreconfigured }) => ({
- label: `${name} ${isPreconfigured ? preconfiguredMessage : ''}`,
- key: id,
- id,
- }));
- const actionTypeRegistered = actionTypeRegistry.get(actionConnector.actionTypeId);
- if (!actionTypeRegistered || actionItem.group !== defaultActionGroupId) return null;
- const ParamsFieldsComponent = actionTypeRegistered.actionParamsFields;
- const checkEnabledResult = checkActionFormActionTypeEnabled(
- actionTypesIndex[actionConnector.actionTypeId],
- connectors.filter((connector) => connector.isPreconfigured)
- );
-
- const accordionContent = checkEnabledResult.isEnabled ? (
-
-
-
-
- }
- labelAppend={
- canSave &&
- actionTypesIndex &&
- actionTypesIndex[actionConnector.actionTypeId].enabledInConfig ? (
- {
- setActiveActionItem({ actionTypeId: actionItem.actionTypeId, index });
- setAddModalVisibility(true);
- }}
- >
-
-
- ) : null
- }
- >
- {
- setActionIdByIndex(selectedOptions[0].id ?? '', index);
- }}
- isClearable={false}
- />
-
-
-
-
- {ParamsFieldsComponent ? (
-
-
-
-
-
- }
- >
-
-
- ) : null}
-
- ) : (
- checkEnabledResult.messageCard
- );
-
- return (
-
-
-
-
-
-
-
-
-
-
-
-
-
- {checkEnabledResult.isEnabled === false && (
-
-
-
- )}
-
-
-
-
-
-
- }
- extraAction={
- {
- const updatedActions = actions.filter(
- (_item: AlertAction, i: number) => i !== index
- );
- setAlertProperty(updatedActions);
- setIsAddActionPanelOpen(
- updatedActions.filter((item: AlertAction) => item.id !== actionItem.id).length ===
- 0
- );
- setActiveActionItem(undefined);
- }}
- />
- }
- paddingSize="l"
- >
- {accordionContent}
-
-
-
- );
- };
-
- const getAddConnectorsForm = (actionItem: AlertAction, index: number) => {
- const actionTypeName = actionTypesIndex
- ? actionTypesIndex[actionItem.actionTypeId].name
- : actionItem.actionTypeId;
- const actionTypeRegistered = actionTypeRegistry.get(actionItem.actionTypeId);
- if (!actionTypeRegistered || actionItem.group !== defaultActionGroupId) return null;
-
- const noConnectorsLabel = (
-
- );
- return (
-
-
-
-
-
-
-
-
-
-
-
-
-
- }
- extraAction={
- {
- const updatedActions = actions.filter(
- (_item: AlertAction, i: number) => i !== index
- );
- setAlertProperty(updatedActions);
- setIsAddActionPanelOpen(
- updatedActions.filter((item: AlertAction) => item.id !== actionItem.id).length ===
- 0
- );
- setActiveActionItem(undefined);
- }}
- />
- }
- paddingSize="l"
- >
- {canSave ? (
- actionItem.id === emptyId) ? (
- noConnectorsLabel
- ) : (
-
- )
- }
- actions={[
- {
- setActiveActionItem({ actionTypeId: actionItem.actionTypeId, index });
- setAddModalVisibility(true);
- }}
- >
-
- ,
- ]}
- />
- ) : (
-
-
-
-
-
- )}
-
-
-
- );
- };
-
function addActionType(actionTypeModel: ActionTypeModel) {
if (!defaultActionGroupId) {
toastNotifications!.addDanger({
@@ -628,116 +271,172 @@ export const ActionForm = ({
});
}
- const alertActionsList = actions.map((actionItem: AlertAction, index: number) => {
- const actionConnector = connectors.find((field) => field.id === actionItem.id);
- // connectors doesn't exists
- if (!actionConnector) {
- return getAddConnectorsForm(actionItem, index);
- }
-
- const actionErrors: { errors: IErrorObject } = actionTypeRegistry
- .get(actionItem.actionTypeId)
- ?.validateParams(actionItem.params);
-
- return getActionTypeForm(actionItem, actionConnector, actionErrors, index);
- });
-
- return (
+ return isLoadingConnectors ? (
+
+
+
+ ) : (
- {isLoadingConnectors ? (
-
+
+
-
- ) : (
-
-
-
-
+
+
+ {actionTypesIndex &&
+ actions.map((actionItem: AlertAction, index: number) => {
+ const actionConnector = connectors.find((field) => field.id === actionItem.id);
+ // connectors doesn't exists
+ if (!actionConnector) {
+ return (
+ {
+ const updatedActions = actions.filter(
+ (_item: AlertAction, i: number) => i !== index
+ );
+ setAlertProperty(updatedActions);
+ setIsAddActionPanelOpen(
+ updatedActions.filter((item: AlertAction) => item.id !== actionItem.id)
+ .length === 0
+ );
+ setActiveActionItem(undefined);
+ }}
+ onAddConnector={() => {
+ setActiveActionItem({ actionTypeId: actionItem.actionTypeId, index });
+ setAddModalVisibility(true);
+ }}
/>
-
-
-
- {alertActionsList}
- {isAddActionPanelOpen === false ? (
-
-
-
-
- setIsAddActionPanelOpen(true)}
- >
-
-
-
-
-
- ) : null}
- {isAddActionPanelOpen ? (
-
-
-
-
-
+ );
+ }
+
+ const actionParamsErrors: ActionTypeFormProps['actionParamsErrors'] = actionTypeRegistry
+ .get(actionItem.actionTypeId)
+ ?.validateParams(actionItem.params);
+
+ return (
+ {
+ setActiveActionItem({ actionTypeId: actionItem.actionTypeId, index });
+ setAddModalVisibility(true);
+ }}
+ onConnectorSelected={(id: string) => {
+ setActionIdByIndex(id, index);
+ }}
+ onDeleteAction={() => {
+ const updatedActions = actions.filter(
+ (_item: AlertAction, i: number) => i !== index
+ );
+ setAlertProperty(updatedActions);
+ setIsAddActionPanelOpen(
+ updatedActions.filter((item: AlertAction) => item.id !== actionItem.id).length ===
+ 0
+ );
+ setActiveActionItem(undefined);
+ }}
+ />
+ );
+ })}
+
+ {isAddActionPanelOpen ? (
+
+
+
+
+
+
+
+
+
+ {hasDisabledByLicenseActionTypes && (
+
+
+
+
-
-
-
- {hasDisabledByLicenseActionTypes && (
-
-
-
-
-
-
-
-
-
- )}
-
-
-
- {isLoadingActionTypes ? (
-
-
-
- ) : (
- actionTypeNodes
- )}
-
-
- ) : null}
+
+
+
+
+ )}
+
+
+
+ {isLoadingActionTypes ? (
+
+
+
+ ) : (
+ actionTypeNodes
+ )}
+
+ ) : (
+
+
+ setIsAddActionPanelOpen(true)}
+ >
+
+
+
+
)}
- {actionTypesIndex && activeActionItem ? (
+ {actionTypesIndex && activeActionItem && addModalVisible ? (
{
connectors.push(savedAction);
setActionIdByIndex(savedAction.id, activeActionItem.index);
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_type_form.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_type_form.tsx
new file mode 100644
index 0000000000000..38468283b9c19
--- /dev/null
+++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_type_form.tsx
@@ -0,0 +1,339 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React, { Fragment, Suspense, useState } from 'react';
+import { i18n } from '@kbn/i18n';
+import { FormattedMessage } from '@kbn/i18n/react';
+import {
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiIcon,
+ EuiSpacer,
+ EuiFormRow,
+ EuiComboBox,
+ EuiAccordion,
+ EuiButtonIcon,
+ EuiButtonEmpty,
+ EuiIconTip,
+ EuiText,
+ EuiFormLabel,
+ EuiFormControlLayout,
+ EuiSuperSelect,
+ EuiLoadingSpinner,
+ EuiBadge,
+} from '@elastic/eui';
+import { IErrorObject, AlertAction, ActionTypeIndex, ActionConnector } from '../../../types';
+import { checkActionFormActionTypeEnabled } from '../../lib/check_action_type_enabled';
+import { hasSaveActionsCapability } from '../../lib/capabilities';
+import { ActionAccordionFormProps } from './action_form';
+
+export type ActionTypeFormProps = {
+ actionItem: AlertAction;
+ actionConnector: ActionConnector;
+ actionParamsErrors: {
+ errors: IErrorObject;
+ };
+ index: number;
+ onAddConnector: () => void;
+ onConnectorSelected: (id: string) => void;
+ onDeleteAction: () => void;
+ setActionParamsProperty: (key: string, value: any, index: number) => void;
+ actionTypesIndex: ActionTypeIndex;
+ connectors: ActionConnector[];
+} & Pick<
+ ActionAccordionFormProps,
+ | 'defaultActionGroupId'
+ | 'actionGroups'
+ | 'setActionGroupIdByIndex'
+ | 'setActionParamsProperty'
+ | 'http'
+ | 'actionTypeRegistry'
+ | 'toastNotifications'
+ | 'docLinks'
+ | 'messageVariables'
+ | 'defaultActionMessage'
+ | 'capabilities'
+>;
+
+const preconfiguredMessage = i18n.translate(
+ 'xpack.triggersActionsUI.sections.actionForm.preconfiguredTitleMessage',
+ {
+ defaultMessage: '(preconfigured)',
+ }
+);
+
+export const ActionTypeForm = ({
+ actionItem,
+ actionConnector,
+ actionParamsErrors,
+ index,
+ onAddConnector,
+ onConnectorSelected,
+ onDeleteAction,
+ setActionParamsProperty,
+ actionTypesIndex,
+ connectors,
+ http,
+ toastNotifications,
+ docLinks,
+ capabilities,
+ actionTypeRegistry,
+ defaultActionGroupId,
+ defaultActionMessage,
+ messageVariables,
+ actionGroups,
+ setActionGroupIdByIndex,
+}: ActionTypeFormProps) => {
+ const [isOpen, setIsOpen] = useState(true);
+
+ const canSave = hasSaveActionsCapability(capabilities);
+ const getSelectedOptions = (actionItemId: string) => {
+ const selectedConnector = connectors.find((connector) => connector.id === actionItemId);
+ if (
+ !selectedConnector ||
+ // if selected connector is not preconfigured and action type is for preconfiguration only,
+ // do not show regular connectors of this type
+ (actionTypesIndex &&
+ !actionTypesIndex[selectedConnector.actionTypeId].enabledInConfig &&
+ !selectedConnector.isPreconfigured)
+ ) {
+ return [];
+ }
+ const optionTitle = `${selectedConnector.name} ${
+ selectedConnector.isPreconfigured ? preconfiguredMessage : ''
+ }`;
+ return [
+ {
+ label: optionTitle,
+ value: optionTitle,
+ id: actionItemId,
+ 'data-test-subj': 'itemActionConnector',
+ },
+ ];
+ };
+
+ const actionType = actionTypesIndex[actionItem.actionTypeId];
+
+ const optionsList = connectors
+ .filter(
+ (connectorItem) =>
+ connectorItem.actionTypeId === actionItem.actionTypeId &&
+ // include only enabled by config connectors or preconfigured
+ (actionType.enabledInConfig || connectorItem.isPreconfigured)
+ )
+ .map(({ name, id, isPreconfigured }) => ({
+ label: `${name} ${isPreconfigured ? preconfiguredMessage : ''}`,
+ key: id,
+ id,
+ }));
+ const actionTypeRegistered = actionTypeRegistry.get(actionConnector.actionTypeId);
+ if (!actionTypeRegistered) return null;
+
+ const ParamsFieldsComponent = actionTypeRegistered.actionParamsFields;
+ const checkEnabledResult = checkActionFormActionTypeEnabled(
+ actionTypesIndex[actionConnector.actionTypeId],
+ connectors.filter((connector) => connector.isPreconfigured)
+ );
+
+ const defaultActionGroup = actionGroups?.find(({ id }) => id === defaultActionGroupId);
+ const selectedActionGroup =
+ actionGroups?.find(({ id }) => id === actionItem.group) ?? defaultActionGroup;
+
+ const accordionContent = checkEnabledResult.isEnabled ? (
+
+ {actionGroups && selectedActionGroup && setActionGroupIdByIndex && (
+
+
+
+
+
+
+ }
+ >
+ ({
+ value,
+ inputDisplay: name,
+ 'data-test-subj': `addNewActionConnectorActionGroup-${index}-option-${value}`,
+ }))}
+ valueOfSelected={selectedActionGroup.id}
+ onChange={(group) => {
+ setActionGroupIdByIndex(group, index);
+ }}
+ />
+
+
+
+
+
+ )}
+
+
+
+ }
+ labelAppend={
+ canSave &&
+ actionTypesIndex &&
+ actionTypesIndex[actionConnector.actionTypeId].enabledInConfig ? (
+
+
+
+ ) : null
+ }
+ >
+ {
+ onConnectorSelected(selectedOptions[0].id ?? '');
+ }}
+ isClearable={false}
+ />
+
+
+
+
+ {ParamsFieldsComponent ? (
+
+
+
+
+
+ }
+ >
+
+
+ ) : null}
+
+ ) : (
+ checkEnabledResult.messageCard
+ );
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+ {selectedActionGroup && !isOpen && (
+
+ {selectedActionGroup.name}
+
+ )}
+
+ {checkEnabledResult.isEnabled === false && (
+
+
+
+ )}
+
+
+
+
+
+
+ }
+ extraAction={
+
+ }
+ >
+ {accordionContent}
+
+
+
+ );
+};
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_inline.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_inline.tsx
new file mode 100644
index 0000000000000..97baf4a36cb4c
--- /dev/null
+++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_inline.tsx
@@ -0,0 +1,153 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React, { Fragment } from 'react';
+import { i18n } from '@kbn/i18n';
+import { FormattedMessage } from '@kbn/i18n/react';
+import {
+ EuiButton,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiIcon,
+ EuiSpacer,
+ EuiAccordion,
+ EuiButtonIcon,
+ EuiEmptyPrompt,
+ EuiCallOut,
+ EuiText,
+} from '@elastic/eui';
+import { AlertAction, ActionTypeIndex } from '../../../types';
+import { hasSaveActionsCapability } from '../../lib/capabilities';
+import { ActionAccordionFormProps } from './action_form';
+
+type AddConnectorInFormProps = {
+ actionTypesIndex: ActionTypeIndex;
+ actionItem: AlertAction;
+ index: number;
+ onAddConnector: () => void;
+ onDeleteConnector: () => void;
+ emptyActionsIds: string[];
+} & Pick;
+
+export const AddConnectorInline = ({
+ actionTypesIndex,
+ actionItem,
+ index,
+ onAddConnector,
+ onDeleteConnector,
+ actionTypeRegistry,
+ emptyActionsIds,
+ defaultActionGroupId,
+ capabilities,
+}: AddConnectorInFormProps) => {
+ const canSave = hasSaveActionsCapability(capabilities);
+
+ const actionTypeName = actionTypesIndex
+ ? actionTypesIndex[actionItem.actionTypeId].name
+ : actionItem.actionTypeId;
+ const actionTypeRegistered = actionTypeRegistry.get(actionItem.actionTypeId);
+ if (!actionTypeRegistered || actionItem.group !== defaultActionGroupId) return null;
+
+ const noConnectorsLabel = (
+
+ );
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+ }
+ extraAction={
+
+ }
+ paddingSize="l"
+ >
+ {canSave ? (
+ actionItem.id === emptyId) ? (
+ noConnectorsLabel
+ ) : (
+
+ )
+ }
+ actions={[
+
+
+ ,
+ ]}
+ />
+ ) : (
+
+
+
+
+
+ )}
+
+
+
+ );
+};
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_modal.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_modal.test.tsx
index cba9eea3cf3f7..71a3936ed5055 100644
--- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_modal.test.tsx
+++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_modal.test.tsx
@@ -65,8 +65,7 @@ describe('connector_add_modal', () => {
const wrapper = mountWithIntl(
{}}
+ onClose={() => {}}
actionType={actionType}
http={deps!.http}
actionTypeRegistry={deps!.actionTypeRegistry}
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_modal.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_modal.tsx
index 13ec8395aa557..de27256bf566c 100644
--- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_modal.tsx
+++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_modal.tsx
@@ -32,8 +32,7 @@ import {
interface ConnectorAddModalProps {
actionType: ActionType;
- addModalVisible: boolean;
- setAddModalVisibility: React.Dispatch>;
+ onClose: () => void;
postSaveEventHandler?: (savedAction: ActionConnector) => void;
http: HttpSetup;
actionTypeRegistry: ActionTypeRegistryContract;
@@ -48,8 +47,7 @@ interface ConnectorAddModalProps {
export const ConnectorAddModal = ({
actionType,
- addModalVisible,
- setAddModalVisibility,
+ onClose,
postSaveEventHandler,
http,
toastNotifications,
@@ -79,14 +77,11 @@ export const ConnectorAddModal = ({
>(undefined);
const closeModal = useCallback(() => {
- setAddModalVisibility(false);
setConnector(initialConnector);
setServerError(undefined);
- }, [initialConnector, setAddModalVisibility]);
+ onClose();
+ }, [initialConnector, onClose]);
- if (!addModalVisible) {
- return null;
- }
const actionTypeModel = actionTypeRegistry.get(actionType.id);
const errors = {
...actionTypeModel?.validateConnector(connector).errors,
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.tsx
index 9a637ea750f81..20ad9a8d7c701 100644
--- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.tsx
+++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.tsx
@@ -3,7 +3,7 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
-import React, { Fragment, useState, useEffect, Suspense } from 'react';
+import React, { Fragment, useState, useEffect, Suspense, useCallback } from 'react';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import {
@@ -153,9 +153,17 @@ export const AlertForm = ({
setAlertTypeModel(alert.alertTypeId ? alertTypeRegistry.get(alert.alertTypeId) : null);
}, [alert, alertTypeRegistry]);
- const setAlertProperty = (key: string, value: any) => {
- dispatch({ command: { type: 'setProperty' }, payload: { key, value } });
- };
+ const setAlertProperty = useCallback(
+ (key: string, value: any) => {
+ dispatch({ command: { type: 'setProperty' }, payload: { key, value } });
+ },
+ [dispatch]
+ );
+
+ const setActions = useCallback(
+ (updatedActions: AlertAction[]) => setAlertProperty('actions', updatedActions),
+ [setAlertProperty]
+ );
const setAlertParams = (key: string, value: any) => {
dispatch({ command: { type: 'setAlertParams' }, payload: { key, value } });
@@ -169,9 +177,12 @@ export const AlertForm = ({
dispatch({ command: { type: 'setAlertActionProperty' }, payload: { key, value, index } });
};
- const setActionParamsProperty = (key: string, value: any, index: number) => {
- dispatch({ command: { type: 'setAlertActionParams' }, payload: { key, value, index } });
- };
+ const setActionParamsProperty = useCallback(
+ (key: string, value: any, index: number) => {
+ dispatch({ command: { type: 'setAlertActionParams' }, payload: { key, value, index } });
+ },
+ [dispatch]
+ );
const tagsOptions = alert.tags ? alert.tags.map((label: string) => ({ label })) : [];
@@ -202,6 +213,7 @@ export const AlertForm = ({
label={item.name}
onClick={() => {
setAlertProperty('alertTypeId', item.id);
+ setActions([]);
setAlertTypeModel(item);
setAlertProperty('params', {});
if (alertTypesIndex && alertTypesIndex.has(item.id)) {
@@ -289,26 +301,25 @@ export const AlertForm = ({
/>
) : null}
- {canShowActions && defaultActionGroupId ? (
+ {canShowActions &&
+ defaultActionGroupId &&
+ alertTypeModel &&
+ alertTypesIndex?.has(alert.alertTypeId) ? (
- a.name.toUpperCase().localeCompare(b.name.toUpperCase())
- )
- : undefined
- }
+ messageVariables={actionVariablesFromAlertType(
+ alertTypesIndex.get(alert.alertTypeId)!
+ ).sort((a, b) => a.name.toUpperCase().localeCompare(b.name.toUpperCase()))}
defaultActionGroupId={defaultActionGroupId}
+ actionGroups={alertTypesIndex.get(alert.alertTypeId)!.actionGroups}
setActionIdByIndex={(id: string, index: number) => setActionProperty('id', id, index)}
- setAlertProperty={(updatedActions: AlertAction[]) =>
- setAlertProperty('actions', updatedActions)
- }
- setActionParamsProperty={(key: string, value: any, index: number) =>
- setActionParamsProperty(key, value, index)
+ setActionGroupIdByIndex={(group: string, index: number) =>
+ setActionProperty('group', group, index)
}
+ setAlertProperty={setActions}
+ setActionParamsProperty={setActionParamsProperty}
http={http}
actionTypeRegistry={actionTypeRegistry}
defaultActionMessage={alertTypeModel?.defaultActionMessage}
diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alert_create_flyout.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alert_create_flyout.ts
index 7d99d3635106d..ee0de582a9bff 100644
--- a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alert_create_flyout.ts
+++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alert_create_flyout.ts
@@ -55,6 +55,12 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
await nameInput.click();
}
+ async function defineAlwaysFiringAlert(alertName: string) {
+ await pageObjects.triggersActionsUI.clickCreateAlertButton();
+ await testSubjects.setValue('alertNameInput', alertName);
+ await testSubjects.click('test.always-firing-SelectOption');
+ }
+
describe('create alert', function () {
before(async () => {
await pageObjects.common.navigateToApp('triggersActions');
@@ -106,6 +112,57 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
await deleteAlerts(alertsToDelete.map((alertItem: { id: string }) => alertItem.id));
});
+ it('should create an alert with actions in multiple groups', async () => {
+ const alertName = generateUniqueKey();
+ await defineAlwaysFiringAlert(alertName);
+
+ // create Slack connector and attach an action using it
+ await testSubjects.click('.slack-ActionTypeSelectOption');
+ await testSubjects.click('addNewActionConnectorButton-.slack');
+ const slackConnectorName = generateUniqueKey();
+ await testSubjects.setValue('nameInput', slackConnectorName);
+ await testSubjects.setValue('slackWebhookUrlInput', 'https://test');
+ await find.clickByCssSelector('[data-test-subj="saveActionButtonModal"]:not(disabled)');
+ const createdConnectorToastTitle = await pageObjects.common.closeToast();
+ expect(createdConnectorToastTitle).to.eql(`Created '${slackConnectorName}'`);
+ await testSubjects.setValue('messageTextArea', 'test message ');
+ await (
+ await find.byCssSelector(
+ '[data-test-subj="alertActionAccordion-0"] [data-test-subj="messageTextArea"]'
+ )
+ ).type('some text ');
+
+ await testSubjects.click('addAlertActionButton');
+ await testSubjects.click('.slack-ActionTypeSelectOption');
+ await testSubjects.setValue('messageTextArea', 'test message ');
+ await (
+ await find.byCssSelector(
+ '[data-test-subj="alertActionAccordion-1"] [data-test-subj="messageTextArea"]'
+ )
+ ).type('some text ');
+
+ await testSubjects.click('addNewActionConnectorActionGroup-1');
+ await testSubjects.click('addNewActionConnectorActionGroup-1-option-other');
+
+ await testSubjects.click('saveAlertButton');
+ const toastTitle = await pageObjects.common.closeToast();
+ expect(toastTitle).to.eql(`Created alert "${alertName}"`);
+ await pageObjects.triggersActionsUI.searchAlerts(alertName);
+ const searchResultsAfterSave = await pageObjects.triggersActionsUI.getAlertsList();
+ expect(searchResultsAfterSave).to.eql([
+ {
+ name: alertName,
+ tagsText: '',
+ alertType: 'Always Firing',
+ interval: '1m',
+ },
+ ]);
+
+ // clean up created alert
+ const alertsToDelete = await getAlertsByName(alertName);
+ await deleteAlerts(alertsToDelete.map((alertItem: { id: string }) => alertItem.id));
+ });
+
it('should show save confirmation before creating alert with no actions', async () => {
const alertName = generateUniqueKey();
await defineAlert(alertName);
diff --git a/x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/server/plugin.ts b/x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/server/plugin.ts
index e3927f6bfffb9..6f9d010378624 100644
--- a/x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/server/plugin.ts
+++ b/x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/server/plugin.ts
@@ -78,6 +78,7 @@ function createAlwaysFiringAlertType(alerts: AlertingSetup) {
{ id: 'default', name: 'Default' },
{ id: 'other', name: 'Other' },
],
+ defaultActionGroupId: 'default',
producer: 'alerts',
async executor(alertExecutorOptions: any) {
const { services, state, params } = alertExecutorOptions;
From d1ef0d6704237cade5ff6a4246e42148dadb0b9e Mon Sep 17 00:00:00 2001
From: Tiago Costa
Date: Mon, 9 Nov 2020 13:11:51 +0000
Subject: [PATCH 3/8] skip flaky suite (#57426)
---
.../functional_with_es_ssl/apps/triggers_actions_ui/details.ts | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/details.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/details.ts
index 9e4006681dc8d..1d86d95b7a796 100644
--- a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/details.ts
+++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/details.ts
@@ -306,7 +306,8 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
});
});
- describe('Alert Instances', function () {
+ // FLAKY: https://github.com/elastic/kibana/issues/57426
+ describe.skip('Alert Instances', function () {
const testRunUuid = uuid.v4();
let alert: any;
From f2f76e104af5d0515773c173475efda7d80c1d31 Mon Sep 17 00:00:00 2001
From: Jean-Louis Leysens
Date: Mon, 9 Nov 2020 14:29:53 +0100
Subject: [PATCH 4/8] [ILM] Migrate Delete phase and name field to Form Lib
(#82834)
* remove use of legacy state system and legacy serialization
* remove legacy min_age input component and re-add missing import
* rename shared -> shared_fields for more clarity
* some more cleanup and fixing regressions on policy name for creating new policy from existing policy
* move extract policy static code to lib folder and remove "policies" dir from services
* fix jest tests and minor policy flyout inconsistency
* remove legacy helper
* fix client integration tests
* fix min for set index priority
* moved save policy function into edit policy section
* remove unused translations
* refactor form files to own edit_policy/form folder
* remove "fix errors" badge to fix UX - users can see errors in line before pressing save so the value of this badge has diminished
* fix i18n after removing phase error badge
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
---
.../edit_policy/edit_policy.helpers.tsx | 5 +
.../edit_policy/edit_policy.test.ts | 3 +-
.../__jest__/components/edit_policy.test.tsx | 242 ++++----
.../public/application/lib/policies.ts | 32 ++
.../sections/edit_policy/components/index.ts | 3 -
.../components/min_age_input_legacy.tsx | 263 ---------
.../components/phase_error_message.tsx | 19 -
.../phases/cold_phase/cold_phase.tsx | 23 +-
.../{ => delete_phase}/delete_phase.tsx | 94 +--
.../components/phases/delete_phase/index.ts | 7 +
.../components/phases/hot_phase/hot_phase.tsx | 12 +-
.../components/cloud_data_tier_callout.tsx | 0
.../components/data_tier_allocation.scss | 0
.../components/data_tier_allocation.tsx | 0
.../components/default_allocation_notice.tsx | 0
.../components/index.ts | 0
.../components/no_node_attributes_warning.tsx | 0
.../components/node_allocation.tsx | 9 +-
.../components/node_attrs_details.tsx | 0
.../components/node_data_provider.tsx | 0
.../components/types.ts | 0
.../data_tier_allocation_field.tsx | 0
.../data_tier_allocation_field/index.ts | 0
.../forcemerge_field.tsx | 6 +-
.../phases/{shared => shared_fields}/index.ts | 2 +
.../min_age_input_field/index.ts | 0
.../min_age_input_field.tsx | 0
.../min_age_input_field/util.ts | 0
.../set_priority_input.tsx | 9 +-
.../snapshot_policies_field.tsx} | 113 ++--
.../phases/warm_phase/warm_phase.tsx | 27 +-
.../components/policy_json_flyout.tsx | 22 +-
.../edit_policy/edit_policy.container.tsx | 23 +-
.../sections/edit_policy/edit_policy.tsx | 537 ++++++++----------
.../edit_policy/edit_policy_context.tsx | 12 +-
.../edit_policy/{ => form}/deserializer.ts | 21 +-
.../sections/edit_policy/form/index.ts | 13 +
.../{form_schema.ts => form/schema.ts} | 53 +-
.../edit_policy/{ => form}/serializer.ts | 24 +-
.../validations.ts} | 77 ++-
.../sections/edit_policy/i18n_texts.ts | 36 ++
.../edit_policy/save_policy.ts} | 15 +-
.../application/sections/edit_policy/types.ts | 5 +
.../services/policies/delete_phase.ts | 88 ---
.../policies/policy_serialization.test.ts | 198 -------
.../services/policies/policy_serialization.ts | 82 ---
.../services/policies/policy_validation.ts | 144 -----
.../public/shared_imports.ts | 3 +
.../translations/translations/ja-JP.json | 6 -
.../translations/translations/zh-CN.json | 6 -
50 files changed, 749 insertions(+), 1485 deletions(-)
create mode 100644 x-pack/plugins/index_lifecycle_management/public/application/lib/policies.ts
delete mode 100644 x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/min_age_input_legacy.tsx
delete mode 100644 x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phase_error_message.tsx
rename x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/{ => delete_phase}/delete_phase.tsx (50%)
create mode 100644 x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/delete_phase/index.ts
rename x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/{shared => shared_fields}/data_tier_allocation_field/components/cloud_data_tier_callout.tsx (100%)
rename x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/{shared => shared_fields}/data_tier_allocation_field/components/data_tier_allocation.scss (100%)
rename x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/{shared => shared_fields}/data_tier_allocation_field/components/data_tier_allocation.tsx (100%)
rename x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/{shared => shared_fields}/data_tier_allocation_field/components/default_allocation_notice.tsx (100%)
rename x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/{shared => shared_fields}/data_tier_allocation_field/components/index.ts (100%)
rename x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/{shared => shared_fields}/data_tier_allocation_field/components/no_node_attributes_warning.tsx (100%)
rename x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/{shared => shared_fields}/data_tier_allocation_field/components/node_allocation.tsx (90%)
rename x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/{shared => shared_fields}/data_tier_allocation_field/components/node_attrs_details.tsx (100%)
rename x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/{shared => shared_fields}/data_tier_allocation_field/components/node_data_provider.tsx (100%)
rename x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/{shared => shared_fields}/data_tier_allocation_field/components/types.ts (100%)
rename x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/{shared => shared_fields}/data_tier_allocation_field/data_tier_allocation_field.tsx (100%)
rename x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/{shared => shared_fields}/data_tier_allocation_field/index.ts (100%)
rename x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/{shared => shared_fields}/forcemerge_field.tsx (94%)
rename x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/{shared => shared_fields}/index.ts (88%)
rename x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/{shared => shared_fields}/min_age_input_field/index.ts (100%)
rename x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/{shared => shared_fields}/min_age_input_field/min_age_input_field.tsx (100%)
rename x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/{shared => shared_fields}/min_age_input_field/util.ts (100%)
rename x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/{shared => shared_fields}/set_priority_input.tsx (83%)
rename x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/{snapshot_policies.tsx => phases/shared_fields/snapshot_policies_field.tsx} (68%)
rename x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/{ => form}/deserializer.ts (82%)
create mode 100644 x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/index.ts
rename x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/{form_schema.ts => form/schema.ts} (90%)
rename x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/{ => form}/serializer.ts (90%)
rename x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/{form_validations.ts => form/validations.ts} (50%)
rename x-pack/plugins/index_lifecycle_management/public/application/{services/policies/policy_save.ts => sections/edit_policy/save_policy.ts} (84%)
delete mode 100644 x-pack/plugins/index_lifecycle_management/public/application/services/policies/delete_phase.ts
delete mode 100644 x-pack/plugins/index_lifecycle_management/public/application/services/policies/policy_serialization.test.ts
delete mode 100644 x-pack/plugins/index_lifecycle_management/public/application/services/policies/policy_serialization.ts
delete mode 100644 x-pack/plugins/index_lifecycle_management/public/application/services/policies/policy_validation.ts
diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.helpers.tsx b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.helpers.tsx
index 0b9f47e188d15..646978dd68153 100644
--- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.helpers.tsx
+++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.helpers.tsx
@@ -221,6 +221,11 @@ export const setup = async () => {
setFreeze,
setIndexPriority: setIndexPriority('cold'),
},
+ delete: {
+ enable: enable('delete'),
+ setMinAgeValue: setMinAgeValue('delete'),
+ setMinAgeUnits: setMinAgeUnits('delete'),
+ },
},
};
};
diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.test.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.test.ts
index 11fadf51f27f8..4ee67d1ed8a19 100644
--- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.test.ts
+++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.test.ts
@@ -367,7 +367,6 @@ describe(' ', () => {
expect(testBed.find('snapshotPolicyCombobox').prop('data-currentvalue')).toEqual([
{
label: DELETE_PHASE_POLICY.policy.phases.delete?.actions.wait_for_snapshot?.policy,
- value: DELETE_PHASE_POLICY.policy.phases.delete?.actions.wait_for_snapshot?.policy,
},
]);
});
@@ -412,7 +411,7 @@ describe(' ', () => {
test('wait for snapshot field should delete action if field is empty', async () => {
const { actions } = testBed;
- actions.setWaitForSnapshotPolicy('');
+ await actions.setWaitForSnapshotPolicy('');
await actions.savePolicy();
const expected = {
diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/components/edit_policy.test.tsx b/x-pack/plugins/index_lifecycle_management/__jest__/components/edit_policy.test.tsx
index 4a3fedfb264ac..43910583ceec9 100644
--- a/x-pack/plugins/index_lifecycle_management/__jest__/components/edit_policy.test.tsx
+++ b/x-pack/plugins/index_lifecycle_management/__jest__/components/edit_policy.test.tsx
@@ -20,27 +20,27 @@ import {
notificationServiceMock,
fatalErrorsServiceMock,
} from '../../../../../src/core/public/mocks';
+
import { usageCollectionPluginMock } from '../../../../../src/plugins/usage_collection/public/mocks';
+
import { CloudSetup } from '../../../cloud/public';
import { EditPolicy } from '../../public/application/sections/edit_policy/edit_policy';
+import {
+ EditPolicyContextProvider,
+ EditPolicyContextValue,
+} from '../../public/application/sections/edit_policy/edit_policy_context';
+
import { KibanaContextProvider } from '../../public/shared_imports';
+
import { init as initHttp } from '../../public/application/services/http';
import { init as initUiMetric } from '../../public/application/services/ui_metric';
import { init as initNotification } from '../../public/application/services/notification';
import { PolicyFromES } from '../../common/types';
-import {
- positiveNumberRequiredMessage,
- policyNameRequiredMessage,
- policyNameStartsWithUnderscoreErrorMessage,
- policyNameContainsCommaErrorMessage,
- policyNameContainsSpaceErrorMessage,
- policyNameMustBeDifferentErrorMessage,
- policyNameAlreadyUsedErrorMessage,
-} from '../../public/application/services/policies/policy_validation';
import { i18nTexts } from '../../public/application/sections/edit_policy/i18n_texts';
import { editPolicyHelpers } from './helpers';
+import { defaultPolicy } from '../../public/application/constants';
// @ts-ignore
initHttp(axios.create({ adapter: axiosXhrAdapter }));
@@ -122,14 +122,11 @@ const noRollover = async (rendered: ReactWrapper) => {
const getNodeAttributeSelect = (rendered: ReactWrapper, phase: string) => {
return findTestSubject(rendered, `${phase}-selectedNodeAttrs`);
};
-const setPolicyName = (rendered: ReactWrapper, policyName: string) => {
+const setPolicyName = async (rendered: ReactWrapper, policyName: string) => {
const policyNameField = findTestSubject(rendered, 'policyNameField');
- policyNameField.simulate('change', { target: { value: policyName } });
- rendered.update();
-};
-const setPhaseAfterLegacy = (rendered: ReactWrapper, phase: string, after: string | number) => {
- const afterInput = rendered.find(`input#${phase}-selectedMinimumAge`);
- afterInput.simulate('change', { target: { value: after } });
+ await act(async () => {
+ policyNameField.simulate('change', { target: { value: policyName } });
+ });
rendered.update();
};
const setPhaseAfter = async (rendered: ReactWrapper, phase: string, after: string | number) => {
@@ -157,6 +154,32 @@ const save = async (rendered: ReactWrapper) => {
});
rendered.update();
};
+
+const MyComponent = ({
+ isCloudEnabled,
+ isNewPolicy,
+ policy: _policy,
+ existingPolicies,
+ getUrlForApp,
+ policyName,
+}: EditPolicyContextValue & { isCloudEnabled: boolean }) => {
+ return (
+
+
+
+
+
+ );
+};
+
describe('edit policy', () => {
beforeAll(() => {
jest.useFakeTimers();
@@ -179,14 +202,14 @@ describe('edit policy', () => {
beforeEach(() => {
component = (
-
-
-
+
);
({ http } = editPolicyHelpers.setup());
@@ -198,62 +221,78 @@ describe('edit policy', () => {
test('should show error when trying to save empty form', async () => {
const rendered = mountWithIntl(component);
await save(rendered);
- expectedErrorMessages(rendered, [policyNameRequiredMessage]);
+ expectedErrorMessages(rendered, [i18nTexts.editPolicy.errors.policyNameRequiredMessage]);
});
test('should show error when trying to save policy name with space', async () => {
const rendered = mountWithIntl(component);
await noRollover(rendered);
- setPolicyName(rendered, 'my policy');
- await save(rendered);
- expectedErrorMessages(rendered, [policyNameContainsSpaceErrorMessage]);
+ await setPolicyName(rendered, 'my policy');
+ waitForFormLibValidation(rendered);
+ expectedErrorMessages(rendered, [i18nTexts.editPolicy.errors.policyNameContainsInvalidChars]);
});
test('should show error when trying to save policy name that is already used', async () => {
const rendered = mountWithIntl(component);
await noRollover(rendered);
- setPolicyName(rendered, 'testy0');
- rendered.update();
- await save(rendered);
- expectedErrorMessages(rendered, [policyNameAlreadyUsedErrorMessage]);
+ await setPolicyName(rendered, 'testy0');
+ waitForFormLibValidation(rendered);
+ expectedErrorMessages(rendered, [
+ i18nTexts.editPolicy.errors.policyNameAlreadyUsedErrorMessage,
+ ]);
});
test('should show error when trying to save as new policy but using the same name', async () => {
component = (
-
);
const rendered = mountWithIntl(component);
findTestSubject(rendered, 'saveAsNewSwitch').simulate('click');
rendered.update();
- setPolicyName(rendered, 'testy0');
- await save(rendered);
- expectedErrorMessages(rendered, [policyNameMustBeDifferentErrorMessage]);
+ await setPolicyName(rendered, 'testy0');
+ waitForFormLibValidation(rendered);
+ expectedErrorMessages(rendered, [
+ i18nTexts.editPolicy.errors.policyNameAlreadyUsedErrorMessage,
+ ]);
});
test('should show error when trying to save policy name with comma', async () => {
const rendered = mountWithIntl(component);
await noRollover(rendered);
- setPolicyName(rendered, 'my,policy');
- await save(rendered);
- expectedErrorMessages(rendered, [policyNameContainsCommaErrorMessage]);
+ await setPolicyName(rendered, 'my,policy');
+ waitForFormLibValidation(rendered);
+ expectedErrorMessages(rendered, [i18nTexts.editPolicy.errors.policyNameContainsInvalidChars]);
});
test('should show error when trying to save policy name starting with underscore', async () => {
const rendered = mountWithIntl(component);
await noRollover(rendered);
- setPolicyName(rendered, '_mypolicy');
- await save(rendered);
- expectedErrorMessages(rendered, [policyNameStartsWithUnderscoreErrorMessage]);
+ await setPolicyName(rendered, '_mypolicy');
+ waitForFormLibValidation(rendered);
+ expectedErrorMessages(rendered, [
+ i18nTexts.editPolicy.errors.policyNameStartsWithUnderscoreErrorMessage,
+ ]);
});
test('should show correct json in policy flyout', async () => {
- const rendered = mountWithIntl(component);
+ const rendered = mountWithIntl(
+
+ );
await act(async () => {
findTestSubject(rendered, 'requestButton').simulate('click');
});
rendered.update();
+
const json = rendered.find(`code`).text();
- const expected = `PUT _ilm/policy/\n${JSON.stringify(
+ const expected = `PUT _ilm/policy/my-policy\n${JSON.stringify(
{
policy: {
phases: {
@@ -282,7 +321,7 @@ describe('edit policy', () => {
test('should show errors when trying to save with no max size and no max age', async () => {
const rendered = mountWithIntl(component);
expect(findTestSubject(rendered, 'rolloverSettingsRequired').exists()).toBeFalsy();
- setPolicyName(rendered, 'mypolicy');
+ await setPolicyName(rendered, 'mypolicy');
const maxSizeInput = findTestSubject(rendered, 'hot-selectedMaxSizeStored');
await act(async () => {
maxSizeInput.simulate('change', { target: { value: '' } });
@@ -298,7 +337,7 @@ describe('edit policy', () => {
});
test('should show number above 0 required error when trying to save with -1 for max size', async () => {
const rendered = mountWithIntl(component);
- setPolicyName(rendered, 'mypolicy');
+ await setPolicyName(rendered, 'mypolicy');
const maxSizeInput = findTestSubject(rendered, 'hot-selectedMaxSizeStored');
await act(async () => {
maxSizeInput.simulate('change', { target: { value: '-1' } });
@@ -309,7 +348,7 @@ describe('edit policy', () => {
});
test('should show number above 0 required error when trying to save with 0 for max size', async () => {
const rendered = mountWithIntl(component);
- setPolicyName(rendered, 'mypolicy');
+ await setPolicyName(rendered, 'mypolicy');
const maxSizeInput = findTestSubject(rendered, 'hot-selectedMaxSizeStored');
await act(async () => {
maxSizeInput.simulate('change', { target: { value: '-1' } });
@@ -319,7 +358,7 @@ describe('edit policy', () => {
});
test('should show number above 0 required error when trying to save with -1 for max age', async () => {
const rendered = mountWithIntl(component);
- setPolicyName(rendered, 'mypolicy');
+ await setPolicyName(rendered, 'mypolicy');
const maxAgeInput = findTestSubject(rendered, 'hot-selectedMaxAge');
await act(async () => {
maxAgeInput.simulate('change', { target: { value: '-1' } });
@@ -329,7 +368,7 @@ describe('edit policy', () => {
});
test('should show number above 0 required error when trying to save with 0 for max age', async () => {
const rendered = mountWithIntl(component);
- setPolicyName(rendered, 'mypolicy');
+ await setPolicyName(rendered, 'mypolicy');
const maxAgeInput = findTestSubject(rendered, 'hot-selectedMaxAge');
await act(async () => {
maxAgeInput.simulate('change', { target: { value: '0' } });
@@ -337,21 +376,21 @@ describe('edit policy', () => {
waitForFormLibValidation(rendered);
expectedErrorMessages(rendered, [i18nTexts.editPolicy.errors.numberGreatThan0Required]);
});
- test('should show forcemerge input when rollover enabled', () => {
+ test('should show forcemerge input when rollover enabled', async () => {
const rendered = mountWithIntl(component);
- setPolicyName(rendered, 'mypolicy');
+ await setPolicyName(rendered, 'mypolicy');
expect(findTestSubject(rendered, 'hot-forceMergeSwitch').exists()).toBeTruthy();
});
test('should hide forcemerge input when rollover is disabled', async () => {
const rendered = mountWithIntl(component);
- setPolicyName(rendered, 'mypolicy');
+ await setPolicyName(rendered, 'mypolicy');
await noRollover(rendered);
waitForFormLibValidation(rendered);
expect(findTestSubject(rendered, 'hot-forceMergeSwitch').exists()).toBeFalsy();
});
test('should show positive number required above zero error when trying to save hot phase with 0 for force merge', async () => {
const rendered = mountWithIntl(component);
- setPolicyName(rendered, 'mypolicy');
+ await setPolicyName(rendered, 'mypolicy');
act(() => {
findTestSubject(rendered, 'hot-forceMergeSwitch').simulate('click');
});
@@ -365,7 +404,7 @@ describe('edit policy', () => {
});
test('should show positive number above 0 required error when trying to save hot phase with -1 for force merge', async () => {
const rendered = mountWithIntl(component);
- setPolicyName(rendered, 'mypolicy');
+ await setPolicyName(rendered, 'mypolicy');
findTestSubject(rendered, 'hot-forceMergeSwitch').simulate('click');
rendered.update();
const forcemergeInput = findTestSubject(rendered, 'hot-selectedForceMergeSegments');
@@ -379,7 +418,7 @@ describe('edit policy', () => {
test('should show positive number required error when trying to save with -1 for index priority', async () => {
const rendered = mountWithIntl(component);
await noRollover(rendered);
- setPolicyName(rendered, 'mypolicy');
+ await setPolicyName(rendered, 'mypolicy');
await setPhaseIndexPriority(rendered, 'hot', '-1');
waitForFormLibValidation(rendered);
expectedErrorMessages(rendered, [i18nTexts.editPolicy.errors.nonNegativeNumberRequired]);
@@ -397,7 +436,7 @@ describe('edit policy', () => {
test('should show number required error when trying to save empty warm phase', async () => {
const rendered = mountWithIntl(component);
await noRollover(rendered);
- setPolicyName(rendered, 'mypolicy');
+ await setPolicyName(rendered, 'mypolicy');
await activatePhase(rendered, 'warm');
await setPhaseAfter(rendered, 'warm', '');
waitForFormLibValidation(rendered);
@@ -406,7 +445,7 @@ describe('edit policy', () => {
test('should allow 0 for phase timing', async () => {
const rendered = mountWithIntl(component);
await noRollover(rendered);
- setPolicyName(rendered, 'mypolicy');
+ await setPolicyName(rendered, 'mypolicy');
await activatePhase(rendered, 'warm');
await setPhaseAfter(rendered, 'warm', '0');
waitForFormLibValidation(rendered);
@@ -415,7 +454,7 @@ describe('edit policy', () => {
test('should show positive number required error when trying to save warm phase with -1 for after', async () => {
const rendered = mountWithIntl(component);
await noRollover(rendered);
- setPolicyName(rendered, 'mypolicy');
+ await setPolicyName(rendered, 'mypolicy');
await activatePhase(rendered, 'warm');
await setPhaseAfter(rendered, 'warm', '-1');
waitForFormLibValidation(rendered);
@@ -424,7 +463,7 @@ describe('edit policy', () => {
test('should show positive number required error when trying to save warm phase with -1 for index priority', async () => {
const rendered = mountWithIntl(component);
await noRollover(rendered);
- setPolicyName(rendered, 'mypolicy');
+ await setPolicyName(rendered, 'mypolicy');
await activatePhase(rendered, 'warm');
await setPhaseAfter(rendered, 'warm', '1');
await setPhaseAfter(rendered, 'warm', '-1');
@@ -434,7 +473,7 @@ describe('edit policy', () => {
test('should show positive number required above zero error when trying to save warm phase with 0 for shrink', async () => {
const rendered = mountWithIntl(component);
await noRollover(rendered);
- setPolicyName(rendered, 'mypolicy');
+ await setPolicyName(rendered, 'mypolicy');
await activatePhase(rendered, 'warm');
act(() => {
findTestSubject(rendered, 'shrinkSwitch').simulate('click');
@@ -451,7 +490,7 @@ describe('edit policy', () => {
test('should show positive number above 0 required error when trying to save warm phase with -1 for shrink', async () => {
const rendered = mountWithIntl(component);
await noRollover(rendered);
- setPolicyName(rendered, 'mypolicy');
+ await setPolicyName(rendered, 'mypolicy');
await activatePhase(rendered, 'warm');
await setPhaseAfter(rendered, 'warm', '1');
act(() => {
@@ -468,7 +507,7 @@ describe('edit policy', () => {
test('should show positive number required above zero error when trying to save warm phase with 0 for force merge', async () => {
const rendered = mountWithIntl(component);
await noRollover(rendered);
- setPolicyName(rendered, 'mypolicy');
+ await setPolicyName(rendered, 'mypolicy');
await activatePhase(rendered, 'warm');
await setPhaseAfter(rendered, 'warm', '1');
act(() => {
@@ -485,7 +524,7 @@ describe('edit policy', () => {
test('should show positive number above 0 required error when trying to save warm phase with -1 for force merge', async () => {
const rendered = mountWithIntl(component);
await noRollover(rendered);
- setPolicyName(rendered, 'mypolicy');
+ await setPolicyName(rendered, 'mypolicy');
await activatePhase(rendered, 'warm');
await setPhaseAfter(rendered, 'warm', '1');
await act(async () => {
@@ -503,7 +542,7 @@ describe('edit policy', () => {
server.respondImmediately = false;
const rendered = mountWithIntl(component);
await noRollover(rendered);
- setPolicyName(rendered, 'mypolicy');
+ await setPolicyName(rendered, 'mypolicy');
await activatePhase(rendered, 'warm');
expect(rendered.find('.euiLoadingSpinner').exists()).toBeTruthy();
expect(rendered.find('.euiCallOut--warning').exists()).toBeFalsy();
@@ -517,7 +556,7 @@ describe('edit policy', () => {
});
const rendered = mountWithIntl(component);
await noRollover(rendered);
- setPolicyName(rendered, 'mypolicy');
+ await setPolicyName(rendered, 'mypolicy');
await activatePhase(rendered, 'warm');
expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy();
await openNodeAttributesSection(rendered, 'warm');
@@ -527,7 +566,7 @@ describe('edit policy', () => {
test('should show node attributes input when attributes exist', async () => {
const rendered = mountWithIntl(component);
await noRollover(rendered);
- setPolicyName(rendered, 'mypolicy');
+ await setPolicyName(rendered, 'mypolicy');
await activatePhase(rendered, 'warm');
expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy();
await openNodeAttributesSection(rendered, 'warm');
@@ -539,7 +578,7 @@ describe('edit policy', () => {
test('should show view node attributes link when attribute selected and show flyout when clicked', async () => {
const rendered = mountWithIntl(component);
await noRollover(rendered);
- setPolicyName(rendered, 'mypolicy');
+ await setPolicyName(rendered, 'mypolicy');
await activatePhase(rendered, 'warm');
expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy();
await openNodeAttributesSection(rendered, 'warm');
@@ -568,7 +607,7 @@ describe('edit policy', () => {
});
const rendered = mountWithIntl(component);
await noRollover(rendered);
- setPolicyName(rendered, 'mypolicy');
+ await setPolicyName(rendered, 'mypolicy');
await activatePhase(rendered, 'warm');
expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy();
expect(findTestSubject(rendered, 'defaultAllocationWarning').exists()).toBeTruthy();
@@ -581,7 +620,7 @@ describe('edit policy', () => {
});
const rendered = mountWithIntl(component);
await noRollover(rendered);
- setPolicyName(rendered, 'mypolicy');
+ await setPolicyName(rendered, 'mypolicy');
await activatePhase(rendered, 'warm');
expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy();
expect(findTestSubject(rendered, 'defaultAllocationNotice').exists()).toBeTruthy();
@@ -594,7 +633,7 @@ describe('edit policy', () => {
});
const rendered = mountWithIntl(component);
await noRollover(rendered);
- setPolicyName(rendered, 'mypolicy');
+ await setPolicyName(rendered, 'mypolicy');
await activatePhase(rendered, 'warm');
expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy();
expect(findTestSubject(rendered, 'defaultAllocationNotice').exists()).toBeFalsy();
@@ -611,7 +650,7 @@ describe('edit policy', () => {
test('should allow 0 for phase timing', async () => {
const rendered = mountWithIntl(component);
await noRollover(rendered);
- setPolicyName(rendered, 'mypolicy');
+ await setPolicyName(rendered, 'mypolicy');
await activatePhase(rendered, 'cold');
await setPhaseAfter(rendered, 'cold', '0');
waitForFormLibValidation(rendered);
@@ -621,7 +660,7 @@ describe('edit policy', () => {
test('should show positive number required error when trying to save cold phase with -1 for after', async () => {
const rendered = mountWithIntl(component);
await noRollover(rendered);
- setPolicyName(rendered, 'mypolicy');
+ await setPolicyName(rendered, 'mypolicy');
await activatePhase(rendered, 'cold');
await setPhaseAfter(rendered, 'cold', '-1');
waitForFormLibValidation(rendered);
@@ -631,7 +670,7 @@ describe('edit policy', () => {
server.respondImmediately = false;
const rendered = mountWithIntl(component);
await noRollover(rendered);
- setPolicyName(rendered, 'mypolicy');
+ await setPolicyName(rendered, 'mypolicy');
await activatePhase(rendered, 'cold');
expect(rendered.find('.euiLoadingSpinner').exists()).toBeTruthy();
expect(rendered.find('.euiCallOut--warning').exists()).toBeFalsy();
@@ -645,7 +684,7 @@ describe('edit policy', () => {
});
const rendered = mountWithIntl(component);
await noRollover(rendered);
- setPolicyName(rendered, 'mypolicy');
+ await setPolicyName(rendered, 'mypolicy');
await activatePhase(rendered, 'cold');
expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy();
await openNodeAttributesSection(rendered, 'cold');
@@ -655,7 +694,7 @@ describe('edit policy', () => {
test('should show node attributes input when attributes exist', async () => {
const rendered = mountWithIntl(component);
await noRollover(rendered);
- setPolicyName(rendered, 'mypolicy');
+ await setPolicyName(rendered, 'mypolicy');
await activatePhase(rendered, 'cold');
expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy();
await openNodeAttributesSection(rendered, 'cold');
@@ -667,7 +706,7 @@ describe('edit policy', () => {
test('should show view node attributes link when attribute selected and show flyout when clicked', async () => {
const rendered = mountWithIntl(component);
await noRollover(rendered);
- setPolicyName(rendered, 'mypolicy');
+ await setPolicyName(rendered, 'mypolicy');
await activatePhase(rendered, 'cold');
expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy();
await openNodeAttributesSection(rendered, 'cold');
@@ -689,7 +728,7 @@ describe('edit policy', () => {
test('should show positive number required error when trying to save with -1 for index priority', async () => {
const rendered = mountWithIntl(component);
await noRollover(rendered);
- setPolicyName(rendered, 'mypolicy');
+ await setPolicyName(rendered, 'mypolicy');
await activatePhase(rendered, 'cold');
await setPhaseAfter(rendered, 'cold', '1');
await setPhaseIndexPriority(rendered, 'cold', '-1');
@@ -704,7 +743,7 @@ describe('edit policy', () => {
});
const rendered = mountWithIntl(component);
await noRollover(rendered);
- setPolicyName(rendered, 'mypolicy');
+ await setPolicyName(rendered, 'mypolicy');
await activatePhase(rendered, 'cold');
expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy();
expect(findTestSubject(rendered, 'defaultAllocationWarning').exists()).toBeTruthy();
@@ -717,7 +756,7 @@ describe('edit policy', () => {
});
const rendered = mountWithIntl(component);
await noRollover(rendered);
- setPolicyName(rendered, 'mypolicy');
+ await setPolicyName(rendered, 'mypolicy');
await activatePhase(rendered, 'cold');
expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy();
expect(findTestSubject(rendered, 'defaultAllocationNotice').exists()).toBeTruthy();
@@ -730,7 +769,7 @@ describe('edit policy', () => {
});
const rendered = mountWithIntl(component);
await noRollover(rendered);
- setPolicyName(rendered, 'mypolicy');
+ await setPolicyName(rendered, 'mypolicy');
await activatePhase(rendered, 'cold');
expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy();
expect(findTestSubject(rendered, 'defaultAllocationNotice').exists()).toBeFalsy();
@@ -740,20 +779,20 @@ describe('edit policy', () => {
test('should allow 0 for phase timing', async () => {
const rendered = mountWithIntl(component);
await noRollover(rendered);
- setPolicyName(rendered, 'mypolicy');
+ await setPolicyName(rendered, 'mypolicy');
await activatePhase(rendered, 'delete');
- setPhaseAfterLegacy(rendered, 'delete', '0');
- await save(rendered);
+ await setPhaseAfter(rendered, 'delete', '0');
+ waitForFormLibValidation(rendered);
expectedErrorMessages(rendered, []);
});
test('should show positive number required error when trying to save delete phase with -1 for after', async () => {
const rendered = mountWithIntl(component);
await noRollover(rendered);
- setPolicyName(rendered, 'mypolicy');
+ await setPolicyName(rendered, 'mypolicy');
await activatePhase(rendered, 'delete');
- setPhaseAfterLegacy(rendered, 'delete', '-1');
- await save(rendered);
- expectedErrorMessages(rendered, [positiveNumberRequiredMessage]);
+ await setPhaseAfter(rendered, 'delete', '-1');
+ waitForFormLibValidation(rendered);
+ expectedErrorMessages(rendered, [i18nTexts.editPolicy.errors.nonNegativeNumberRequired]);
});
});
describe('not on cloud', () => {
@@ -768,7 +807,7 @@ describe('edit policy', () => {
});
const rendered = mountWithIntl(component);
await noRollover(rendered);
- setPolicyName(rendered, 'mypolicy');
+ await setPolicyName(rendered, 'mypolicy');
await activatePhase(rendered, 'warm');
expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy();
@@ -782,14 +821,13 @@ describe('edit policy', () => {
describe('on cloud', () => {
beforeEach(() => {
component = (
-
-
-
+
);
({ http } = editPolicyHelpers.setup());
({ server, httpRequestsMockHelpers } = http);
@@ -808,7 +846,7 @@ describe('edit policy', () => {
});
const rendered = mountWithIntl(component);
await noRollover(rendered);
- setPolicyName(rendered, 'mypolicy');
+ await setPolicyName(rendered, 'mypolicy');
await activatePhase(rendered, 'warm');
expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy();
@@ -829,7 +867,7 @@ describe('edit policy', () => {
});
const rendered = mountWithIntl(component);
await noRollover(rendered);
- setPolicyName(rendered, 'mypolicy');
+ await setPolicyName(rendered, 'mypolicy');
await activatePhase(rendered, 'warm');
expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy();
@@ -849,7 +887,7 @@ describe('edit policy', () => {
});
const rendered = mountWithIntl(component);
await noRollover(rendered);
- setPolicyName(rendered, 'mypolicy');
+ await setPolicyName(rendered, 'mypolicy');
await activatePhase(rendered, 'cold');
expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy();
expect(findTestSubject(rendered, 'cloudDataTierCallout').exists()).toBeTruthy();
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/lib/policies.ts b/x-pack/plugins/index_lifecycle_management/public/application/lib/policies.ts
new file mode 100644
index 0000000000000..c4a91978a3765
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/public/application/lib/policies.ts
@@ -0,0 +1,32 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { PolicyFromES } from '../../../common/types';
+
+export const splitSizeAndUnits = (field: string): { size: string; units: string } => {
+ let size = '';
+ let units = '';
+
+ const result = /(\d+)(\w+)/.exec(field);
+ if (result) {
+ size = result[1];
+ units = result[2];
+ }
+
+ return {
+ size,
+ units,
+ };
+};
+
+export const getPolicyByName = (
+ policies: PolicyFromES[] | null | undefined,
+ policyName: string = ''
+): PolicyFromES | undefined => {
+ if (policies && policies.length > 0) {
+ return policies.find((policy: PolicyFromES) => policy.name === policyName);
+ }
+};
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/index.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/index.ts
index a04608338718e..326f6ff87dc3b 100644
--- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/index.ts
+++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/index.ts
@@ -7,11 +7,8 @@
export { ActiveBadge } from './active_badge';
export { ErrableFormRow } from './form_errors';
export { LearnMoreLink } from './learn_more_link';
-export { MinAgeInput } from './min_age_input_legacy';
export { OptionalLabel } from './optional_label';
-export { PhaseErrorMessage } from './phase_error_message';
export { PolicyJsonFlyout } from './policy_json_flyout';
-export { SnapshotPolicies } from './snapshot_policies';
export { DescribedFormField } from './described_form_field';
export * from './phases';
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/min_age_input_legacy.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/min_age_input_legacy.tsx
deleted file mode 100644
index 6fcf35b799289..0000000000000
--- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/min_age_input_legacy.tsx
+++ /dev/null
@@ -1,263 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License;
- * you may not use this file except in compliance with the Elastic License.
- */
-import React from 'react';
-import { FormattedMessage } from '@kbn/i18n/react';
-import { i18n } from '@kbn/i18n';
-
-import { EuiFieldNumber, EuiFlexGroup, EuiFlexItem, EuiFormRow, EuiSelect } from '@elastic/eui';
-
-import { LearnMoreLink } from './learn_more_link';
-import { ErrableFormRow } from './form_errors';
-import { PhaseValidationErrors, propertyof } from '../../../services/policies/policy_validation';
-import { PhaseWithMinAge, Phases } from '../../../../../common/types';
-
-function getTimingLabelForPhase(phase: keyof Phases) {
- // NOTE: Hot phase isn't necessary, because indices begin in the hot phase.
- switch (phase) {
- case 'warm':
- return i18n.translate('xpack.indexLifecycleMgmt.editPolicy.phaseWarm.minimumAgeLabel', {
- defaultMessage: 'Timing for warm phase',
- });
-
- case 'cold':
- return i18n.translate('xpack.indexLifecycleMgmt.editPolicy.phaseCold.minimumAgeLabel', {
- defaultMessage: 'Timing for cold phase',
- });
-
- case 'delete':
- return i18n.translate('xpack.indexLifecycleMgmt.editPolicy.phaseDelete.minimumAgeLabel', {
- defaultMessage: 'Timing for delete phase',
- });
- }
-}
-
-function getUnitsAriaLabelForPhase(phase: keyof Phases) {
- // NOTE: Hot phase isn't necessary, because indices begin in the hot phase.
- switch (phase) {
- case 'warm':
- return i18n.translate(
- 'xpack.indexLifecycleMgmt.editPolicy.phaseWarm.minimumAgeUnitsAriaLabel',
- {
- defaultMessage: 'Units for timing of warm phase',
- }
- );
-
- case 'cold':
- return i18n.translate(
- 'xpack.indexLifecycleMgmt.editPolicy.phaseCold.minimumAgeUnitsAriaLabel',
- {
- defaultMessage: 'Units for timing of cold phase',
- }
- );
-
- case 'delete':
- return i18n.translate(
- 'xpack.indexLifecycleMgmt.editPolicy.phaseDelete.minimumAgeUnitsAriaLabel',
- {
- defaultMessage: 'Units for timing of delete phase',
- }
- );
- }
-}
-
-interface Props {
- rolloverEnabled: boolean;
- errors?: PhaseValidationErrors;
- phase: keyof Phases & string;
- phaseData: T;
- setPhaseData: (dataKey: keyof T & string, value: string) => void;
- isShowingErrors: boolean;
-}
-
-export const MinAgeInput = ({
- rolloverEnabled,
- errors,
- phaseData,
- phase,
- setPhaseData,
- isShowingErrors,
-}: React.PropsWithChildren>): React.ReactElement => {
- let daysOptionLabel;
- let hoursOptionLabel;
- let minutesOptionLabel;
- let secondsOptionLabel;
- let millisecondsOptionLabel;
- let microsecondsOptionLabel;
- let nanosecondsOptionLabel;
-
- if (rolloverEnabled) {
- daysOptionLabel = i18n.translate(
- 'xpack.indexLifecycleMgmt.editPolicy.rolloverDaysOptionLabel',
- {
- defaultMessage: 'days from rollover',
- }
- );
-
- hoursOptionLabel = i18n.translate(
- 'xpack.indexLifecycleMgmt.editPolicy.rolloverHoursOptionLabel',
- {
- defaultMessage: 'hours from rollover',
- }
- );
- minutesOptionLabel = i18n.translate(
- 'xpack.indexLifecycleMgmt.editPolicy.rolloverMinutesOptionLabel',
- {
- defaultMessage: 'minutes from rollover',
- }
- );
-
- secondsOptionLabel = i18n.translate(
- 'xpack.indexLifecycleMgmt.editPolicy.rolloverSecondsOptionLabel',
- {
- defaultMessage: 'seconds from rollover',
- }
- );
- millisecondsOptionLabel = i18n.translate(
- 'xpack.indexLifecycleMgmt.editPolicy.rolloverMilliSecondsOptionLabel',
- {
- defaultMessage: 'milliseconds from rollover',
- }
- );
-
- microsecondsOptionLabel = i18n.translate(
- 'xpack.indexLifecycleMgmt.editPolicy.rolloverMicroSecondsOptionLabel',
- {
- defaultMessage: 'microseconds from rollover',
- }
- );
-
- nanosecondsOptionLabel = i18n.translate(
- 'xpack.indexLifecycleMgmt.editPolicy.rolloverNanoSecondsOptionLabel',
- {
- defaultMessage: 'nanoseconds from rollover',
- }
- );
- } else {
- daysOptionLabel = i18n.translate(
- 'xpack.indexLifecycleMgmt.editPolicy.creationDaysOptionLabel',
- {
- defaultMessage: 'days from index creation',
- }
- );
-
- hoursOptionLabel = i18n.translate(
- 'xpack.indexLifecycleMgmt.editPolicy.creationHoursOptionLabel',
- {
- defaultMessage: 'hours from index creation',
- }
- );
-
- minutesOptionLabel = i18n.translate(
- 'xpack.indexLifecycleMgmt.editPolicy.creationMinutesOptionLabel',
- {
- defaultMessage: 'minutes from index creation',
- }
- );
-
- secondsOptionLabel = i18n.translate(
- 'xpack.indexLifecycleMgmt.editPolicy.creationSecondsOptionLabel',
- {
- defaultMessage: 'seconds from index creation',
- }
- );
-
- millisecondsOptionLabel = i18n.translate(
- 'xpack.indexLifecycleMgmt.editPolicy.creationMilliSecondsOptionLabel',
- {
- defaultMessage: 'milliseconds from index creation',
- }
- );
-
- microsecondsOptionLabel = i18n.translate(
- 'xpack.indexLifecycleMgmt.editPolicy.creationMicroSecondsOptionLabel',
- {
- defaultMessage: 'microseconds from index creation',
- }
- );
-
- nanosecondsOptionLabel = i18n.translate(
- 'xpack.indexLifecycleMgmt.editPolicy.creationNanoSecondsOptionLabel',
- {
- defaultMessage: 'nanoseconds from index creation',
- }
- );
- }
-
- // check that these strings are valid properties
- const selectedMinimumAgeProperty = propertyof('selectedMinimumAge');
- const selectedMinimumAgeUnitsProperty = propertyof('selectedMinimumAgeUnits');
- return (
-
-
-
- }
- />
- }
- >
- {
- setPhaseData(selectedMinimumAgeProperty, e.target.value);
- }}
- min={0}
- />
-
-
-
-
- setPhaseData(selectedMinimumAgeUnitsProperty, e.target.value)}
- options={[
- {
- value: 'd',
- text: daysOptionLabel,
- },
- {
- value: 'h',
- text: hoursOptionLabel,
- },
- {
- value: 'm',
- text: minutesOptionLabel,
- },
- {
- value: 's',
- text: secondsOptionLabel,
- },
- {
- value: 'ms',
- text: millisecondsOptionLabel,
- },
- {
- value: 'micros',
- text: microsecondsOptionLabel,
- },
- {
- value: 'nanos',
- text: nanosecondsOptionLabel,
- },
- ]}
- />
-
-
-
- );
-};
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phase_error_message.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phase_error_message.tsx
deleted file mode 100644
index 750f68543f221..0000000000000
--- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phase_error_message.tsx
+++ /dev/null
@@ -1,19 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License;
- * you may not use this file except in compliance with the Elastic License.
- */
-import React from 'react';
-import { EuiBadge } from '@elastic/eui';
-import { FormattedMessage } from '@kbn/i18n/react';
-
-export const PhaseErrorMessage = ({ isShowingErrors }: { isShowingErrors: boolean }) => {
- return isShowingErrors ? (
-
-
-
- ) : null;
-};
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/cold_phase/cold_phase.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/cold_phase/cold_phase.tsx
index 84e955a91ad7c..b87243bd1a9a1 100644
--- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/cold_phase/cold_phase.tsx
+++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/cold_phase/cold_phase.tsx
@@ -13,19 +13,13 @@ import { EuiDescribedFormGroup, EuiTextColor } from '@elastic/eui';
import { Phases } from '../../../../../../../common/types';
-import {
- useFormData,
- useFormContext,
- UseField,
- ToggleField,
- NumericField,
-} from '../../../../../../shared_imports';
+import { useFormData, UseField, ToggleField, NumericField } from '../../../../../../shared_imports';
import { useEditPolicyContext } from '../../../edit_policy_context';
-import { LearnMoreLink, ActiveBadge, PhaseErrorMessage, DescribedFormField } from '../../';
+import { LearnMoreLink, ActiveBadge, DescribedFormField } from '../../';
-import { MinAgeInputField, DataTierAllocationField, SetPriorityInput } from '../shared';
+import { MinAgeInputField, DataTierAllocationField, SetPriorityInput } from '../shared_fields';
const i18nTexts = {
dataTierAllocation: {
@@ -43,15 +37,13 @@ const formFieldPaths = {
};
export const ColdPhase: FunctionComponent = () => {
- const { originalPolicy } = useEditPolicyContext();
- const form = useFormContext();
+ const { policy } = useEditPolicyContext();
const [formData] = useFormData({
watch: [formFieldPaths.enabled],
});
const enabled = get(formData, formFieldPaths.enabled);
- const isShowingErrors = form.isValid === false;
return (
@@ -66,8 +58,7 @@ export const ColdPhase: FunctionComponent = () => {
defaultMessage="Cold phase"
/>
{' '}
- {enabled && !isShowingErrors ?
: null}
-
+ {enabled &&
}
}
titleSize="s"
@@ -128,9 +119,7 @@ export const ColdPhase: FunctionComponent = () => {
'xpack.indexLifecycleMgmt.editPolicy.coldPhase.numberOfReplicas.switchLabel',
{ defaultMessage: 'Set replicas' }
),
- initialValue: Boolean(
- originalPolicy.phases.cold?.actions?.allocate?.number_of_replicas
- ),
+ initialValue: Boolean(policy.phases.cold?.actions?.allocate?.number_of_replicas),
}}
fullWidth
>
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/delete_phase.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/delete_phase/delete_phase.tsx
similarity index 50%
rename from x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/delete_phase.tsx
rename to x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/delete_phase/delete_phase.tsx
index 78ae66327654c..37323b97edc92 100644
--- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/delete_phase.tsx
+++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/delete_phase/delete_phase.tsx
@@ -7,53 +7,24 @@
import React, { FunctionComponent, Fragment } from 'react';
import { get } from 'lodash';
import { FormattedMessage } from '@kbn/i18n/react';
-import { EuiDescribedFormGroup, EuiSwitch, EuiTextColor, EuiFormRow } from '@elastic/eui';
+import { EuiDescribedFormGroup, EuiTextColor, EuiFormRow } from '@elastic/eui';
-import { DeletePhase as DeletePhaseInterface, Phases } from '../../../../../../common/types';
+import { useFormData, UseField, ToggleField } from '../../../../../../shared_imports';
-import { useFormData } from '../../../../../shared_imports';
+import { ActiveBadge, LearnMoreLink, OptionalLabel } from '../../index';
-import { PhaseValidationErrors } from '../../../../services/policies/policy_validation';
+import { MinAgeInputField, SnapshotPoliciesField } from '../shared_fields';
-import {
- ActiveBadge,
- LearnMoreLink,
- OptionalLabel,
- PhaseErrorMessage,
- MinAgeInput,
- SnapshotPolicies,
-} from '../';
-import { useRolloverPath } from './shared';
-
-const deleteProperty: keyof Phases = 'delete';
-const phaseProperty = (propertyName: keyof DeletePhaseInterface) => propertyName;
-
-interface Props {
- setPhaseData: (key: keyof DeletePhaseInterface & string, value: string | boolean) => void;
- phaseData: DeletePhaseInterface;
- isShowingErrors: boolean;
- errors?: PhaseValidationErrors;
- getUrlForApp: (
- appId: string,
- options?: {
- path?: string;
- absolute?: boolean;
- }
- ) => string;
-}
+const formFieldPaths = {
+ enabled: '_meta.delete.enabled',
+};
-export const DeletePhase: FunctionComponent = ({
- setPhaseData,
- phaseData,
- errors,
- isShowingErrors,
- getUrlForApp,
-}) => {
+export const DeletePhase: FunctionComponent = () => {
const [formData] = useFormData({
- watch: useRolloverPath,
+ watch: formFieldPaths.enabled,
});
- const hotPhaseRolloverEnabled = get(formData, useRolloverPath);
+ const enabled = get(formData, formFieldPaths.enabled);
return (
@@ -66,8 +37,7 @@ export const DeletePhase: FunctionComponent
= ({
defaultMessage="Delete phase"
/>
{' '}
- {phaseData.phaseEnabled && !isShowingErrors ? : null}
-
+ {enabled && }
}
titleSize="s"
@@ -79,39 +49,23 @@ export const DeletePhase: FunctionComponent = ({
defaultMessage="You no longer need your index. You can define when it is safe to delete it."
/>
-
- }
- id={`${deleteProperty}-${phaseProperty('phaseEnabled')}`}
- checked={phaseData.phaseEnabled}
- onChange={(e) => {
- setPhaseData(phaseProperty('phaseEnabled'), e.target.checked);
+
}
fullWidth
>
- {phaseData.phaseEnabled ? (
-
- errors={errors}
- phaseData={phaseData}
- phase={deleteProperty}
- isShowingErrors={isShowingErrors}
- setPhaseData={setPhaseData}
- rolloverEnabled={hotPhaseRolloverEnabled}
- />
- ) : (
-
- )}
+ {enabled && }
- {phaseData.phaseEnabled ? (
+ {enabled ? (
@@ -145,11 +99,7 @@ export const DeletePhase: FunctionComponent = ({
}
>
- setPhaseData(phaseProperty('waitForSnapshotPolicy'), value)}
- getUrlForApp={getUrlForApp}
- />
+
) : null}
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/delete_phase/index.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/delete_phase/index.ts
new file mode 100644
index 0000000000000..488e4e26cfce0
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/delete_phase/index.ts
@@ -0,0 +1,7 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+export { DeletePhase } from './delete_phase';
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/hot_phase/hot_phase.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/hot_phase/hot_phase.tsx
index a184ddf5148b9..629c1388f61fb 100644
--- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/hot_phase/hot_phase.tsx
+++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/hot_phase/hot_phase.tsx
@@ -19,7 +19,6 @@ import {
import { Phases } from '../../../../../../../common/types';
import {
- useFormContext,
useFormData,
UseField,
SelectField,
@@ -29,26 +28,24 @@ import {
import { i18nTexts } from '../../../i18n_texts';
-import { ROLLOVER_EMPTY_VALIDATION } from '../../../form_validations';
+import { ROLLOVER_EMPTY_VALIDATION } from '../../../form';
import { ROLLOVER_FORM_PATHS } from '../../../constants';
-import { LearnMoreLink, ActiveBadge, PhaseErrorMessage } from '../../';
+import { LearnMoreLink, ActiveBadge } from '../../';
-import { Forcemerge, SetPriorityInput, useRolloverPath } from '../shared';
+import { Forcemerge, SetPriorityInput, useRolloverPath } from '../shared_fields';
import { maxSizeStoredUnits, maxAgeUnits } from './constants';
const hotProperty: keyof Phases = 'hot';
export const HotPhase: FunctionComponent = () => {
- const form = useFormContext();
const [formData] = useFormData({
watch: useRolloverPath,
});
const isRolloverEnabled = get(formData, useRolloverPath);
- const isShowingErrors = form.isValid === false;
const [showEmptyRolloverFieldsError, setShowEmptyRolloverFieldsError] = useState(false);
return (
@@ -62,8 +59,7 @@ export const HotPhase: FunctionComponent = () => {
defaultMessage="Hot phase"
/>
{' '}
- {isShowingErrors ? null : }
-
+
}
titleSize="s"
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared/data_tier_allocation_field/components/cloud_data_tier_callout.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/cloud_data_tier_callout.tsx
similarity index 100%
rename from x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared/data_tier_allocation_field/components/cloud_data_tier_callout.tsx
rename to x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/cloud_data_tier_callout.tsx
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared/data_tier_allocation_field/components/data_tier_allocation.scss b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/data_tier_allocation.scss
similarity index 100%
rename from x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared/data_tier_allocation_field/components/data_tier_allocation.scss
rename to x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/data_tier_allocation.scss
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared/data_tier_allocation_field/components/data_tier_allocation.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/data_tier_allocation.tsx
similarity index 100%
rename from x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared/data_tier_allocation_field/components/data_tier_allocation.tsx
rename to x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/data_tier_allocation.tsx
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared/data_tier_allocation_field/components/default_allocation_notice.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/default_allocation_notice.tsx
similarity index 100%
rename from x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared/data_tier_allocation_field/components/default_allocation_notice.tsx
rename to x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/default_allocation_notice.tsx
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared/data_tier_allocation_field/components/index.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/index.ts
similarity index 100%
rename from x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared/data_tier_allocation_field/components/index.ts
rename to x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/index.ts
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared/data_tier_allocation_field/components/no_node_attributes_warning.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/no_node_attributes_warning.tsx
similarity index 100%
rename from x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared/data_tier_allocation_field/components/no_node_attributes_warning.tsx
rename to x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/no_node_attributes_warning.tsx
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared/data_tier_allocation_field/components/node_allocation.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/node_allocation.tsx
similarity index 90%
rename from x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared/data_tier_allocation_field/components/node_allocation.tsx
rename to x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/node_allocation.tsx
index 407bb9ea92e85..c1676d7074dbc 100644
--- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared/data_tier_allocation_field/components/node_allocation.tsx
+++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/node_allocation.tsx
@@ -10,12 +10,8 @@ import { FormattedMessage } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
import { EuiButtonEmpty, EuiText, EuiSpacer } from '@elastic/eui';
-import { PhaseWithAllocationAction } from '../../../../../../../../../common/types';
-
import { UseField, SelectField, useFormData } from '../../../../../../../../shared_imports';
-import { propertyof } from '../../../../../../../services/policies/policy_validation';
-
import { LearnMoreLink } from '../../../../learn_more_link';
import { NodeAttrsDetails } from './node_attrs_details';
@@ -61,9 +57,6 @@ export const NodeAllocation: FunctionComponent = ({ phase, nodes })
nodeOptions.sort((a, b) => a.value.localeCompare(b.value));
- // check that this string is a valid property
- const nodeAttrsProperty = propertyof('selectedNodeAttrs');
-
return (
<>
@@ -100,7 +93,7 @@ export const NodeAllocation: FunctionComponent = ({ phase, nodes })
) : undefined,
euiFieldProps: {
- 'data-test-subj': `${phase}-${nodeAttrsProperty}`,
+ 'data-test-subj': `${phase}-selectedNodeAttrs`,
options: [{ text: i18nTexts.doNotModifyAllocationOption, value: '' }].concat(
nodeOptions
),
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared/data_tier_allocation_field/components/node_attrs_details.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/node_attrs_details.tsx
similarity index 100%
rename from x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared/data_tier_allocation_field/components/node_attrs_details.tsx
rename to x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/node_attrs_details.tsx
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared/data_tier_allocation_field/components/node_data_provider.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/node_data_provider.tsx
similarity index 100%
rename from x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared/data_tier_allocation_field/components/node_data_provider.tsx
rename to x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/node_data_provider.tsx
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared/data_tier_allocation_field/components/types.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/types.ts
similarity index 100%
rename from x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared/data_tier_allocation_field/components/types.ts
rename to x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/types.ts
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared/data_tier_allocation_field/data_tier_allocation_field.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/data_tier_allocation_field.tsx
similarity index 100%
rename from x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared/data_tier_allocation_field/data_tier_allocation_field.tsx
rename to x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/data_tier_allocation_field.tsx
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared/data_tier_allocation_field/index.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/index.ts
similarity index 100%
rename from x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared/data_tier_allocation_field/index.ts
rename to x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/index.ts
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared/forcemerge_field.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/forcemerge_field.tsx
similarity index 94%
rename from x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared/forcemerge_field.tsx
rename to x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/forcemerge_field.tsx
index b410bd0e6b3b0..b05d49be497cd 100644
--- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared/forcemerge_field.tsx
+++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/forcemerge_field.tsx
@@ -21,11 +21,11 @@ interface Props {
}
export const Forcemerge: React.FunctionComponent = ({ phase }) => {
- const { originalPolicy } = useEditPolicyContext();
+ const { policy } = useEditPolicyContext();
const initialToggleValue = useMemo(() => {
- return Boolean(originalPolicy.phases[phase]?.actions?.forcemerge);
- }, [originalPolicy, phase]);
+ return Boolean(policy.phases[phase]?.actions?.forcemerge);
+ }, [policy, phase]);
return (
= ({ phase }) => {
- const phaseIndexPriorityProperty = propertyof('phaseIndexPriority');
return (
= ({ phase }) => {
componentProps={{
fullWidth: false,
euiFieldProps: {
- 'data-test-subj': `${phase}-${phaseIndexPriorityProperty}`,
- min: 1,
+ 'data-test-subj': `${phase}-phaseIndexPriority`,
+ min: 0,
},
}}
/>
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/snapshot_policies.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/snapshot_policies_field.tsx
similarity index 68%
rename from x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/snapshot_policies.tsx
rename to x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/snapshot_policies_field.tsx
index cc2849b5c8e9c..e9f9f331e410a 100644
--- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/snapshot_policies.tsx
+++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/snapshot_policies_field.tsx
@@ -4,52 +4,39 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import React, { Fragment } from 'react';
-
+import React from 'react';
+import { get } from 'lodash';
import { FormattedMessage } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
-import { ApplicationStart } from 'kibana/public';
import {
EuiButtonIcon,
EuiCallOut,
- EuiComboBox,
EuiComboBoxOptionOption,
EuiLink,
EuiSpacer,
} from '@elastic/eui';
-import { useLoadSnapshotPolicies } from '../../../services/api';
+import { UseField, ComboBoxField, useFormData } from '../../../../../../shared_imports';
+import { useLoadSnapshotPolicies } from '../../../../../services/api';
+import { useEditPolicyContext } from '../../../edit_policy_context';
+
+const waitForSnapshotFormField = 'phases.delete.actions.wait_for_snapshot.policy';
-interface Props {
- value: string;
- onChange: (value: string) => void;
- getUrlForApp: ApplicationStart['getUrlForApp'];
-}
-export const SnapshotPolicies: React.FunctionComponent = ({
- value,
- onChange,
- getUrlForApp,
-}) => {
+export const SnapshotPoliciesField: React.FunctionComponent = () => {
+ const { getUrlForApp } = useEditPolicyContext();
const { error, isLoading, data, resendRequest } = useLoadSnapshotPolicies();
+ const [formData] = useFormData({
+ watch: waitForSnapshotFormField,
+ });
+
+ const selectedSnapshotPolicy = get(formData, waitForSnapshotFormField);
const policies = data.map((name: string) => ({
label: name,
value: name,
}));
- const onComboChange = (options: EuiComboBoxOptionOption[]) => {
- if (options.length > 0) {
- onChange(options[0].label);
- } else {
- onChange('');
- }
- };
-
- const onCreateOption = (newValue: string) => {
- onChange(newValue);
- };
-
const getUrlForSnapshotPolicyWizard = () => {
return getUrlForApp('management', {
path: `data/snapshot_restore/add_policy`,
@@ -59,14 +46,14 @@ export const SnapshotPolicies: React.FunctionComponent = ({
let calloutContent;
if (error) {
calloutContent = (
-
+ <>
+ <>
= ({
}
)}
/>
-
+ >
}
>
= ({
defaultMessage="Refresh this field and enter the name of an existing snapshot policy."
/>
-
+ >
);
} else if (data.length === 0) {
calloutContent = (
-
+ <>
= ({
}}
/>
-
+ >
);
- } else if (value && !data.includes(value)) {
+ } else if (selectedSnapshotPolicy && !data.includes(selectedSnapshotPolicy)) {
calloutContent = (
-
+ <>
= ({
}}
/>
-
+ >
);
}
return (
-
-
+ path={waitForSnapshotFormField}>
+ {(field) => {
+ const singleSelectionArray: [selectedSnapshot?: string] = field.value
+ ? [field.value]
+ : [];
+
+ return (
+ {
+ field.setValue(newOption);
},
- ]
- : []
- }
- onChange={onComboChange}
- noSuggestions={!!(error || data.length === 0)}
- />
+ onChange: (options: EuiComboBoxOptionOption[]) => {
+ if (options.length > 0) {
+ field.setValue(options[0].label);
+ } else {
+ field.setValue('');
+ }
+ },
+ }}
+ />
+ );
+ }}
+
{calloutContent}
-
+ >
);
};
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/warm_phase/warm_phase.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/warm_phase/warm_phase.tsx
index 06c16e8bdd5ab..94fd2ee9edaca 100644
--- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/warm_phase/warm_phase.tsx
+++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/warm_phase/warm_phase.tsx
@@ -17,23 +17,17 @@ import {
EuiDescribedFormGroup,
} from '@elastic/eui';
-import {
- useFormData,
- UseField,
- ToggleField,
- useFormContext,
- NumericField,
-} from '../../../../../../shared_imports';
+import { useFormData, UseField, ToggleField, NumericField } from '../../../../../../shared_imports';
import { Phases } from '../../../../../../../common/types';
-import { useRolloverPath, MinAgeInputField, Forcemerge, SetPriorityInput } from '../shared';
+import { useRolloverPath, MinAgeInputField, Forcemerge, SetPriorityInput } from '../shared_fields';
import { useEditPolicyContext } from '../../../edit_policy_context';
-import { LearnMoreLink, ActiveBadge, PhaseErrorMessage, DescribedFormField } from '../../';
+import { LearnMoreLink, ActiveBadge, DescribedFormField } from '../../';
-import { DataTierAllocationField } from '../shared';
+import { DataTierAllocationField } from '../shared_fields';
const i18nTexts = {
shrinkLabel: i18n.translate('xpack.indexLifecycleMgmt.warmPhase.shrinkIndexLabel', {
@@ -54,8 +48,7 @@ const formFieldPaths = {
};
export const WarmPhase: FunctionComponent = () => {
- const { originalPolicy } = useEditPolicyContext();
- const form = useFormContext();
+ const { policy } = useEditPolicyContext();
const [formData] = useFormData({
watch: [useRolloverPath, formFieldPaths.enabled, formFieldPaths.warmPhaseOnRollover],
});
@@ -63,7 +56,6 @@ export const WarmPhase: FunctionComponent = () => {
const enabled = get(formData, formFieldPaths.enabled);
const hotPhaseRolloverEnabled = get(formData, useRolloverPath);
const warmPhaseOnRollover = get(formData, formFieldPaths.warmPhaseOnRollover);
- const isShowingErrors = form.isValid === false;
return (
@@ -77,8 +69,7 @@ export const WarmPhase: FunctionComponent = () => {
defaultMessage="Warm phase"
/>
{' '}
- {enabled && !isShowingErrors ?
: null}
-
+ {enabled &&
}
}
titleSize="s"
@@ -161,9 +152,7 @@ export const WarmPhase: FunctionComponent = () => {
'xpack.indexLifecycleMgmt.editPolicy.warmPhase.numberOfReplicas.switchLabel',
{ defaultMessage: 'Set replicas' }
),
- initialValue: Boolean(
- originalPolicy.phases.warm?.actions?.allocate?.number_of_replicas
- ),
+ initialValue: Boolean(policy.phases.warm?.actions?.allocate?.number_of_replicas),
}}
fullWidth
>
@@ -203,7 +192,7 @@ export const WarmPhase: FunctionComponent = () => {
'data-test-subj': 'shrinkSwitch',
label: i18nTexts.shrinkLabel,
'aria-label': i18nTexts.shrinkLabel,
- initialValue: Boolean(originalPolicy.phases.warm?.actions?.shrink),
+ initialValue: Boolean(policy.phases.warm?.actions?.shrink),
}}
fullWidth
>
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/policy_json_flyout.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/policy_json_flyout.tsx
index 7098b018d6dfd..a8b1680ebde07 100644
--- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/policy_json_flyout.tsx
+++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/policy_json_flyout.tsx
@@ -7,7 +7,6 @@
import React, { useCallback, useEffect, useState } from 'react';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
-
import {
EuiButtonEmpty,
EuiCodeBlock,
@@ -25,19 +24,15 @@ import {
import { SerializedPolicy } from '../../../../../common/types';
import { useFormContext, useFormData } from '../../../../shared_imports';
+
import { FormInternal } from '../types';
interface Props {
- legacyPolicy: SerializedPolicy;
close: () => void;
policyName: string;
}
-export const PolicyJsonFlyout: React.FunctionComponent = ({
- policyName,
- close,
- legacyPolicy,
-}) => {
+export const PolicyJsonFlyout: React.FunctionComponent = ({ policyName, close }) => {
/**
* policy === undefined: we are checking validity
* policy === null: we have determined the policy is invalid
@@ -51,20 +46,11 @@ export const PolicyJsonFlyout: React.FunctionComponent = ({
const updatePolicy = useCallback(async () => {
setPolicy(undefined);
if (await validateForm()) {
- const p = getFormData() as SerializedPolicy;
- setPolicy({
- ...legacyPolicy,
- phases: {
- ...legacyPolicy.phases,
- hot: p.phases.hot,
- warm: p.phases.warm,
- cold: p.phases.cold,
- },
- });
+ setPolicy(getFormData() as SerializedPolicy);
} else {
setPolicy(null);
}
- }, [setPolicy, getFormData, legacyPolicy, validateForm]);
+ }, [setPolicy, getFormData, validateForm]);
useEffect(() => {
updatePolicy();
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.container.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.container.tsx
index c82a420b74857..ebef80871b83d 100644
--- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.container.tsx
+++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.container.tsx
@@ -12,8 +12,11 @@ import { FormattedMessage } from '@kbn/i18n/react';
import { useKibana } from '../../../shared_imports';
import { useLoadPoliciesList } from '../../services/api';
+import { getPolicyByName } from '../../lib/policies';
+import { defaultPolicy } from '../../constants';
import { EditPolicy as PresentationComponent } from './edit_policy';
+import { EditPolicyContextProvider } from './edit_policy_context';
interface RouterProps {
policyName: string;
@@ -44,6 +47,7 @@ export const EditPolicy: React.FunctionComponent {
breadcrumbService.setBreadcrumbs('editPolicy');
}, [breadcrumbService]);
+
if (isLoading) {
return (
+
+
+
);
};
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.tsx
index 5397f5da2d6bb..1abbe884c2dc2 100644
--- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.tsx
+++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.tsx
@@ -4,7 +4,8 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import React, { Fragment, useEffect, useState, useCallback, useMemo } from 'react';
+import React, { Fragment, useEffect, useState, useMemo } from 'react';
+import { get } from 'lodash';
import { RouteComponentProps } from 'react-router-dom';
@@ -16,7 +17,6 @@ import {
EuiButton,
EuiButtonEmpty,
EuiDescribedFormGroup,
- EuiFieldText,
EuiFlexGroup,
EuiFlexItem,
EuiFormRow,
@@ -30,31 +30,13 @@ import {
EuiTitle,
} from '@elastic/eui';
-import { useForm, Form } from '../../../shared_imports';
+import { useForm, Form, UseField, TextField, useFormData } from '../../../shared_imports';
import { toasts } from '../../services/notification';
-import { LegacyPolicy, PolicyFromES, SerializedPolicy } from '../../../../common/types';
-
-import { defaultPolicy } from '../../constants';
-
-import {
- validatePolicy,
- ValidationErrors,
- findFirstError,
-} from '../../services/policies/policy_validation';
-
-import { savePolicy } from '../../services/policies/policy_save';
+import { savePolicy } from './save_policy';
import {
- deserializePolicy,
- getPolicyByName,
- initializeNewPolicy,
- legacySerializePolicy,
-} from '../../services/policies/policy_serialization';
-
-import {
- ErrableFormRow,
LearnMoreLink,
PolicyJsonFlyout,
ColdPhase,
@@ -63,93 +45,66 @@ import {
WarmPhase,
} from './components';
-import { schema } from './form_schema';
-import { deserializer } from './deserializer';
-import { createSerializer } from './serializer';
+import { schema, deserializer, createSerializer, createPolicyNameValidations } from './form';
-import { EditPolicyContextProvider } from './edit_policy_context';
+import { useEditPolicyContext } from './edit_policy_context';
+import { FormInternal } from './types';
export interface Props {
- policies: PolicyFromES[];
- policyName: string;
- getUrlForApp: (
- appId: string,
- options?: {
- path?: string;
- absolute?: boolean;
- }
- ) => string;
history: RouteComponentProps['history'];
}
-const mergeAllSerializedPolicies = (
- serializedPolicy: SerializedPolicy,
- legacySerializedPolicy: SerializedPolicy
-): SerializedPolicy => {
- return {
- ...legacySerializedPolicy,
- phases: {
- ...legacySerializedPolicy.phases,
- hot: serializedPolicy.phases.hot,
- warm: serializedPolicy.phases.warm,
- cold: serializedPolicy.phases.cold,
- },
- };
-};
+const policyNamePath = 'name';
-export const EditPolicy: React.FunctionComponent = ({
- policies,
- policyName,
- history,
- getUrlForApp,
-}) => {
+export const EditPolicy: React.FunctionComponent = ({ history }) => {
useEffect(() => {
window.scrollTo(0, 0);
}, []);
- const [isShowingErrors, setIsShowingErrors] = useState(false);
- const [errors, setErrors] = useState();
const [isShowingPolicyJsonFlyout, setIsShowingPolicyJsonFlyout] = useState(false);
-
- const existingPolicy = getPolicyByName(policies, policyName);
+ const {
+ isNewPolicy,
+ policy: currentPolicy,
+ existingPolicies,
+ policyName,
+ } = useEditPolicyContext();
const serializer = useMemo(() => {
- return createSerializer(existingPolicy?.policy);
- }, [existingPolicy?.policy]);
+ return createSerializer(isNewPolicy ? undefined : currentPolicy);
+ }, [isNewPolicy, currentPolicy]);
- const originalPolicy = existingPolicy?.policy ?? defaultPolicy;
+ const [saveAsNew, setSaveAsNew] = useState(isNewPolicy);
+ const originalPolicyName: string = isNewPolicy ? '' : policyName!;
const { form } = useForm({
schema,
- defaultValue: originalPolicy,
+ defaultValue: {
+ ...currentPolicy,
+ name: originalPolicyName,
+ },
deserializer,
serializer,
});
- const [policy, setPolicy] = useState(() =>
- existingPolicy ? deserializePolicy(existingPolicy) : initializeNewPolicy(policyName)
+ const [formData] = useFormData({ form, watch: policyNamePath });
+ const currentPolicyName = get(formData, policyNamePath);
+
+ const policyNameValidations = useMemo(
+ () =>
+ createPolicyNameValidations({
+ originalPolicyName,
+ policies: existingPolicies,
+ saveAsNewPolicy: saveAsNew,
+ }),
+ [originalPolicyName, existingPolicies, saveAsNew]
);
- const isNewPolicy: boolean = !Boolean(existingPolicy);
- const [saveAsNew, setSaveAsNew] = useState(isNewPolicy);
- const originalPolicyName: string = existingPolicy ? existingPolicy.name : '';
-
const backToPolicyList = () => {
history.push('/policies');
};
const submit = async () => {
- setIsShowingErrors(true);
- const { data: formLibPolicy, isValid: newIsValid } = await form.submit();
- const [legacyIsValid, validationErrors] = validatePolicy(
- saveAsNew,
- policy,
- policies,
- originalPolicyName
- );
- setErrors(validationErrors);
-
- const isValid = legacyIsValid && newIsValid;
+ const { data: policy, isValid } = await form.submit();
if (!isValid) {
toasts.addDanger(
@@ -157,22 +112,11 @@ export const EditPolicy: React.FunctionComponent = ({
defaultMessage: 'Please fix the errors on this page.',
})
);
- // This functionality will not be required for once form lib is fully adopted for this form
- // because errors are reported as fields are edited.
- if (!legacyIsValid) {
- const firstError = findFirstError(validationErrors);
- const errorRowId = `${firstError ? firstError.replace('.', '-') : ''}-row`;
- const element = document.getElementById(errorRowId);
- if (element) {
- element.scrollIntoView({ block: 'center', inline: 'nearest' });
- }
- }
} else {
- const readSerializedPolicy = () => {
- const legacySerializedPolicy = legacySerializePolicy(policy, existingPolicy?.policy);
- return mergeAllSerializedPolicies(formLibPolicy, legacySerializedPolicy);
- };
- const success = await savePolicy(readSerializedPolicy, isNewPolicy || saveAsNew);
+ const success = await savePolicy(
+ { ...policy, name: saveAsNew ? currentPolicyName : originalPolicyName },
+ isNewPolicy || saveAsNew
+ );
if (success) {
backToPolicyList();
}
@@ -183,248 +127,217 @@ export const EditPolicy: React.FunctionComponent = ({
setIsShowingPolicyJsonFlyout(!isShowingPolicyJsonFlyout);
};
- const setPhaseData = useCallback(
- (phase: keyof LegacyPolicy['phases'], key: string, value: any) => {
- setPolicy((nextPolicy) => ({
- ...nextPolicy,
- phases: {
- ...nextPolicy.phases,
- [phase]: { ...nextPolicy.phases[phase], [key]: value },
- },
- }));
- },
- [setPolicy]
- );
-
- const setDeletePhaseData = useCallback(
- (key: string, value: any) => setPhaseData('delete', key, value),
- [setPhaseData]
- );
-
return (
-
-
-
-
-
-
- {isNewPolicy
- ? i18n.translate('xpack.indexLifecycleMgmt.editPolicy.createPolicyMessage', {
- defaultMessage: 'Create an index lifecycle policy',
- })
- : i18n.translate('xpack.indexLifecycleMgmt.editPolicy.editPolicyMessage', {
- defaultMessage: 'Edit index lifecycle policy {originalPolicyName}',
- values: { originalPolicyName },
- })}
-
-
-
-
+ }
+ titleSize="s"
+ fullWidth
+ >
+
+ path={policyNamePath}
+ config={{
+ label: i18n.translate('xpack.indexLifecycleMgmt.editPolicy.policyNameLabel', {
+ defaultMessage: 'Policy name',
+ }),
+ helpText: i18n.translate(
+ 'xpack.indexLifecycleMgmt.editPolicy.validPolicyNameMessage',
+ {
+ defaultMessage:
+ 'A policy name cannot start with an underscore and cannot contain a question mark or a space.',
+ }
+ ),
+ validations: policyNameValidations,
+ }}
+ component={TextField}
+ componentProps={{
+ fullWidth: false,
+ euiFieldProps: {
+ 'data-test-subj': 'policyNameField',
+ },
+ }}
+ />
+
+ ) : null}
-
+
-
+
-
+
-
+
-
+
-
+
-
+
- 0
- }
- getUrlForApp={getUrlForApp}
- setPhaseData={setDeletePhaseData}
- phaseData={policy.phases.delete}
- />
+
-
-
-
-
-
-
-
- {saveAsNew ? (
-
- ) : (
-
- )}
-
-
-
-
-
+
+
+
+
+
+
+
+ {saveAsNew ? (
-
-
-
-
-
-
-
- {isShowingPolicyJsonFlyout ? (
-
- ) : (
+ ) : (
+
+ )}
+
+
+
+
+
- )}
-
-
-
-
- {isShowingPolicyJsonFlyout ? (
- setIsShowingPolicyJsonFlyout(false)}
- />
- ) : null}
-
-
-
-
-
-
+
+
+
+
+
+
+
+ {isShowingPolicyJsonFlyout ? (
+
+ ) : (
+
+ )}
+
+
+
+
+ {isShowingPolicyJsonFlyout ? (
+ setIsShowingPolicyJsonFlyout(false)}
+ />
+ ) : null}
+
+
+
+
+
);
};
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy_context.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy_context.tsx
index 4748c26d6cec1..da5f940b1b6c8 100644
--- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy_context.tsx
+++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy_context.tsx
@@ -5,10 +5,16 @@
*/
import React, { createContext, ReactChild, useContext } from 'react';
-import { SerializedPolicy } from '../../../../common/types';
+import { ApplicationStart } from 'kibana/public';
-interface EditPolicyContextValue {
- originalPolicy: SerializedPolicy;
+import { PolicyFromES, SerializedPolicy } from '../../../../common/types';
+
+export interface EditPolicyContextValue {
+ isNewPolicy: boolean;
+ policy: SerializedPolicy;
+ existingPolicies: PolicyFromES[];
+ getUrlForApp: ApplicationStart['getUrlForApp'];
+ policyName?: string;
}
const EditPolicyContext = createContext(null as any);
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/deserializer.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/deserializer.ts
similarity index 82%
rename from x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/deserializer.ts
rename to x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/deserializer.ts
index f0294a5391d21..5af8807f2dec8 100644
--- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/deserializer.ts
+++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/deserializer.ts
@@ -6,17 +6,17 @@
import { produce } from 'immer';
-import { SerializedPolicy } from '../../../../common/types';
+import { SerializedPolicy } from '../../../../../common/types';
-import { splitSizeAndUnits } from '../../services/policies/policy_serialization';
+import { splitSizeAndUnits } from '../../../lib/policies';
-import { determineDataTierAllocationType } from '../../lib';
+import { determineDataTierAllocationType } from '../../../lib';
-import { FormInternal } from './types';
+import { FormInternal } from '../types';
export const deserializer = (policy: SerializedPolicy): FormInternal => {
const {
- phases: { hot, warm, cold },
+ phases: { hot, warm, cold, delete: deletePhase },
} = policy;
const _meta: FormInternal['_meta'] = {
@@ -37,6 +37,9 @@ export const deserializer = (policy: SerializedPolicy): FormInternal => {
dataTierAllocationType: determineDataTierAllocationType(cold?.actions),
freezeEnabled: Boolean(cold?.actions?.freeze),
},
+ delete: {
+ enabled: Boolean(deletePhase),
+ },
};
return produce(
@@ -86,6 +89,14 @@ export const deserializer = (policy: SerializedPolicy): FormInternal => {
draft._meta.cold.minAgeUnit = minAge.units;
}
}
+
+ if (draft.phases.delete) {
+ if (draft.phases.delete.min_age) {
+ const minAge = splitSizeAndUnits(draft.phases.delete.min_age);
+ draft.phases.delete.min_age = minAge.size;
+ draft._meta.delete.minAgeUnit = minAge.units;
+ }
+ }
}
);
};
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/index.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/index.ts
new file mode 100644
index 0000000000000..82fa478832582
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/index.ts
@@ -0,0 +1,13 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+export { deserializer } from './deserializer';
+
+export { createSerializer } from './serializer';
+
+export { schema } from './schema';
+
+export * from './validations';
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form_schema.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/schema.ts
similarity index 90%
rename from x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form_schema.ts
rename to x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/schema.ts
index 070f03f74b954..4d20db4018740 100644
--- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form_schema.ts
+++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/schema.ts
@@ -6,18 +6,19 @@
import { i18n } from '@kbn/i18n';
-import { FormSchema, fieldValidators } from '../../../shared_imports';
-import { defaultSetPriority, defaultPhaseIndexPriority } from '../../constants';
+import { FormSchema, fieldValidators } from '../../../../shared_imports';
+import { defaultSetPriority, defaultPhaseIndexPriority } from '../../../constants';
-import { FormInternal } from './types';
+import { FormInternal } from '../types';
import {
ifExistsNumberGreaterThanZero,
ifExistsNumberNonNegative,
rolloverThresholdsValidator,
-} from './form_validations';
+ minAgeValidator,
+} from './validations';
-import { i18nTexts } from './i18n_texts';
+import { i18nTexts } from '../i18n_texts';
const { emptyField, numberGreaterThanField } = fieldValidators;
@@ -97,6 +98,18 @@ export const schema: FormSchema = {
label: i18nTexts.editPolicy.allocationNodeAttributeFieldLabel,
},
},
+ delete: {
+ enabled: {
+ defaultValue: false,
+ label: i18n.translate(
+ 'xpack.indexLifecycleMgmt.editPolicy.deletePhase.activateWarmPhaseSwitchLabel',
+ { defaultMessage: 'Activate delete phase' }
+ ),
+ },
+ minAgeUnit: {
+ defaultValue: 'd',
+ },
+ },
},
phases: {
hot: {
@@ -177,15 +190,7 @@ export const schema: FormSchema = {
defaultValue: '0',
validations: [
{
- validator: (arg) =>
- numberGreaterThanField({
- than: 0,
- allowEquality: true,
- message: i18nTexts.editPolicy.errors.nonNegativeNumberRequired,
- })({
- ...arg,
- value: arg.value === '' ? -Infinity : parseInt(arg.value, 10),
- }),
+ validator: minAgeValidator,
},
],
},
@@ -256,15 +261,7 @@ export const schema: FormSchema = {
defaultValue: '0',
validations: [
{
- validator: (arg) =>
- numberGreaterThanField({
- than: 0,
- allowEquality: true,
- message: i18nTexts.editPolicy.errors.nonNegativeNumberRequired,
- })({
- ...arg,
- value: arg.value === '' ? -Infinity : parseInt(arg.value, 10),
- }),
+ validator: minAgeValidator,
},
],
},
@@ -292,5 +289,15 @@ export const schema: FormSchema = {
},
},
},
+ delete: {
+ min_age: {
+ defaultValue: '0',
+ validations: [
+ {
+ validator: minAgeValidator,
+ },
+ ],
+ },
+ },
},
};
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/serializer.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/serializer.ts
similarity index 90%
rename from x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/serializer.ts
rename to x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/serializer.ts
index 564b5a2c4e397..2274efda426ad 100644
--- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/serializer.ts
+++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/serializer.ts
@@ -4,12 +4,11 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { isEmpty } from 'lodash';
+import { isEmpty, isNumber } from 'lodash';
-import { SerializedPolicy, SerializedActionWithAllocation } from '../../../../common/types';
+import { SerializedPolicy, SerializedActionWithAllocation } from '../../../../../common/types';
-import { FormInternal, DataAllocationMetaFields } from './types';
-import { isNumber } from '../../services/policies/policy_serialization';
+import { FormInternal, DataAllocationMetaFields } from '../types';
const serializeAllocateAction = (
{ dataTierAllocationType, allocationNodeAttribute }: DataAllocationMetaFields,
@@ -165,5 +164,22 @@ export const createSerializer = (originalPolicy?: SerializedPolicy) => (
}
}
+ /**
+ * DELETE PHASE SERIALIZATION
+ */
+ if (policy.phases.delete) {
+ if (policy.phases.delete.min_age) {
+ policy.phases.delete.min_age = `${policy.phases.delete.min_age}${_meta.delete.minAgeUnit}`;
+ }
+
+ if (originalPolicy?.phases.delete?.actions) {
+ const { wait_for_snapshot: __, ...rest } = originalPolicy.phases.delete.actions;
+ policy.phases.delete.actions = {
+ ...policy.phases.delete.actions,
+ ...rest,
+ };
+ }
+ }
+
return policy;
};
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form_validations.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/validations.ts
similarity index 50%
rename from x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form_validations.ts
rename to x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/validations.ts
index 9c855ccb41624..f2e26a552efc9 100644
--- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form_validations.ts
+++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/validations.ts
@@ -4,13 +4,15 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { fieldValidators, ValidationFunc } from '../../../shared_imports';
+import { fieldValidators, ValidationFunc, ValidationConfig } from '../../../../shared_imports';
-import { ROLLOVER_FORM_PATHS } from './constants';
+import { ROLLOVER_FORM_PATHS } from '../constants';
-import { i18nTexts } from './i18n_texts';
+import { i18nTexts } from '../i18n_texts';
+import { PolicyFromES } from '../../../../../common/types';
+import { FormInternal } from '../types';
-const { numberGreaterThanField } = fieldValidators;
+const { numberGreaterThanField, containsCharsField, emptyField, startsWithField } = fieldValidators;
const createIfNumberExistsValidator = ({
than,
@@ -46,7 +48,7 @@ export const ifExistsNumberNonNegative = createIfNumberExistsValidator({
* A special validation type used to keep track of validation errors for
* the rollover threshold values not being set (e.g., age and doc count)
*/
-export const ROLLOVER_EMPTY_VALIDATION = 'EMPTY';
+export const ROLLOVER_EMPTY_VALIDATION = 'ROLLOVER_EMPTY_VALIDATION';
/**
* An ILM policy requires that for rollover a value must be set for one of the threshold values.
@@ -87,3 +89,68 @@ export const rolloverThresholdsValidator: ValidationFunc = ({ form }) => {
fields[ROLLOVER_FORM_PATHS.maxSize].clearErrors(ROLLOVER_EMPTY_VALIDATION);
}
};
+
+export const minAgeValidator: ValidationFunc = (arg) =>
+ numberGreaterThanField({
+ than: 0,
+ allowEquality: true,
+ message: i18nTexts.editPolicy.errors.nonNegativeNumberRequired,
+ })({
+ ...arg,
+ value: arg.value === '' ? -Infinity : parseInt(arg.value, 10),
+ });
+
+export const createPolicyNameValidations = ({
+ policies,
+ saveAsNewPolicy,
+ originalPolicyName,
+}: {
+ policies: PolicyFromES[];
+ saveAsNewPolicy: boolean;
+ originalPolicyName?: string;
+}): Array> => {
+ return [
+ {
+ validator: emptyField(i18nTexts.editPolicy.errors.policyNameRequiredMessage),
+ },
+ {
+ validator: startsWithField({
+ message: i18nTexts.editPolicy.errors.policyNameStartsWithUnderscoreErrorMessage,
+ char: '_',
+ }),
+ },
+ {
+ validator: containsCharsField({
+ message: i18nTexts.editPolicy.errors.policyNameContainsInvalidChars,
+ chars: [',', ' '],
+ }),
+ },
+ {
+ validator: (arg) => {
+ const policyName = arg.value;
+ if (window.TextEncoder && new window.TextEncoder().encode(policyName).length > 255) {
+ return {
+ message: i18nTexts.editPolicy.errors.policyNameTooLongErrorMessage,
+ };
+ }
+ },
+ },
+ {
+ validator: (arg) => {
+ const policyName = arg.value;
+ if (saveAsNewPolicy && policyName === originalPolicyName) {
+ return {
+ message: i18nTexts.editPolicy.errors.policyNameMustBeDifferentErrorMessage,
+ };
+ } else if (policyName !== originalPolicyName) {
+ const policyNames = policies.map((existingPolicy) => existingPolicy.name);
+ if (policyNames.includes(policyName)) {
+ return {
+ message: i18nTexts.editPolicy.errors.policyNameAlreadyUsedErrorMessage,
+ };
+ }
+ }
+ },
+ },
+ ];
+};
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/i18n_texts.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/i18n_texts.ts
index 1fba69b7634ae..ccd5d3a568fe3 100644
--- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/i18n_texts.ts
+++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/i18n_texts.ts
@@ -98,6 +98,42 @@ export const i18nTexts = {
defaultMessage: 'Only non-negative numbers are allowed.',
}
),
+ policyNameContainsInvalidChars: i18n.translate(
+ 'xpack.indexLifecycleMgmt.editPolicy.errors.policyNameContainsInvalidCharsError',
+ {
+ defaultMessage: 'A policy name cannot contain spaces or commas.',
+ }
+ ),
+ policyNameAlreadyUsedErrorMessage: i18n.translate(
+ 'xpack.indexLifecycleMgmt.editPolicy.policyNameAlreadyUsedError',
+ {
+ defaultMessage: 'That policy name is already used.',
+ }
+ ),
+ policyNameMustBeDifferentErrorMessage: i18n.translate(
+ 'xpack.indexLifecycleMgmt.editPolicy.differentPolicyNameRequiredError',
+ {
+ defaultMessage: 'The policy name must be different.',
+ }
+ ),
+ policyNameRequiredMessage: i18n.translate(
+ 'xpack.indexLifecycleMgmt.editPolicy.policyNameRequiredError',
+ {
+ defaultMessage: 'A policy name is required.',
+ }
+ ),
+ policyNameStartsWithUnderscoreErrorMessage: i18n.translate(
+ 'xpack.indexLifecycleMgmt.editPolicy.policyNameStartsWithUnderscoreError',
+ {
+ defaultMessage: 'A policy name cannot start with an underscore.',
+ }
+ ),
+ policyNameTooLongErrorMessage: i18n.translate(
+ 'xpack.indexLifecycleMgmt.editPolicy.policyNameTooLongError',
+ {
+ defaultMessage: 'A policy name cannot be longer than 255 bytes.',
+ }
+ ),
},
},
};
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/services/policies/policy_save.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/save_policy.ts
similarity index 84%
rename from x-pack/plugins/index_lifecycle_management/public/application/services/policies/policy_save.ts
rename to x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/save_policy.ts
index 9cf622e830cb2..e2ab6a8817ef6 100644
--- a/x-pack/plugins/index_lifecycle_management/public/application/services/policies/policy_save.ts
+++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/save_policy.ts
@@ -3,23 +3,22 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
-
import { i18n } from '@kbn/i18n';
import { METRIC_TYPE } from '@kbn/analytics';
import { SerializedPolicy } from '../../../../common/types';
-import { savePolicy as savePolicyApi } from '../api';
-import { showApiError } from '../api_errors';
-import { getUiMetricsForPhases, trackUiMetric } from '../ui_metric';
+
import { UIM_POLICY_CREATE, UIM_POLICY_UPDATE } from '../../constants';
-import { toasts } from '../notification';
+
+import { toasts } from '../../services/notification';
+import { savePolicy as savePolicyApi } from '../../services/api';
+import { getUiMetricsForPhases, trackUiMetric } from '../../services/ui_metric';
+import { showApiError } from '../../services/api_errors';
export const savePolicy = async (
- readSerializedPolicy: () => SerializedPolicy,
+ serializedPolicy: SerializedPolicy,
isNew: boolean
): Promise => {
- const serializedPolicy = readSerializedPolicy();
-
try {
await savePolicyApi(serializedPolicy);
} catch (err) {
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/types.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/types.ts
index 1884f8dbc0619..dc3d8a640e682 100644
--- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/types.ts
+++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/types.ts
@@ -38,6 +38,10 @@ interface ColdPhaseMetaFields extends DataAllocationMetaFields, MinAgeField {
freezeEnabled: boolean;
}
+interface DeletePhaseMetaFields extends MinAgeField {
+ enabled: boolean;
+}
+
/**
* Describes the shape of data after deserialization.
*/
@@ -50,5 +54,6 @@ export interface FormInternal extends SerializedPolicy {
hot: HotPhaseMetaFields;
warm: WarmPhaseMetaFields;
cold: ColdPhaseMetaFields;
+ delete: DeletePhaseMetaFields;
};
}
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/services/policies/delete_phase.ts b/x-pack/plugins/index_lifecycle_management/public/application/services/policies/delete_phase.ts
deleted file mode 100644
index 6ada039d45cd9..0000000000000
--- a/x-pack/plugins/index_lifecycle_management/public/application/services/policies/delete_phase.ts
+++ /dev/null
@@ -1,88 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License;
- * you may not use this file except in compliance with the Elastic License.
- */
-
-import { DeletePhase, SerializedDeletePhase } from '../../../../common/types';
-import { serializedPhaseInitialization } from '../../constants';
-import { isNumber, splitSizeAndUnits } from './policy_serialization';
-import {
- numberRequiredMessage,
- PhaseValidationErrors,
- positiveNumberRequiredMessage,
-} from './policy_validation';
-
-const deletePhaseInitialization: DeletePhase = {
- phaseEnabled: false,
- selectedMinimumAge: '0',
- selectedMinimumAgeUnits: 'd',
- waitForSnapshotPolicy: '',
-};
-
-export const deletePhaseFromES = (phaseSerialized?: SerializedDeletePhase): DeletePhase => {
- const phase = { ...deletePhaseInitialization };
- if (phaseSerialized === undefined || phaseSerialized === null) {
- return phase;
- }
-
- phase.phaseEnabled = true;
- if (phaseSerialized.min_age) {
- const { size: minAge, units: minAgeUnits } = splitSizeAndUnits(phaseSerialized.min_age);
- phase.selectedMinimumAge = minAge;
- phase.selectedMinimumAgeUnits = minAgeUnits;
- }
-
- if (phaseSerialized.actions) {
- const actions = phaseSerialized.actions;
-
- if (actions.wait_for_snapshot) {
- phase.waitForSnapshotPolicy = actions.wait_for_snapshot.policy;
- }
- }
-
- return phase;
-};
-
-export const deletePhaseToES = (
- phase: DeletePhase,
- originalEsPhase?: SerializedDeletePhase
-): SerializedDeletePhase => {
- if (!originalEsPhase) {
- originalEsPhase = { ...serializedPhaseInitialization };
- }
- const esPhase = { ...originalEsPhase };
-
- if (isNumber(phase.selectedMinimumAge)) {
- esPhase.min_age = `${phase.selectedMinimumAge}${phase.selectedMinimumAgeUnits}`;
- }
-
- esPhase.actions = esPhase.actions ? { ...esPhase.actions } : {};
-
- if (phase.waitForSnapshotPolicy) {
- esPhase.actions.wait_for_snapshot = {
- policy: phase.waitForSnapshotPolicy,
- };
- } else {
- delete esPhase.actions.wait_for_snapshot;
- }
-
- return esPhase;
-};
-
-export const validateDeletePhase = (phase: DeletePhase): PhaseValidationErrors => {
- if (!phase.phaseEnabled) {
- return {};
- }
-
- const phaseErrors = {} as PhaseValidationErrors;
-
- // min age needs to be a positive number
- if (!isNumber(phase.selectedMinimumAge)) {
- phaseErrors.selectedMinimumAge = [numberRequiredMessage];
- } else if (parseInt(phase.selectedMinimumAge, 10) < 0) {
- phaseErrors.selectedMinimumAge = [positiveNumberRequiredMessage];
- }
-
- return { ...phaseErrors };
-};
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/services/policies/policy_serialization.test.ts b/x-pack/plugins/index_lifecycle_management/public/application/services/policies/policy_serialization.test.ts
deleted file mode 100644
index 19481b39a2c80..0000000000000
--- a/x-pack/plugins/index_lifecycle_management/public/application/services/policies/policy_serialization.test.ts
+++ /dev/null
@@ -1,198 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License;
- * you may not use this file except in compliance with the Elastic License.
- */
-// Prefer importing entire lodash library, e.g. import { get } from "lodash"
-// eslint-disable-next-line no-restricted-imports
-import cloneDeep from 'lodash/cloneDeep';
-import { deserializePolicy, legacySerializePolicy } from './policy_serialization';
-import { defaultNewDeletePhase } from '../../constants';
-
-describe('Policy serialization', () => {
- test('serialize a policy using "default" data allocation', () => {
- expect(
- legacySerializePolicy(
- {
- name: 'test',
- phases: {
- delete: { ...defaultNewDeletePhase },
- },
- },
- {
- name: 'test',
- phases: {
- hot: { actions: {} },
- },
- }
- )
- ).toEqual({
- name: 'test',
- phases: {},
- });
- });
-
- test('serialize a policy using "custom" data allocation', () => {
- expect(
- legacySerializePolicy(
- {
- name: 'test',
- phases: {
- delete: { ...defaultNewDeletePhase },
- },
- },
- {
- name: 'test',
- phases: {
- hot: { actions: {} },
- },
- }
- )
- ).toEqual({
- name: 'test',
- phases: {},
- });
- });
-
- test('serialize a policy using "custom" data allocation with no node attributes', () => {
- expect(
- legacySerializePolicy(
- {
- name: 'test',
- phases: {
- delete: { ...defaultNewDeletePhase },
- },
- },
- {
- name: 'test',
- phases: {
- hot: { actions: {} },
- },
- }
- )
- ).toEqual({
- // There should be no allocation action in any phases...
- name: 'test',
- phases: {},
- });
- });
-
- test('serialize a policy using "none" data allocation with no node attributes', () => {
- expect(
- legacySerializePolicy(
- {
- name: 'test',
- phases: {
- delete: { ...defaultNewDeletePhase },
- },
- },
- {
- name: 'test',
- phases: {
- hot: { actions: {} },
- },
- }
- )
- ).toEqual({
- // There should be no allocation action in any phases...
- name: 'test',
- phases: {},
- });
- });
-
- test('serialization does not alter the original policy', () => {
- const originalPolicy = {
- name: 'test',
- phases: {},
- };
-
- const originalClone = cloneDeep(originalPolicy);
-
- const deserializedPolicy = {
- name: 'test',
- phases: {
- delete: { ...defaultNewDeletePhase },
- },
- };
-
- legacySerializePolicy(deserializedPolicy, originalPolicy);
- expect(originalPolicy).toEqual(originalClone);
- });
-
- test('serialize a policy using "best_compression" codec for forcemerge', () => {
- expect(
- legacySerializePolicy(
- {
- name: 'test',
- phases: {
- delete: { ...defaultNewDeletePhase },
- },
- },
- {
- name: 'test',
- phases: {
- hot: { actions: {} },
- },
- }
- )
- ).toEqual({
- name: 'test',
- phases: {},
- });
- });
-
- test('de-serialize a policy using "best_compression" codec for forcemerge', () => {
- expect(
- deserializePolicy({
- modified_date: Date.now().toString(),
- name: 'test',
- version: 1,
- policy: {
- name: 'test',
- phases: {
- hot: {
- actions: {
- rollover: {
- max_age: '30d',
- max_size: '50gb',
- },
- forcemerge: {
- max_num_segments: 1,
- index_codec: 'best_compression',
- },
- set_priority: {
- priority: 100,
- },
- },
- },
- },
- },
- })
- ).toEqual({
- name: 'test',
- phases: {
- delete: { ...defaultNewDeletePhase },
- },
- });
- });
-
- test('delete "best_compression" codec for forcemerge if disabled in UI', () => {
- expect(
- legacySerializePolicy(
- {
- name: 'test',
- phases: {
- delete: { ...defaultNewDeletePhase },
- },
- },
- {
- name: 'test',
- phases: {},
- }
- )
- ).toEqual({
- name: 'test',
- phases: {},
- });
- });
-});
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/services/policies/policy_serialization.ts b/x-pack/plugins/index_lifecycle_management/public/application/services/policies/policy_serialization.ts
deleted file mode 100644
index 55e9d88dcd383..0000000000000
--- a/x-pack/plugins/index_lifecycle_management/public/application/services/policies/policy_serialization.ts
+++ /dev/null
@@ -1,82 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License;
- * you may not use this file except in compliance with the Elastic License.
- */
-
-import { LegacyPolicy, PolicyFromES, SerializedPolicy } from '../../../../common/types';
-
-import { defaultNewDeletePhase, serializedPhaseInitialization } from '../../constants';
-
-import { deletePhaseFromES, deletePhaseToES } from './delete_phase';
-
-export const splitSizeAndUnits = (field: string): { size: string; units: string } => {
- let size = '';
- let units = '';
-
- const result = /(\d+)(\w+)/.exec(field);
- if (result) {
- size = result[1];
- units = result[2];
- }
-
- return {
- size,
- units,
- };
-};
-
-export const isNumber = (value: any): boolean => value !== '' && value !== null && isFinite(value);
-
-export const getPolicyByName = (
- policies: PolicyFromES[] | null | undefined,
- policyName: string = ''
-): PolicyFromES | undefined => {
- if (policies && policies.length > 0) {
- return policies.find((policy: PolicyFromES) => policy.name === policyName);
- }
-};
-
-export const initializeNewPolicy = (newPolicyName: string = ''): LegacyPolicy => {
- return {
- name: newPolicyName,
- phases: {
- delete: { ...defaultNewDeletePhase },
- },
- };
-};
-
-export const deserializePolicy = (policy: PolicyFromES): LegacyPolicy => {
- const {
- name,
- policy: { phases },
- } = policy;
-
- return {
- name,
- phases: {
- delete: deletePhaseFromES(phases.delete),
- },
- };
-};
-
-export const legacySerializePolicy = (
- policy: LegacyPolicy,
- originalEsPolicy: SerializedPolicy = {
- name: policy.name,
- phases: { hot: { ...serializedPhaseInitialization } },
- }
-): SerializedPolicy => {
- const serializedPolicy = {
- name: policy.name,
- phases: {},
- } as SerializedPolicy;
-
- if (policy.phases.delete.phaseEnabled) {
- serializedPolicy.phases.delete = deletePhaseToES(
- policy.phases.delete,
- originalEsPolicy.phases.delete
- );
- }
- return serializedPolicy;
-};
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/services/policies/policy_validation.ts b/x-pack/plugins/index_lifecycle_management/public/application/services/policies/policy_validation.ts
deleted file mode 100644
index 79c909c433f33..0000000000000
--- a/x-pack/plugins/index_lifecycle_management/public/application/services/policies/policy_validation.ts
+++ /dev/null
@@ -1,144 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License;
- * you may not use this file except in compliance with the Elastic License.
- */
-
-import { i18n } from '@kbn/i18n';
-import { DeletePhase, LegacyPolicy, PolicyFromES } from '../../../../common/types';
-import { validateDeletePhase } from './delete_phase';
-
-export const propertyof = (propertyName: keyof T & string) => propertyName;
-
-export const numberRequiredMessage = i18n.translate(
- 'xpack.indexLifecycleMgmt.editPolicy.numberRequiredError',
- {
- defaultMessage: 'A number is required.',
- }
-);
-
-// TODO validation includes 0 -> should be non-negative number?
-export const positiveNumberRequiredMessage = i18n.translate(
- 'xpack.indexLifecycleMgmt.editPolicy.positiveNumberRequiredError',
- {
- defaultMessage: 'Only positive numbers are allowed.',
- }
-);
-
-export const positiveNumbersAboveZeroErrorMessage = i18n.translate(
- 'xpack.indexLifecycleMgmt.editPolicy.positiveNumberAboveZeroRequiredError',
- {
- defaultMessage: 'Only numbers above 0 are allowed.',
- }
-);
-
-export const policyNameRequiredMessage = i18n.translate(
- 'xpack.indexLifecycleMgmt.editPolicy.policyNameRequiredError',
- {
- defaultMessage: 'A policy name is required.',
- }
-);
-
-export const policyNameStartsWithUnderscoreErrorMessage = i18n.translate(
- 'xpack.indexLifecycleMgmt.editPolicy.policyNameStartsWithUnderscoreError',
- {
- defaultMessage: 'A policy name cannot start with an underscore.',
- }
-);
-export const policyNameContainsCommaErrorMessage = i18n.translate(
- 'xpack.indexLifecycleMgmt.editPolicy.policyNameContainsCommaError',
- {
- defaultMessage: 'A policy name cannot include a comma.',
- }
-);
-export const policyNameContainsSpaceErrorMessage = i18n.translate(
- 'xpack.indexLifecycleMgmt.editPolicy.policyNameContainsSpaceError',
- {
- defaultMessage: 'A policy name cannot include a space.',
- }
-);
-
-export const policyNameTooLongErrorMessage = i18n.translate(
- 'xpack.indexLifecycleMgmt.editPolicy.policyNameTooLongError',
- {
- defaultMessage: 'A policy name cannot be longer than 255 bytes.',
- }
-);
-export const policyNameMustBeDifferentErrorMessage = i18n.translate(
- 'xpack.indexLifecycleMgmt.editPolicy.differentPolicyNameRequiredError',
- {
- defaultMessage: 'The policy name must be different.',
- }
-);
-export const policyNameAlreadyUsedErrorMessage = i18n.translate(
- 'xpack.indexLifecycleMgmt.editPolicy.policyNameAlreadyUsedError',
- {
- defaultMessage: 'That policy name is already used.',
- }
-);
-export type PhaseValidationErrors = {
- [P in keyof Partial]: string[];
-};
-
-export interface ValidationErrors {
- delete: PhaseValidationErrors;
- policyName: string[];
-}
-
-export const validatePolicy = (
- saveAsNew: boolean,
- policy: LegacyPolicy,
- policies: PolicyFromES[],
- originalPolicyName: string
-): [boolean, ValidationErrors] => {
- const policyNameErrors: string[] = [];
- if (!policy.name) {
- policyNameErrors.push(policyNameRequiredMessage);
- } else {
- if (policy.name.startsWith('_')) {
- policyNameErrors.push(policyNameStartsWithUnderscoreErrorMessage);
- }
- if (policy.name.includes(',')) {
- policyNameErrors.push(policyNameContainsCommaErrorMessage);
- }
- if (policy.name.includes(' ')) {
- policyNameErrors.push(policyNameContainsSpaceErrorMessage);
- }
- if (window.TextEncoder && new window.TextEncoder().encode(policy.name).length > 255) {
- policyNameErrors.push(policyNameTooLongErrorMessage);
- }
-
- if (saveAsNew && policy.name === originalPolicyName) {
- policyNameErrors.push(policyNameMustBeDifferentErrorMessage);
- } else if (policy.name !== originalPolicyName) {
- const policyNames = policies.map((existingPolicy) => existingPolicy.name);
- if (policyNames.includes(policy.name)) {
- policyNameErrors.push(policyNameAlreadyUsedErrorMessage);
- }
- }
- }
-
- const deletePhaseErrors = validateDeletePhase(policy.phases.delete);
- const isValid = policyNameErrors.length === 0 && Object.keys(deletePhaseErrors).length === 0;
- return [
- isValid,
- {
- policyName: [...policyNameErrors],
- delete: deletePhaseErrors,
- },
- ];
-};
-
-export const findFirstError = (errors?: ValidationErrors): string | undefined => {
- if (!errors) {
- return;
- }
-
- if (errors.policyName.length > 0) {
- return propertyof('policyName');
- }
-
- if (Object.keys(errors.delete).length > 0) {
- return `${propertyof('delete')}.${Object.keys(errors.delete)[0]}`;
- }
-};
diff --git a/x-pack/plugins/index_lifecycle_management/public/shared_imports.ts b/x-pack/plugins/index_lifecycle_management/public/shared_imports.ts
index 023aeba57aa7a..a127574d5bad0 100644
--- a/x-pack/plugins/index_lifecycle_management/public/shared_imports.ts
+++ b/x-pack/plugins/index_lifecycle_management/public/shared_imports.ts
@@ -18,6 +18,7 @@ export {
getFieldValidityAndErrorMessage,
useFormContext,
FormSchema,
+ ValidationConfig,
} from '../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib';
export { fieldValidators } from '../../../../src/plugins/es_ui_shared/static/forms/helpers';
@@ -27,6 +28,8 @@ export {
NumericField,
SelectField,
SuperSelectField,
+ ComboBoxField,
+ TextField,
} from '../../../../src/plugins/es_ui_shared/static/forms/components';
export { KibanaContextProvider } from '../../../../src/plugins/kibana_react/public';
diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json
index e7784846598e4..baa4f37791007 100644
--- a/x-pack/plugins/translations/translations/ja-JP.json
+++ b/x-pack/plugins/translations/translations/ja-JP.json
@@ -9119,24 +9119,18 @@
"xpack.indexLifecycleMgmt.editPolicy.nodeAttributesReloadButton": "再試行",
"xpack.indexLifecycleMgmt.editPolicy.nodeDetailsLoadingFailedTitle": "ノード属性詳細を読み込めません",
"xpack.indexLifecycleMgmt.editPolicy.nodeDetailsReloadButton": "再試行",
- "xpack.indexLifecycleMgmt.editPolicy.numberRequiredError": "数字が必要です。",
"xpack.indexLifecycleMgmt.editPolicy.phaseCold.minimumAgeLabel": "コールドフェーズのタイミング",
"xpack.indexLifecycleMgmt.editPolicy.phaseCold.minimumAgeUnitsAriaLabel": "コールドフェーズのタイミングの単位",
"xpack.indexLifecycleMgmt.editPolicy.phaseDelete.minimumAgeLabel": "削除フェーズのタイミング",
"xpack.indexLifecycleMgmt.editPolicy.phaseDelete.minimumAgeUnitsAriaLabel": "削除フェーズのタイミングの単位",
- "xpack.indexLifecycleMgmt.editPolicy.phaseErrorMessage": "エラーを修正してください",
"xpack.indexLifecycleMgmt.editPolicy.phaseWarm.minimumAgeLabel": "ウォームフェーズのタイミング",
"xpack.indexLifecycleMgmt.editPolicy.phaseWarm.minimumAgeUnitsAriaLabel": "ウォームフェーズのタイミングの単位",
"xpack.indexLifecycleMgmt.editPolicy.policiesLoading": "ポリシーを読み込み中…",
"xpack.indexLifecycleMgmt.editPolicy.policyNameAlreadyUsedError": "このポリシー名は既に使用されています。",
- "xpack.indexLifecycleMgmt.editPolicy.policyNameContainsCommaError": "ポリシー名にはコンマを使用できません。",
- "xpack.indexLifecycleMgmt.editPolicy.policyNameContainsSpaceError": "ポリシー名にはスペースを使用できません。",
"xpack.indexLifecycleMgmt.editPolicy.policyNameLabel": "ポリシー名",
"xpack.indexLifecycleMgmt.editPolicy.policyNameRequiredError": "ポリシー名が必要です。",
"xpack.indexLifecycleMgmt.editPolicy.policyNameStartsWithUnderscoreError": "ポリシー名の頭にアンダーラインを使用することはできません。",
"xpack.indexLifecycleMgmt.editPolicy.policyNameTooLongError": "ポリシー名は 255 バイト未満である必要があります。",
- "xpack.indexLifecycleMgmt.editPolicy.positiveNumberAboveZeroRequiredError": "0 よりも大きい数字のみ使用できます。",
- "xpack.indexLifecycleMgmt.editPolicy.positiveNumberRequiredError": "プラスの数字のみ使用できます。",
"xpack.indexLifecycleMgmt.editPolicy.rolloverDaysOptionLabel": "ロールオーバーからの経過日数",
"xpack.indexLifecycleMgmt.editPolicy.rolloverHoursOptionLabel": "ロールオーバーからの経過時間数",
"xpack.indexLifecycleMgmt.editPolicy.rolloverMicroSecondsOptionLabel": "ロールオーバーからの経過時間(マイクロ秒)",
diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json
index f3cd662bacba7..c4274524928fd 100644
--- a/x-pack/plugins/translations/translations/zh-CN.json
+++ b/x-pack/plugins/translations/translations/zh-CN.json
@@ -9128,24 +9128,18 @@
"xpack.indexLifecycleMgmt.editPolicy.nodeAttributesReloadButton": "重试",
"xpack.indexLifecycleMgmt.editPolicy.nodeDetailsLoadingFailedTitle": "无法加载节点属性详情",
"xpack.indexLifecycleMgmt.editPolicy.nodeDetailsReloadButton": "重试",
- "xpack.indexLifecycleMgmt.editPolicy.numberRequiredError": "数字必填。",
"xpack.indexLifecycleMgmt.editPolicy.phaseCold.minimumAgeLabel": "冷阶段计时",
"xpack.indexLifecycleMgmt.editPolicy.phaseCold.minimumAgeUnitsAriaLabel": "冷阶段计时单位",
"xpack.indexLifecycleMgmt.editPolicy.phaseDelete.minimumAgeLabel": "删除阶段计时",
"xpack.indexLifecycleMgmt.editPolicy.phaseDelete.minimumAgeUnitsAriaLabel": "删除阶段计时单位",
- "xpack.indexLifecycleMgmt.editPolicy.phaseErrorMessage": "修复错误",
"xpack.indexLifecycleMgmt.editPolicy.phaseWarm.minimumAgeLabel": "温阶段计时",
"xpack.indexLifecycleMgmt.editPolicy.phaseWarm.minimumAgeUnitsAriaLabel": "温阶段计时单位",
"xpack.indexLifecycleMgmt.editPolicy.policiesLoading": "正在加载策略……",
"xpack.indexLifecycleMgmt.editPolicy.policyNameAlreadyUsedError": "该策略名称已被使用。",
- "xpack.indexLifecycleMgmt.editPolicy.policyNameContainsCommaError": "策略名称不能包含逗号。",
- "xpack.indexLifecycleMgmt.editPolicy.policyNameContainsSpaceError": "策略名称不能包含空格。",
"xpack.indexLifecycleMgmt.editPolicy.policyNameLabel": "策略名称",
"xpack.indexLifecycleMgmt.editPolicy.policyNameRequiredError": "策略名称必填。",
"xpack.indexLifecycleMgmt.editPolicy.policyNameStartsWithUnderscoreError": "策略名称不能以下划线开头。",
"xpack.indexLifecycleMgmt.editPolicy.policyNameTooLongError": "策略名称的长度不能大于 255 字节。",
- "xpack.indexLifecycleMgmt.editPolicy.positiveNumberAboveZeroRequiredError": "仅允许使用 0 以上的数字。",
- "xpack.indexLifecycleMgmt.editPolicy.positiveNumberRequiredError": "仅允许使用正数。",
"xpack.indexLifecycleMgmt.editPolicy.rolloverDaysOptionLabel": "天(自滚动更新)",
"xpack.indexLifecycleMgmt.editPolicy.rolloverHoursOptionLabel": "小时(自滚动更新)",
"xpack.indexLifecycleMgmt.editPolicy.rolloverMicroSecondsOptionLabel": "微秒(自滚动更新)",
From c78cf35ba8bfdff5f61d3932cbacd45a69e4f773 Mon Sep 17 00:00:00 2001
From: Dhruv Bodani
Date: Mon, 9 Nov 2020 19:05:05 +0530
Subject: [PATCH 5/8] Added `defaultActionMessage` to index threshold alert UI
type definition (#80936)
* resolves https://github.com/elastic/kibana/issues/78148
Adds a `defaultActionMessage` to the index threshold alert, so that the `message` parameter for actions will be pre-filled with a useful message
---
.../index_threshold/action_context.test.ts | 3 ++
.../index_threshold/action_context.ts | 7 ++--
.../index_threshold/alert_type.test.ts | 4 ++
.../alert_types/index_threshold/alert_type.ts | 12 ++++++
.../builtin_alert_types/threshold/index.ts | 7 +++-
.../index_threshold/alert.ts | 41 ++++++++++++++-----
.../alert_create_flyout.ts | 5 ++-
7 files changed, 63 insertions(+), 16 deletions(-)
diff --git a/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/action_context.test.ts b/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/action_context.test.ts
index 3f5addb77cb33..48847686828a9 100644
--- a/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/action_context.test.ts
+++ b/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/action_context.test.ts
@@ -25,6 +25,7 @@ describe('ActionContext', () => {
date: '2020-01-01T00:00:00.000Z',
group: '[group]',
value: 42,
+ function: 'count > 4',
};
const context = addMessages({ name: '[alert-name]' }, base, params);
expect(context.title).toMatchInlineSnapshot(
@@ -53,6 +54,7 @@ describe('ActionContext', () => {
date: '2020-01-01T00:00:00.000Z',
group: '[group]',
value: 42,
+ function: 'avg([aggField]) > 4.2',
};
const context = addMessages({ name: '[alert-name]' }, base, params);
expect(context.title).toMatchInlineSnapshot(
@@ -80,6 +82,7 @@ describe('ActionContext', () => {
date: '2020-01-01T00:00:00.000Z',
group: '[group]',
value: 4,
+ function: 'count between 4,5',
};
const context = addMessages({ name: '[alert-name]' }, base, params);
expect(context.title).toMatchInlineSnapshot(
diff --git a/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/action_context.ts b/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/action_context.ts
index 5135e31e9322c..9bb0df9d07fd4 100644
--- a/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/action_context.ts
+++ b/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/action_context.ts
@@ -27,6 +27,8 @@ export interface BaseActionContext extends AlertInstanceContext {
date: string;
// the value that met the threshold
value: number;
+ // the function that is used
+ function: string;
}
export function addMessages(
@@ -42,9 +44,6 @@ export function addMessages(
},
});
- const agg = params.aggField ? `${params.aggType}(${params.aggField})` : `${params.aggType}`;
- const humanFn = `${agg} ${params.thresholdComparator} ${params.threshold.join(',')}`;
-
const window = `${params.timeWindowSize}${params.timeWindowUnit}`;
const message = i18n.translate(
'xpack.stackAlerts.indexThreshold.alertTypeContextMessageDescription',
@@ -55,7 +54,7 @@ export function addMessages(
name: alertInfo.name,
group: baseContext.group,
value: baseContext.value,
- function: humanFn,
+ function: baseContext.function,
window,
date: baseContext.date,
},
diff --git a/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/alert_type.test.ts b/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/alert_type.test.ts
index 2f0cf3cbbcd16..d75f3af22ab06 100644
--- a/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/alert_type.test.ts
+++ b/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/alert_type.test.ts
@@ -46,6 +46,10 @@ describe('alertType', () => {
"description": "The value that exceeded the threshold.",
"name": "value",
},
+ Object {
+ "description": "A string describing the threshold comparator and threshold",
+ "name": "function",
+ },
],
"params": Array [
Object {
diff --git a/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/alert_type.ts b/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/alert_type.ts
index 2a1ed429b7fe1..e0a9cd981dac0 100644
--- a/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/alert_type.ts
+++ b/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/alert_type.ts
@@ -83,6 +83,13 @@ export function getAlertType(service: Service): AlertType {
return {
@@ -107,6 +114,7 @@ export function getAlertType(service: Service): AlertType import('./expression')),
validate: validateExpression,
+ defaultActionMessage: i18n.translate(
+ 'xpack.triggersActionsUI.components.builtinAlertTypes.threshold.alertDefaultActionMessage',
+ {
+ defaultMessage: `alert \\{\\{alertName\\}\\} group \\{\\{context.group\\}\\} value \\{\\{context.value\\}\\} exceeded threshold \\{\\{context.function\\}\\} over \\{\\{params.timeWindowSize\\}\\}\\{\\{params.timeWindowUnit\\}\\} on \\{\\{context.date\\}\\}`,
+ }
+ ),
requiresAppContext: false,
};
}
diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/index_threshold/alert.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/index_threshold/alert.ts
index 92db0458c0639..c05fa6cf051ff 100644
--- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/index_threshold/alert.ts
+++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/index_threshold/alert.ts
@@ -15,6 +15,7 @@ import {
ObjectRemover,
} from '../../../../../common/lib';
import { createEsDocuments } from './create_test_data';
+import { getAlertType } from '../../../../../../../plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/';
const ALERT_TYPE_ID = '.index-threshold';
const ACTION_TYPE_ID = '.index';
@@ -26,6 +27,8 @@ const ALERT_INTERVALS_TO_WRITE = 5;
const ALERT_INTERVAL_SECONDS = 3;
const ALERT_INTERVAL_MILLIS = ALERT_INTERVAL_SECONDS * 1000;
+const DefaultActionMessage = getAlertType().defaultActionMessage;
+
// eslint-disable-next-line import/no-default-export
export default function alertTests({ getService }: FtrProviderContext) {
const supertest = getService('supertest');
@@ -62,6 +65,10 @@ export default function alertTests({ getService }: FtrProviderContext) {
await esTestIndexToolOutput.destroy();
});
+ it('has a default action message', () => {
+ expect(DefaultActionMessage).to.be.ok();
+ });
+
// The tests below create two alerts, one that will fire, one that will
// never fire; the tests ensure the ones that should fire, do fire, and
// those that shouldn't fire, do not fire.
@@ -85,7 +92,7 @@ export default function alertTests({ getService }: FtrProviderContext) {
const docs = await waitForDocs(2);
for (const doc of docs) {
const { group } = doc._source;
- const { name, value, title, message } = doc._source.params;
+ const { name, title, message } = doc._source.params;
expect(name).to.be('always fire');
expect(group).to.be('all documents');
@@ -93,9 +100,8 @@ export default function alertTests({ getService }: FtrProviderContext) {
// we'll check title and message in this test, but not subsequent ones
expect(title).to.be('alert always fire group all documents exceeded threshold');
- const expectedPrefix = `alert always fire group all documents value ${value} exceeded threshold count > -1 over`;
- const messagePrefix = message.substr(0, expectedPrefix.length);
- expect(messagePrefix).to.be(expectedPrefix);
+ const messagePattern = /alert always fire group all documents value \d+ exceeded threshold count > -1 over 15s on \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z/;
+ expect(message).to.match(messagePattern);
}
});
@@ -128,10 +134,13 @@ export default function alertTests({ getService }: FtrProviderContext) {
for (const doc of docs) {
const { group } = doc._source;
- const { name } = doc._source.params;
+ const { name, message } = doc._source.params;
expect(name).to.be('always fire');
if (group === 'group-0') inGroup0++;
+
+ const messagePattern = /alert always fire group group-\d value \d+ exceeded threshold count .+ over 15s on \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z/;
+ expect(message).to.match(messagePattern);
}
// there should be 2 docs in group-0, rando split between others
@@ -163,9 +172,12 @@ export default function alertTests({ getService }: FtrProviderContext) {
const docs = await waitForDocs(2);
for (const doc of docs) {
- const { name } = doc._source.params;
+ const { name, message } = doc._source.params;
expect(name).to.be('always fire');
+
+ const messagePattern = /alert always fire group all documents value \d+ exceeded threshold sum\(testedValue\) between 0,1000000 over 15s on \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z/;
+ expect(message).to.match(messagePattern);
}
});
@@ -195,9 +207,12 @@ export default function alertTests({ getService }: FtrProviderContext) {
const docs = await waitForDocs(4);
for (const doc of docs) {
- const { name } = doc._source.params;
+ const { name, message } = doc._source.params;
expect(name).to.be('always fire');
+
+ const messagePattern = /alert always fire group all documents value .+ exceeded threshold avg\(testedValue\) .+ 0 over 15s on \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z/;
+ expect(message).to.match(messagePattern);
}
});
@@ -232,10 +247,13 @@ export default function alertTests({ getService }: FtrProviderContext) {
for (const doc of docs) {
const { group } = doc._source;
- const { name } = doc._source.params;
+ const { name, message } = doc._source.params;
expect(name).to.be('always fire');
if (group === 'group-2') inGroup2++;
+
+ const messagePattern = /alert always fire group group-. value \d+ exceeded threshold max\(testedValue\) .* 0 over 15s on \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z/;
+ expect(message).to.match(messagePattern);
}
// there should be 2 docs in group-2, rando split between others
@@ -274,10 +292,13 @@ export default function alertTests({ getService }: FtrProviderContext) {
for (const doc of docs) {
const { group } = doc._source;
- const { name } = doc._source.params;
+ const { name, message } = doc._source.params;
expect(name).to.be('always fire');
if (group === 'group-0') inGroup0++;
+
+ const messagePattern = /alert always fire group group-. value \d+ exceeded threshold min\(testedValue\) .* 0 over 15s on \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z/;
+ expect(message).to.match(messagePattern);
}
// there should be 2 docs in group-0, rando split between others
@@ -329,7 +350,7 @@ export default function alertTests({ getService }: FtrProviderContext) {
name: '{{{alertName}}}',
value: '{{{context.value}}}',
title: '{{{context.title}}}',
- message: '{{{context.message}}}',
+ message: DefaultActionMessage,
},
date: '{{{context.date}}}',
// TODO: I wanted to write the alert value here, but how?
diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alert_create_flyout.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alert_create_flyout.ts
index ee0de582a9bff..0f6da936f8644 100644
--- a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alert_create_flyout.ts
+++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alert_create_flyout.ts
@@ -79,10 +79,13 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
await find.clickByCssSelector('[data-test-subj="saveActionButtonModal"]:not(disabled)');
const createdConnectorToastTitle = await pageObjects.common.closeToast();
expect(createdConnectorToastTitle).to.eql(`Created '${slackConnectorName}'`);
+ const messageTextArea = await find.byCssSelector('[data-test-subj="messageTextArea"]');
+ expect(await messageTextArea.getAttribute('value')).to.eql(
+ 'alert {{alertName}} group {{context.group}} value {{context.value}} exceeded threshold {{context.function}} over {{params.timeWindowSize}}{{params.timeWindowUnit}} on {{context.date}}'
+ );
await testSubjects.setValue('messageTextArea', 'test message ');
await testSubjects.click('messageAddVariableButton');
await testSubjects.click('variableMenuButton-0');
- const messageTextArea = await find.byCssSelector('[data-test-subj="messageTextArea"]');
expect(await messageTextArea.getAttribute('value')).to.eql('test message {{alertId}}');
await messageTextArea.type(' some additional text ');
From 0217073b8f555b68a7487c5c52325e462a5232af Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Cau=C3=AA=20Marcondes?=
<55978943+cauemarcondes@users.noreply.github.com>
Date: Mon, 9 Nov 2020 11:03:07 -0300
Subject: [PATCH 6/8] [APM] Transition to Elastic charts for all relevant APM
charts (#80298)
* adding elastic charts
* fixing some stuff
* refactoring
* fixing ts issues
* fixing unit test
* fix i18n
* adding isLoading prop
* adding annotations toggle, replacing transaction error rate to elastic chart
* adding loading state
* adding empty message
* fixing i18n
* removing unused files
* fixing i18n
* removing e2e test since elastic charts uses canvas
* addressing pr comments
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
---
.../apm/e2e/cypress/integration/apm.feature | 3 +-
.../cypress/support/step_definitions/apm.ts | 13 -
.../ErrorGroupDetails/Distribution/index.tsx | 141 +-
.../TransactionDetails/Distribution/index.tsx | 198 ++-
.../app/TransactionDetails/index.tsx | 9 +-
.../TransactionOverview.test.tsx | 14 +-
.../app/TransactionOverview/index.tsx | 23 +-
.../components/app/service_overview/index.tsx | 16 +-
.../TransactionBreakdownGraph/index.tsx | 143 +-
.../shared/TransactionBreakdown/index.tsx | 8 +-
.../shared/charts/Histogram/SingleRect.js | 29 -
.../Histogram/__test__/Histogram.test.js | 119 --
.../__snapshots__/Histogram.test.js.snap | 1504 -----------------
.../charts/Histogram/__test__/response.json | 106 --
.../shared/charts/Histogram/index.js | 319 ----
.../TransactionLineChart/index.tsx | 70 -
.../shared/charts/TransactionCharts/index.tsx | 133 +-
.../TransactionCharts/use_formatter.test.tsx | 106 +-
.../charts/TransactionCharts/use_formatter.ts | 38 +-
.../shared/charts/annotations/index.tsx | 45 +
.../shared/charts/chart_container.test.tsx | 91 +-
.../shared/charts/chart_container.tsx | 41 +-
.../legacy.tsx | 112 --
.../shared/charts/line_chart/index.tsx | 16 +-
.../index.tsx | 56 +-
.../public/hooks/useTransactionBreakdown.ts | 2 +-
.../hooks/useTransactionDistribution.ts | 2 +-
.../apm/public/hooks/useTransactionList.ts | 4 +-
.../apm/public/hooks/use_annotations.ts | 38 +
.../apm/public/selectors/chartSelectors.ts | 41 +-
.../lib/errors/distribution/get_buckets.ts | 2 +-
.../translations/translations/ja-JP.json | 9 -
.../translations/translations/zh-CN.json | 9 -
33 files changed, 718 insertions(+), 2742 deletions(-)
delete mode 100644 x-pack/plugins/apm/public/components/shared/charts/Histogram/SingleRect.js
delete mode 100644 x-pack/plugins/apm/public/components/shared/charts/Histogram/__test__/Histogram.test.js
delete mode 100644 x-pack/plugins/apm/public/components/shared/charts/Histogram/__test__/__snapshots__/Histogram.test.js.snap
delete mode 100644 x-pack/plugins/apm/public/components/shared/charts/Histogram/__test__/response.json
delete mode 100644 x-pack/plugins/apm/public/components/shared/charts/Histogram/index.js
delete mode 100644 x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/TransactionLineChart/index.tsx
create mode 100644 x-pack/plugins/apm/public/components/shared/charts/annotations/index.tsx
delete mode 100644 x-pack/plugins/apm/public/components/shared/charts/erroneous_transactions_rate_chart/legacy.tsx
rename x-pack/plugins/apm/public/components/shared/charts/{erroneous_transactions_rate_chart => transaction_error_rate_chart}/index.tsx (64%)
create mode 100644 x-pack/plugins/apm/public/hooks/use_annotations.ts
diff --git a/x-pack/plugins/apm/e2e/cypress/integration/apm.feature b/x-pack/plugins/apm/e2e/cypress/integration/apm.feature
index 285615108266b..494a6b5fadb5b 100644
--- a/x-pack/plugins/apm/e2e/cypress/integration/apm.feature
+++ b/x-pack/plugins/apm/e2e/cypress/integration/apm.feature
@@ -3,5 +3,4 @@ Feature: APM
Scenario: Transaction duration charts
Given a user browses the APM UI application
When the user inspects the opbeans-node service
- Then should redirect to correct path with correct params
- And should have correct y-axis ticks
+ Then should redirect to correct path with correct params
\ No newline at end of file
diff --git a/x-pack/plugins/apm/e2e/cypress/support/step_definitions/apm.ts b/x-pack/plugins/apm/e2e/cypress/support/step_definitions/apm.ts
index 50c620dca9ddf..42c2bc7ffd318 100644
--- a/x-pack/plugins/apm/e2e/cypress/support/step_definitions/apm.ts
+++ b/x-pack/plugins/apm/e2e/cypress/support/step_definitions/apm.ts
@@ -29,16 +29,3 @@ Then(`should redirect to correct path with correct params`, () => {
cy.url().should('contain', `/app/apm/services/opbeans-node/transactions`);
cy.url().should('contain', `transactionType=request`);
});
-
-Then(`should have correct y-axis ticks`, () => {
- const yAxisTick =
- '[data-cy=transaction-duration-charts] .rv-xy-plot__axis--vertical .rv-xy-plot__axis__tick__text';
-
- // wait for all loading to finish
- cy.get('kbnLoadingIndicator').should('not.be.visible');
-
- // literal assertions because snapshot() doesn't retry
- cy.get(yAxisTick).eq(2).should('have.text', '55 ms');
- cy.get(yAxisTick).eq(1).should('have.text', '28 ms');
- cy.get(yAxisTick).eq(0).should('have.text', '0 ms');
-});
diff --git a/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/Distribution/index.tsx b/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/Distribution/index.tsx
index e17dd9a9eb038..a17bf7e93e466 100644
--- a/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/Distribution/index.tsx
+++ b/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/Distribution/index.tsx
@@ -4,31 +4,24 @@
* you may not use this file except in compliance with the Elastic License.
*/
+import {
+ Axis,
+ Chart,
+ HistogramBarSeries,
+ niceTimeFormatter,
+ Position,
+ ScaleType,
+ Settings,
+ SettingsSpec,
+ TooltipValue,
+} from '@elastic/charts';
import { EuiTitle } from '@elastic/eui';
-import theme from '@elastic/eui/dist/eui_theme_light.json';
-import numeral from '@elastic/numeral';
-import { i18n } from '@kbn/i18n';
import d3 from 'd3';
-import { scaleUtc } from 'd3-scale';
-import { mean } from 'lodash';
import React from 'react';
import { asRelativeDateTimeRange } from '../../../../../common/utils/formatters';
-import { getTimezoneOffsetInMs } from '../../../shared/charts/CustomPlot/getTimezoneOffsetInMs';
-// @ts-expect-error
-import Histogram from '../../../shared/charts/Histogram';
-import { EmptyMessage } from '../../../shared/EmptyMessage';
-
-interface IBucket {
- key: number;
- count: number | undefined;
-}
-
-// TODO: cleanup duplication of this in distribution/get_distribution.ts (ErrorDistributionAPIResponse) and transactions/distribution/index.ts (TransactionDistributionAPIResponse)
-interface IDistribution {
- noHits: boolean;
- buckets: IBucket[];
- bucketSize: number;
-}
+// eslint-disable-next-line @kbn/eslint/no-restricted-paths
+import type { ErrorDistributionAPIResponse } from '../../../../../server/lib/errors/distribution/get_distribution';
+import { useTheme } from '../../../../hooks/useTheme';
interface FormattedBucket {
x0: number;
@@ -37,13 +30,9 @@ interface FormattedBucket {
}
export function getFormattedBuckets(
- buckets: IBucket[],
+ buckets: ErrorDistributionAPIResponse['buckets'],
bucketSize: number
-): FormattedBucket[] | null {
- if (!buckets) {
- return null;
- }
-
+): FormattedBucket[] {
return buckets.map(({ count, key }) => {
return {
x0: key,
@@ -54,76 +43,66 @@ export function getFormattedBuckets(
}
interface Props {
- distribution: IDistribution;
+ distribution: ErrorDistributionAPIResponse;
title: React.ReactNode;
}
-const tooltipHeader = (bucket: FormattedBucket) =>
- asRelativeDateTimeRange(bucket.x0, bucket.x);
-
export function ErrorDistribution({ distribution, title }: Props) {
+ const theme = useTheme();
const buckets = getFormattedBuckets(
distribution.buckets,
distribution.bucketSize
);
- if (!buckets) {
- return (
-
- );
- }
-
- const averageValue = mean(buckets.map((bucket) => bucket.y)) || 0;
const xMin = d3.min(buckets, (d) => d.x0);
- const xMax = d3.max(buckets, (d) => d.x);
- const tickFormat = scaleUtc().domain([xMin, xMax]).tickFormat();
+ const xMax = d3.max(buckets, (d) => d.x0);
+
+ const xFormatter = niceTimeFormatter([xMin, xMax]);
+
+ const tooltipProps: SettingsSpec['tooltip'] = {
+ headerFormatter: (tooltip: TooltipValue) => {
+ const serie = buckets.find((bucket) => bucket.x0 === tooltip.value);
+ if (serie) {
+ return asRelativeDateTimeRange(serie.x0, serie.x);
+ }
+ return `${tooltip.value}`;
+ },
+ };
return (
{title}
-
bucket.x}
- xType="time-utc"
- formatX={(value: Date) => {
- const time = value.getTime();
- return tickFormat(new Date(time - getTimezoneOffsetInMs(time)));
- }}
- buckets={buckets}
- bucketSize={distribution.bucketSize}
- formatYShort={(value: number) =>
- i18n.translate('xpack.apm.errorGroupDetails.occurrencesShortLabel', {
- defaultMessage: '{occCount} occ.',
- values: { occCount: value },
- })
- }
- formatYLong={(value: number) =>
- i18n.translate('xpack.apm.errorGroupDetails.occurrencesLongLabel', {
- defaultMessage:
- '{occCount} {occCount, plural, one {occurrence} other {occurrences}}',
- values: { occCount: value },
- })
- }
- legends={[
- {
- color: theme.euiColorVis1,
- // 0a abbreviates large whole numbers with metric prefixes like: 1000 = 1k, 32000 = 32k, 1000000 = 1m
- legendValue: numeral(averageValue).format('0a'),
- title: i18n.translate('xpack.apm.errorGroupDetails.avgLabel', {
- defaultMessage: 'Avg.',
- }),
- legendClickDisabled: true,
- },
- ]}
- />
+
);
}
diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/Distribution/index.tsx b/x-pack/plugins/apm/public/components/app/TransactionDetails/Distribution/index.tsx
index 67125d41635a9..bf1bda793179f 100644
--- a/x-pack/plugins/apm/public/components/app/TransactionDetails/Distribution/index.tsx
+++ b/x-pack/plugins/apm/public/components/app/TransactionDetails/Distribution/index.tsx
@@ -4,22 +4,37 @@
* you may not use this file except in compliance with the Elastic License.
*/
+import {
+ Axis,
+ Chart,
+ ElementClickListener,
+ GeometryValue,
+ HistogramBarSeries,
+ Position,
+ RectAnnotation,
+ ScaleType,
+ Settings,
+ SettingsSpec,
+ TooltipValue,
+ XYChartSeriesIdentifier,
+} from '@elastic/charts';
import { EuiIconTip, EuiTitle } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import d3 from 'd3';
import { isEmpty } from 'lodash';
import React, { useCallback } from 'react';
import { ValuesType } from 'utility-types';
+import { useTheme } from '../../../../../../observability/public';
import { getDurationFormatter } from '../../../../../common/utils/formatters';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
-import { TransactionDistributionAPIResponse } from '../../../../../server/lib/transactions/distribution';
+import type { TransactionDistributionAPIResponse } from '../../../../../server/lib/transactions/distribution';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
-import { DistributionBucket } from '../../../../../server/lib/transactions/distribution/get_buckets';
+import type { DistributionBucket } from '../../../../../server/lib/transactions/distribution/get_buckets';
import { IUrlParams } from '../../../../context/UrlParamsContext/types';
-// @ts-expect-error
-import Histogram from '../../../shared/charts/Histogram';
+import { FETCH_STATUS } from '../../../../hooks/useFetcher';
+import { unit } from '../../../../style/variables';
+import { ChartContainer } from '../../../shared/charts/chart_container';
import { EmptyMessage } from '../../../shared/EmptyMessage';
-import { LoadingStatePrompt } from '../../../shared/LoadingStatePrompt';
interface IChartPoint {
x0: number;
@@ -31,10 +46,10 @@ interface IChartPoint {
}
export function getFormattedBuckets(
- buckets: DistributionBucket[],
- bucketSize: number
+ buckets?: DistributionBucket[],
+ bucketSize?: number
) {
- if (!buckets) {
+ if (!buckets || !bucketSize) {
return [];
}
@@ -74,7 +89,7 @@ const getFormatYLong = (transactionType: string | undefined) => (t: number) => {
'xpack.apm.transactionDetails.transactionsDurationDistributionChart.requestTypeUnitLongLabel',
{
defaultMessage:
- '{transCount, plural, =0 {# request} one {# request} other {# requests}}',
+ '{transCount, plural, =0 {request} one {request} other {requests}}',
values: {
transCount: t,
},
@@ -84,7 +99,7 @@ const getFormatYLong = (transactionType: string | undefined) => (t: number) => {
'xpack.apm.transactionDetails.transactionsDurationDistributionChart.transactionTypeUnitLongLabel',
{
defaultMessage:
- '{transCount, plural, =0 {# transaction} one {# transaction} other {# transactions}}',
+ '{transCount, plural, =0 {transaction} one {transaction} other {transactions}}',
values: {
transCount: t,
},
@@ -95,21 +110,21 @@ const getFormatYLong = (transactionType: string | undefined) => (t: number) => {
interface Props {
distribution?: TransactionDistributionAPIResponse;
urlParams: IUrlParams;
- isLoading: boolean;
+ fetchStatus: FETCH_STATUS;
bucketIndex: number;
onBucketClick: (
bucket: ValuesType
) => void;
}
-export function TransactionDistribution(props: Props) {
- const {
- distribution,
- urlParams: { transactionType },
- isLoading,
- bucketIndex,
- onBucketClick,
- } = props;
+export function TransactionDistribution({
+ distribution,
+ urlParams: { transactionType },
+ fetchStatus,
+ bucketIndex,
+ onBucketClick,
+}: Props) {
+ const theme = useTheme();
/* eslint-disable-next-line react-hooks/exhaustive-deps */
const formatYShort = useCallback(getFormatYShort(transactionType), [
@@ -122,12 +137,10 @@ export function TransactionDistribution(props: Props) {
]);
// no data in response
- if (!distribution || distribution.noHits) {
- // only show loading state if there is no data - else show stale data until new data has loaded
- if (isLoading) {
- return ;
- }
-
+ if (
+ (!distribution || distribution.noHits) &&
+ fetchStatus !== FETCH_STATUS.LOADING
+ ) {
return (
{
- return bucket.key === chartPoint.x0;
- });
-
- return clickedBucket;
- }
-
const buckets = getFormattedBuckets(
- distribution.buckets,
- distribution.bucketSize
+ distribution?.buckets,
+ distribution?.bucketSize
);
- const xMax = d3.max(buckets, (d) => d.x) || 0;
+ const xMin = d3.min(buckets, (d) => d.x0) || 0;
+ const xMax = d3.max(buckets, (d) => d.x0) || 0;
const timeFormatter = getDurationFormatter(xMax);
+ const tooltipProps: SettingsSpec['tooltip'] = {
+ headerFormatter: (tooltip: TooltipValue) => {
+ const serie = buckets.find((bucket) => bucket.x0 === tooltip.value);
+ if (serie) {
+ const xFormatted = timeFormatter(serie.x);
+ const x0Formatted = timeFormatter(serie.x0);
+ return `${x0Formatted.value} - ${xFormatted.value} ${xFormatted.unit}`;
+ }
+ return `${timeFormatter(tooltip.value)}`;
+ },
+ };
+
+ const onBarClick: ElementClickListener = (elements) => {
+ const chartPoint = elements[0][0] as GeometryValue;
+ const clickedBucket = distribution?.buckets.find((bucket) => {
+ return bucket.key === chartPoint.x;
+ });
+ if (clickedBucket) {
+ onBucketClick(clickedBucket);
+ }
+ };
+
+ const selectedBucket = buckets[bucketIndex];
+
return (
@@ -181,42 +211,66 @@ export function TransactionDistribution(props: Props) {
/>
-
-
{
- const clickedBucket = getBucketFromChartPoint(chartPoint);
-
- if (clickedBucket) {
- onBucketClick(clickedBucket);
- }
- }}
- formatX={(time: number) => timeFormatter(time).formatted}
- formatYShort={formatYShort}
- formatYLong={formatYLong}
- verticalLineHover={(point: IChartPoint) =>
- isEmpty(getBucketFromChartPoint(point)?.samples)
- }
- backgroundHover={(point: IChartPoint) =>
- !isEmpty(getBucketFromChartPoint(point)?.samples)
- }
- tooltipHeader={(point: IChartPoint) => {
- const xFormatted = timeFormatter(point.x);
- const x0Formatted = timeFormatter(point.x0);
- return `${x0Formatted.value} - ${xFormatted.value} ${xFormatted.unit}`;
- }}
- tooltipFooter={(point: IChartPoint) =>
- isEmpty(getBucketFromChartPoint(point)?.samples) &&
- i18n.translate(
- 'xpack.apm.transactionDetails.transactionsDurationDistributionChart.noSampleTooltip',
- {
- defaultMessage: 'No sample available for this bucket',
- }
- )
- }
- />
+
+
+
+ {selectedBucket && (
+
+ )}
+ timeFormatter(time).formatted}
+ />
+ formatYShort(value)}
+ />
+ value}
+ minBarHeight={2}
+ id="transactionDurationDistribution"
+ name={(series: XYChartSeriesIdentifier) => {
+ const bucketCount = series.splitAccessors.get(
+ series.yAccessor
+ ) as number;
+ return formatYLong(bucketCount);
+ }}
+ splitSeriesAccessors={['y']}
+ xScaleType={ScaleType.Linear}
+ yScaleType={ScaleType.Linear}
+ xAccessor="x0"
+ yAccessors={['y']}
+ data={buckets}
+ color={theme.eui.euiColorVis1}
+ />
+
+
);
}
diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/index.tsx b/x-pack/plugins/apm/public/components/app/TransactionDetails/index.tsx
index efdd7b1f34221..e4c36b028e55c 100644
--- a/x-pack/plugins/apm/public/components/app/TransactionDetails/index.tsx
+++ b/x-pack/plugins/apm/public/components/app/TransactionDetails/index.tsx
@@ -52,7 +52,11 @@ export function TransactionDetails({
status: distributionStatus,
} = useTransactionDistribution(urlParams);
- const { data: transactionChartsData } = useTransactionCharts();
+ const {
+ data: transactionChartsData,
+ status: transactionChartsStatus,
+ } = useTransactionCharts();
+
const { waterfall, exceedsMax, status: waterfallStatus } = useWaterfall(
urlParams
);
@@ -121,6 +125,7 @@ export function TransactionDetails({
@@ -131,7 +136,7 @@ export function TransactionDetails({
{
diff --git a/x-pack/plugins/apm/public/components/app/TransactionOverview/TransactionOverview.test.tsx b/x-pack/plugins/apm/public/components/app/TransactionOverview/TransactionOverview.test.tsx
index b7d1b93600a73..c530a7e1489ad 100644
--- a/x-pack/plugins/apm/public/components/app/TransactionOverview/TransactionOverview.test.tsx
+++ b/x-pack/plugins/apm/public/components/app/TransactionOverview/TransactionOverview.test.tsx
@@ -4,12 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import {
- fireEvent,
- getByText,
- queryByLabelText,
- render,
-} from '@testing-library/react';
+import { fireEvent, getByText, queryByLabelText } from '@testing-library/react';
import { createMemoryHistory } from 'history';
import { CoreStart } from 'kibana/public';
import React from 'react';
@@ -20,7 +15,10 @@ import { UrlParamsProvider } from '../../../context/UrlParamsContext';
import { IUrlParams } from '../../../context/UrlParamsContext/types';
import * as useFetcherHook from '../../../hooks/useFetcher';
import * as useServiceTransactionTypesHook from '../../../hooks/useServiceTransactionTypes';
-import { disableConsoleWarning } from '../../../utils/testHelpers';
+import {
+ disableConsoleWarning,
+ renderWithTheme,
+} from '../../../utils/testHelpers';
import { fromQuery } from '../../shared/Links/url_helpers';
import { TransactionOverview } from './';
@@ -54,7 +52,7 @@ function setup({
jest.spyOn(useFetcherHook, 'useFetcher').mockReturnValue({} as any);
- return render(
+ return renderWithTheme(
diff --git a/x-pack/plugins/apm/public/components/app/TransactionOverview/index.tsx b/x-pack/plugins/apm/public/components/app/TransactionOverview/index.tsx
index 5444d2d521f37..df9e673ed4847 100644
--- a/x-pack/plugins/apm/public/components/app/TransactionOverview/index.tsx
+++ b/x-pack/plugins/apm/public/components/app/TransactionOverview/index.tsx
@@ -22,7 +22,7 @@ import React, { useMemo } from 'react';
import { useLocation } from 'react-router-dom';
import { useTrackPageview } from '../../../../../observability/public';
import { Projection } from '../../../../common/projections';
-import { LegacyChartsSyncContextProvider as ChartsSyncContextProvider } from '../../../context/charts_sync_context';
+import { TRANSACTION_PAGE_LOAD } from '../../../../common/transaction_types';
import { IUrlParams } from '../../../context/UrlParamsContext/types';
import { useServiceTransactionTypes } from '../../../hooks/useServiceTransactionTypes';
import { useTransactionCharts } from '../../../hooks/useTransactionCharts';
@@ -33,11 +33,10 @@ import { ElasticDocsLink } from '../../shared/Links/ElasticDocsLink';
import { fromQuery, toQuery } from '../../shared/Links/url_helpers';
import { LocalUIFilters } from '../../shared/LocalUIFilters';
import { TransactionTypeFilter } from '../../shared/LocalUIFilters/TransactionTypeFilter';
+import { Correlations } from '../Correlations';
import { TransactionList } from './TransactionList';
import { useRedirect } from './useRedirect';
-import { TRANSACTION_PAGE_LOAD } from '../../../../common/transaction_types';
import { UserExperienceCallout } from './user_experience_callout';
-import { Correlations } from '../Correlations';
function getRedirectLocation({
urlParams,
@@ -83,7 +82,10 @@ export function TransactionOverview({ serviceName }: TransactionOverviewProps) {
})
);
- const { data: transactionCharts } = useTransactionCharts();
+ const {
+ data: transactionCharts,
+ status: transactionChartsStatus,
+ } = useTransactionCharts();
useTrackPageview({ app: 'apm', path: 'transaction_overview' });
useTrackPageview({ app: 'apm', path: 'transaction_overview', delay: 15000 });
@@ -135,12 +137,11 @@ export function TransactionOverview({ serviceName }: TransactionOverviewProps) {
>
)}
-
-
-
+
@@ -190,7 +191,7 @@ export function TransactionOverview({ serviceName }: TransactionOverviewProps) {
diff --git a/x-pack/plugins/apm/public/components/app/service_overview/index.tsx b/x-pack/plugins/apm/public/components/app/service_overview/index.tsx
index 342152b572f1e..016ee3daf6b51 100644
--- a/x-pack/plugins/apm/public/components/app/service_overview/index.tsx
+++ b/x-pack/plugins/apm/public/components/app/service_overview/index.tsx
@@ -11,7 +11,7 @@ import styled from 'styled-components';
import { useTrackPageview } from '../../../../../observability/public';
import { isRumAgentName } from '../../../../common/agent_name';
import { ChartsSyncContextProvider } from '../../../context/charts_sync_context';
-import { ErroneousTransactionsRateChart } from '../../shared/charts/erroneous_transactions_rate_chart';
+import { TransactionErrorRateChart } from '../../shared/charts/transaction_error_rate_chart';
import { ErrorOverviewLink } from '../../shared/Links/apm/ErrorOverviewLink';
import { ServiceMapLink } from '../../shared/Links/apm/ServiceMapLink';
import { TransactionOverviewLink } from '../../shared/Links/apm/TransactionOverviewLink';
@@ -125,19 +125,7 @@ export function ServiceOverview({
{!isRumAgentName(agentName) && (
-
-
-
- {i18n.translate(
- 'xpack.apm.serviceOverview.errorRateChartTitle',
- {
- defaultMessage: 'Error rate',
- }
- )}
-
-
-
-
+
)}
diff --git a/x-pack/plugins/apm/public/components/shared/TransactionBreakdown/TransactionBreakdownGraph/index.tsx b/x-pack/plugins/apm/public/components/shared/TransactionBreakdown/TransactionBreakdownGraph/index.tsx
index b908eb8da4d03..05cae589c19fc 100644
--- a/x-pack/plugins/apm/public/components/shared/TransactionBreakdown/TransactionBreakdownGraph/index.tsx
+++ b/x-pack/plugins/apm/public/components/shared/TransactionBreakdown/TransactionBreakdownGraph/index.tsx
@@ -4,62 +4,113 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { throttle } from 'lodash';
-import React, { useMemo } from 'react';
+import {
+ AreaSeries,
+ Axis,
+ Chart,
+ niceTimeFormatter,
+ Placement,
+ Position,
+ ScaleType,
+ Settings,
+} from '@elastic/charts';
+import moment from 'moment';
+import React, { useEffect } from 'react';
+import { useHistory } from 'react-router-dom';
import { asPercent } from '../../../../../common/utils/formatters';
-import { useUiTracker } from '../../../../../../observability/public';
-import { NOT_AVAILABLE_LABEL } from '../../../../../common/i18n';
-import { Maybe } from '../../../../../typings/common';
-import { Coordinate, TimeSeries } from '../../../../../typings/timeseries';
+import { TimeSeries } from '../../../../../typings/timeseries';
+import { FETCH_STATUS } from '../../../../hooks/useFetcher';
import { useUrlParams } from '../../../../hooks/useUrlParams';
-import { isValidCoordinateValue } from '../../../../utils/isValidCoordinateValue';
-import { getEmptySeries } from '../../charts/CustomPlot/getEmptySeries';
-import { TransactionLineChart } from '../../charts/TransactionCharts/TransactionLineChart';
+import { useChartsSync as useChartsSync2 } from '../../../../hooks/use_charts_sync';
+import { unit } from '../../../../style/variables';
+import { Annotations } from '../../charts/annotations';
+import { ChartContainer } from '../../charts/chart_container';
+import { onBrushEnd } from '../../charts/helper/helper';
+
+const XY_HEIGHT = unit * 16;
interface Props {
- timeseries: TimeSeries[];
- noHits: boolean;
+ fetchStatus: FETCH_STATUS;
+ timeseries?: TimeSeries[];
}
-const tickFormatY = (y: Maybe) => {
- return asPercent(y ?? 0, 1);
-};
+export function TransactionBreakdownGraph({ fetchStatus, timeseries }: Props) {
+ const history = useHistory();
+ const chartRef = React.createRef();
+ const { event, setEvent } = useChartsSync2();
+ const { urlParams } = useUrlParams();
+ const { start, end } = urlParams;
-const formatTooltipValue = (coordinate: Coordinate) => {
- return isValidCoordinateValue(coordinate.y)
- ? asPercent(coordinate.y, 1)
- : NOT_AVAILABLE_LABEL;
-};
+ useEffect(() => {
+ if (event.chartId !== 'timeSpentBySpan' && chartRef.current) {
+ chartRef.current.dispatchExternalPointerEvent(event);
+ }
+ }, [chartRef, event]);
-function TransactionBreakdownGraph({ timeseries, noHits }: Props) {
- const { urlParams } = useUrlParams();
- const { rangeFrom, rangeTo } = urlParams;
- const trackApmEvent = useUiTracker({ app: 'apm' });
- const handleHover = useMemo(
- () =>
- throttle(() => trackApmEvent({ metric: 'hover_breakdown_chart' }), 60000),
- [trackApmEvent]
- );
+ const min = moment.utc(start).valueOf();
+ const max = moment.utc(end).valueOf();
- const emptySeries =
- rangeFrom && rangeTo
- ? getEmptySeries(
- new Date(rangeFrom).getTime(),
- new Date(rangeTo).getTime()
- )
- : [];
+ const xFormatter = niceTimeFormatter([min, max]);
return (
-
+
+
+ onBrushEnd({ x, history })}
+ showLegend
+ showLegendExtra
+ legendPosition={Position.Bottom}
+ xDomain={{ min, max }}
+ flatLegend
+ onPointerUpdate={(currEvent: any) => {
+ setEvent(currEvent);
+ }}
+ externalPointerEvents={{
+ tooltip: { visible: true, placement: Placement.Bottom },
+ }}
+ />
+
+ asPercent(y ?? 0, 1)}
+ />
+
+
+
+ {timeseries?.length ? (
+ timeseries.map((serie) => {
+ return (
+
+ );
+ })
+ ) : (
+ // When timeseries is empty, loads an AreaSeries chart to show the default empty message.
+
+ )}
+
+
);
}
-
-export { TransactionBreakdownGraph };
diff --git a/x-pack/plugins/apm/public/components/shared/TransactionBreakdown/index.tsx b/x-pack/plugins/apm/public/components/shared/TransactionBreakdown/index.tsx
index 55826497ca385..9b0c041aaf7b5 100644
--- a/x-pack/plugins/apm/public/components/shared/TransactionBreakdown/index.tsx
+++ b/x-pack/plugins/apm/public/components/shared/TransactionBreakdown/index.tsx
@@ -5,16 +5,13 @@
*/
import { EuiFlexGroup, EuiFlexItem, EuiPanel, EuiTitle } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
-import { isEmpty } from 'lodash';
import React from 'react';
-import { FETCH_STATUS } from '../../../hooks/useFetcher';
import { useTransactionBreakdown } from '../../../hooks/useTransactionBreakdown';
import { TransactionBreakdownGraph } from './TransactionBreakdownGraph';
function TransactionBreakdown() {
const { data, status } = useTransactionBreakdown();
const { timeseries } = data;
- const noHits = isEmpty(timeseries) && status === FETCH_STATUS.SUCCESS;
return (
@@ -29,7 +26,10 @@ function TransactionBreakdown() {
-
+
diff --git a/x-pack/plugins/apm/public/components/shared/charts/Histogram/SingleRect.js b/x-pack/plugins/apm/public/components/shared/charts/Histogram/SingleRect.js
deleted file mode 100644
index ca85ee961f5d8..0000000000000
--- a/x-pack/plugins/apm/public/components/shared/charts/Histogram/SingleRect.js
+++ /dev/null
@@ -1,29 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License;
- * you may not use this file except in compliance with the Elastic License.
- */
-
-import React from 'react';
-import PropTypes from 'prop-types';
-
-function SingleRect({ innerHeight, marginTop, style, x, width }) {
- return (
-
- );
-}
-
-SingleRect.requiresSVG = true;
-SingleRect.propTypes = {
- x: PropTypes.number.isRequired,
-};
-
-export default SingleRect;
diff --git a/x-pack/plugins/apm/public/components/shared/charts/Histogram/__test__/Histogram.test.js b/x-pack/plugins/apm/public/components/shared/charts/Histogram/__test__/Histogram.test.js
deleted file mode 100644
index 03fd039a3401e..0000000000000
--- a/x-pack/plugins/apm/public/components/shared/charts/Histogram/__test__/Histogram.test.js
+++ /dev/null
@@ -1,119 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License;
- * you may not use this file except in compliance with the Elastic License.
- */
-
-import React from 'react';
-
-import d3 from 'd3';
-import { HistogramInner } from '../index';
-import response from './response.json';
-import {
- disableConsoleWarning,
- toJson,
- mountWithTheme,
-} from '../../../../../utils/testHelpers';
-import { getFormattedBuckets } from '../../../../app/TransactionDetails/Distribution/index';
-import {
- asInteger,
- getDurationFormatter,
-} from '../../../../../../common/utils/formatters';
-
-describe('Histogram', () => {
- let mockConsole;
- let wrapper;
-
- const onClick = jest.fn();
-
- beforeAll(() => {
- mockConsole = disableConsoleWarning('Warning: componentWillReceiveProps');
- });
-
- afterAll(() => {
- mockConsole.mockRestore();
- });
-
- beforeEach(() => {
- const buckets = getFormattedBuckets(response.buckets, response.bucketSize);
- const xMax = d3.max(buckets, (d) => d.x);
- const timeFormatter = getDurationFormatter(xMax);
-
- wrapper = mountWithTheme(
- timeFormatter(time).formatted}
- formatYShort={(t) => `${asInteger(t)} occ.`}
- formatYLong={(t) => `${asInteger(t)} occurrences`}
- tooltipHeader={(bucket) => {
- const xFormatted = timeFormatter(bucket.x);
- const x0Formatted = timeFormatter(bucket.x0);
- return `${x0Formatted.value} - ${xFormatted.value} ${xFormatted.unit}`;
- }}
- width={800}
- />
- );
- });
-
- describe('Initially', () => {
- it('should have default markup', () => {
- expect(toJson(wrapper)).toMatchSnapshot();
- });
-
- it('should not show tooltip', () => {
- expect(wrapper.find('Tooltip').length).toBe(0);
- });
- });
-
- describe('when hovering over an empty bucket', () => {
- beforeEach(() => {
- wrapper.find('.rv-voronoi__cell').at(2).simulate('mouseOver');
- });
-
- it('should not display tooltip', () => {
- expect(wrapper.find('Tooltip').length).toBe(0);
- });
- });
-
- describe('when hovering over a non-empty bucket', () => {
- beforeEach(() => {
- wrapper.find('.rv-voronoi__cell').at(7).simulate('mouseOver');
- });
-
- it('should display tooltip', () => {
- const tooltips = wrapper.find('Tooltip');
-
- expect(tooltips.length).toBe(1);
- expect(tooltips.prop('header')).toBe('811 - 927 ms');
- expect(tooltips.prop('tooltipPoints')).toEqual([
- { value: '49 occurrences' },
- ]);
- expect(tooltips.prop('x')).toEqual(869010);
- expect(tooltips.prop('y')).toEqual(27.5);
- });
-
- it('should have correct markup for tooltip', () => {
- const tooltips = wrapper.find('Tooltip');
- expect(toJson(tooltips)).toMatchSnapshot();
- });
- });
-
- describe('when clicking on a non-empty bucket', () => {
- beforeEach(() => {
- wrapper.find('.rv-voronoi__cell').at(7).simulate('click');
- });
-
- it('should call onClick with bucket', () => {
- expect(onClick).toHaveBeenCalledWith({
- style: { cursor: 'pointer' },
- xCenter: 869010,
- x0: 811076,
- x: 926944,
- y: 49,
- });
- });
- });
-});
diff --git a/x-pack/plugins/apm/public/components/shared/charts/Histogram/__test__/__snapshots__/Histogram.test.js.snap b/x-pack/plugins/apm/public/components/shared/charts/Histogram/__test__/__snapshots__/Histogram.test.js.snap
deleted file mode 100644
index a31b9735628ab..0000000000000
--- a/x-pack/plugins/apm/public/components/shared/charts/Histogram/__test__/__snapshots__/Histogram.test.js.snap
+++ /dev/null
@@ -1,1504 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`Histogram Initially should have default markup 1`] = `
-.c0 {
- -webkit-user-select: none;
- -moz-user-select: none;
- -ms-user-select: none;
- user-select: none;
- position: absolute;
- top: 0;
- left: 0;
-}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 0 ms
-
-
-
-
-
- 500 ms
-
-
-
-
-
- 1,000 ms
-
-
-
-
-
- 1,500 ms
-
-
-
-
-
- 2,000 ms
-
-
-
-
-
- 2,500 ms
-
-
-
-
-
- 3,000 ms
-
-
-
-
-
-
-
-
-
- 0 occ.
-
-
-
-
-
- 28 occ.
-
-
-
-
-
- 55 occ.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-`;
-
-exports[`Histogram when hovering over a non-empty bucket should have correct markup for tooltip 1`] = `
-.c0 {
- margin: 0 16px;
- -webkit-transform: translateY(-50%);
- -ms-transform: translateY(-50%);
- transform: translateY(-50%);
- border: 1px solid #d3dae6;
- background: #ffffff;
- border-radius: 4px;
- font-size: 14px;
- color: #000000;
-}
-
-.c1 {
- background: #f5f7fa;
- border-bottom: 1px solid #d3dae6;
- border-radius: 4px 4px 0 0;
- padding: 8px;
- color: #98a2b3;
-}
-
-.c2 {
- margin: 8px;
- margin-right: 16px;
- font-size: 12px;
-}
-
-.c4 {
- color: #98a2b3;
- margin: 8px;
- font-size: 12px;
-}
-
-.c3 {
- color: #69707d;
- font-size: 14px;
-}
-
-
-
-
-
-
- 811 - 927 ms
-
-
-
-
-
-
- 49 occurrences
-
-
-
-
-
-
-
-
-
-
-`;
diff --git a/x-pack/plugins/apm/public/components/shared/charts/Histogram/__test__/response.json b/x-pack/plugins/apm/public/components/shared/charts/Histogram/__test__/response.json
deleted file mode 100644
index 302e105dfa997..0000000000000
--- a/x-pack/plugins/apm/public/components/shared/charts/Histogram/__test__/response.json
+++ /dev/null
@@ -1,106 +0,0 @@
-{
- "buckets": [
- { "key": 0, "count": 0 },
- { "key": 115868, "count": 0 },
- { "key": 231736, "count": 0 },
- { "key": 347604, "count": 0 },
- { "key": 463472, "count": 0 },
- {
- "key": 579340,
- "count": 8,
- "samples": [
- {
- "transactionId": "99437ee4-08d4-41f5-9b2b-93cc32ec3dfb"
- }
- ]
- },
- {
- "key": 695208,
- "count": 23,
- "samples": [
- {
- "transactionId": "d327611b-e999-4942-a94f-c60208940180"
- }
- ]
- },
- {
- "key": 811076,
- "count": 49,
- "samples": [
- {
- "transactionId": "99c50a5b-44b4-4289-a3d1-a2815d128192"
- }
- ]
- },
- {
- "key": 926944,
- "count": 51,
- "transactionId": "9706a1ec-23f5-4ce8-97e8-69ce35fb0a9a"
- },
- {
- "key": 1042812,
- "count": 46,
- "transactionId": "f8d360c3-dd5e-47b6-b082-9e0bf821d3b2"
- },
- {
- "key": 1158680,
- "count": 13,
- "samples": [
- {
- "transactionId": "8486d3e2-7f15-48df-aa37-6ee9955adbd2"
- }
- ]
- },
- {
- "key": 1274548,
- "count": 7,
- "transactionId": "54b4b5a7-f065-4cab-9016-534e58f4fc0a"
- },
- {
- "key": 1390416,
- "count": 4,
- "transactionId": "8cfac2a3-38e7-4d3a-9792-d008b4bcb867"
- },
- {
- "key": 1506284,
- "count": 3,
- "transactionId": "ce3f3bd3-a37c-419e-bb9c-5db956ded149"
- },
- { "key": 1622152, "count": 0 },
- {
- "key": 1738020,
- "count": 4,
- "transactionId": "2300174b-85d8-40ba-a6cb-eeba2a49debf"
- },
- { "key": 1853888, "count": 0 },
- { "key": 1969756, "count": 0 },
- {
- "key": 2085624,
- "count": 1,
- "transactionId": "774955a4-2ba3-4461-81a6-65759db4805d"
- },
- { "key": 2201492, "count": 0 },
- { "key": 2317360, "count": 0 },
- { "key": 2433228, "count": 0 },
- { "key": 2549096, "count": 0 },
- { "key": 2664964, "count": 0 },
- {
- "key": 2780832,
- "count": 1,
- "transactionId": "035d1b9d-af71-46cf-8910-57bd4faf412d"
- },
- {
- "key": 2896700,
- "count": 1,
- "transactionId": "4a845b32-9de4-4796-8ef4-d7bbdedc9099"
- },
- { "key": 3012568, "count": 0 },
- {
- "key": 3128436,
- "count": 1,
- "transactionId": "68620ffb-7a1b-4f8e-b9bb-009fa5b092be"
- }
- ],
- "bucketSize": 115868,
- "defaultBucketIndex": 12
-}
diff --git a/x-pack/plugins/apm/public/components/shared/charts/Histogram/index.js b/x-pack/plugins/apm/public/components/shared/charts/Histogram/index.js
deleted file mode 100644
index 3b2109d68c613..0000000000000
--- a/x-pack/plugins/apm/public/components/shared/charts/Histogram/index.js
+++ /dev/null
@@ -1,319 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License;
- * you may not use this file except in compliance with the Elastic License.
- */
-
-import React, { PureComponent } from 'react';
-import d3 from 'd3';
-import { isEmpty } from 'lodash';
-import PropTypes from 'prop-types';
-import { scaleLinear } from 'd3-scale';
-import styled from 'styled-components';
-import SingleRect from './SingleRect';
-import {
- XYPlot,
- XAxis,
- YAxis,
- HorizontalGridLines,
- VerticalRectSeries,
- Voronoi,
- makeWidthFlexible,
- VerticalGridLines,
-} from 'react-vis';
-import { unit } from '../../../../style/variables';
-import Tooltip from '../Tooltip';
-import theme from '@elastic/eui/dist/eui_theme_light.json';
-import { tint } from 'polished';
-import { getTimeTicksTZ, getDomainTZ } from '../helper/timezone';
-import Legends from '../CustomPlot/Legends';
-import StatusText from '../CustomPlot/StatusText';
-import { i18n } from '@kbn/i18n';
-import { isValidCoordinateValue } from '../../../../utils/isValidCoordinateValue';
-
-const XY_HEIGHT = unit * 10;
-const XY_MARGIN = {
- top: unit,
- left: unit * 5,
- right: unit,
- bottom: unit * 2,
-};
-
-const X_TICK_TOTAL = 8;
-
-// position absolutely to make sure that window resizing/zooming works
-const ChartsWrapper = styled.div`
- user-select: none;
- position: absolute;
- top: 0;
- left: 0;
-`;
-
-export class HistogramInner extends PureComponent {
- constructor(props) {
- super(props);
- this.state = {
- hoveredBucket: {},
- };
- }
-
- onClick = (bucket) => {
- if (this.props.onClick) {
- this.props.onClick(bucket);
- }
- };
-
- onHover = (bucket) => {
- this.setState({ hoveredBucket: bucket });
- };
-
- onBlur = () => {
- this.setState({ hoveredBucket: {} });
- };
-
- getChartData(items, selectedItem) {
- const yMax = d3.max(items, (d) => d.y);
- const MINIMUM_BUCKET_SIZE = yMax * 0.02;
-
- return items.map((item) => {
- const padding = (item.x - item.x0) / 20;
- return {
- ...item,
- color:
- item === selectedItem
- ? theme.euiColorVis1
- : tint(0.5, theme.euiColorVis1),
- x0: item.x0 + padding,
- x: item.x - padding,
- y: item.y > 0 ? Math.max(item.y, MINIMUM_BUCKET_SIZE) : 0,
- };
- });
- }
-
- render() {
- const {
- backgroundHover,
- bucketIndex,
- buckets,
- bucketSize,
- formatX,
- formatYShort,
- formatYLong,
- tooltipFooter,
- tooltipHeader,
- verticalLineHover,
- width: XY_WIDTH,
- height,
- legends,
- } = this.props;
- const { hoveredBucket } = this.state;
- if (isEmpty(buckets) || XY_WIDTH === 0) {
- return null;
- }
-
- const isTimeSeries =
- this.props.xType === 'time' || this.props.xType === 'time-utc';
-
- const xMin = d3.min(buckets, (d) => d.x0);
- const xMax = d3.max(buckets, (d) => d.x);
- const yMin = 0;
- const yMax = d3.max(buckets, (d) => d.y);
- const selectedBucket = buckets[bucketIndex];
- const chartData = this.getChartData(buckets, selectedBucket);
-
- const x = scaleLinear()
- .domain([xMin, xMax])
- .range([XY_MARGIN.left, XY_WIDTH - XY_MARGIN.right]);
-
- const y = scaleLinear().domain([yMin, yMax]).range([XY_HEIGHT, 0]).nice();
-
- const [xMinZone, xMaxZone] = getDomainTZ(xMin, xMax);
- const xTickValues = isTimeSeries
- ? getTimeTicksTZ({
- domain: [xMinZone, xMaxZone],
- totalTicks: X_TICK_TOTAL,
- width: XY_WIDTH,
- })
- : undefined;
-
- const xDomain = x.domain();
- const yDomain = y.domain();
- const yTickValues = [0, yDomain[1] / 2, yDomain[1]];
- const shouldShowTooltip =
- hoveredBucket.x > 0 && (hoveredBucket.y > 0 || isTimeSeries);
-
- const showVerticalLineHover = verticalLineHover(hoveredBucket);
- const showBackgroundHover = backgroundHover(hoveredBucket);
-
- const hasValidCoordinates = buckets.some((bucket) =>
- isValidCoordinateValue(bucket.y)
- );
- const noHits = this.props.noHits || !hasValidCoordinates;
-
- const xyPlotProps = {
- dontCheckIfEmpty: true,
- xType: this.props.xType,
- width: XY_WIDTH,
- height: XY_HEIGHT,
- margin: XY_MARGIN,
- xDomain: xDomain,
- yDomain: yDomain,
- };
-
- const xAxisProps = {
- style: { strokeWidth: '1px' },
- marginRight: 10,
- tickSize: 0,
- tickTotal: X_TICK_TOTAL,
- tickFormat: formatX,
- tickValues: xTickValues,
- };
-
- const emptyStateChart = (
-
-
-
-
- );
-
- return (
-
-
- {noHits ? (
- <>{emptyStateChart}>
- ) : (
- <>
-
-
-
-
-
- {showBackgroundHover && (
-
- )}
-
- {shouldShowTooltip && (
-
- )}
-
- {selectedBucket && (
-
- )}
-
-
-
- {showVerticalLineHover && hoveredBucket?.x && (
-
- )}
-
- {
- return {
- ...bucket,
- xCenter: (bucket.x0 + bucket.x) / 2,
- };
- })}
- onClick={this.onClick}
- onHover={this.onHover}
- onBlur={this.onBlur}
- x={(d) => x(d.xCenter)}
- y={() => 1}
- />
-
-
- {legends && (
- {}}
- truncateLegends={false}
- noHits={noHits}
- />
- )}
- >
- )}
-
-
- );
- }
-}
-
-HistogramInner.propTypes = {
- backgroundHover: PropTypes.func,
- bucketIndex: PropTypes.number,
- buckets: PropTypes.array.isRequired,
- bucketSize: PropTypes.number.isRequired,
- formatX: PropTypes.func,
- formatYLong: PropTypes.func,
- formatYShort: PropTypes.func,
- onClick: PropTypes.func,
- tooltipFooter: PropTypes.func,
- tooltipHeader: PropTypes.func,
- verticalLineHover: PropTypes.func,
- width: PropTypes.number.isRequired,
- height: PropTypes.number,
- xType: PropTypes.string,
- legends: PropTypes.array,
- noHits: PropTypes.bool,
-};
-
-HistogramInner.defaultProps = {
- backgroundHover: () => null,
- formatYLong: (value) => value,
- formatYShort: (value) => value,
- tooltipFooter: () => null,
- tooltipHeader: () => null,
- verticalLineHover: () => null,
- xType: 'linear',
- noHits: false,
- height: XY_HEIGHT,
-};
-
-export default makeWidthFlexible(HistogramInner);
diff --git a/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/TransactionLineChart/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/TransactionLineChart/index.tsx
deleted file mode 100644
index 2e4b51af00d6b..0000000000000
--- a/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/TransactionLineChart/index.tsx
+++ /dev/null
@@ -1,70 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License;
- * you may not use this file except in compliance with the Elastic License.
- */
-
-import React, { useCallback } from 'react';
-import { Coordinate, TimeSeries } from '../../../../../../typings/timeseries';
-import { useLegacyChartsSync as useChartsSync } from '../../../../../hooks/use_charts_sync';
-// @ts-expect-error
-import CustomPlot from '../../CustomPlot';
-
-interface Props {
- series: TimeSeries[];
- truncateLegends?: boolean;
- tickFormatY: (y: number) => React.ReactNode;
- formatTooltipValue: (c: Coordinate) => React.ReactNode;
- yMax?: string | number;
- height?: number;
- stacked?: boolean;
- onHover?: () => void;
- visibleLegendCount?: number;
- onToggleLegend?: (disabledSeriesState: boolean[]) => void;
-}
-
-function TransactionLineChart(props: Props) {
- const {
- series,
- tickFormatY,
- formatTooltipValue,
- yMax = 'max',
- height,
- truncateLegends,
- stacked = false,
- onHover,
- visibleLegendCount,
- onToggleLegend,
- } = props;
-
- const syncedChartsProps = useChartsSync();
-
- // combine callback for syncedChartsProps.onHover and props.onHover
- const combinedOnHover = useCallback(
- (hoverX: number) => {
- if (onHover) {
- onHover();
- }
- return syncedChartsProps.onHover(hoverX);
- },
- [syncedChartsProps, onHover]
- );
-
- return (
-
- );
-}
-
-export { TransactionLineChart };
diff --git a/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/index.tsx
index b3c0c3b6de857..2a5948d0ebf0b 100644
--- a/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/index.tsx
+++ b/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/index.tsx
@@ -20,104 +20,107 @@ import {
TRANSACTION_REQUEST,
TRANSACTION_ROUTE_CHANGE,
} from '../../../../../common/transaction_types';
+import { asDecimal, tpmUnit } from '../../../../../common/utils/formatters';
import { Coordinate } from '../../../../../typings/timeseries';
+import { ChartsSyncContextProvider } from '../../../../context/charts_sync_context';
import { LicenseContext } from '../../../../context/LicenseContext';
import { IUrlParams } from '../../../../context/UrlParamsContext/types';
+import { FETCH_STATUS } from '../../../../hooks/useFetcher';
import { ITransactionChartData } from '../../../../selectors/chartSelectors';
-import { asDecimal, tpmUnit } from '../../../../../common/utils/formatters';
import { isValidCoordinateValue } from '../../../../utils/isValidCoordinateValue';
-import { ErroneousTransactionsRateChart } from '../erroneous_transactions_rate_chart/legacy';
import { TransactionBreakdown } from '../../TransactionBreakdown';
-import {
- getResponseTimeTickFormatter,
- getResponseTimeTooltipFormatter,
-} from './helper';
+import { LineChart } from '../line_chart';
+import { TransactionErrorRateChart } from '../transaction_error_rate_chart/';
+import { getResponseTimeTickFormatter } from './helper';
import { MLHeader } from './ml_header';
-import { TransactionLineChart } from './TransactionLineChart';
import { useFormatter } from './use_formatter';
interface TransactionChartProps {
charts: ITransactionChartData;
urlParams: IUrlParams;
+ fetchStatus: FETCH_STATUS;
}
export function TransactionCharts({
charts,
urlParams,
+ fetchStatus,
}: TransactionChartProps) {
const getTPMFormatter = (t: number) => {
- const unit = tpmUnit(urlParams.transactionType);
- return `${asDecimal(t)} ${unit}`;
+ return `${asDecimal(t)} ${tpmUnit(urlParams.transactionType)}`;
};
- const getTPMTooltipFormatter = (p: Coordinate) => {
- return isValidCoordinateValue(p.y)
- ? getTPMFormatter(p.y)
- : NOT_AVAILABLE_LABEL;
+ const getTPMTooltipFormatter = (y: Coordinate['y']) => {
+ return isValidCoordinateValue(y) ? getTPMFormatter(y) : NOT_AVAILABLE_LABEL;
};
const { transactionType } = urlParams;
const { responseTimeSeries, tpmSeries } = charts;
- const { formatter, setDisabledSeriesState } = useFormatter(
- responseTimeSeries
- );
+ const { formatter, toggleSerie } = useFormatter(responseTimeSeries);
return (
<>
-
-
-
-
-
-
- {responseTimeLabel(transactionType)}
-
-
-
- {(license) => (
-
- )}
-
-
-
-
-
+
+
+
+
+
+
+
+ {responseTimeLabel(transactionType)}
+
+
+
+ {(license) => (
+
+ )}
+
+
+ {
+ if (serie) {
+ toggleSerie(serie);
+ }
+ }}
+ />
+
+
-
-
-
- {tpmLabel(transactionType)}
-
-
-
-
-
+
+
+
+ {tpmLabel(transactionType)}
+
+
+
+
+
-
+
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
>
);
}
diff --git a/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/use_formatter.test.tsx b/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/use_formatter.test.tsx
index fc873cbda7bf2..958a5db6b66c9 100644
--- a/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/use_formatter.test.tsx
+++ b/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/use_formatter.test.tsx
@@ -3,38 +3,17 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
-import React from 'react';
+import { SeriesIdentifier } from '@elastic/charts';
+import { renderHook } from '@testing-library/react-hooks';
+import { act } from 'react-test-renderer';
+import { toMicroseconds } from '../../../../../common/utils/formatters';
import { TimeSeries } from '../../../../../typings/timeseries';
import { useFormatter } from './use_formatter';
-import { render, fireEvent, act } from '@testing-library/react';
-import { toMicroseconds } from '../../../../../common/utils/formatters';
-
-function MockComponent({
- timeSeries,
- disabledSeries,
- value,
-}: {
- timeSeries: TimeSeries[];
- disabledSeries: boolean[];
- value: number;
-}) {
- const { formatter, setDisabledSeriesState } = useFormatter(timeSeries);
-
- const onDisableSeries = () => {
- setDisabledSeriesState(disabledSeries);
- };
-
- return (
-
- disable series
- {formatter(value).formatted}
-
- );
-}
describe('useFormatter', () => {
const timeSeries = ([
{
+ title: 'avg',
data: [
{ x: 1, y: toMicroseconds(11, 'minutes') },
{ x: 2, y: toMicroseconds(1, 'minutes') },
@@ -42,6 +21,7 @@ describe('useFormatter', () => {
],
},
{
+ title: '95th percentile',
data: [
{ x: 1, y: toMicroseconds(120, 'seconds') },
{ x: 2, y: toMicroseconds(1, 'minutes') },
@@ -49,6 +29,7 @@ describe('useFormatter', () => {
],
},
{
+ title: '99th percentile',
data: [
{ x: 1, y: toMicroseconds(60, 'seconds') },
{ x: 2, y: toMicroseconds(5, 'minutes') },
@@ -56,54 +37,47 @@ describe('useFormatter', () => {
],
},
] as unknown) as TimeSeries[];
+
it('returns new formatter when disabled series state changes', () => {
- const { getByText } = render(
-
- );
- expect(getByText('2.0 min')).toBeInTheDocument();
+ const { result } = renderHook(() => useFormatter(timeSeries));
+ expect(
+ result.current.formatter(toMicroseconds(120, 'seconds')).formatted
+ ).toEqual('2.0 min');
+
act(() => {
- fireEvent.click(getByText('disable series'));
+ result.current.toggleSerie({
+ specId: 'avg',
+ } as SeriesIdentifier);
});
- expect(getByText('120 s')).toBeInTheDocument();
+
+ expect(
+ result.current.formatter(toMicroseconds(120, 'seconds')).formatted
+ ).toEqual('120 s');
});
+
it('falls back to the first formatter when disabled series is empty', () => {
- const { getByText } = render(
-
- );
- expect(getByText('2.0 min')).toBeInTheDocument();
+ const { result } = renderHook(() => useFormatter(timeSeries));
+ expect(
+ result.current.formatter(toMicroseconds(120, 'seconds')).formatted
+ ).toEqual('2.0 min');
+
act(() => {
- fireEvent.click(getByText('disable series'));
+ result.current.toggleSerie({
+ specId: 'avg',
+ } as SeriesIdentifier);
});
- expect(getByText('2.0 min')).toBeInTheDocument();
- // const { formatter, setDisabledSeriesState } = useFormatter(timeSeries);
- // expect(formatter(toMicroseconds(120, 'seconds'))).toEqual('2.0 min');
- // setDisabledSeriesState([true, true, false]);
- // expect(formatter(toMicroseconds(120, 'seconds'))).toEqual('2.0 min');
- });
- it('falls back to the first formatter when disabled series is all true', () => {
- const { getByText } = render(
-
- );
- expect(getByText('2.0 min')).toBeInTheDocument();
+
+ expect(
+ result.current.formatter(toMicroseconds(120, 'seconds')).formatted
+ ).toEqual('120 s');
+
act(() => {
- fireEvent.click(getByText('disable series'));
+ result.current.toggleSerie({
+ specId: 'avg',
+ } as SeriesIdentifier);
});
- expect(getByText('2.0 min')).toBeInTheDocument();
- // const { formatter, setDisabledSeriesState } = useFormatter(timeSeries);
- // expect(formatter(toMicroseconds(120, 'seconds'))).toEqual('2.0 min');
- // setDisabledSeriesState([true, true, false]);
- // expect(formatter(toMicroseconds(120, 'seconds'))).toEqual('2.0 min');
+ expect(
+ result.current.formatter(toMicroseconds(120, 'seconds')).formatted
+ ).toEqual('2.0 min');
});
});
diff --git a/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/use_formatter.ts b/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/use_formatter.ts
index d4694bc3caf1d..1475ec2934e95 100644
--- a/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/use_formatter.ts
+++ b/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/use_formatter.ts
@@ -4,8 +4,9 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { useState, Dispatch, SetStateAction } from 'react';
-import { isEmpty } from 'lodash';
+import { SeriesIdentifier } from '@elastic/charts';
+import { omit } from 'lodash';
+import { useState } from 'react';
import {
getDurationFormatter,
TimeFormatter,
@@ -14,17 +15,36 @@ import { TimeSeries } from '../../../../../typings/timeseries';
import { getMaxY } from './helper';
export const useFormatter = (
- series: TimeSeries[]
+ series?: TimeSeries[]
): {
formatter: TimeFormatter;
- setDisabledSeriesState: Dispatch>;
+ toggleSerie: (disabledSerie: SeriesIdentifier) => void;
} => {
- const [disabledSeriesState, setDisabledSeriesState] = useState([]);
- const visibleSeries = series.filter(
- (serie, index) => disabledSeriesState[index] !== true
+ const [disabledSeries, setDisabledSeries] = useState<
+ Record
+ >({});
+
+ const visibleSeries = series?.filter(
+ (serie) => disabledSeries[serie.title] === undefined
);
- const maxY = getMaxY(isEmpty(visibleSeries) ? series : visibleSeries);
+
+ const maxY = getMaxY(visibleSeries || series || []);
const formatter = getDurationFormatter(maxY);
- return { formatter, setDisabledSeriesState };
+ const toggleSerie = ({ specId }: SeriesIdentifier) => {
+ if (disabledSeries[specId] !== undefined) {
+ setDisabledSeries((prevState) => {
+ return omit(prevState, specId);
+ });
+ } else {
+ setDisabledSeries((prevState) => {
+ return { ...prevState, [specId]: 0 };
+ });
+ }
+ };
+
+ return {
+ formatter,
+ toggleSerie,
+ };
};
diff --git a/x-pack/plugins/apm/public/components/shared/charts/annotations/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/annotations/index.tsx
new file mode 100644
index 0000000000000..683c66b2a96fe
--- /dev/null
+++ b/x-pack/plugins/apm/public/components/shared/charts/annotations/index.tsx
@@ -0,0 +1,45 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import {
+ AnnotationDomainTypes,
+ LineAnnotation,
+ Position,
+} from '@elastic/charts';
+import { EuiIcon } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+import React from 'react';
+import { asAbsoluteDateTime } from '../../../../../common/utils/formatters';
+import { useTheme } from '../../../../hooks/useTheme';
+import { useAnnotations } from '../../../../hooks/use_annotations';
+
+export function Annotations() {
+ const { annotations } = useAnnotations();
+ const theme = useTheme();
+
+ if (!annotations.length) {
+ return null;
+ }
+
+ const color = theme.eui.euiColorSecondary;
+
+ return (
+ ({
+ dataValue: annotation['@timestamp'],
+ header: asAbsoluteDateTime(annotation['@timestamp']),
+ details: `${i18n.translate('xpack.apm.chart.annotation.version', {
+ defaultMessage: 'Version',
+ })} ${annotation.text}`,
+ }))}
+ style={{ line: { strokeWidth: 1, stroke: color, opacity: 1 } }}
+ marker={ }
+ markerPosition={Position.Top}
+ />
+ );
+}
diff --git a/x-pack/plugins/apm/public/components/shared/charts/chart_container.test.tsx b/x-pack/plugins/apm/public/components/shared/charts/chart_container.test.tsx
index 409cb69575ca9..c0e8f869ce647 100644
--- a/x-pack/plugins/apm/public/components/shared/charts/chart_container.test.tsx
+++ b/x-pack/plugins/apm/public/components/shared/charts/chart_container.test.tsx
@@ -5,30 +5,97 @@
*/
import { render } from '@testing-library/react';
import React from 'react';
+import { FETCH_STATUS } from '../../../hooks/useFetcher';
import { ChartContainer } from './chart_container';
describe('ChartContainer', () => {
- describe('when isLoading is true', () => {
- it('shows loading the indicator', () => {
- const component = render(
-
+ describe('loading indicator', () => {
+ it('shows loading when status equals to Loading or Pending and has no data', () => {
+ [FETCH_STATUS.PENDING, FETCH_STATUS.LOADING].map((status) => {
+ const { queryAllByTestId } = render(
+
+ My amazing component
+
+ );
+
+ expect(queryAllByTestId('loading')[0]).toBeInTheDocument();
+ });
+ });
+ it('does not show loading when status equals to Loading or Pending and has data', () => {
+ [FETCH_STATUS.PENDING, FETCH_STATUS.LOADING].map((status) => {
+ const { queryAllByText } = render(
+
+ My amazing component
+
+ );
+ expect(queryAllByText('My amazing component')[0]).toBeInTheDocument();
+ });
+ });
+ });
+
+ describe('failure indicator', () => {
+ it('shows failure message when status equals to Failure and has data', () => {
+ const { getByText } = render(
+
My amazing component
);
-
- expect(component.getByTestId('loading')).toBeInTheDocument();
+ expect(
+ getByText(
+ 'An error happened when trying to fetch data. Please try again'
+ )
+ ).toBeInTheDocument();
+ });
+ it('shows failure message when status equals to Failure and has no data', () => {
+ const { getByText } = render(
+
+ My amazing component
+
+ );
+ expect(
+ getByText(
+ 'An error happened when trying to fetch data. Please try again'
+ )
+ ).toBeInTheDocument();
});
});
- describe('when isLoading is false', () => {
- it('does not show the loading indicator', () => {
- const component = render(
-
+ describe('render component', () => {
+ it('shows children component when status Success and has data', () => {
+ const { getByText } = render(
+
My amazing component
);
-
- expect(component.queryByTestId('loading')).not.toBeInTheDocument();
+ expect(getByText('My amazing component')).toBeInTheDocument();
+ });
+ it('shows children component when status Success and has no data', () => {
+ const { getByText } = render(
+
+ My amazing component
+
+ );
+ expect(getByText('My amazing component')).toBeInTheDocument();
});
});
});
diff --git a/x-pack/plugins/apm/public/components/shared/charts/chart_container.tsx b/x-pack/plugins/apm/public/components/shared/charts/chart_container.tsx
index a6f579308597f..b4486f1e9b94a 100644
--- a/x-pack/plugins/apm/public/components/shared/charts/chart_container.tsx
+++ b/x-pack/plugins/apm/public/components/shared/charts/chart_container.tsx
@@ -3,27 +3,56 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
-import { EuiLoadingChart } from '@elastic/eui';
+
+import { EuiLoadingChart, EuiText } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
import React from 'react';
+import { FETCH_STATUS } from '../../../hooks/useFetcher';
interface Props {
- isLoading: boolean;
+ hasData: boolean;
+ status: FETCH_STATUS;
height: number;
children: React.ReactNode;
}
-export function ChartContainer({ isLoading, children, height }: Props) {
+export function ChartContainer({ children, height, status, hasData }: Props) {
+ if (
+ !hasData &&
+ (status === FETCH_STATUS.LOADING || status === FETCH_STATUS.PENDING)
+ ) {
+ return ;
+ }
+
+ if (status === FETCH_STATUS.FAILURE) {
+ return ;
+ }
+
+ return {children}
;
+}
+
+function LoadingChartPlaceholder({ height }: { height: number }) {
return (
- {isLoading && }
- {children}
+
);
}
+
+function FailedChartPlaceholder({ height }: { height: number }) {
+ return (
+
+ {i18n.translate('xpack.apm.chart.error', {
+ defaultMessage:
+ 'An error happened when trying to fetch data. Please try again',
+ })}
+
+ );
+}
diff --git a/x-pack/plugins/apm/public/components/shared/charts/erroneous_transactions_rate_chart/legacy.tsx b/x-pack/plugins/apm/public/components/shared/charts/erroneous_transactions_rate_chart/legacy.tsx
deleted file mode 100644
index 29102f606414f..0000000000000
--- a/x-pack/plugins/apm/public/components/shared/charts/erroneous_transactions_rate_chart/legacy.tsx
+++ /dev/null
@@ -1,112 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License;
- * you may not use this file except in compliance with the Elastic License.
- */
-import { EuiPanel, EuiSpacer, EuiTitle } from '@elastic/eui';
-import theme from '@elastic/eui/dist/eui_theme_light.json';
-import { i18n } from '@kbn/i18n';
-import { max } from 'lodash';
-import React, { useCallback } from 'react';
-import { useParams } from 'react-router-dom';
-import { asPercent } from '../../../../../common/utils/formatters';
-import { useLegacyChartsSync as useChartsSync } from '../../../../hooks/use_charts_sync';
-import { useFetcher } from '../../../../hooks/useFetcher';
-import { useUrlParams } from '../../../../hooks/useUrlParams';
-import { callApmApi } from '../../../../services/rest/createCallApmApi';
-// @ts-expect-error
-import CustomPlot from '../CustomPlot';
-
-const tickFormatY = (y?: number | null) => {
- return asPercent(y || 0, 1);
-};
-
-/**
- * "Legacy" version of this chart using react-vis charts. See index.tsx for the
- * Elastic Charts version.
- *
- * This will be removed with #70290.
- */
-export function ErroneousTransactionsRateChart() {
- const { serviceName } = useParams<{ serviceName?: string }>();
- const { urlParams, uiFilters } = useUrlParams();
- const syncedChartsProps = useChartsSync();
-
- const { start, end, transactionType, transactionName } = urlParams;
-
- const { data } = useFetcher(() => {
- if (serviceName && start && end) {
- return callApmApi({
- pathname:
- '/api/apm/services/{serviceName}/transaction_groups/error_rate',
- params: {
- path: {
- serviceName,
- },
- query: {
- start,
- end,
- transactionType,
- transactionName,
- uiFilters: JSON.stringify(uiFilters),
- },
- },
- });
- }
- }, [serviceName, start, end, uiFilters, transactionType, transactionName]);
-
- const combinedOnHover = useCallback(
- (hoverX: number) => {
- return syncedChartsProps.onHover(hoverX);
- },
- [syncedChartsProps]
- );
-
- const errorRates = data?.transactionErrorRate || [];
- const maxRate = max(errorRates.map((errorRate) => errorRate.y));
-
- return (
-
-
-
- {i18n.translate('xpack.apm.errorRateChart.title', {
- defaultMessage: 'Transaction error rate',
- })}
-
-
-
-
- Number.isFinite(y) ? tickFormatY(y) : 'N/A'
- }
- />
-
- );
-}
diff --git a/x-pack/plugins/apm/public/components/shared/charts/line_chart/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/line_chart/index.tsx
index 3f2a08ecb7641..507acc49d89db 100644
--- a/x-pack/plugins/apm/public/components/shared/charts/line_chart/index.tsx
+++ b/x-pack/plugins/apm/public/components/shared/charts/line_chart/index.tsx
@@ -20,15 +20,17 @@ import moment from 'moment';
import React, { useEffect } from 'react';
import { useHistory } from 'react-router-dom';
import { TimeSeries } from '../../../../../typings/timeseries';
+import { FETCH_STATUS } from '../../../../hooks/useFetcher';
import { useUrlParams } from '../../../../hooks/useUrlParams';
import { useChartsSync } from '../../../../hooks/use_charts_sync';
import { unit } from '../../../../style/variables';
+import { Annotations } from '../annotations';
import { ChartContainer } from '../chart_container';
import { onBrushEnd } from '../helper/helper';
interface Props {
id: string;
- isLoading: boolean;
+ fetchStatus: FETCH_STATUS;
onToggleLegend?: LegendItemListener;
timeseries: TimeSeries[];
/**
@@ -38,18 +40,20 @@ interface Props {
/**
* Formatter for legend and tooltip values
*/
- yTickFormat: (y: number) => string;
+ yTickFormat?: (y: number) => string;
+ showAnnotations?: boolean;
}
const XY_HEIGHT = unit * 16;
export function LineChart({
id,
- isLoading,
+ fetchStatus,
onToggleLegend,
timeseries,
yLabelFormat,
yTickFormat,
+ showAnnotations = true,
}: Props) {
const history = useHistory();
const chartRef = React.createRef();
@@ -84,7 +88,7 @@ export function LineChart({
);
return (
-
+
onBrushEnd({ x, history })}
@@ -115,11 +119,13 @@ export function LineChart({
id="y-axis"
ticks={3}
position={Position.Left}
- tickFormat={yTickFormat}
+ tickFormat={yTickFormat ? yTickFormat : yLabelFormat}
labelFormat={yLabelFormat}
showGridLines
/>
+ {showAnnotations && }
+
{timeseries.map((serie) => {
return (
();
const { urlParams, uiFilters } = useUrlParams();
@@ -56,25 +61,32 @@ export function ErroneousTransactionsRateChart() {
const errorRates = data?.transactionErrorRate || [];
return (
-
+
+
+
+ {i18n.translate('xpack.apm.errorRate', {
+ defaultMessage: 'Error rate',
+ })}
+
+
+
+
);
}
diff --git a/x-pack/plugins/apm/public/hooks/useTransactionBreakdown.ts b/x-pack/plugins/apm/public/hooks/useTransactionBreakdown.ts
index 08d2300c3254a..0705383ecb0ca 100644
--- a/x-pack/plugins/apm/public/hooks/useTransactionBreakdown.ts
+++ b/x-pack/plugins/apm/public/hooks/useTransactionBreakdown.ts
@@ -15,7 +15,7 @@ export function useTransactionBreakdown() {
uiFilters,
} = useUrlParams();
- const { data = { timeseries: [] }, error, status } = useFetcher(
+ const { data = { timeseries: undefined }, error, status } = useFetcher(
(callApmApi) => {
if (serviceName && start && end && transactionType) {
return callApmApi({
diff --git a/x-pack/plugins/apm/public/hooks/useTransactionDistribution.ts b/x-pack/plugins/apm/public/hooks/useTransactionDistribution.ts
index a5096a314388c..8c76225d03486 100644
--- a/x-pack/plugins/apm/public/hooks/useTransactionDistribution.ts
+++ b/x-pack/plugins/apm/public/hooks/useTransactionDistribution.ts
@@ -10,7 +10,7 @@ import { IUrlParams } from '../context/UrlParamsContext/types';
import { useFetcher } from './useFetcher';
import { useUiFilters } from '../context/UrlParamsContext';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
-import { TransactionDistributionAPIResponse } from '../../server/lib/transactions/distribution';
+import type { TransactionDistributionAPIResponse } from '../../server/lib/transactions/distribution';
import { toQuery, fromQuery } from '../components/shared/Links/url_helpers';
import { maybe } from '../../common/utils/maybe';
diff --git a/x-pack/plugins/apm/public/hooks/useTransactionList.ts b/x-pack/plugins/apm/public/hooks/useTransactionList.ts
index 9c3a18b9c0d0d..b2c2cc30f78ec 100644
--- a/x-pack/plugins/apm/public/hooks/useTransactionList.ts
+++ b/x-pack/plugins/apm/public/hooks/useTransactionList.ts
@@ -14,8 +14,8 @@ type TransactionsAPIResponse = APIReturnType<
'/api/apm/services/{serviceName}/transaction_groups'
>;
-const DEFAULT_RESPONSE: TransactionsAPIResponse = {
- items: [],
+const DEFAULT_RESPONSE: Partial = {
+ items: undefined,
isAggregationAccurate: true,
bucketSize: 0,
};
diff --git a/x-pack/plugins/apm/public/hooks/use_annotations.ts b/x-pack/plugins/apm/public/hooks/use_annotations.ts
new file mode 100644
index 0000000000000..2b1c2bec52b3d
--- /dev/null
+++ b/x-pack/plugins/apm/public/hooks/use_annotations.ts
@@ -0,0 +1,38 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import { useParams } from 'react-router-dom';
+import { callApmApi } from '../services/rest/createCallApmApi';
+import { useFetcher } from './useFetcher';
+import { useUrlParams } from './useUrlParams';
+
+const INITIAL_STATE = { annotations: [] };
+
+export function useAnnotations() {
+ const { serviceName } = useParams<{ serviceName?: string }>();
+ const { urlParams, uiFilters } = useUrlParams();
+ const { start, end } = urlParams;
+ const { environment } = uiFilters;
+
+ const { data = INITIAL_STATE } = useFetcher(() => {
+ if (start && end && serviceName) {
+ return callApmApi({
+ pathname: '/api/apm/services/{serviceName}/annotation/search',
+ params: {
+ path: {
+ serviceName,
+ },
+ query: {
+ start,
+ end,
+ environment,
+ },
+ },
+ });
+ }
+ }, [start, end, environment, serviceName]);
+
+ return data;
+}
diff --git a/x-pack/plugins/apm/public/selectors/chartSelectors.ts b/x-pack/plugins/apm/public/selectors/chartSelectors.ts
index 8c6093859f969..450f02f70c6a4 100644
--- a/x-pack/plugins/apm/public/selectors/chartSelectors.ts
+++ b/x-pack/plugins/apm/public/selectors/chartSelectors.ts
@@ -31,40 +31,37 @@ export interface ITpmBucket {
}
export interface ITransactionChartData {
- tpmSeries: ITpmBucket[];
- responseTimeSeries: TimeSeries[];
+ tpmSeries?: ITpmBucket[];
+ responseTimeSeries?: TimeSeries[];
mlJobId: string | undefined;
}
-const INITIAL_DATA = {
- apmTimeseries: {
- responseTimes: {
- avg: [],
- p95: [],
- p99: [],
- },
- tpmBuckets: [],
- overallAvgDuration: null,
- },
+const INITIAL_DATA: Partial = {
+ apmTimeseries: undefined,
anomalyTimeseries: undefined,
};
export function getTransactionCharts(
{ transactionType }: IUrlParams,
- { apmTimeseries, anomalyTimeseries }: TimeSeriesAPIResponse = INITIAL_DATA
+ charts = INITIAL_DATA
): ITransactionChartData {
- const tpmSeries = getTpmSeries(apmTimeseries, transactionType);
-
- const responseTimeSeries = getResponseTimeSeries({
- apmTimeseries,
- anomalyTimeseries,
- });
+ const { apmTimeseries, anomalyTimeseries } = charts;
- return {
- tpmSeries,
- responseTimeSeries,
+ const transactionCharts: ITransactionChartData = {
+ tpmSeries: undefined,
+ responseTimeSeries: undefined,
mlJobId: anomalyTimeseries?.jobId,
};
+
+ if (apmTimeseries) {
+ transactionCharts.tpmSeries = getTpmSeries(apmTimeseries, transactionType);
+
+ transactionCharts.responseTimeSeries = getResponseTimeSeries({
+ apmTimeseries,
+ anomalyTimeseries,
+ });
+ }
+ return transactionCharts;
}
export function getResponseTimeSeries({
diff --git a/x-pack/plugins/apm/server/lib/errors/distribution/get_buckets.ts b/x-pack/plugins/apm/server/lib/errors/distribution/get_buckets.ts
index a42710947a792..b12dd73a20986 100644
--- a/x-pack/plugins/apm/server/lib/errors/distribution/get_buckets.ts
+++ b/x-pack/plugins/apm/server/lib/errors/distribution/get_buckets.ts
@@ -73,6 +73,6 @@ export async function getBuckets({
return {
noHits: resp.hits.total.value === 0,
- buckets,
+ buckets: resp.hits.total.value > 0 ? buckets : [],
};
}
diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json
index baa4f37791007..485b24dced346 100644
--- a/x-pack/plugins/translations/translations/ja-JP.json
+++ b/x-pack/plugins/translations/translations/ja-JP.json
@@ -4875,22 +4875,15 @@
"xpack.apm.error.prompt.title": "申し訳ございませんが、エラーが発生しました :(",
"xpack.apm.errorCountAlert.name": "エラー数しきい値",
"xpack.apm.errorCountAlertTrigger.errors": " エラー",
- "xpack.apm.errorGroupDetails.avgLabel": "平均",
"xpack.apm.errorGroupDetails.culpritLabel": "原因",
"xpack.apm.errorGroupDetails.errorGroupTitle": "エラーグループ {errorGroupId}",
"xpack.apm.errorGroupDetails.errorOccurrenceTitle": "エラーのオカレンス",
"xpack.apm.errorGroupDetails.exceptionMessageLabel": "例外メッセージ",
"xpack.apm.errorGroupDetails.logMessageLabel": "ログメッセージ",
- "xpack.apm.errorGroupDetails.noErrorsLabel": "エラーが見つかりませんでした",
"xpack.apm.errorGroupDetails.occurrencesChartLabel": "オカレンス",
- "xpack.apm.errorGroupDetails.occurrencesLongLabel": "{occCount} {occCount, plural, one {件の発生} other {件の発生}}",
- "xpack.apm.errorGroupDetails.occurrencesShortLabel": "{occCount} 件",
"xpack.apm.errorGroupDetails.relatedTransactionSample": "関連トランザクションサンプル",
"xpack.apm.errorGroupDetails.unhandledLabel": "未対応",
"xpack.apm.errorGroupDetails.viewOccurrencesInDiscoverButtonLabel": "ディスカバリで {occurrencesCount} 件の{occurrencesCount, plural, one {ドキュメント} other {ドキュメント}}を表示。",
- "xpack.apm.errorRateChart.avgLabel": "平均",
- "xpack.apm.errorRateChart.rateLabel": "レート",
- "xpack.apm.errorRateChart.title": "トランザクションエラー率",
"xpack.apm.errorsTable.errorMessageAndCulpritColumnLabel": "エラーメッセージと原因",
"xpack.apm.errorsTable.groupIdColumnDescription": "スタックトレースのハッシュ。動的パラメータのため、エラーメッセージが異なる場合でも、類似したエラーをグループ化します。",
"xpack.apm.errorsTable.groupIdColumnLabel": "グループ ID",
@@ -4917,7 +4910,6 @@
"xpack.apm.header.badge.readOnly.text": "読み込み専用",
"xpack.apm.header.badge.readOnly.tooltip": "を保存できませんでした",
"xpack.apm.helpMenu.upgradeAssistantLink": "アップグレードアシスタント",
- "xpack.apm.histogram.plot.noDataLabel": "この時間範囲のデータがありません。",
"xpack.apm.home.alertsMenu.alerts": "アラート",
"xpack.apm.home.alertsMenu.createAnomalyAlert": "異常アラートを作成",
"xpack.apm.home.alertsMenu.createThresholdAlert": "しきい値アラートを作成",
@@ -5256,7 +5248,6 @@
"xpack.apm.transactionDetails.traceNotFound": "選択されたトレースが見つかりません",
"xpack.apm.transactionDetails.traceSampleTitle": "トレースのサンプル",
"xpack.apm.transactionDetails.transactionLabel": "トランザクション",
- "xpack.apm.transactionDetails.transactionsDurationDistributionChart.noSampleTooltip": "このバケットに利用可能なサンプルがありません",
"xpack.apm.transactionDetails.transactionsDurationDistributionChart.requestTypeUnitLongLabel": "{transCount, plural, =0 {# request} 1 {# 件のリクエスト} other {# 件のリクエスト}}",
"xpack.apm.transactionDetails.transactionsDurationDistributionChart.transactionTypeUnitLongLabel": "{transCount, plural, =0 {# transaction} 1 {# 件のトランザクション} other {# 件のトランザクション}}",
"xpack.apm.transactionDetails.transactionsDurationDistributionChart.unitShortLabel": "{transCount} {transType, select, request {件のリクエスト} other {件のトランザクション}}",
diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json
index c4274524928fd..98d13011d3306 100644
--- a/x-pack/plugins/translations/translations/zh-CN.json
+++ b/x-pack/plugins/translations/translations/zh-CN.json
@@ -4877,22 +4877,15 @@
"xpack.apm.error.prompt.title": "抱歉,发生错误 :(",
"xpack.apm.errorCountAlert.name": "错误计数阈值",
"xpack.apm.errorCountAlertTrigger.errors": " 错误",
- "xpack.apm.errorGroupDetails.avgLabel": "平均",
"xpack.apm.errorGroupDetails.culpritLabel": "原因",
"xpack.apm.errorGroupDetails.errorGroupTitle": "错误组 {errorGroupId}",
"xpack.apm.errorGroupDetails.errorOccurrenceTitle": "错误发生",
"xpack.apm.errorGroupDetails.exceptionMessageLabel": "异常消息",
"xpack.apm.errorGroupDetails.logMessageLabel": "日志消息",
- "xpack.apm.errorGroupDetails.noErrorsLabel": "未找到任何错误",
"xpack.apm.errorGroupDetails.occurrencesChartLabel": "发生次数",
- "xpack.apm.errorGroupDetails.occurrencesLongLabel": "{occCount} 次{occCount, plural, one {出现} other {出现}}",
- "xpack.apm.errorGroupDetails.occurrencesShortLabel": "{occCount} 次发生",
"xpack.apm.errorGroupDetails.relatedTransactionSample": "相关的事务样本",
"xpack.apm.errorGroupDetails.unhandledLabel": "未处理",
"xpack.apm.errorGroupDetails.viewOccurrencesInDiscoverButtonLabel": "在 Discover 查看 {occurrencesCount} 个 {occurrencesCount, plural, one {匹配项} other {匹配项}}。",
- "xpack.apm.errorRateChart.avgLabel": "平均",
- "xpack.apm.errorRateChart.rateLabel": "比率",
- "xpack.apm.errorRateChart.title": "事务错误率",
"xpack.apm.errorsTable.errorMessageAndCulpritColumnLabel": "错误消息和原因",
"xpack.apm.errorsTable.groupIdColumnDescription": "堆栈跟踪的哈希。将类似错误分组在一起,即使因动态参数造成错误消息不同。",
"xpack.apm.errorsTable.groupIdColumnLabel": "组 ID",
@@ -4919,7 +4912,6 @@
"xpack.apm.header.badge.readOnly.text": "只读",
"xpack.apm.header.badge.readOnly.tooltip": "无法保存",
"xpack.apm.helpMenu.upgradeAssistantLink": "升级助手",
- "xpack.apm.histogram.plot.noDataLabel": "此时间范围内没有数据。",
"xpack.apm.home.alertsMenu.alerts": "告警",
"xpack.apm.home.alertsMenu.createAnomalyAlert": "创建异常告警",
"xpack.apm.home.alertsMenu.createThresholdAlert": "创建阈值告警",
@@ -5260,7 +5252,6 @@
"xpack.apm.transactionDetails.traceNotFound": "找不到所选跟踪",
"xpack.apm.transactionDetails.traceSampleTitle": "跟踪样例",
"xpack.apm.transactionDetails.transactionLabel": "事务",
- "xpack.apm.transactionDetails.transactionsDurationDistributionChart.noSampleTooltip": "此存储桶没有可用样例",
"xpack.apm.transactionDetails.transactionsDurationDistributionChart.requestTypeUnitLongLabel": "{transCount, plural, =0 {# 个请求} one {# 个请求} other {# 个请求}}",
"xpack.apm.transactionDetails.transactionsDurationDistributionChart.transactionTypeUnitLongLabel": "{transCount, plural, =0 {# 个事务} one {# 个事务} other {# 个事务}}",
"xpack.apm.transactionDetails.transactionsDurationDistributionChart.unitShortLabel": "{transCount} 个{transType, select, request {请求} other {事务}}",
From 09aec4defd1d20f6e0fb4089359ffd8de11ff966 Mon Sep 17 00:00:00 2001
From: Marco Liberati
Date: Mon, 9 Nov 2020 15:21:15 +0100
Subject: [PATCH 7/8] Indexpattern edit field formatter API fix for scripted
field (#82876)
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
---
.../index_patterns/index_pattern.test.ts | 18 ++++++++++++++++++
.../index_patterns/index_pattern.ts | 18 +++++++++---------
2 files changed, 27 insertions(+), 9 deletions(-)
diff --git a/src/plugins/data/common/index_patterns/index_patterns/index_pattern.test.ts b/src/plugins/data/common/index_patterns/index_patterns/index_pattern.test.ts
index 9085ae07bbe3e..145901509d1c5 100644
--- a/src/plugins/data/common/index_patterns/index_patterns/index_pattern.test.ts
+++ b/src/plugins/data/common/index_patterns/index_patterns/index_pattern.test.ts
@@ -196,6 +196,24 @@ describe('IndexPattern', () => {
});
});
+ describe('getFormatterForField', () => {
+ test('should return the default one for empty objects', () => {
+ indexPattern.setFieldFormat('scriptedFieldWithEmptyFormatter', {});
+ expect(
+ indexPattern.getFormatterForField({
+ name: 'scriptedFieldWithEmptyFormatter',
+ type: 'number',
+ esTypes: ['long'],
+ })
+ ).toEqual(
+ expect.objectContaining({
+ convert: expect.any(Function),
+ getConverterFor: expect.any(Function),
+ })
+ );
+ });
+ });
+
describe('toSpec', () => {
test('should match snapshot', () => {
const formatter = {
diff --git a/src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts b/src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts
index a0f27078543a9..4508d7b1d9082 100644
--- a/src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts
+++ b/src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts
@@ -291,15 +291,15 @@ export class IndexPattern implements IIndexPattern {
getFormatterForField(
field: IndexPatternField | IndexPatternField['spec'] | IFieldType
): FieldFormat {
- const formatSpec = this.fieldFormatMap[field.name];
- if (formatSpec) {
- return this.fieldFormats.getInstance(formatSpec.id, formatSpec.params);
- } else {
- return this.fieldFormats.getDefaultInstance(
- field.type as KBN_FIELD_TYPES,
- field.esTypes as ES_FIELD_TYPES[]
- );
+ const fieldFormat = this.getFormatterForFieldNoDefault(field.name);
+ if (fieldFormat) {
+ return fieldFormat;
}
+
+ return this.fieldFormats.getDefaultInstance(
+ field.type as KBN_FIELD_TYPES,
+ field.esTypes as ES_FIELD_TYPES[]
+ );
}
/**
@@ -308,7 +308,7 @@ export class IndexPattern implements IIndexPattern {
*/
getFormatterForFieldNoDefault(fieldname: string) {
const formatSpec = this.fieldFormatMap[fieldname];
- if (formatSpec) {
+ if (formatSpec?.id) {
return this.fieldFormats.getInstance(formatSpec.id, formatSpec.params);
}
}
From 97e2dc853b5d0d12b87691bb447f6475de0bbfec Mon Sep 17 00:00:00 2001
From: Marco Liberati
Date: Mon, 9 Nov 2020 15:22:21 +0100
Subject: [PATCH 8/8] [Lens] Extend Datasource props validation with
VisualizationGroups (#82607)
* :sparkles: First pass with visualization validation + error messages
* :fire: Remove indexpattern error handling for now
* :label: Fix type issues
* :white_check_mark: Add getErrorMessage test for data table
* :white_check_mark: Add tests for pie and metric error messages
* :globe_with_meridians: Fix i18n checks issues
* :bug: Fix last issue
* :white_check_mark: Add more tests for the XY visualization validation code
* :ok_hand: Included all feedback from first review
* :pencil2: Off by one message
* :globe_with_meridians: Fix i18n duplicate id
* :globe_with_meridians: Fix last i18n issue
* :bug: Fixed a hook reflow issue
* :recycle:+:white_check_mark: Reworked validation flow + tests
* :label: Fix type issue
* :bug: Improved XY corner cases validation logic
* :bug: Fix empty datatable scenario
* :sparkles: + :white_check_mark: Improved error messages for invalid datasources + tests
* :globe_with_meridians: Add missing i18n translation
* :label: Fix type issues
* :globe_with_meridians: Fix i18n issues
* :sparkles: Filter out suggestions which fail to build
* :truck: Migrate datatable validation logic to the building phase, handling it as building state
* :label: Fix type issue
* :pencil2: Add comment for future enhancements
* :pencil2: Updated comment
* :world_with_meridians: Refactor axis labels
* :globe_with_meridians: Reworked few validation messages
* :bug: Fix break down validation + percentage charts
* :white_check_mark: Align tests with new validation logic
* :recycle: Fix suggestion panel validation to match main panel
* :globe_with_meridians: Fix i18n issues
* :wrench: Fix some refs for validation checks in suggestions
* :bug: Fix missing key prop in multiple errors scenario
* :bug: Fix swtich issue from XY to partition
* :globe_with_meridians: Fix i18n messages and aligned tests
* :bug: Fix suggestions switching bug
* :refactor: Add more validation + refactored validation logic in a single place
* :pencil2: Add note about lint hooks disable rule
* :rotating_light: Fix linting issue
* :building_construction: Add infra API for datasource advanced validation
* :white_check_mark: Align tests with new API
* :white_check_mark: Fix type issues in tests
* :ok_hand: Early exists added
* :sparkles: Add layers groups to the API
* :white_check_mark: Fix some broken test after the validation change
* :ok_hand: Move to disctionary shape
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
---
.../editor_frame/config_panel/layer_panel.tsx | 1 +
.../editor_frame/editor_frame.test.tsx | 9 ++++---
.../editor_frame/state_helpers.ts | 26 +++++++++++++++++--
.../dimension_panel/dimension_panel.test.tsx | 1 +
.../dimension_panel/droppable.test.ts | 1 +
.../indexpattern_datasource/indexpattern.tsx | 2 +-
x-pack/plugins/lens/public/types.ts | 6 ++++-
7 files changed, 39 insertions(+), 7 deletions(-)
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 c9d99bcfb6d8d..0332f11aa78b3 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
@@ -451,6 +451,7 @@ export function LayerPanel(
columnId: activeId,
filterOperations: activeGroup.filterOperations,
suggestedPriority: activeGroup?.suggestedPriority,
+ dimensionGroups: groups,
setState: (newState: unknown) => {
props.updateAll(
datasourceId,
diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.test.tsx
index f7a6f0597bf9c..b3ea14efbae80 100644
--- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.test.tsx
+++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.test.tsx
@@ -601,7 +601,8 @@ describe('editor_frame', () => {
setDatasourceState(updatedState);
});
- expect(mockVisualization.getConfiguration).toHaveBeenCalledTimes(2);
+ // validation requires to calls this getConfiguration API
+ expect(mockVisualization.getConfiguration).toHaveBeenCalledTimes(6);
expect(mockVisualization.getConfiguration).toHaveBeenLastCalledWith(
expect.objectContaining({
state: updatedState,
@@ -680,7 +681,8 @@ describe('editor_frame', () => {
setDatasourceState({});
});
- expect(mockVisualization.getConfiguration).toHaveBeenCalledTimes(2);
+ // validation requires to calls this getConfiguration API
+ expect(mockVisualization.getConfiguration).toHaveBeenCalledTimes(6);
expect(mockVisualization.getConfiguration).toHaveBeenLastCalledWith(
expect.objectContaining({
frame: expect.objectContaining({
@@ -1193,7 +1195,8 @@ describe('editor_frame', () => {
instance.find('[data-test-subj="lnsSuggestion"]').at(2).simulate('click');
});
- expect(mockVisualization.getConfiguration).toHaveBeenCalledTimes(1);
+ // validation requires to calls this getConfiguration API
+ expect(mockVisualization.getConfiguration).toHaveBeenCalledTimes(4);
expect(mockVisualization.getConfiguration).toHaveBeenCalledWith(
expect.objectContaining({
state: suggestionVisState,
diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_helpers.ts b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_helpers.ts
index 28ad6c531e255..647c0f3ac9cca 100644
--- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_helpers.ts
+++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_helpers.ts
@@ -6,7 +6,13 @@
import { SavedObjectReference } from 'kibana/public';
import { Ast } from '@kbn/interpreter/common';
-import { Datasource, DatasourcePublicAPI, FramePublicAPI, Visualization } from '../../types';
+import {
+ Datasource,
+ DatasourcePublicAPI,
+ FramePublicAPI,
+ Visualization,
+ VisualizationDimensionGroupConfig,
+} from '../../types';
import { buildExpression } from './expression_helpers';
import { Document } from '../../persistence/saved_object_store';
import { VisualizeFieldContext } from '../../../../../../src/plugins/ui_actions/public';
@@ -104,8 +110,24 @@ export const validateDatasourceAndVisualization = (
longMessage: string;
}>
| undefined => {
+ const layersGroups =
+ currentVisualizationState &&
+ currentVisualization
+ ?.getLayerIds(currentVisualizationState)
+ .reduce>((memo, layerId) => {
+ const groups = currentVisualization?.getConfiguration({
+ frame: frameAPI,
+ layerId,
+ state: currentVisualizationState,
+ }).groups;
+ if (groups) {
+ memo[layerId] = groups;
+ }
+ return memo;
+ }, {});
+
const datasourceValidationErrors = currentDatasourceState
- ? currentDataSource?.getErrorMessages(currentDatasourceState)
+ ? currentDataSource?.getErrorMessages(currentDatasourceState, layersGroups)
: undefined;
const visualizationValidationErrors = currentVisualizationState
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx
index 829bd333ce2cc..92a4dad14dd25 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx
@@ -174,6 +174,7 @@ describe('IndexPatternDimensionEditorPanel', () => {
} as unknown) as DataPublicPluginStart['fieldFormats'],
} as unknown) as DataPublicPluginStart,
core: {} as CoreSetup,
+ dimensionGroups: [],
};
jest.clearAllMocks();
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable.test.ts
index bbd1d4e0ae3ab..dd696f8be357f 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable.test.ts
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable.test.ts
@@ -146,6 +146,7 @@ describe('IndexPatternDimensionEditorPanel', () => {
} as unknown) as DataPublicPluginStart['fieldFormats'],
} as unknown) as DataPublicPluginStart,
core: {} as CoreSetup,
+ dimensionGroups: [],
};
jest.clearAllMocks();
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx
index ecca1b878e9a7..fa106e90d518a 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx
@@ -343,7 +343,7 @@ export function getIndexPatternDatasource({
getDatasourceSuggestionsFromCurrentState,
getDatasourceSuggestionsForVisualizeField,
- getErrorMessages(state) {
+ getErrorMessages(state, layersGroups) {
if (!state) {
return;
}
diff --git a/x-pack/plugins/lens/public/types.ts b/x-pack/plugins/lens/public/types.ts
index 3c96579fdc943..4ad849c5d441e 100644
--- a/x-pack/plugins/lens/public/types.ts
+++ b/x-pack/plugins/lens/public/types.ts
@@ -184,7 +184,10 @@ export interface Datasource {
) => Array>;
getPublicAPI: (props: PublicAPIProps) => DatasourcePublicAPI;
- getErrorMessages: (state: T) => Array<{ shortMessage: string; longMessage: string }> | undefined;
+ getErrorMessages: (
+ state: T,
+ layersGroups?: Record
+ ) => Array<{ shortMessage: string; longMessage: string }> | undefined;
/**
* uniqueLabels of dimensions exposed for aria-labels of dragged dimensions
*/
@@ -242,6 +245,7 @@ export type DatasourceDimensionEditorProps = DatasourceDimensionPro
setState: StateSetter;
core: Pick;
dateRange: DateRange;
+ dimensionGroups: VisualizationDimensionGroupConfig[];
};
export type DatasourceDimensionTriggerProps = DatasourceDimensionProps & {