Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -460,6 +460,7 @@
"@kbn/dashboard-enhanced-plugin": "link:x-pack/platform/plugins/shared/dashboard_enhanced",
"@kbn/dashboard-markdown": "link:src/platform/plugins/shared/dashboard_markdown",
"@kbn/dashboard-plugin": "link:src/platform/plugins/shared/dashboard",
"@kbn/dashboards-selector": "link:src/platform/packages/shared/dashboards/dashboards-selector",
"@kbn/data-forge": "link:x-pack/platform/packages/shared/kbn-data-forge",
"@kbn/data-grid-in-table-search": "link:src/platform/packages/shared/kbn-data-grid-in-table-search",
"@kbn/data-plugin": "link:src/platform/plugins/shared/data",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# `@kbn/dashboards-selector`

A reusable Elastic UI component for selecting Kibana dashboards.

---

## Quick-start

```tsx
import React, { useState } from 'react';
import { DashboardsSelector } from '@kbn/dashboards-selector';
import type { ContentManagementPublicStart } from '@kbn/content-management-plugin/public';

interface Props {
contentManagement: ContentManagementPublicStart;
}

export const MyComponent = ({ contentManagement }: Props) => {
// Persist only the dashboard ids in your form state
const [dashboardsFormData, setDashboardsFormData] = useState<Array<{ id: string }>>([]);

return (
<DashboardsSelector
contentManagement={contentManagement}
dashboardsFormData={dashboardsFormData}
onChange={(opts) => {
// opts => Array<EuiComboBoxOptionOption<string>>
setDashboardsFormData(opts.map(({ value }) => ({ id: value })));
}}
placeholder="Select one or more dashboards"
/>
);
};
```

---

## `DashboardsSelector` API reference

| Prop | Type | Required | Description |
| -------------------- | ------------------------------------------------------------ | -------- | ----------------------------------------------------------------------------------------------------------------------- |
| `contentManagement` | `ContentManagementPublicStart` | ✔︎ | Instance of the Content Management service obtained from Kibana core start services. |
| `dashboardsFormData` | `Array<{ id: string }>` | ✔︎ | The **current** form value. Only the dashboard id is required. Used to pre-select dashboards when the component mounts. |
| `onChange` | `(selected: Array<EuiComboBoxOptionOption<string>>) => void` | ✔︎ | Callback fired every time the user selection changes. |
| `placeholder` | `string` | ✖︎ | Placeholder text shown when nothing is selected. Default: `"Select dashboards"`. |
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
/*
* 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 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 userEvent from '@testing-library/user-event';

const MOCK_FIRST_DASHBOARD_ID = 'dashboard-1';
const MOCK_SECOND_DASHBOARD_ID = 'dashboard-2';
const MOCK_FIRST_DASHBOARD_TITLE = 'First Dashboard';
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: [],
};

const MOCK_SECOND_DASHBOARD = {
status: 'success',
id: MOCK_SECOND_DASHBOARD_ID,
attributes: { title: MOCK_SECOND_DASHBOARD_TITLE },
references: [],
};

const mockFetchDashboard = jest.fn();
const mockFetchDashboards = jest
.fn()
.mockResolvedValue([MOCK_FIRST_DASHBOARD, MOCK_SECOND_DASHBOARD]);

// 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 mockOnChange = jest.fn();

describe('DashboardsSelector', () => {
beforeEach(() => {
mockFetchDashboard.mockResolvedValueOnce(MOCK_FIRST_DASHBOARD);
mockFetchDashboard.mockResolvedValueOnce(MOCK_SECOND_DASHBOARD);
});

afterEach(() => {
jest.clearAllMocks();
});

const contentManagement = contentManagementMock.createStartContract();

it('renders the component', () => {
render(
<DashboardsSelector
contentManagement={contentManagement}
dashboardsFormData={[]}
onChange={mockOnChange}
placeholder={MOCK_PLACEHOLDER}
/>
);

// Check that the component renders with the placeholder text
expect(screen.getByTestId('dashboardsSelector')).toBeInTheDocument();
expect(screen.getByPlaceholderText(MOCK_PLACEHOLDER)).toBeInTheDocument();
});

it('displays selected dashboard titles from dashboardsFormData', async () => {
render(
<DashboardsSelector
contentManagement={contentManagement}
dashboardsFormData={[{ id: MOCK_FIRST_DASHBOARD_ID }, { id: MOCK_SECOND_DASHBOARD_ID }]}
onChange={mockOnChange}
placeholder={MOCK_PLACEHOLDER}
/>
);

// 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);
});

it('debounces and triggers dashboard search with user input in the ComboBox', async () => {
render(
<DashboardsSelector
contentManagement={contentManagement}
dashboardsFormData={[]}
onChange={mockOnChange}
placeholder={MOCK_PLACEHOLDER}
/>
);

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(screen.getByText(MOCK_FIRST_DASHBOARD_TITLE)).toBeInTheDocument();
});
});

it('fetches dashboard list when combobox is focused', async () => {
render(
<DashboardsSelector
contentManagement={contentManagement}
dashboardsFormData={[]}
onChange={mockOnChange}
placeholder={MOCK_PLACEHOLDER}
/>
);
const searchInput = screen.getByPlaceholderText(MOCK_PLACEHOLDER);
fireEvent.focus(searchInput);

await waitFor(() => {
expect(mockFetchDashboards).toHaveBeenCalledWith(expect.objectContaining({ limit: 100 }));
});
});

it('does not fetch dashboard list when combobox is not focused', async () => {
render(
<DashboardsSelector
contentManagement={contentManagement}
dashboardsFormData={[]}
onChange={mockOnChange}
placeholder={MOCK_PLACEHOLDER}
/>
);

expect(mockFetchDashboards).not.toHaveBeenCalled();
});

it('dispatches selected dashboards on change', async () => {
render(
<DashboardsSelector
contentManagement={contentManagement}
dashboardsFormData={[{ id: MOCK_FIRST_DASHBOARD_ID }]}
onChange={mockOnChange}
placeholder={MOCK_PLACEHOLDER}
/>
);

// 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: '*' })
);
});

// 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 },
{ label: MOCK_SECOND_DASHBOARD_TITLE, value: MOCK_SECOND_DASHBOARD_ID },
]);
});

// Verify that both selected options are now displayed
expect(screen.getByText(MOCK_FIRST_DASHBOARD_TITLE)).toBeInTheDocument();
expect(screen.getByText(MOCK_SECOND_DASHBOARD_TITLE)).toBeInTheDocument();
});
});
Loading