diff --git a/src/platform/packages/shared/dashboards/dashboards-selector/components/dashboards_selector.test.tsx b/src/platform/packages/shared/dashboards/dashboards-selector/components/dashboards_selector.test.tsx
index c1ec883a70290..4a9d5657cd27f 100644
--- a/src/platform/packages/shared/dashboards/dashboards-selector/components/dashboards_selector.test.tsx
+++ b/src/platform/packages/shared/dashboards/dashboards-selector/components/dashboards_selector.test.tsx
@@ -10,7 +10,7 @@
import React from 'react';
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
import { DashboardsSelector } from './dashboards_selector';
-import { contentManagementMock } from '@kbn/content-management-plugin/public/mocks';
+import type { UiActionsStart } from '@kbn/ui-actions-plugin/public';
import userEvent from '@testing-library/user-event';
const MOCK_FIRST_DASHBOARD_ID = 'dashboard-1';
@@ -20,57 +20,99 @@ const MOCK_SECOND_DASHBOARD_TITLE = 'Second Dashboard';
const MOCK_PLACEHOLDER = 'Select a dashboard';
const MOCK_FIRST_DASHBOARD = {
- status: 'success',
id: MOCK_FIRST_DASHBOARD_ID,
- attributes: { title: MOCK_FIRST_DASHBOARD_TITLE },
- references: [],
+ isManaged: false,
+ title: MOCK_FIRST_DASHBOARD_TITLE,
};
const MOCK_SECOND_DASHBOARD = {
- status: 'success',
id: MOCK_SECOND_DASHBOARD_ID,
- attributes: { title: MOCK_SECOND_DASHBOARD_TITLE },
- references: [],
+ isManaged: false,
+ title: MOCK_SECOND_DASHBOARD_TITLE,
};
-const mockFetchDashboard = jest.fn();
-const mockFetchDashboards = jest
- .fn()
- .mockResolvedValue([MOCK_FIRST_DASHBOARD, MOCK_SECOND_DASHBOARD]);
+const mockSearchExecute = jest.fn((context: any) => {
+ if (context.onResults) {
+ context.onResults([MOCK_FIRST_DASHBOARD, MOCK_SECOND_DASHBOARD]);
+ }
+});
+
+const mockGetByIdExecute = jest.fn((context: any) => {
+ if (context.onResults && context.ids) {
+ const requestedDashboards = context.ids
+ .map((id: string) => {
+ if (id === MOCK_FIRST_DASHBOARD_ID) return MOCK_FIRST_DASHBOARD;
+ if (id === MOCK_SECOND_DASHBOARD_ID) return MOCK_SECOND_DASHBOARD;
+ return null;
+ })
+ .filter(Boolean);
+ context.onResults(requestedDashboards);
+ }
+});
-// Mock the dashboard service
-jest.mock('../services/dashboard_service', () => ({
- dashboardServiceProvider: jest.fn(() => ({
- fetchDashboards: (options: { limit: number; text: string }) => mockFetchDashboards(options),
- fetchDashboard: (id: string) => mockFetchDashboard(id),
- })),
-}));
+const mockSearchAction = {
+ execute: mockSearchExecute,
+};
+
+const mockGetDashboardsByIdsAction = {
+ execute: mockGetByIdExecute,
+};
+
+const mockGetAction = jest.fn((actionId: string) => {
+ if (actionId === 'getDashboardsByIdsAction') {
+ return Promise.resolve(mockGetDashboardsByIdsAction);
+ }
+ return Promise.resolve(mockSearchAction);
+});
const mockOnChange = jest.fn();
describe('DashboardsSelector', () => {
beforeEach(() => {
- mockFetchDashboard.mockResolvedValueOnce(MOCK_FIRST_DASHBOARD);
- mockFetchDashboard.mockResolvedValueOnce(MOCK_SECOND_DASHBOARD);
+ jest.clearAllMocks();
+ mockGetAction.mockImplementation((actionId: string) => {
+ if (actionId === 'getDashboardsByIdsAction') {
+ return Promise.resolve(mockGetDashboardsByIdsAction);
+ }
+ return Promise.resolve(mockSearchAction);
+ });
+ mockSearchExecute.mockImplementation((context: any) => {
+ if (context.onResults) {
+ context.onResults([MOCK_FIRST_DASHBOARD, MOCK_SECOND_DASHBOARD]);
+ }
+ });
+ mockGetByIdExecute.mockImplementation((context: any) => {
+ if (context.onResults && context.ids) {
+ const requestedDashboards = context.ids
+ .map((id: string) => {
+ if (id === MOCK_FIRST_DASHBOARD_ID) return MOCK_FIRST_DASHBOARD;
+ if (id === MOCK_SECOND_DASHBOARD_ID) return MOCK_SECOND_DASHBOARD;
+ return null;
+ })
+ .filter(Boolean);
+ context.onResults(requestedDashboards);
+ }
+ });
});
afterEach(() => {
jest.clearAllMocks();
});
- const contentManagement = contentManagementMock.createStartContract();
+ const mockUiActions = {
+ getAction: mockGetAction,
+ } as unknown as UiActionsStart;
it('renders the component', () => {
render(
);
- // Check that the component renders with the placeholder text
expect(screen.getByTestId('dashboardsSelector')).toBeInTheDocument();
expect(screen.getByPlaceholderText(MOCK_PLACEHOLDER)).toBeInTheDocument();
});
@@ -78,28 +120,25 @@ describe('DashboardsSelector', () => {
it('displays selected dashboard titles from dashboardsFormData', async () => {
render(
);
- // Wait for the dashboard titles to be fetched and displayed
await waitFor(() => {
expect(screen.getByText(MOCK_FIRST_DASHBOARD_TITLE)).toBeInTheDocument();
expect(screen.getByText(MOCK_SECOND_DASHBOARD_TITLE)).toBeInTheDocument();
});
- // Verify that fetchDashboard was called for each dashboard ID
- expect(mockFetchDashboard).toHaveBeenCalledWith(MOCK_FIRST_DASHBOARD_ID);
- expect(mockFetchDashboard).toHaveBeenCalledWith(MOCK_SECOND_DASHBOARD_ID);
+ expect(mockGetAction).toHaveBeenCalledWith('getDashboardsByIdsAction');
});
it('debounces and triggers dashboard search with user input in the ComboBox', async () => {
render(
{
const searchInput = screen.getByPlaceholderText(MOCK_PLACEHOLDER);
await userEvent.type(searchInput, MOCK_FIRST_DASHBOARD_TITLE);
- // Assert that fetchDashboards was called with the correct search value
- // Wait for the next tick to allow state update and effect to run
await waitFor(() => {
expect(searchInput).toHaveValue(MOCK_FIRST_DASHBOARD_TITLE);
- expect(mockFetchDashboards).toHaveBeenCalledWith(
- expect.objectContaining({ limit: 100, text: `${MOCK_FIRST_DASHBOARD_TITLE}*` })
+ expect(mockSearchExecute).toHaveBeenCalledWith(
+ expect.objectContaining({
+ search: {
+ search: MOCK_FIRST_DASHBOARD_TITLE,
+ per_page: 100,
+ },
+ trigger: { id: 'searchDashboards' },
+ })
);
expect(screen.getByText(MOCK_FIRST_DASHBOARD_TITLE)).toBeInTheDocument();
@@ -125,7 +168,7 @@ describe('DashboardsSelector', () => {
it('fetches dashboard list when combobox is focused', async () => {
render(
{
fireEvent.focus(searchInput);
await waitFor(() => {
- expect(mockFetchDashboards).toHaveBeenCalledWith(expect.objectContaining({ limit: 100 }));
+ expect(mockSearchExecute).toHaveBeenCalledWith(
+ expect.objectContaining({
+ search: {
+ search: '',
+ per_page: 100,
+ },
+ })
+ );
});
});
it('does not fetch dashboard list when combobox is not focused', async () => {
render(
);
- expect(mockFetchDashboards).not.toHaveBeenCalled();
+ expect(mockSearchExecute).not.toHaveBeenCalled();
});
it('dispatches selected dashboards on change', async () => {
render(
);
- // Click on the combobox to open it
const searchInput = screen.getByPlaceholderText(MOCK_PLACEHOLDER);
fireEvent.focus(searchInput);
- // Wait for the dropdown to open and options to load
await waitFor(() => {
- expect(mockFetchDashboards).toHaveBeenCalledWith(
- expect.objectContaining({ limit: 100, text: '*' })
+ expect(mockSearchExecute).toHaveBeenCalled();
+ expect(mockSearchExecute).toHaveBeenLastCalledWith(
+ expect.objectContaining({
+ search: {
+ search: '',
+ per_page: 100,
+ },
+ })
);
});
- // Wait for the second dashboard option to appear in the dropdown
await waitFor(() => {
expect(screen.getByText(MOCK_SECOND_DASHBOARD_TITLE)).toBeInTheDocument();
});
- // Click on the second dashboard option to select it
await userEvent.click(screen.getByText(MOCK_SECOND_DASHBOARD_TITLE));
- // Verify that the onChange callback was called with both dashboards
await waitFor(() => {
expect(mockOnChange).toHaveBeenCalledWith([
{ label: MOCK_FIRST_DASHBOARD_TITLE, value: MOCK_FIRST_DASHBOARD_ID },
@@ -189,7 +240,6 @@ describe('DashboardsSelector', () => {
]);
});
- // Verify that both selected options are now displayed
expect(screen.getByText(MOCK_FIRST_DASHBOARD_TITLE)).toBeInTheDocument();
expect(screen.getByText(MOCK_SECOND_DASHBOARD_TITLE)).toBeInTheDocument();
});
diff --git a/src/platform/packages/shared/dashboards/dashboards-selector/components/dashboards_selector.tsx b/src/platform/packages/shared/dashboards/dashboards-selector/components/dashboards_selector.tsx
index 35d6b78d7096f..e8ca061018232 100644
--- a/src/platform/packages/shared/dashboards/dashboards-selector/components/dashboards_selector.tsx
+++ b/src/platform/packages/shared/dashboards/dashboards-selector/components/dashboards_selector.tsx
@@ -7,25 +7,66 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { i18n } from '@kbn/i18n';
-import type { ContentManagementPublicStart } from '@kbn/content-management-plugin/public';
+import type { UiActionsStart, ActionExecutionContext } from '@kbn/ui-actions-plugin/public';
import type { EuiComboBoxOptionOption } from '@elastic/eui';
import { EuiComboBox } from '@elastic/eui';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { debounce } from 'lodash';
-import { dashboardServiceProvider, type DashboardItem } from '../services/dashboard_service';
interface DashboardOption {
value: string;
label: string;
}
+interface Dashboard {
+ id: string;
+ isManaged: boolean;
+ title: string;
+}
+
+async function searchDashboards(
+ uiActions: UiActionsStart,
+ options: { search?: string; perPage?: number } = {}
+): Promise {
+ const { search, perPage = 100 } = options;
+ const searchAction = await uiActions.getAction('searchDashboardAction');
+ return new Promise(function (resolve) {
+ searchAction.execute({
+ onResults(dashboards: Dashboard[]) {
+ resolve(dashboards);
+ },
+ search: {
+ search,
+ per_page: perPage,
+ },
+ trigger: { id: 'searchDashboards' },
+ } as ActionExecutionContext);
+ });
+}
+
+async function getDashboardsById(uiActions: UiActionsStart, ids: string[]): Promise {
+ if (!ids.length) {
+ return [];
+ }
+ const getDashboardsByIdsAction = await uiActions.getAction('getDashboardsByIdsAction');
+ return new Promise(function (resolve) {
+ getDashboardsByIdsAction.execute({
+ onResults(dashboards: Dashboard[]) {
+ resolve(dashboards);
+ },
+ ids,
+ trigger: { id: 'getDashboardsById' },
+ } as ActionExecutionContext);
+ });
+}
+
export function DashboardsSelector({
- contentManagement,
+ uiActions,
dashboardsFormData,
onChange,
placeholder,
}: {
- contentManagement: ContentManagementPublicStart;
+ uiActions: UiActionsStart;
dashboardsFormData: { id: string }[];
onChange: (selectedOptions: Array>) => void;
placeholder?: string;
@@ -41,50 +82,25 @@ export function DashboardsSelector({
const [isComboBoxOpen, setIsComboBoxOpen] = useState(false);
const fetchDashboardTitles = useCallback(async () => {
- if (!dashboardsFormData?.length || !contentManagement) {
+ if (!dashboardsFormData?.length) {
return;
}
try {
- const dashboardPromises = dashboardsFormData.map(async (dashboard) => {
- try {
- const fetchedDashboard = await dashboardServiceProvider(contentManagement).fetchDashboard(
- dashboard.id
- );
-
- // Only return the dashboard if it exists, fetch was successful, and has a title
- if (
- fetchedDashboard &&
- fetchedDashboard.status === 'success' &&
- fetchedDashboard.attributes?.title
- ) {
- return {
- label: fetchedDashboard.attributes.title,
- value: dashboard.id,
- };
- }
- // Return null if dashboard doesn't have required data
- return null;
- } catch (dashboardError) {
- /**
- * Swallow the error that is thrown, since this just means the selected dashboard was deleted
- * Return null when dashboard fetch fails
- */
- return null;
- }
- });
-
- const results = await Promise.all(dashboardPromises);
+ const dashboardIds = dashboardsFormData.map((dashboard) => dashboard.id);
+ const dashboards = await getDashboardsById(uiActions, dashboardIds);
- // Filter out null results and cast to the expected type
- const validDashboards = results.filter(Boolean) as Array>;
+ const validDashboards = dashboards.map((dashboard) => ({
+ label: dashboard.title,
+ value: dashboard.id,
+ }));
setSelectedDashboards(validDashboards);
} catch (error) {
// Set empty array or handle the error appropriately
setSelectedDashboards([]);
}
- }, [dashboardsFormData, contentManagement]);
+ }, [dashboardsFormData, uiActions]);
useEffect(() => {
fetchDashboardTitles();
@@ -105,24 +121,25 @@ export function DashboardsSelector({
[]
);
- const getDashboardItem = (dashboard: DashboardItem) => ({
- value: dashboard.id,
- label: dashboard.attributes.title,
- });
-
const loadDashboards = useCallback(async () => {
- if (contentManagement) {
- setLoading(true);
- const dashboards = await dashboardServiceProvider(contentManagement)
- .fetchDashboards({ limit: 100, text: `${searchValue}*` })
- .catch(() => {});
- const dashboardOptions = (dashboards ?? []).map((dashboard: DashboardItem) =>
- getDashboardItem(dashboard)
- );
+ setLoading(true);
+ try {
+ const trimmedSearch = searchValue.trim();
+ const dashboards = await searchDashboards(uiActions, {
+ search: trimmedSearch,
+ perPage: 100,
+ });
+ const dashboardOptions = dashboards.map((dashboard) => ({
+ value: dashboard.id,
+ label: dashboard.title,
+ }));
setDashboardList(dashboardOptions);
+ } catch (error) {
+ setDashboardList([]);
+ } finally {
setLoading(false);
}
- }, [contentManagement, searchValue]);
+ }, [uiActions, searchValue]);
useEffect(() => {
if (isComboBoxOpen) {
diff --git a/src/platform/packages/shared/dashboards/dashboards-selector/moon.yml b/src/platform/packages/shared/dashboards/dashboards-selector/moon.yml
index e69b2da4acf0d..8098ff8c7bc7b 100644
--- a/src/platform/packages/shared/dashboards/dashboards-selector/moon.yml
+++ b/src/platform/packages/shared/dashboards/dashboards-selector/moon.yml
@@ -18,10 +18,8 @@ project:
metadata:
sourceRoot: src/platform/packages/shared/dashboards/dashboards-selector
dependsOn:
- - '@kbn/content-management-plugin'
- - '@kbn/content-management-utils'
- - '@kbn/core'
- '@kbn/i18n'
+ - '@kbn/ui-actions-plugin'
tags:
- shared-browser
- package
diff --git a/src/platform/packages/shared/dashboards/dashboards-selector/services/dashboard_service.test.ts b/src/platform/packages/shared/dashboards/dashboards-selector/services/dashboard_service.test.ts
deleted file mode 100644
index 5c1b2506988a4..0000000000000
--- a/src/platform/packages/shared/dashboards/dashboards-selector/services/dashboard_service.test.ts
+++ /dev/null
@@ -1,83 +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", the "GNU Affero General Public License v3.0 only", and the "Server Side
- * Public License v 1"; you may not use this file except in compliance with, at
- * your election, the "Elastic License 2.0", the "GNU Affero General Public
- * License v3.0 only", or the "Server Side Public License, v 1".
- */
-
-import { dashboardServiceProvider } from './dashboard_service';
-import { contentManagementMock } from '@kbn/content-management-plugin/public/mocks';
-
-describe('DashboardService', () => {
- const contentManagement = contentManagementMock.createStartContract();
- const dashboardService = dashboardServiceProvider(contentManagement);
-
- test('should fetch dashboards', async () => {
- // arrange
- const searchMock = jest.spyOn(contentManagement.client, 'search').mockResolvedValue({
- total: 0,
- hits: [],
- });
-
- const resp = await dashboardService.fetchDashboards({ text: 'test*' });
-
- expect(searchMock).toHaveBeenCalledWith({
- contentTypeId: 'dashboard',
- query: {
- text: 'test*',
- },
- options: {
- fields: ['title', 'description'],
- includeReferences: ['tag'],
- },
- });
- expect(resp).toEqual([]);
-
- searchMock.mockRestore();
- });
-
- test('should fetch dashboard by id', async () => {
- // mock get to resolve with a dashboard
- const getMock = jest.spyOn(contentManagement.client, 'get').mockResolvedValue({
- item: {
- error: null,
- attributes: {
- title: 'Dashboard 1',
- },
- references: [],
- },
- });
-
- // act
- const resp = await dashboardService.fetchDashboard('1');
-
- // assert
- expect(getMock).toHaveBeenCalledWith({ contentTypeId: 'dashboard', id: '1' });
- expect(resp).toEqual({
- status: 'success',
- id: '1',
- attributes: {
- title: 'Dashboard 1',
- },
- references: [],
- });
-
- getMock.mockRestore();
- });
-
- test('should return an error if dashboard id is not found', async () => {
- const getMock = jest.spyOn(contentManagement.client, 'get').mockRejectedValue({
- message: 'Dashboard not found',
- });
-
- const resp = await dashboardService.fetchDashboard('2');
- expect(getMock).toHaveBeenCalledWith({ contentTypeId: 'dashboard', id: '2' });
- expect(resp).toEqual({
- status: 'error',
- id: '2',
- error: 'Dashboard not found',
- });
- });
-});
diff --git a/src/platform/packages/shared/dashboards/dashboards-selector/services/dashboard_service.ts b/src/platform/packages/shared/dashboards/dashboards-selector/services/dashboard_service.ts
deleted file mode 100644
index fbe7adc7f68d9..0000000000000
--- a/src/platform/packages/shared/dashboards/dashboards-selector/services/dashboard_service.ts
+++ /dev/null
@@ -1,105 +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", the "GNU Affero General Public License v3.0 only", and the "Server Side
- * Public License v 1"; you may not use this file except in compliance with, at
- * your election, the "Elastic License 2.0", the "GNU Affero General Public
- * License v3.0 only", or the "Server Side Public License, v 1".
- */
-
-import type { SearchQuery } from '@kbn/content-management-plugin/common';
-import type { ContentManagementPublicStart } from '@kbn/content-management-plugin/public';
-import type { Reference, ContentManagementCrudTypes } from '@kbn/content-management-utils';
-import type { SavedObjectError } from '@kbn/core/public';
-import type { GetIn } from '@kbn/content-management-plugin/common';
-
-const DASHBOARD_CONTENT_TYPE_ID = 'dashboard';
-export type DashboardGetIn = GetIn;
-
-export type FindDashboardsByIdResponse = { id: string } & (
- | { status: 'success'; attributes: any; references: Reference[] }
- | { status: 'error'; error: SavedObjectError }
-);
-
-export interface DashboardItem {
- id: string;
- attributes: any; // DashboardAttributes is exported in the Dashboard plugin and this causes a cycle dependency. Get feedback on the best approach here
-}
-
-export type DashboardService = ReturnType;
-export type DashboardItems = Awaited>;
-
-export function dashboardServiceProvider(contentManagementService: ContentManagementPublicStart) {
- return {
- /**
- * Fetch dashboards
- * @param query - The query to search for dashboards
- * @returns - The dashboards that match the query
- */
- async fetchDashboards(query: SearchQuery = {}): Promise {
- const response = await contentManagementService.client.search({
- contentTypeId: 'dashboard',
- query,
- options: { fields: ['title', 'description'], includeReferences: ['tag'] },
- });
-
- // Assert the type of response to access hits property
- return (response as { hits: DashboardItem[] }).hits;
- },
- /**
- * Fetch dashboard by id
- * @param id - The id of the dashboard to fetch
- * @returns - The dashboard with the given id
- * @throws - An error if the dashboard does not exist
- */
- async fetchDashboard(id: string): Promise {
- try {
- const response = await contentManagementService.client.get<
- DashboardGetIn,
- ContentManagementCrudTypes<
- typeof DASHBOARD_CONTENT_TYPE_ID,
- any,
- object,
- object,
- object
- >['GetOut']
- >({
- contentTypeId: 'dashboard',
- id,
- });
- if (response.item.error) {
- throw response.item.error;
- }
-
- return {
- id,
- status: 'success',
- attributes: response.item.attributes,
- references: response.item.references,
- };
- } catch (error) {
- return {
- status: 'error',
- error: error.body || error.message,
- id,
- };
- }
- },
-
- async fetchDashboardsByIds(ids: string[]) {
- const findPromises = ids.map((id) => this.fetchDashboard(id));
- const results = await Promise.all(findPromises);
- return results as FindDashboardsByIdResponse[];
- },
- /**
- * Fetch only the dashboards that still exist
- * @param ids - The ids of the dashboards to fetch
- * @returns - The dashboards that exist
- */
- async fetchValidDashboards(ids: string[]) {
- const responses = await this.fetchDashboardsByIds(ids);
- const existingDashboards = responses.filter(({ status }) => status === 'success');
- return existingDashboards;
- },
- };
-}
diff --git a/src/platform/packages/shared/dashboards/dashboards-selector/tsconfig.json b/src/platform/packages/shared/dashboards/dashboards-selector/tsconfig.json
index c1af2d18f1b0a..5d239155faa79 100644
--- a/src/platform/packages/shared/dashboards/dashboards-selector/tsconfig.json
+++ b/src/platform/packages/shared/dashboards/dashboards-selector/tsconfig.json
@@ -16,9 +16,7 @@
"target/**/*"
],
"kbn_references": [
- "@kbn/content-management-plugin",
- "@kbn/content-management-utils",
- "@kbn/core",
"@kbn/i18n",
+ "@kbn/ui-actions-plugin",
]
}
diff --git a/src/platform/packages/shared/response-ops/rule_form/moon.yml b/src/platform/packages/shared/response-ops/rule_form/moon.yml
index 08bbb672a1979..2e52cb2f51ad0 100644
--- a/src/platform/packages/shared/response-ops/rule_form/moon.yml
+++ b/src/platform/packages/shared/response-ops/rule_form/moon.yml
@@ -45,6 +45,7 @@ dependsOn:
- '@kbn/content-management-plugin'
- '@kbn/dashboards-selector'
- '@kbn/react-query'
+ - '@kbn/ui-actions-plugin'
tags:
- shared-browser
- package
diff --git a/src/platform/packages/shared/response-ops/rule_form/src/rule_details/rule_dashboards.tsx b/src/platform/packages/shared/response-ops/rule_form/src/rule_details/rule_dashboards.tsx
index b441075a0b144..386356e5b6bc0 100644
--- a/src/platform/packages/shared/response-ops/rule_form/src/rule_details/rule_dashboards.tsx
+++ b/src/platform/packages/shared/response-ops/rule_form/src/rule_details/rule_dashboards.tsx
@@ -10,7 +10,7 @@
import React, { useMemo } from 'react';
import type { EuiComboBoxOptionOption } from '@elastic/eui';
import { EuiFlexGroup, EuiFlexItem, EuiFormRow, EuiSpacer } from '@elastic/eui';
-import type { ContentManagementPublicStart } from '@kbn/content-management-plugin/public';
+import type { UiActionsStart } from '@kbn/ui-actions-plugin/public';
import { DashboardsSelector } from '@kbn/dashboards-selector';
import { OptionalFieldLabel } from '../optional_field_label';
import { useRuleFormState, useRuleFormDispatch } from '../hooks';
@@ -22,10 +22,10 @@ import {
import { LabelWithTooltip } from './label_with_tooltip';
export interface Props {
- contentManagement: ContentManagementPublicStart;
+ uiActions: UiActionsStart;
}
-export const RuleDashboards = ({ contentManagement }: Props) => {
+export const RuleDashboards = ({ uiActions }: Props) => {
const { formData } = useRuleFormState();
const dispatch = useRuleFormDispatch();
const dashboardsFormData = useMemo(
@@ -64,7 +64,7 @@ export const RuleDashboards = ({ contentManagement }: Props) => {
labelAppend={OptionalFieldLabel}
>
{
const { formData, baseErrors, plugins } = useRuleFormState();
- const { contentManagement } = plugins;
+ const { uiActions } = plugins;
const dispatch = useRuleFormDispatch();
@@ -160,7 +160,7 @@ export const RuleDetails = () => {
value={formData.artifacts?.investigation_guide?.blob ?? ''}
/>
- {contentManagement && }
+ {uiActions && }
>
);
diff --git a/src/platform/packages/shared/response-ops/rule_form/src/rule_form.tsx b/src/platform/packages/shared/response-ops/rule_form/src/rule_form.tsx
index 8ba134fd44428..6f526d1cba189 100644
--- a/src/platform/packages/shared/response-ops/rule_form/src/rule_form.tsx
+++ b/src/platform/packages/shared/response-ops/rule_form/src/rule_form.tsx
@@ -90,6 +90,7 @@ export const RuleForm = (
actionTypeRegistry,
fieldsMetadata,
contentManagement,
+ uiActions,
} = _plugins;
const ruleFormComponent = useMemo(() => {
@@ -110,6 +111,7 @@ export const RuleForm = (
actionTypeRegistry,
fieldsMetadata,
contentManagement,
+ uiActions,
};
// Passing the MetaData type all the way down the component hierarchy is unnecessary, this type is
@@ -186,6 +188,7 @@ export const RuleForm = (
actionTypeRegistry,
fieldsMetadata,
contentManagement,
+ uiActions,
onChangeMetaData,
id,
ruleTypeId,
@@ -196,6 +199,7 @@ export const RuleForm = (
connectorFeatureId,
initialMetadata,
initialEditStep,
+ focusTrapProps,
consumer,
multiConsumerSelection,
hideInterval,
@@ -204,7 +208,6 @@ export const RuleForm = (
shouldUseRuleProducer,
canShowConsumerSelection,
initialValues,
- focusTrapProps,
]);
return (
diff --git a/src/platform/packages/shared/response-ops/rule_form/src/types.ts b/src/platform/packages/shared/response-ops/rule_form/src/types.ts
index 677676a760764..c785594ae1ced 100644
--- a/src/platform/packages/shared/response-ops/rule_form/src/types.ts
+++ b/src/platform/packages/shared/response-ops/rule_form/src/types.ts
@@ -25,6 +25,7 @@ import type { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/
import type { ActionConnector, ActionTypeRegistryContract } from '@kbn/alerts-ui-shared';
import type { FieldsMetadataPublicStart } from '@kbn/fields-metadata-plugin/public';
import type { ContentManagementPublicStart } from '@kbn/content-management-plugin/public';
+import type { UiActionsStart } from '@kbn/ui-actions-plugin/public';
import type {
MinimumScheduleInterval,
Rule,
@@ -73,6 +74,7 @@ export interface RuleFormPlugins {
actionTypeRegistry: ActionTypeRegistryContract;
fieldsMetadata: FieldsMetadataPublicStart;
contentManagement?: ContentManagementPublicStart;
+ uiActions?: UiActionsStart;
}
export interface RuleFormState<
@@ -111,4 +113,4 @@ export interface ValidationResult {
errors: Record;
}
-export type RuleDashboardsPlugins = Pick;
+export type RuleDashboardsPlugins = Pick;
diff --git a/src/platform/packages/shared/response-ops/rule_form/tsconfig.json b/src/platform/packages/shared/response-ops/rule_form/tsconfig.json
index 0e0ed485a6f29..c7b68718c2cef 100644
--- a/src/platform/packages/shared/response-ops/rule_form/tsconfig.json
+++ b/src/platform/packages/shared/response-ops/rule_form/tsconfig.json
@@ -34,5 +34,6 @@
"@kbn/content-management-plugin",
"@kbn/dashboards-selector",
"@kbn/react-query",
+ "@kbn/ui-actions-plugin",
]
}
diff --git a/src/platform/plugins/shared/dashboard/public/dashboard_client/get_dashboard_by_id_action.ts b/src/platform/plugins/shared/dashboard/public/dashboard_client/get_dashboard_by_id_action.ts
new file mode 100644
index 0000000000000..74a0e06016d7d
--- /dev/null
+++ b/src/platform/plugins/shared/dashboard/public/dashboard_client/get_dashboard_by_id_action.ts
@@ -0,0 +1,41 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the "Elastic License
+ * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
+ * Public License v 1"; you may not use this file except in compliance with, at
+ * your election, the "Elastic License 2.0", the "GNU Affero General Public
+ * License v3.0 only", or the "Server Side Public License, v 1".
+ */
+
+import type { ActionDefinition } from '@kbn/ui-actions-plugin/public/actions';
+import { dashboardClient } from './dashboard_client';
+
+interface Context {
+ onResults: (dashboards: Array<{ id: string; isManaged: boolean; title: string }>) => void;
+ ids: string[];
+}
+
+export const getDashboardsByIdsAction: ActionDefinition = {
+ id: 'getDashboardsByIdsAction',
+ execute: async (context: Context) => {
+ const dashboards = await Promise.all(
+ context.ids.map(async (id) => {
+ try {
+ return await dashboardClient.get(id);
+ } catch {
+ return null;
+ }
+ })
+ );
+
+ context.onResults(
+ dashboards
+ .filter((dashboard): dashboard is NonNullable => dashboard !== null)
+ .map(({ id, data, meta }) => ({
+ id,
+ isManaged: Boolean(meta.managed),
+ title: data.title ?? '',
+ }))
+ );
+ },
+};
diff --git a/src/platform/plugins/shared/dashboard/public/dashboard_client/index.ts b/src/platform/plugins/shared/dashboard/public/dashboard_client/index.ts
index 427874c37bd98..fbca782cfd8e9 100644
--- a/src/platform/plugins/shared/dashboard/public/dashboard_client/index.ts
+++ b/src/platform/plugins/shared/dashboard/public/dashboard_client/index.ts
@@ -11,5 +11,6 @@ export { checkForDuplicateDashboardTitle } from './check_for_duplicate_dashboard
export { dashboardClient } from './dashboard_client';
export { findService } from './find_service';
export { searchAction } from './search_action';
+export { getDashboardsByIdsAction } from './get_dashboard_by_id_action';
export type { FindDashboardsByIdResponse, FindDashboardsService } from './types';
diff --git a/src/platform/plugins/shared/dashboard/public/plugin.tsx b/src/platform/plugins/shared/dashboard/public/plugin.tsx
index a20a6e72255f5..0c1c389b472bc 100644
--- a/src/platform/plugins/shared/dashboard/public/plugin.tsx
+++ b/src/platform/plugins/shared/dashboard/public/plugin.tsx
@@ -292,6 +292,11 @@ export class DashboardPlugin
return searchAction;
});
+ plugins.uiActions.registerActionAsync('getDashboardsByIdsAction', async () => {
+ const { getDashboardsByIdsAction } = await import('./dashboard_client');
+ return getDashboardsByIdsAction;
+ });
+
return {
registerDashboardPanelSettings,
findDashboardsService: async () => {
diff --git a/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/connectors_app.tsx b/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/connectors_app.tsx
index 145ae24fe62ab..4f4f80f187fe7 100644
--- a/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/connectors_app.tsx
+++ b/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/connectors_app.tsx
@@ -25,6 +25,7 @@ import { QueryClientProvider } from '@kbn/react-query';
import type { Storage } from '@kbn/kibana-utils-plugin/public';
import type { ActionsPublicPluginSetup } from '@kbn/actions-plugin/public';
import type { SharePluginStart } from '@kbn/share-plugin/public';
+import type { UiActionsStart } from '@kbn/ui-actions-plugin/public';
import { suspendedComponentWithProps } from './lib/suspended_component_with_props';
import type { ActionTypeRegistryContract, RuleTypeRegistryContract } from '../types';
@@ -59,6 +60,7 @@ export interface TriggersAndActionsUiServices extends CoreStart {
unifiedSearch: UnifiedSearchPublicPluginStart;
share: SharePluginStart;
isServerless: boolean;
+ uiActions?: UiActionsStart;
}
export const renderApp = (deps: TriggersAndActionsUiServices) => {
diff --git a/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/rules_app.tsx b/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/rules_app.tsx
index b908b3de0cdfc..dc48c804cce05 100644
--- a/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/rules_app.tsx
+++ b/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/rules_app.tsx
@@ -42,6 +42,7 @@ import type { CloudSetup } from '@kbn/cloud-plugin/public';
import type { FieldsMetadataPublicStart } from '@kbn/fields-metadata-plugin/public';
import type { SharePluginStart } from '@kbn/share-plugin/public';
import type { ContentManagementPublicStart } from '@kbn/content-management-plugin/public';
+import type { UiActionsStart } from '@kbn/ui-actions-plugin/public';
import { suspendedComponentWithProps } from './lib/suspended_component_with_props';
import type { ActionTypeRegistryContract, RuleTypeRegistryContract } from '../types';
import type { Section } from './constants';
@@ -87,6 +88,7 @@ export interface TriggersAndActionsUiServices extends CoreStart {
fieldsMetadata: FieldsMetadataPublicStart;
share?: SharePluginStart;
contentManagement?: ContentManagementPublicStart;
+ uiActions?: UiActionsStart;
}
export const renderApp = (deps: TriggersAndActionsUiServices) => {
diff --git a/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/sections/rule_form/rule_form_route.tsx b/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/sections/rule_form/rule_form_route.tsx
index cc89c8189baf0..a9655b511101d 100644
--- a/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/sections/rule_form/rule_form_route.tsx
+++ b/x-pack/platform/plugins/shared/triggers_actions_ui/public/application/sections/rule_form/rule_form_route.tsx
@@ -31,6 +31,7 @@ export const RuleFormRoute = () => {
ruleTypeRegistry,
actionTypeRegistry,
contentManagement,
+ uiActions,
chrome,
setBreadcrumbs,
...startServices
@@ -104,6 +105,7 @@ export const RuleFormRoute = () => {
ruleTypeRegistry,
actionTypeRegistry,
contentManagement,
+ uiActions,
...startServices,
}}
initialValues={ruleTemplate}
diff --git a/x-pack/platform/plugins/shared/triggers_actions_ui/public/plugin.ts b/x-pack/platform/plugins/shared/triggers_actions_ui/public/plugin.ts
index 90a30c0fcd629..a98c54759bad6 100644
--- a/x-pack/platform/plugins/shared/triggers_actions_ui/public/plugin.ts
+++ b/x-pack/platform/plugins/shared/triggers_actions_ui/public/plugin.ts
@@ -319,6 +319,7 @@ export class Plugin
fieldsMetadata: pluginsStart.fieldsMetadata,
contentManagement: pluginsStart.contentManagement,
share: pluginsStart.share,
+ uiActions: pluginsStart.uiActions,
});
},
});
@@ -368,6 +369,7 @@ export class Plugin
share: pluginsStart.share,
kibanaFeatures,
isServerless,
+ uiActions: pluginsStart.uiActions,
});
},
});
@@ -417,6 +419,7 @@ export class Plugin
fieldFormats: pluginsStart.fieldFormats,
lens: pluginsStart.lens,
fieldsMetadata: pluginsStart.fieldsMetadata,
+ uiActions: pluginsStart.uiActions,
});
},
});
diff --git a/x-pack/solutions/observability/plugins/slo/public/pages/slo_edit/components/slo_edit_form_description_section.tsx b/x-pack/solutions/observability/plugins/slo/public/pages/slo_edit/components/slo_edit_form_description_section.tsx
index 1673dc0554e66..4c23130b80b33 100644
--- a/x-pack/solutions/observability/plugins/slo/public/pages/slo_edit/components/slo_edit_form_description_section.tsx
+++ b/x-pack/solutions/observability/plugins/slo/public/pages/slo_edit/components/slo_edit_form_description_section.tsx
@@ -32,6 +32,7 @@ export function SloEditFormDescriptionSection() {
const { suggestions } = useFetchSLOSuggestions();
const { services } = useKibana();
+ const { uiActions } = services;
return (
(
field.onChange(selected.map((d) => ({ id: d.value })))}
diff --git a/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/contexts/synthetics_shared_context.tsx b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/contexts/synthetics_shared_context.tsx
index 2c85c038e3748..e26a5a05e3905 100644
--- a/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/contexts/synthetics_shared_context.tsx
+++ b/x-pack/solutions/observability/plugins/synthetics/public/apps/synthetics/contexts/synthetics_shared_context.tsx
@@ -56,6 +56,7 @@ export const SyntheticsSharedContext: React.FC<
slo: startPlugins.slo,
serverless: startPlugins.serverless,
charts: startPlugins.charts,
+ uiActions: startPlugins.uiActions,
}}
>
diff --git a/x-pack/solutions/observability/test/observability_functional/apps/observability/pages/rule_details_page.ts b/x-pack/solutions/observability/test/observability_functional/apps/observability/pages/rule_details_page.ts
index 38bacf3c0b105..6e4243bfb786e 100644
--- a/x-pack/solutions/observability/test/observability_functional/apps/observability/pages/rule_details_page.ts
+++ b/x-pack/solutions/observability/test/observability_functional/apps/observability/pages/rule_details_page.ts
@@ -4,12 +4,6 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
import expect from '@kbn/expect';
import type { FtrProviderContext } from '../../../ftr_provider_context';
@@ -239,5 +233,66 @@ export default ({ getService }: FtrProviderContext) => {
await testSubjects.missingOrFail('actions');
});
});
+
+ describe('Related dashboards', function () {
+ const comboBox = getService('comboBox');
+ const kibanaServer = getService('kibanaServer');
+ let testDashboardId: string;
+ const testDashboardTitle = `Test Dashboard for Rule Details ${Date.now()}`;
+
+ before(async () => {
+ const dashboardResponse = await kibanaServer.savedObjects.create({
+ type: 'dashboard',
+ overwrite: false,
+ attributes: {
+ title: testDashboardTitle,
+ description: 'Test dashboard for rule details functional test',
+ panelsJSON: '[]',
+ kibanaSavedObjectMeta: {
+ searchSourceJSON: '{}',
+ },
+ },
+ });
+
+ testDashboardId = dashboardResponse.id;
+ });
+
+ after(async () => {
+ if (testDashboardId) {
+ await kibanaServer.savedObjects.delete({
+ type: 'dashboard',
+ id: testDashboardId,
+ });
+ }
+ });
+
+ it('should display dashboard options in "Related dashboards" dropdown when editing rule', async () => {
+ await observability.alerts.common.navigateToRuleDetailsByRuleId(logThresholdRuleId);
+ await retry.waitFor(
+ 'Rule details to be visible',
+ async () => await testSubjects.exists('ruleDetails')
+ );
+
+ await testSubjects.click('actions');
+ await testSubjects.click('editRuleButton');
+
+ await retry.waitFor(
+ 'Rule form to be visible',
+ async () => await testSubjects.exists('ruleDetailsNameInput')
+ );
+
+ await retry.waitFor(
+ 'Dashboard selector to be visible',
+ async () => await testSubjects.exists('dashboardsSelector')
+ );
+
+ const optionsText = await comboBox.getOptionsList('dashboardsSelector');
+
+ expect(optionsText.length).to.be.greaterThan(0);
+ expect(optionsText.trim()).to.not.be.empty();
+
+ expect(optionsText).to.contain(testDashboardTitle);
+ });
+ });
});
};