Skip to content

Commit

Permalink
[Synonyms UI] Search synonyms set list (elastic#206931)
Browse files Browse the repository at this point in the history
## Summary

Adds Synonyms set table and endpoint to the synonyms. Actions are just a
placeholder and will be working in next PR following this with Delete
modal.

<img width="1161" alt="Screenshot 2025-01-16 at 13 43 44"
src="https://github.com/user-attachments/assets/bc410a58-85e0-4e89-baff-e7a427d82ecd"
/>
<img width="1163" alt="Screenshot 2025-01-16 at 13 43 55"
src="https://github.com/user-attachments/assets/e087bd51-71a9-49a5-936e-00fde2492ddd"
/>



### Checklist

Check the PR satisfies following conditions. 

Reviewers should verify this PR satisfies this list as well.

- [x] Any text added follows [EUI's writing
guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses
sentence case text and includes [i18n
support](https://github.com/elastic/kibana/blob/main/src/platform/packages/shared/kbn-i18n/README.md)
- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
- [ ] [Flaky Test
Runner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was
used on any tests changed
- [x] The PR description includes the appropriate Release Notes section,
and the correct `release_note:*` label is applied per the
[guidelines](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)

---------

Co-authored-by: Liam Thompson <32779855+leemthompo@users.noreply.github.com>
Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
  • Loading branch information
3 people authored Jan 20, 2025
1 parent 49d1cea commit 3543852
Show file tree
Hide file tree
Showing 13 changed files with 450 additions and 9 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

export enum APIRoutes {
SYNONYM_SETS = '/internal/search_synonyms/synonyms',
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

export const DEFAULT_PAGE_VALUE: Page = {
from: 0,
size: 10,
};

export interface Pagination {
pageIndex: number;
pageSize: number;
totalItemCount: number;
}

export interface Page {
from: number; // current page index, 0-based
size: number;
}

export interface Paginate<T> {
_meta: Pagination;
data: T[];
}

export function paginationToPage(pagination: Pagination): Page {
return {
from: pagination.pageIndex * pagination.pageSize,
size: pagination.pageSize,
};
}
export function pageToPagination(page: { from: number; size: number; total: number }) {
// Prevent divide-by-zero-error
const pageIndex = page.size ? Math.trunc(page.from / page.size) : 0;
return {
pageIndex,
pageSize: page.size,
totalItemCount: page.total,
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,11 @@ import { KibanaRenderContextProvider } from '@kbn/react-kibana-context-render';
import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
import { I18nProvider } from '@kbn/i18n-react';
import { Route, Router, Routes } from '@kbn/shared-ux-router';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { AppPluginStartDependencies } from './types';
import { SearchSynonymsOverview } from './components/overview/overview';

const queryClient = new QueryClient({});
export const renderApp = async (
core: CoreStart,
services: AppPluginStartDependencies,
Expand All @@ -24,13 +26,15 @@ export const renderApp = async (
<KibanaRenderContextProvider {...core}>
<KibanaContextProvider services={{ ...core, ...services }}>
<I18nProvider>
<Router history={services.history}>
<Routes>
<Route path="/">
<SearchSynonymsOverview />
</Route>
</Routes>
</Router>
<QueryClientProvider client={queryClient}>
<Router history={services.history}>
<Routes>
<Route path="/">
<SearchSynonymsOverview />
</Route>
</Routes>
</Router>
</QueryClientProvider>
</I18nProvider>
</KibanaContextProvider>
</KibanaRenderContextProvider>,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,17 @@
import React, { useMemo } from 'react';

import { KibanaPageTemplate } from '@kbn/shared-ux-page-kibana-template';
import { EuiLoadingSpinner } from '@elastic/eui';
import { useKibana } from '../../hooks/use_kibana';
import { SynonymSets } from '../synonym_sets/synonym_sets';
import { useFetchSynonymsSets } from '../../hooks/use_fetch_synonyms_sets';
import { EmptyPrompt } from '../empty_prompt/empty_prompt';

export const SearchSynonymsOverview = () => {
const {
services: { console: consolePlugin, history, searchNavigation },
} = useKibana();
const { data: synonymsData, isInitialLoading } = useFetchSynonymsSets();

const embeddableConsole = useMemo(
() => (consolePlugin?.EmbeddableConsole ? <consolePlugin.EmbeddableConsole /> : null),
Expand All @@ -28,7 +32,16 @@ export const SearchSynonymsOverview = () => {
data-test-subj="searchSynonymsOverviewPage"
solutionNav={searchNavigation?.useClassicNavigation(history)}
>
<EmptyPrompt />
<KibanaPageTemplate.Section restrictWidth>
{isInitialLoading && <EuiLoadingSpinner />}

{!isInitialLoading && synonymsData && synonymsData._meta.totalItemCount > 0 && (
<SynonymSets />
)}
{!isInitialLoading && synonymsData && synonymsData._meta.totalItemCount === 0 && (
<EmptyPrompt />
)}
</KibanaPageTemplate.Section>
{embeddableConsole}
</KibanaPageTemplate>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import React from 'react';

import { render, screen } from '@testing-library/react';
import { SynonymSets } from './synonym_sets';

jest.mock('../../hooks/use_fetch_synonyms_sets', () => ({
useFetchSynonymsSets: () => ({
data: {
data: [
{
synonyms_set: 'Synonyms Set 1',
count: 2,
},
{
synonyms_set: 'Synonyms Set 2',
count: 3,
},
],
_meta: { pageIndex: 0, pageSize: 10, totalItemCount: 2 },
},
isLoading: false,
isError: false,
}),
}));

describe('Search Synonym Sets list', () => {
it('should render the list with synonym sets', () => {
render(<SynonymSets />);
const synonymSetTable = screen.getByTestId('synonyms-set-table');
expect(synonymSetTable).toBeInTheDocument();

const synonymSetItemNames = screen.getAllByTestId('synonyms-set-item-name');
expect(synonymSetItemNames).toHaveLength(2);
expect(synonymSetItemNames[0].textContent).toBe('Synonyms Set 1');
expect(synonymSetItemNames[1].textContent).toBe('Synonyms Set 2');

const synonymSetItemRuleCounts = screen.getAllByTestId('synonyms-set-item-rule-count');
expect(synonymSetItemRuleCounts).toHaveLength(2);
expect(synonymSetItemRuleCounts[0].textContent).toBe('2');
expect(synonymSetItemRuleCounts[1].textContent).toBe('3');

const synonymSetItemPageSize = screen.getByTestId('tablePaginationPopoverButton');
const synonymSetPageButton = screen.getByTestId('pagination-button-0');
expect(synonymSetItemPageSize).toBeInTheDocument();
expect(synonymSetPageButton).toBeInTheDocument();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/*
* 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 { SynonymsGetSynonymsSetsSynonymsSetItem } from '@elastic/elasticsearch/lib/api/types';
import { EuiBasicTable, EuiBasicTableColumn } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import React, { useState } from 'react';
import { DEFAULT_PAGE_VALUE, paginationToPage } from '../../../common/pagination';
import { useFetchSynonymsSets } from '../../hooks/use_fetch_synonyms_sets';

export const SynonymSets = () => {
const [pageIndex, setPageIndex] = useState(0);
const [pageSize, setPageSize] = useState(DEFAULT_PAGE_VALUE.size);
const { from } = paginationToPage({ pageIndex, pageSize, totalItemCount: 0 });
const { data: synonyms } = useFetchSynonymsSets({ from, size: pageSize });

if (!synonyms) {
return null;
}

const pagination = {
initialPageSize: 10,
pageSizeOptions: [10, 25, 50],
...synonyms._meta,
pageSize,
pageIndex,
};
const columns: Array<EuiBasicTableColumn<SynonymsGetSynonymsSetsSynonymsSetItem>> = [
{
field: 'synonyms_set',
name: i18n.translate('xpack.searchSynonyms.synonymsSetTable.nameColumn', {
defaultMessage: 'Synonyms Set',
}),
render: (name: string) => <div data-test-subj="synonyms-set-item-name">{name}</div>,
},
{
field: 'count',
name: i18n.translate('xpack.searchSynonyms.synonymsSetTable.ruleCount', {
defaultMessage: 'Rule Count',
}),
render: (ruleCount: number) => (
<div data-test-subj="synonyms-set-item-rule-count">{ruleCount}</div>
),
},
];
return (
<div>
<EuiBasicTable
data-test-subj="synonyms-set-table"
items={synonyms.data}
columns={columns}
pagination={pagination}
onChange={({ page: changedPage }) => {
setPageIndex(changedPage.index);
setPageSize(changedPage.size);
}}
/>
</div>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/*
* 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 { renderHook, waitFor } from '@testing-library/react';

const mockHttpGet = jest.fn();

jest.mock('@tanstack/react-query', () => ({
useQuery: jest.fn().mockImplementation(async ({ queryKey, queryFn, opts }) => {
try {
const res = await queryFn();
return Promise.resolve(res);
} catch (e) {
// opts.onError(e);
}
}),
}));

jest.mock('./use_kibana', () => ({
useKibana: jest.fn().mockReturnValue({
services: {
http: {
get: mockHttpGet,
},
notifications: {
toasts: {
addError: jest.fn(),
},
},
},
}),
}));

describe('useFetchSynonymsSet Hook', () => {
beforeEach(() => {
jest.clearAllMocks();
});

it('should return synonyms set', async () => {
const synonyms = [
{
id: '1',
synonyms: ['foo', 'bar'],
},
];
mockHttpGet.mockReturnValue(synonyms);
const { useFetchSynonymsSets } = jest.requireActual('./use_fetch_synonyms_sets');

const { result } = renderHook(() => useFetchSynonymsSets());
await waitFor(() => expect(result.current).resolves.toStrictEqual(synonyms));
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/*
* 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 { useQuery } from '@tanstack/react-query';
import type { SynonymsGetSynonymsSetsSynonymsSetItem } from '@elastic/elasticsearch/lib/api/types';
import { DEFAULT_PAGE_VALUE, Page, Paginate } from '../../common/pagination';
import { APIRoutes } from '../../common/api_routes';
import { useKibana } from './use_kibana';

export const useFetchSynonymsSets = (page: Page = DEFAULT_PAGE_VALUE) => {
const {
services: { http },
} = useKibana();
return useQuery({
queryKey: ['synonyms-sets-fetch', page.from, page.size],
queryFn: async () => {
return await http.get<Paginate<SynonymsGetSynonymsSetsSynonymsSetItem>>(
APIRoutes.SYNONYM_SETS,
{
query: { from: page.from, size: page.size },
}
);
},
});
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/*
* 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 { ElasticsearchClient } from '@kbn/core/server';
import { fetchSynonymSets } from './fetch_synonym_sets';

describe('fetch synonym sets lib function', () => {
const mockClient = {
security: {
hasPrivileges: jest.fn(),
},
synonyms: {
getSynonymsSets: jest.fn(),
},
};

const client = () => mockClient as unknown as ElasticsearchClient;

beforeEach(() => {
jest.clearAllMocks();
});
it('should return synonym sets', async () => {
mockClient.synonyms.getSynonymsSets.mockResolvedValue({
count: 2,
results: [
{
synonyms_set: 'my_synonyms_set',
count: 2,
},
{
synonyms_set: 'my_synonyms_set_2',
count: 3,
},
],
});

const result = await fetchSynonymSets(client(), { from: 0, size: 10 });
expect(result).toEqual({
_meta: {
pageIndex: 0,
pageSize: 10,
totalItemCount: 2,
},
data: [
{
synonyms_set: 'my_synonyms_set',
count: 2,
},
{
synonyms_set: 'my_synonyms_set_2',
count: 3,
},
],
});
});
});
Loading

0 comments on commit 3543852

Please sign in to comment.