diff --git a/src/plugins/chart_expressions/expression_partition_vis/public/components/__snapshots__/partition_vis_component.test.tsx.snap b/src/plugins/chart_expressions/expression_partition_vis/public/components/__snapshots__/partition_vis_component.test.tsx.snap
index 0fcee477c99de0..a9fc01e3fdffb1 100644
--- a/src/plugins/chart_expressions/expression_partition_vis/public/components/__snapshots__/partition_vis_component.test.tsx.snap
+++ b/src/plugins/chart_expressions/expression_partition_vis/public/components/__snapshots__/partition_vis_component.test.tsx.snap
@@ -784,6 +784,9 @@ exports[`PartitionVisComponent should render correct structure for mosaic 1`] =
Array [
Object {
"fillLabel": Object {
+ "clipText": true,
+ "maxFontSize": 14,
+ "minFontSize": 14,
"valueFont": Object {
"fontWeight": 700,
},
@@ -1865,7 +1868,7 @@ exports[`PartitionVisComponent should render correct structure for waffle 1`] =
ariaUseDefaultSummary={true}
baseTheme={Object {}}
debugState={false}
- flatLegend={false}
+ flatLegend={true}
legendAction={[Function]}
legendColorPicker={[Function]}
legendMaxDepth={1}
@@ -1901,7 +1904,7 @@ exports[`PartitionVisComponent should render correct structure for waffle 1`] =
},
"maxFontSize": 16,
"minFontSize": 10,
- "outerSizeRatio": 1,
+ "outerSizeRatio": undefined,
"sectorLineStroke": undefined,
"sectorLineWidth": 1.5,
},
@@ -1975,7 +1978,7 @@ exports[`PartitionVisComponent should render correct structure for waffle 1`] =
},
]
}
- id="mosaic"
+ id="waffle"
layers={
Array [
Object {
@@ -1994,7 +1997,7 @@ exports[`PartitionVisComponent should render correct structure for waffle 1`] =
},
]
}
- layout="mosaic"
+ layout="waffle"
percentFormatter={[Function]}
smallMultiples="__pie_chart_sm__"
valueAccessor={[Function]}
diff --git a/src/plugins/chart_expressions/expression_partition_vis/public/components/partition_vis_component.test.tsx b/src/plugins/chart_expressions/expression_partition_vis/public/components/partition_vis_component.test.tsx
index 70c120e4fd7599..8e601c0f01e2a9 100644
--- a/src/plugins/chart_expressions/expression_partition_vis/public/components/partition_vis_component.test.tsx
+++ b/src/plugins/chart_expressions/expression_partition_vis/public/components/partition_vis_component.test.tsx
@@ -136,7 +136,7 @@ describe('PartitionVisComponent', function () {
diff --git a/src/plugins/chart_expressions/expression_partition_vis/public/utils/layers/get_layers.ts b/src/plugins/chart_expressions/expression_partition_vis/public/utils/layers/get_layers.ts
index 1ba15fe2a055fa..c8d7d1f30b4d54 100644
--- a/src/plugins/chart_expressions/expression_partition_vis/public/utils/layers/get_layers.ts
+++ b/src/plugins/chart_expressions/expression_partition_vis/public/utils/layers/get_layers.ts
@@ -58,7 +58,10 @@ export const getLayers = (
groupByRollup: (d: Datum) => (col.id ? d[col.id] ?? EMPTY_SLICE : col.name),
showAccessor: (d: Datum) => d !== EMPTY_SLICE,
nodeLabel: (d: unknown) => getNodeLabel(d, col, formatters, formatter.deserialize),
- fillLabel,
+ fillLabel:
+ layerIndex === 0 && chartType === ChartTypes.MOSAIC
+ ? { ...fillLabel, minFontSize: 14, maxFontSize: 14, clipText: true }
+ : fillLabel,
sortPredicate,
shape: {
fillColor: (d) =>
diff --git a/src/plugins/vis_types/heatmap/public/to_ast.ts b/src/plugins/vis_types/heatmap/public/to_ast.ts
index a5a14f5412dca8..966dea1edbe3a9 100644
--- a/src/plugins/vis_types/heatmap/public/to_ast.ts
+++ b/src/plugins/vis_types/heatmap/public/to_ast.ts
@@ -40,6 +40,7 @@ const prepareGrid = (params: HeatmapVisParams) => {
const gridConfig = buildExpressionFunction('heatmap_grid', {
isCellLabelVisible: params.valueAxes?.[0].labels.show ?? false,
isXAxisLabelVisible: true,
+ isYAxisLabelVisible: true,
isYAxisTitleVisible: true,
isXAxisTitleVisible: true,
});
diff --git a/src/plugins/vis_types/xy/public/__snapshots__/to_ast.test.ts.snap b/src/plugins/vis_types/xy/public/__snapshots__/to_ast.test.ts.snap
index 048b07dbf34ed9..aa2a68204108a5 100644
--- a/src/plugins/vis_types/xy/public/__snapshots__/to_ast.test.ts.snap
+++ b/src/plugins/vis_types/xy/public/__snapshots__/to_ast.test.ts.snap
@@ -33,7 +33,7 @@ Object {
"top",
],
"legendSize": Array [
- "small",
+ "auto",
],
"maxLegendLines": Array [
1,
diff --git a/src/plugins/vis_types/xy/public/to_ast.ts b/src/plugins/vis_types/xy/public/to_ast.ts
index 46ff05f4426a62..e92ca8cda82d2d 100644
--- a/src/plugins/vis_types/xy/public/to_ast.ts
+++ b/src/plugins/vis_types/xy/public/to_ast.ts
@@ -7,12 +7,13 @@
*/
import moment from 'moment';
-
+import { Position } from '@elastic/charts';
import {
VisToExpressionAst,
getVisSchemas,
DateHistogramParams,
HistogramParams,
+ LegendSize,
} from '@kbn/visualizations-plugin/public';
import { buildExpression, buildExpressionFunction } from '@kbn/expressions-plugin/public';
import { BUCKET_TYPES } from '@kbn/data-plugin/public';
@@ -202,6 +203,11 @@ export const toExpressionAst: VisToExpressionAst = async (vis, params
}
}
});
+ let legendSize = vis.params.legendSize;
+
+ if (vis.params.legendPosition === Position.Top || vis.params.legendPosition === Position.Bottom) {
+ legendSize = LegendSize.AUTO;
+ }
const visTypeXy = buildExpressionFunction(visName, {
type: vis.type.name as XyVisType,
@@ -209,7 +215,7 @@ export const toExpressionAst: VisToExpressionAst = async (vis, params
addTimeMarker: vis.params.addTimeMarker,
truncateLegend: vis.params.truncateLegend,
maxLegendLines: vis.params.maxLegendLines,
- legendSize: vis.params.legendSize,
+ legendSize,
addLegend: vis.params.addLegend,
addTooltip: vis.params.addTooltip,
legendPosition: vis.params.legendPosition,
diff --git a/x-pack/plugins/enterprise_search/kibana.json b/x-pack/plugins/enterprise_search/kibana.json
index bc12b7f447ab41..4556e5dc96106f 100644
--- a/x-pack/plugins/enterprise_search/kibana.json
+++ b/x-pack/plugins/enterprise_search/kibana.json
@@ -2,7 +2,7 @@
"id": "enterpriseSearch",
"version": "kibana",
"kibanaVersion": "kibana",
- "requiredPlugins": ["features", "spaces", "security", "licensing", "data", "charts", "infra", "cloud"],
+ "requiredPlugins": ["features", "spaces", "security", "licensing", "data", "charts", "infra", "cloud", "esUiShared"],
"configPath": ["enterpriseSearch"],
"optionalPlugins": ["usageCollection", "home", "cloud", "customIntegrations"],
"server": true,
diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/connector_package/update_connector_scheduling_api_logic.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/connector_package/update_connector_scheduling_api_logic.ts
new file mode 100644
index 00000000000000..47679ee1caa0e1
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/connector_package/update_connector_scheduling_api_logic.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
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { createApiLogic } from '../../../shared/api_logic/create_api_logic';
+import { HttpLogic } from '../../../shared/http';
+import { ConnectorScheduling } from '../index/fetch_index_api_logic';
+
+export interface UpdateConnectorSchedulingArgs {
+ connectorId: string;
+ scheduling: ConnectorScheduling;
+}
+
+export const updateConnectorScheduling = async ({
+ connectorId,
+ scheduling: { enabled, interval },
+}: UpdateConnectorSchedulingArgs) => {
+ const route = `/internal/enterprise_search/connectors/${connectorId}/scheduling`;
+
+ await HttpLogic.values.http.post(route, {
+ body: JSON.stringify({ enabled, interval }),
+ });
+ return { enabled, interval };
+};
+
+export const UpdateConnectorSchedulingApiLogic = createApiLogic(
+ ['content', 'update_connector_scheduling_api_logic'],
+ updateConnectorScheduling
+);
diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/index/fetch_index_api_logic.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/index/fetch_index_api_logic.ts
index 53ec3c1a92a82e..adfd99a8a2d240 100644
--- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/index/fetch_index_api_logic.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/index/fetch_index_api_logic.ts
@@ -14,6 +14,10 @@ export interface KeyValuePair {
}
export type ConnectorConfiguration = Record;
+export interface ConnectorScheduling {
+ enabled: boolean;
+ interval: string;
+}
export interface Connector {
api_key_id: string | null;
@@ -25,10 +29,10 @@ export interface Connector {
last_synced: string | null;
scheduling: {
enabled: boolean;
- interval: string | null; // crontab syntax
+ interval: string; // crontab syntax
};
service_type: string | null;
- status: string | null;
+ status: string;
sync_error: string | null;
sync_now: boolean;
sync_status: string | null;
diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/connector/connector_configuration_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/connector/connector_configuration_logic.test.ts
index 20bedee0a2d071..e7368ef67cebbc 100644
--- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/connector/connector_configuration_logic.test.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/connector/connector_configuration_logic.test.ts
@@ -11,10 +11,6 @@ import { ConnectorConfigurationApiLogic } from '../../../api/connector_package/u
import { ConnectorConfigurationLogic } from './connector_configuration_logic';
-// jest.mock('../../api', () => ({
-// AppLogic: { values: { isOrganization: true } },
-// }));
-
const DEFAULT_VALUES = {
configState: { foo: 'bar' },
isEditing: false,
@@ -22,7 +18,7 @@ const DEFAULT_VALUES = {
describe('ConnectorConfigurationLogic', () => {
const { mount } = new LogicMounter(ConnectorConfigurationLogic);
- const { clearFlashMessages, flashAPIErrors } = mockFlashMessageHelpers;
+ const { clearFlashMessages, flashAPIErrors, flashSuccessToast } = mockFlashMessageHelpers;
beforeEach(() => {
jest.clearAllMocks();
@@ -94,5 +90,14 @@ describe('ConnectorConfigurationLogic', () => {
expect(flashAPIErrors).toHaveBeenCalledWith('error');
});
});
+ describe('apiSuccess', () => {
+ it('should call flashAPIError', () => {
+ ConnectorConfigurationLogic.actions.apiSuccess({
+ configuration: {},
+ indexName: 'name',
+ });
+ expect(flashSuccessToast).toHaveBeenCalledWith('Configuration successfully updated');
+ });
+ });
});
});
diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/connector/connector_configuration_logic.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/connector/connector_configuration_logic.ts
index 43ab912849bd9f..cb4bfe2e5621fa 100644
--- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/connector/connector_configuration_logic.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/connector/connector_configuration_logic.ts
@@ -7,8 +7,14 @@
import { kea, MakeLogicType } from 'kea';
+import { i18n } from '@kbn/i18n';
+
import { Actions } from '../../../../shared/api_logic/create_api_logic';
-import { clearFlashMessages, flashAPIErrors } from '../../../../shared/flash_messages';
+import {
+ clearFlashMessages,
+ flashAPIErrors,
+ flashSuccessToast,
+} from '../../../../shared/flash_messages';
import {
ConnectorConfigurationApiLogic,
@@ -54,6 +60,13 @@ export const ConnectorConfigurationLogic = kea<
},
listeners: {
apiError: (error) => flashAPIErrors(error),
+ apiSuccess: () =>
+ flashSuccessToast(
+ i18n.translate(
+ 'xpack.enterpriseSearch.content.indices.configurationConnector.configuration.successToast.title',
+ { defaultMessage: 'Configuration successfully updated' }
+ )
+ ),
makeRequest: () => clearFlashMessages(),
},
reducers: ({ props }) => ({
diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/connector/connector_scheduling.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/connector/connector_scheduling.tsx
new file mode 100644
index 00000000000000..2f0b46520b89b9
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/connector/connector_scheduling.tsx
@@ -0,0 +1,185 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React, { useState } from 'react';
+
+import { useActions, useValues } from 'kea';
+
+import {
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiText,
+ EuiSwitch,
+ EuiPanel,
+ EuiSpacer,
+ EuiButton,
+ EuiButtonEmpty,
+} from '@elastic/eui';
+import { CronEditor, Frequency } from '@kbn/es-ui-shared-plugin/public';
+import { i18n } from '@kbn/i18n';
+
+import { Status } from '../../../../../../common/types/api';
+import { UnsavedChangesPrompt } from '../../../../shared/unsaved_changes_prompt';
+import { UpdateConnectorSchedulingApiLogic } from '../../../api/connector_package/update_connector_scheduling_api_logic';
+import { FetchIndexApiLogic } from '../../../api/index/fetch_index_api_logic';
+
+import { ConnectorSchedulingLogic } from './connector_scheduling_logic';
+
+export const ConnectorSchedulingComponent: React.FC = () => {
+ const { data } = useValues(FetchIndexApiLogic);
+ const { status } = useValues(UpdateConnectorSchedulingApiLogic);
+ const { makeRequest } = useActions(UpdateConnectorSchedulingApiLogic);
+ const { hasChanges } = useValues(ConnectorSchedulingLogic);
+ const { setHasChanges } = useActions(ConnectorSchedulingLogic);
+
+ const schedulingInput = data?.connector?.scheduling;
+ const [scheduling, setScheduling] = useState(schedulingInput);
+ const [fieldToPreferredValueMap, setFieldToPreferredValueMap] = useState({});
+ const [simpleCron, setSimpleCron] = useState<{
+ expression: string;
+ frequency: Frequency;
+ }>({
+ expression: schedulingInput?.interval ?? '',
+ frequency: schedulingInput?.interval ? cronToFrequency(schedulingInput.interval) : 'HOUR',
+ });
+
+ const editor = scheduling && (
+ {
+ setSimpleCron({
+ expression,
+ frequency,
+ });
+ setFieldToPreferredValueMap(newFieldToPreferredValueMap);
+ setScheduling({ ...scheduling, interval: expression });
+ setHasChanges(true);
+ }}
+ />
+ );
+
+ return scheduling ? (
+ <>
+
+
+
+
+
+ {
+ setScheduling({ ...scheduling, enabled: e.target.checked });
+ setHasChanges(true);
+ }}
+ />
+
+
+
+ {scheduling.enabled
+ ? i18n.translate(
+ 'xpack.enterpriseSearch.content.indices.connectorScheduling.switch.enabled.description',
+ {
+ defaultMessage:
+ 'This source will automatically be kept in sync according to the schedule set below.',
+ }
+ )
+ : i18n.translate(
+ 'xpack.enterpriseSearch.content.indices.connectorScheduling.switch.disabled.description',
+ { defaultMessage: 'Source content will not be kept in sync.' }
+ )}
+
+
+ {editor}
+
+
+
+ {
+ setScheduling(schedulingInput);
+ setSimpleCron({
+ expression: schedulingInput?.interval ?? '',
+ frequency: schedulingInput?.interval
+ ? cronToFrequency(schedulingInput.interval)
+ : 'HOUR',
+ });
+ setHasChanges(false);
+ }}
+ >
+ {i18n.translate(
+ 'xpack.enterpriseSearch.content.indices.connectorScheduling.resetButton.label',
+ { defaultMessage: 'Reset' }
+ )}
+
+
+
+
+ makeRequest({ connectorId: data?.connector?.id ?? '', scheduling })
+ }
+ >
+ {i18n.translate(
+ 'xpack.enterpriseSearch.content.indices.connectorScheduling.saveButton.label',
+ { defaultMessage: 'Save' }
+ )}
+
+
+
+
+
+
+ >
+ ) : (
+ <>>
+ );
+};
+
+export interface Schedule {
+ days: string;
+ hours: string;
+ minutes: string;
+}
+
+function cronToFrequency(cron: string): Frequency {
+ const fields = cron.split(' ');
+ if (fields.length < 4) {
+ return 'YEAR';
+ }
+ if (fields[1] === '*') {
+ return 'MINUTE';
+ }
+ if (fields[2] === '*') {
+ return 'HOUR';
+ }
+ if (fields[3] === '*') {
+ return 'DAY';
+ }
+ if (fields[4] === '?') {
+ return 'WEEK';
+ }
+ if (fields[4] === '*') {
+ return 'MONTH';
+ }
+ return 'YEAR';
+}
diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/connector/connector_scheduling_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/connector/connector_scheduling_logic.test.ts
new file mode 100644
index 00000000000000..2bf0b7172247c9
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/connector/connector_scheduling_logic.test.ts
@@ -0,0 +1,78 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { LogicMounter, mockFlashMessageHelpers } from '../../../../__mocks__/kea_logic';
+
+import { UpdateConnectorSchedulingApiLogic } from '../../../api/connector_package/update_connector_scheduling_api_logic';
+
+import { ConnectorSchedulingLogic } from './connector_scheduling_logic';
+
+describe('ConnectorSchedulingLogic', () => {
+ const { mount } = new LogicMounter(ConnectorSchedulingLogic);
+ const { clearFlashMessages, flashAPIErrors, flashSuccessToast } = mockFlashMessageHelpers;
+ const DEFAULT_VALUES = {
+ hasChanges: false,
+ };
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ mount({});
+ });
+ it('has expected default values', () => {
+ expect(ConnectorSchedulingLogic.values).toEqual(DEFAULT_VALUES);
+ });
+
+ describe('reducers', () => {
+ describe('hasChanges', () => {
+ it('should set false on apiSuccess', () => {
+ ConnectorSchedulingLogic.actions.setHasChanges(true);
+ UpdateConnectorSchedulingApiLogic.actions.apiSuccess({
+ enabled: false,
+ interval: '',
+ });
+ expect(ConnectorSchedulingLogic.values).toEqual({
+ ...DEFAULT_VALUES,
+ hasChanges: false,
+ });
+ });
+ it('should set hasChanges on setHasChanges', () => {
+ ConnectorSchedulingLogic.actions.setHasChanges(true);
+ expect(ConnectorSchedulingLogic.values).toEqual({
+ ...DEFAULT_VALUES,
+ hasChanges: true,
+ });
+ });
+ });
+ });
+
+ describe('actions', () => {
+ describe('makeRequest', () => {
+ it('should call clearFlashMessages', () => {
+ ConnectorSchedulingLogic.actions.makeRequest({
+ connectorId: 'id',
+ scheduling: {
+ enabled: true,
+ interval: 'interval',
+ },
+ });
+ expect(clearFlashMessages).toHaveBeenCalled();
+ });
+ });
+ describe('apiError', () => {
+ it('should call flashAPIError', () => {
+ ConnectorSchedulingLogic.actions.apiError('error' as any);
+ expect(flashAPIErrors).toHaveBeenCalledWith('error');
+ });
+ });
+ describe('apiSuccess', () => {
+ it('should call flashAPIError', () => {
+ ConnectorSchedulingLogic.actions.apiSuccess('success' as any);
+ expect(flashSuccessToast).toHaveBeenCalledWith('Scheduling successfully updated');
+ });
+ });
+ });
+});
diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/connector/connector_scheduling_logic.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/connector/connector_scheduling_logic.ts
new file mode 100644
index 00000000000000..6ab8072d27ecf4
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/connector/connector_scheduling_logic.ts
@@ -0,0 +1,63 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { kea, MakeLogicType } from 'kea';
+
+import { i18n } from '@kbn/i18n';
+
+import { Actions } from '../../../../shared/api_logic/create_api_logic';
+import {
+ clearFlashMessages,
+ flashAPIErrors,
+ flashSuccessToast,
+} from '../../../../shared/flash_messages';
+
+import {
+ UpdateConnectorSchedulingApiLogic,
+ UpdateConnectorSchedulingArgs,
+} from '../../../api/connector_package/update_connector_scheduling_api_logic';
+import { ConnectorScheduling } from '../../../api/index/fetch_index_api_logic';
+
+type ConnectorSchedulingActions = Pick<
+ Actions,
+ 'apiError' | 'apiSuccess' | 'makeRequest'
+> & { setHasChanges: (hasChanges: boolean) => { hasChanges: boolean } };
+
+interface ConnectorSchedulingValues {
+ hasChanges: boolean;
+}
+
+export const ConnectorSchedulingLogic = kea<
+ MakeLogicType
+>({
+ actions: {
+ setHasChanges: (hasChanges) => ({ hasChanges }),
+ },
+ connect: {
+ actions: [UpdateConnectorSchedulingApiLogic, ['apiError', 'apiSuccess', 'makeRequest']],
+ },
+ listeners: {
+ apiError: (error) => flashAPIErrors(error),
+ apiSuccess: () =>
+ flashSuccessToast(
+ i18n.translate(
+ 'xpack.enterpriseSearch.content.indices.configurationConnector.scheduling.successToast.title',
+ { defaultMessage: 'Scheduling successfully updated' }
+ )
+ ),
+ makeRequest: () => clearFlashMessages(),
+ },
+ reducers: {
+ hasChanges: [
+ false,
+ {
+ apiSuccess: () => false,
+ setHasChanges: (_, { hasChanges }) => hasChanges,
+ },
+ ],
+ },
+});
diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/scheduling.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/scheduling.tsx
deleted file mode 100644
index f9d9f1a0381d8c..00000000000000
--- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/scheduling.tsx
+++ /dev/null
@@ -1,14 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import React from 'react';
-
-export const SearchIndexScheduling: React.FC = () => {
- // TODO If index && !index.connector then do something
-
- return <>Scheduling>;
-};
diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/search_index.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/search_index.tsx
index 300792ea129082..2963aa4b40520f 100644
--- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/search_index.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/search_index.tsx
@@ -26,11 +26,11 @@ import { EnterpriseSearchContentPageTemplate } from '../layout/page_template';
import { baseBreadcrumbs } from '../search_indices';
import { ConnectorConfiguration } from './connector/connector_configuration';
+import { ConnectorSchedulingComponent } from './connector/connector_scheduling';
import { SearchIndexDocuments } from './documents';
import { SearchIndexDomainManagement } from './domain_management';
import { SearchIndexIndexMappings } from './index_mappings';
import { SearchIndexOverview } from './overview';
-import { SearchIndexScheduling } from './scheduling';
export enum SearchIndexTabId {
// all indices
@@ -90,7 +90,7 @@ export const SearchIndex: React.FC = () => {
}),
},
{
- content: ,
+ content: ,
id: SearchIndexTabId.SCHEDULING,
name: i18n.translate('xpack.enterpriseSearch.content.searchIndex.schedulingTabLabel', {
defaultMessage: 'Scheduling',
diff --git a/x-pack/plugins/enterprise_search/server/lib/connectors/update_connector_scheduling.test.ts b/x-pack/plugins/enterprise_search/server/lib/connectors/update_connector_scheduling.test.ts
new file mode 100644
index 00000000000000..41116505086009
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/server/lib/connectors/update_connector_scheduling.test.ts
@@ -0,0 +1,87 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { IScopedClusterClient } from '@kbn/core/server';
+
+import { CONNECTORS_INDEX } from '../..';
+
+import { updateConnectorScheduling } from './update_connector_scheduling';
+
+describe('addConnector lib function', () => {
+ const mockClient = {
+ asCurrentUser: {
+ get: jest.fn(),
+ index: jest.fn(),
+ },
+ asInternalUser: {},
+ };
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('should update connector scheduling', async () => {
+ mockClient.asCurrentUser.get.mockImplementationOnce(() => {
+ return Promise.resolve({
+ _source: {
+ api_key_id: null,
+ configuration: {},
+ created_at: null,
+ index_name: 'index_name',
+ last_seen: null,
+ last_synced: null,
+ scheduling: { enabled: true, interval: '1 2 3 4 5' },
+ service_type: null,
+ status: 'not connected',
+ sync_error: null,
+ sync_now: false,
+ sync_status: null,
+ },
+ index: CONNECTORS_INDEX,
+ });
+ });
+ mockClient.asCurrentUser.index.mockImplementation(() => ({ _id: 'fakeId' }));
+
+ await expect(
+ updateConnectorScheduling(mockClient as unknown as IScopedClusterClient, 'connectorId', {
+ enabled: true,
+ interval: '1 2 3 4 5',
+ })
+ ).resolves.toEqual({ _id: 'fakeId' });
+ expect(mockClient.asCurrentUser.index).toHaveBeenCalledWith({
+ document: {
+ api_key_id: null,
+ configuration: {},
+ created_at: null,
+ index_name: 'index_name',
+ last_seen: null,
+ last_synced: null,
+ scheduling: { enabled: true, interval: '1 2 3 4 5' },
+ service_type: null,
+ status: 'not connected',
+ sync_error: null,
+ sync_now: false,
+ sync_status: null,
+ },
+ id: 'connectorId',
+ index: CONNECTORS_INDEX,
+ });
+ });
+
+ it('should not create index if there is no connector', async () => {
+ mockClient.asCurrentUser.get.mockImplementationOnce(() => {
+ return Promise.resolve({});
+ });
+ await expect(
+ updateConnectorScheduling(mockClient as unknown as IScopedClusterClient, 'connectorId', {
+ enabled: true,
+ interval: '1 2 3 4 5',
+ })
+ ).rejects.toEqual(new Error('Could not find document'));
+ expect(mockClient.asCurrentUser.index).not.toHaveBeenCalled();
+ });
+});
diff --git a/x-pack/plugins/enterprise_search/server/lib/connectors/update_connector_scheduling.ts b/x-pack/plugins/enterprise_search/server/lib/connectors/update_connector_scheduling.ts
new file mode 100644
index 00000000000000..4fde352d4030e9
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/server/lib/connectors/update_connector_scheduling.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
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { IScopedClusterClient } from '@kbn/core/server';
+import { i18n } from '@kbn/i18n';
+
+import { CONNECTORS_INDEX } from '../..';
+
+import { Connector, ConnectorScheduling } from '../../types/connector';
+
+export const updateConnectorScheduling = async (
+ client: IScopedClusterClient,
+ connectorId: string,
+ scheduling: ConnectorScheduling
+) => {
+ const connectorResult = await client.asCurrentUser.get({
+ id: connectorId,
+ index: CONNECTORS_INDEX,
+ });
+ const connector = connectorResult._source;
+ if (connector) {
+ return await client.asCurrentUser.index({
+ document: { ...connector, scheduling },
+ id: connectorId,
+ index: CONNECTORS_INDEX,
+ });
+ } else {
+ throw new Error(
+ i18n.translate('xpack.enterpriseSearch.server.connectors.scheduling.error', {
+ defaultMessage: 'Could not find document',
+ })
+ );
+ }
+};
diff --git a/x-pack/plugins/enterprise_search/server/routes/enterprise_search/connectors.ts b/x-pack/plugins/enterprise_search/server/routes/enterprise_search/connectors.ts
index 4851fd8cad4d56..9e6337211c5e3b 100644
--- a/x-pack/plugins/enterprise_search/server/routes/enterprise_search/connectors.ts
+++ b/x-pack/plugins/enterprise_search/server/routes/enterprise_search/connectors.ts
@@ -10,6 +10,7 @@ import { i18n } from '@kbn/i18n';
import { addConnector } from '../../lib/connectors/add_connector';
import { updateConnectorConfiguration } from '../../lib/connectors/update_connector_configuration';
+import { updateConnectorScheduling } from '../../lib/connectors/update_connector_scheduling';
import { RouteDependencies } from '../../plugin';
@@ -40,21 +41,46 @@ export function registerConnectorRoutes({ router }: RouteDependencies) {
);
router.post(
{
- path: '/internal/enterprise_search/connectors/{indexId}/configuration',
+ path: '/internal/enterprise_search/connectors/{connectorId}/configuration',
validate: {
body: schema.recordOf(
schema.string(),
schema.object({ label: schema.string(), value: schema.nullable(schema.string()) })
),
params: schema.object({
- indexId: schema.string(),
+ connectorId: schema.string(),
}),
},
},
async (context, request, response) => {
const { client } = (await context.core).elasticsearch;
try {
- await updateConnectorConfiguration(client, request.params.indexId, request.body);
+ await updateConnectorConfiguration(client, request.params.connectorId, request.body);
+ return response.ok();
+ } catch (error) {
+ return response.customError({
+ body: i18n.translate('xpack.enterpriseSearch.server.routes.updateConnector.error', {
+ defaultMessage: 'Error fetching data from Enterprise Search',
+ }),
+ statusCode: 502,
+ });
+ }
+ }
+ );
+ router.post(
+ {
+ path: '/internal/enterprise_search/connectors/{connectorId}/scheduling',
+ validate: {
+ body: schema.object({ enabled: schema.boolean(), interval: schema.string() }),
+ params: schema.object({
+ connectorId: schema.string(),
+ }),
+ },
+ },
+ async (context, request, response) => {
+ const { client } = (await context.core).elasticsearch;
+ try {
+ await updateConnectorScheduling(client, request.params.connectorId, request.body);
return response.ok();
} catch (error) {
return response.customError({
diff --git a/x-pack/plugins/enterprise_search/server/types/connector.ts b/x-pack/plugins/enterprise_search/server/types/connector.ts
index fbae385b702556..38685419938e21 100644
--- a/x-pack/plugins/enterprise_search/server/types/connector.ts
+++ b/x-pack/plugins/enterprise_search/server/types/connector.ts
@@ -11,6 +11,10 @@ export interface KeyValuePair {
}
export type ConnectorConfiguration = Record;
+export interface ConnectorScheduling {
+ enabled: boolean;
+ interval: string;
+}
export interface Connector {
api_key_id: string | null;
@@ -19,10 +23,7 @@ export interface Connector {
index_name: string;
last_seen: string | null;
last_synced: string | null;
- scheduling: {
- enabled: boolean;
- interval: string | null; // crontab syntax
- };
+ scheduling: ConnectorScheduling;
service_type: string | null;
status: string | null;
sync_error: string | null;
diff --git a/x-pack/plugins/observability/public/config/paths.ts b/x-pack/plugins/observability/public/config/paths.ts
index 7f6599ef3c4835..5b5055afdf8796 100644
--- a/x-pack/plugins/observability/public/config/paths.ts
+++ b/x-pack/plugins/observability/public/config/paths.ts
@@ -12,7 +12,8 @@ export const paths = {
observability: {
alerts: ALERT_PAGE_LINK,
rules: RULES_PAGE_LINK,
- ruleDetails: (ruleId: string) => `${RULES_PAGE_LINK}/${encodeURI(ruleId)}`,
+ ruleDetails: (ruleId?: string | null) =>
+ ruleId ? `${RULES_PAGE_LINK}/${encodeURI(ruleId)}` : RULES_PAGE_LINK,
},
management: {
rules: '/app/management/insightsAndAlerting/triggersActions/rules',
diff --git a/x-pack/plugins/observability/public/pages/cases/cases.tsx b/x-pack/plugins/observability/public/pages/cases/cases.tsx
index e2d853efbf45f9..ee7af69937881b 100644
--- a/x-pack/plugins/observability/public/pages/cases/cases.tsx
+++ b/x-pack/plugins/observability/public/pages/cases/cases.tsx
@@ -7,13 +7,15 @@
import React, { Suspense, useCallback, useState } from 'react';
-import { useKibana } from '../../utils/kibana_react';
+import { useKibana } from '@kbn/kibana-react-plugin/public';
import { CASES_OWNER, CASES_PATH } from './constants';
import { usePluginContext } from '../../hooks/use_plugin_context';
import { LazyAlertsFlyout } from '../..';
import { useFetchAlertDetail } from './use_fetch_alert_detail';
import { useFetchAlertData } from './use_fetch_alert_data';
import { UseGetUserCasesPermissions } from '../../hooks/use_get_user_cases_permissions';
+import { paths } from '../../config';
+import { ObservabilityAppServices } from '../../application/types';
interface CasesProps {
permissions: UseGetUserCasesPermissions;
@@ -21,8 +23,11 @@ interface CasesProps {
export const Cases = React.memo(({ permissions }) => {
const {
cases,
- application: { getUrlForApp, navigateToApp },
- } = useKibana().services;
+ http: {
+ basePath: { prepend },
+ },
+ application: { navigateToUrl },
+ } = useKibana().services;
const { observabilityRuleTypeRegistry } = usePluginContext();
const [selectedAlertId, setSelectedAlertId] = useState('');
const casesPermissions = { all: permissions.crud, read: permissions.read };
@@ -55,17 +60,16 @@ export const Cases = React.memo(({ permissions }) => {
},
ruleDetailsNavigation: {
href: (ruleId) => {
- return getUrlForApp('management', {
- path: `/insightsAndAlerting/triggersActions/rule/${ruleId}`,
- });
+ return prepend(paths.observability.ruleDetails(ruleId));
},
onClick: async (ruleId, ev) => {
+ const ruleLink = prepend(paths.observability.ruleDetails(ruleId));
+
if (ev != null) {
ev.preventDefault();
}
- return navigateToApp('management', {
- path: `/insightsAndAlerting/triggersActions/rule/${ruleId}`,
- });
+
+ return navigateToUrl(ruleLink);
},
},
})}
diff --git a/x-pack/test/accessibility/apps/tags.ts b/x-pack/test/accessibility/apps/tags.ts
index da51f2f0535e2a..5369ad2d2db6b6 100644
--- a/x-pack/test/accessibility/apps/tags.ts
+++ b/x-pack/test/accessibility/apps/tags.ts
@@ -71,7 +71,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await a11y.testAppSnapshot();
});
- it('tag assignment panel meets a11y requirements', async () => {
+ // https://github.com/elastic/kibana/issues/135985 inconsistent test failure
+ it.skip('tag assignment panel meets a11y requirements', async () => {
await testSubjects.click('euiCollapsedItemActionsButton');
const actionOnTag = 'assign';
await PageObjects.tagManagement.clickActionItem(actionOnTag);
@@ -79,6 +80,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
});
it('tag management page with connections column populated meets a11y requirements', async () => {
+ await testSubjects.click('euiCollapsedItemActionsButton');
+ const actionOnTag = 'assign';
+ await PageObjects.tagManagement.clickActionItem(actionOnTag);
await testSubjects.click('assignFlyout-selectAllButton');
await testSubjects.click('assignFlyoutConfirmButton');
diff --git a/x-pack/test/apm_api_integration/tests/infrastructure/generate_data.ts b/x-pack/test/apm_api_integration/tests/infrastructure/generate_data.ts
new file mode 100644
index 00000000000000..b673e541e756b2
--- /dev/null
+++ b/x-pack/test/apm_api_integration/tests/infrastructure/generate_data.ts
@@ -0,0 +1,51 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+import { apm, timerange } from '@elastic/apm-synthtrace';
+import type { ApmSynthtraceEsClient } from '@elastic/apm-synthtrace';
+
+export async function generateData({
+ synthtraceEsClient,
+ start,
+ end,
+}: {
+ synthtraceEsClient: ApmSynthtraceEsClient;
+ start: number;
+ end: number;
+}) {
+ const serviceRunsInContainerInstance = apm
+ .service('synth-go', 'production', 'go')
+ .instance('instance-a');
+
+ const serviceInstance = apm.service('synth-java', 'production', 'java').instance('instance-b');
+
+ await synthtraceEsClient.index(
+ timerange(start, end)
+ .interval('1m')
+ .generator((timestamp) => {
+ return [
+ serviceRunsInContainerInstance
+ .transaction('GET /apple 🍎')
+ .defaults({
+ 'container.id': 'foo',
+ 'host.hostname': 'bar',
+ 'kubernetes.pod.name': 'baz',
+ })
+ .timestamp(timestamp)
+ .duration(1000)
+ .success(),
+ serviceInstance
+ .transaction('GET /banana 🍌')
+ .defaults({
+ 'host.hostname': 'bar',
+ })
+ .timestamp(timestamp)
+ .duration(1000)
+ .success(),
+ ];
+ })
+ );
+}
diff --git a/x-pack/test/apm_api_integration/tests/infrastructure/infrastructure_attributes.spec.ts b/x-pack/test/apm_api_integration/tests/infrastructure/infrastructure_attributes.spec.ts
new file mode 100644
index 00000000000000..39a45a9b396c71
--- /dev/null
+++ b/x-pack/test/apm_api_integration/tests/infrastructure/infrastructure_attributes.spec.ts
@@ -0,0 +1,86 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+import expect from '@kbn/expect';
+import { FtrProviderContext } from '../../common/ftr_provider_context';
+import { generateData } from './generate_data';
+
+export default function ApiTest({ getService }: FtrProviderContext) {
+ const registry = getService('registry');
+ const apmApiClient = getService('apmApiClient');
+ const synthtraceEsClient = getService('synthtraceEsClient');
+
+ const start = new Date('2021-01-01T00:00:00.000Z').getTime();
+ const end = new Date('2021-01-01T00:15:00.000Z').getTime() - 1;
+
+ async function callApi(serviceName: string) {
+ const response = await apmApiClient.readUser({
+ endpoint: 'GET /internal/apm/services/{serviceName}/infrastructure_attributes',
+ params: {
+ path: {
+ serviceName,
+ },
+ query: {
+ start: new Date(start).toISOString(),
+ end: new Date(end).toISOString(),
+ environment: 'ENVIRONMENT_ALL',
+ kuery: '',
+ },
+ },
+ });
+ return response;
+ }
+
+ registry.when(
+ 'Infrastructure attributes when data is not loaded',
+ { config: 'basic', archives: [] },
+ () => {
+ it('handles the empty state', async () => {
+ const response = await callApi('synth-go');
+ expect(response.status).to.be(200);
+ expect(response.body.containerIds.length).to.be(0);
+ expect(response.body.hostNames.length).to.be(0);
+ expect(response.body.podNames.length).to.be(0);
+ });
+ }
+ );
+
+ registry.when(
+ 'Infrastructure attributes',
+ { config: 'basic', archives: ['apm_mappings_only_8.0.0'] },
+ () => {
+ describe('when data is loaded', () => {
+ beforeEach(async () => {
+ await generateData({ start, end, synthtraceEsClient });
+ });
+
+ afterEach(() => synthtraceEsClient.clean());
+
+ describe('when service runs in container', () => {
+ it('returns arrays of container ids and pod names', async () => {
+ const response = await callApi('synth-go');
+ expect(response.status).to.be(200);
+ expect(response.body.containerIds.length).to.be(1);
+ // hostNames is always returning empty
+ // we can not test the infra indices api call with synthtrace
+ expect(response.body.hostNames.length).to.be(0);
+ expect(response.body.podNames.length).to.be(1);
+ });
+ });
+
+ describe('when service does NOT run in container', () => {
+ it('returns array of host names', async () => {
+ const response = await callApi('synth-java');
+ expect(response.status).to.be(200);
+ expect(response.body.containerIds.length).to.be(0);
+ expect(response.body.hostNames.length).to.be(1);
+ expect(response.body.podNames.length).to.be(0);
+ });
+ });
+ });
+ }
+ );
+}
diff --git a/x-pack/test/functional/services/cases/navigation.ts b/x-pack/test/functional/services/cases/navigation.ts
index 4aca20c01aaf18..8af4d581d93cd8 100644
--- a/x-pack/test/functional/services/cases/navigation.ts
+++ b/x-pack/test/functional/services/cases/navigation.ts
@@ -12,9 +12,9 @@ export function CasesNavigationProvider({ getPageObject, getService }: FtrProvid
const testSubjects = getService('testSubjects');
return {
- async navigateToApp() {
- await common.navigateToApp('cases');
- await testSubjects.existOrFail('cases-app', { timeout: 2000 });
+ async navigateToApp(app: string = 'cases', appSelector: string = 'cases-app') {
+ await common.navigateToApp(app);
+ await testSubjects.existOrFail(appSelector, { timeout: 2000 });
},
async navigateToConfigurationPage() {
diff --git a/x-pack/test/functional/services/observability/alerts/add_to_case.ts b/x-pack/test/functional/services/observability/alerts/add_to_case.ts
index eac7541e15585c..7211325fad2a1c 100644
--- a/x-pack/test/functional/services/observability/alerts/add_to_case.ts
+++ b/x-pack/test/functional/services/observability/alerts/add_to_case.ts
@@ -55,7 +55,7 @@ export function ObservabilityAlertsAddToCaseProvider({ getService }: FtrProvider
return await (await testSubjects.find('euiFlyoutCloseButton')).click();
};
- const getAddtoExistingCaseModalOrFail = async () => {
+ const getAddToExistingCaseModalOrFail = async () => {
return await testSubjects.existOrFail(SELECT_CASE_MODAL);
};
@@ -70,6 +70,6 @@ export function ObservabilityAlertsAddToCaseProvider({ getService }: FtrProvider
closeFlyout,
addToNewCaseButtonClick,
addToExistingCaseButtonClick,
- getAddtoExistingCaseModalOrFail,
+ getAddToExistingCaseModalOrFail,
};
}
diff --git a/x-pack/test/functional/services/observability/index.ts b/x-pack/test/functional/services/observability/index.ts
index 0d167ae5d516e2..8990641cb524b1 100644
--- a/x-pack/test/functional/services/observability/index.ts
+++ b/x-pack/test/functional/services/observability/index.ts
@@ -10,11 +10,11 @@ import { ObservabilityUsersProvider } from './users';
import { ObservabilityAlertsProvider } from './alerts';
export function ObservabilityProvider(context: FtrProviderContext) {
- const users = ObservabilityUsersProvider(context);
const alerts = ObservabilityAlertsProvider(context);
+ const users = ObservabilityUsersProvider(context);
return {
- users,
alerts,
+ users,
};
}
diff --git a/x-pack/test/observability_functional/apps/observability/index.ts b/x-pack/test/observability_functional/apps/observability/index.ts
index c4ad070c459813..1fbe8f5f975e5f 100644
--- a/x-pack/test/observability_functional/apps/observability/index.ts
+++ b/x-pack/test/observability_functional/apps/observability/index.ts
@@ -10,6 +10,7 @@ import { FtrProviderContext } from '../../ftr_provider_context';
export default function ({ loadTestFile }: FtrProviderContext) {
describe('ObservabilityApp', function () {
loadTestFile(require.resolve('./pages/alerts'));
+ loadTestFile(require.resolve('./pages/cases/case_details'));
loadTestFile(require.resolve('./pages/alerts/add_to_case'));
loadTestFile(require.resolve('./pages/alerts/alert_status'));
loadTestFile(require.resolve('./pages/alerts/pagination'));
diff --git a/x-pack/test/observability_functional/apps/observability/pages/alerts/add_to_case.ts b/x-pack/test/observability_functional/apps/observability/pages/alerts/add_to_case.ts
index 918133ca53dfc8..c444638140ec2c 100644
--- a/x-pack/test/observability_functional/apps/observability/pages/alerts/add_to_case.ts
+++ b/x-pack/test/observability_functional/apps/observability/pages/alerts/add_to_case.ts
@@ -62,7 +62,7 @@ export default ({ getService, getPageObjects }: FtrProviderContext) => {
await retry.try(async () => {
await observability.alerts.addToCase.addToExistingCaseButtonClick();
- await observability.alerts.addToCase.getAddtoExistingCaseModalOrFail();
+ await observability.alerts.addToCase.getAddToExistingCaseModalOrFail();
});
});
});
diff --git a/x-pack/test/observability_functional/apps/observability/pages/cases/case_details.ts b/x-pack/test/observability_functional/apps/observability/pages/cases/case_details.ts
new file mode 100644
index 00000000000000..340131c14b6a13
--- /dev/null
+++ b/x-pack/test/observability_functional/apps/observability/pages/cases/case_details.ts
@@ -0,0 +1,74 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import expect from '@kbn/expect';
+import { CommentType } from '@kbn/cases-plugin/common/api';
+import { FtrProviderContext } from '../../../../ftr_provider_context';
+
+export default ({ getPageObjects, getService }: FtrProviderContext) => {
+ const browser = getService('browser');
+ const esArchiver = getService('esArchiver');
+ const cases = getService('cases');
+ const observability = getService('observability');
+ const find = getService('find');
+ const PageObjects = getPageObjects(['common', 'header']);
+
+ describe('Cases', () => {
+ before(async () => {
+ await esArchiver.load('x-pack/test/functional/es_archives/observability/alerts');
+ await esArchiver.load('x-pack/test/functional/es_archives/infra/metrics_and_logs');
+ });
+
+ after(async () => {
+ await esArchiver.unload('x-pack/test/functional/es_archives/infra/metrics_and_logs');
+ await esArchiver.unload('x-pack/test/functional/es_archives/observability/alerts');
+ });
+
+ describe('Case detail rule link', () => {
+ before(async () => {
+ await observability.users.setTestUserRole(
+ observability.users.defineBasicObservabilityRole({
+ observabilityCases: ['all'],
+ logs: ['all'],
+ })
+ );
+
+ const owner = 'observability';
+ const caseData = await cases.api.createCase({
+ title: 'Sample case',
+ owner,
+ });
+ await cases.api.createAttachment({
+ caseId: caseData.id,
+ params: {
+ alertId: ['alert-id'],
+ index: ['.internal.alerts-observability.alerts-default-000001'],
+ rule: { id: 'rule-id', name: 'My rule name' },
+ type: CommentType.alert,
+ owner,
+ },
+ });
+ });
+
+ after(async () => {
+ await cases.api.deleteAllCases();
+ });
+
+ it('should link to observability rule pages in case details', async () => {
+ await cases.navigation.navigateToApp('observabilityCases', 'cases-all-title');
+ await cases.casesTable.waitForCasesToBeListed();
+ await cases.casesTable.goToFirstListedCase();
+ await PageObjects.header.waitUntilLoadingHasFinished();
+
+ await (await find.byCssSelector('[data-test-subj*="alert-rule-link"]')).click();
+
+ const url = await browser.getCurrentUrl();
+ expect(url.includes('/app/observability/alerts/rules')).to.be(true);
+ });
+ });
+ });
+};