Skip to content

Commit

Permalink
[Fix] [Synonyms UI] Fix infinite loading when permissions missing (#2…
Browse files Browse the repository at this point in the history
…11530)

## Summary

Fixes infinite loading when user had missing permissions.
<img width="948" alt="Screenshot 2025-02-18 at 12 24 22"
src="https://github.com/user-attachments/assets/975c46ef-a729-4bec-9442-fdb38b59fe19"
/>

### 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)
- [ ] [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
- [x] This was checked for breaking HTTP API changes, and any breaking
changes have been approved by the breaking-change committee. The
`release_note:breaking` label should be applied in these situations.
- [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)

(cherry picked from commit c0da61a)
  • Loading branch information
efegurkan committed Feb 21, 2025
1 parent 7711ae2 commit e7fa8a2
Show file tree
Hide file tree
Showing 5 changed files with 169 additions and 46 deletions.
Original file line number Diff line number Diff line change
@@ -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 React from 'react';

import { EuiEmptyPrompt } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';

const ERROR_MESSAGES = {
generic: {
title: (
<FormattedMessage id="xpack.search.synonyms.errorTitle" defaultMessage="An error occurred" />
),
body: (
<FormattedMessage
id="xpack.search.synonyms.errorDescription"
defaultMessage="An error occured while fetching synonyms. Check Kibana logs for more information."
/>
),
},
missingPermissions: {
title: (
<FormattedMessage
id="xpack.search.synonyms.missingPermissionsTitle"
defaultMessage="Missing permissions"
/>
),
body: (
<FormattedMessage
id="xpack.search.synonyms.missingPermissionsDescription"
defaultMessage="You do not have the necessary permissions to manage synonyms. Contact your system administrator."
/>
),
},
};

export const ErrorPrompt: React.FC<{ errorType: 'missingPermissions' | 'generic' }> = ({
errorType,
}) => {
return (
<EuiEmptyPrompt
iconType="logoEnterpriseSearch"
title={<h2>{ERROR_MESSAGES[errorType].title}</h2>}
body={<p>{ERROR_MESSAGES[errorType].body}</p>}
/>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/*
* 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 { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { render, screen } from '@testing-library/react';
import React from 'react';
import { SearchSynonymsOverview } from './overview';
import { I18nProvider } from '@kbn/i18n-react';
import { useFetchSynonymsSets } from '../../hooks/use_fetch_synonyms_sets';

jest.mock('../../hooks/use_fetch_synonyms_sets', () => ({
useFetchSynonymsSets: jest.fn(() => ({
data: undefined,
isLoading: false,
isError: true,
error: { body: { statusCode: 500 } },
})),
}));

describe('Search Synonyms Overview', () => {
const queryClient = new QueryClient();
const Wrapper = ({ children }: { children?: React.ReactNode }) => (
<I18nProvider>
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
</I18nProvider>
);
it('should show error prompt when we get a generic error', () => {
render(
<Wrapper>
<SearchSynonymsOverview />
</Wrapper>
);

expect(screen.getByText('An error occurred')).toBeInTheDocument();
});

it('should show error prompt when we get a missing permissions error', () => {
(useFetchSynonymsSets as jest.Mock).mockReturnValue({
data: undefined,
isLoading: false,
isError: true,
error: { body: { statusCode: 403 } },
});

render(
<Wrapper>
<SearchSynonymsOverview />
</Wrapper>
);

expect(screen.getByText('Missing permissions')).toBeInTheDocument();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,14 @@ import { SynonymSets } from '../synonym_sets/synonym_sets';
import { useFetchSynonymsSets } from '../../hooks/use_fetch_synonyms_sets';
import { EmptyPrompt } from '../empty_prompt/empty_prompt';
import { CreateSynonymsSetModal } from '../synonym_sets/create_new_set_modal';
import { ErrorPrompt } from '../error_prompt/error_prompt';
import { isPermissionError } from '../../utils/synonyms_utils';

export const SearchSynonymsOverview = () => {
const {
services: { console: consolePlugin, history, searchNavigation },
} = useKibana();
const { data: synonymsData, isInitialLoading } = useFetchSynonymsSets();
const { data: synonymsData, isInitialLoading, isError, error } = useFetchSynonymsSets();
const [isCreateModalVisible, setIsCreateModalVisible] = useState(false);

const embeddableConsole = useMemo(
Expand All @@ -44,50 +46,52 @@ export const SearchSynonymsOverview = () => {
solutionNav={searchNavigation?.useClassicNavigation(history)}
color="primary"
>
<KibanaPageTemplate.Header
pageTitle="Synonyms"
restrictWidth
color="primary"
rightSideItems={[
<EuiFlexGroup alignItems="center">
<EuiFlexItem grow={false}>
<EuiLink
data-test-subj="searchSynonymsSearchSynonymsOverviewApiDocumentationLink"
external
target="_blank"
href={docLinks.synonymsApi}
>
<FormattedMessage
id="xpack.searchSynonyms.synonymsSetDetail.documentationLink"
defaultMessage="API Documentation"
/>
</EuiLink>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton
data-test-subj="searchSynonymsSearchSynonymsOverviewCreateButton"
fill
iconType="plusInCircle"
onClick={() => {
setIsCreateModalVisible(true);
}}
>
<FormattedMessage
id="xpack.searchSynonyms.synonymsSetDetail.createButton"
defaultMessage="Create"
/>
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>,
]}
>
<EuiText>
<FormattedMessage
id="xpack.searchSynonyms.synonymsSetDetail.description"
defaultMessage="Create and manage synonym sets and synonym rules."
/>
</EuiText>
</KibanaPageTemplate.Header>
{synonymsData && !isInitialLoading && !isError && (
<KibanaPageTemplate.Header
pageTitle="Synonyms"
restrictWidth
color="primary"
rightSideItems={[
<EuiFlexGroup alignItems="center">
<EuiFlexItem grow={false}>
<EuiLink
data-test-subj="searchSynonymsSearchSynonymsOverviewApiDocumentationLink"
external
target="_blank"
href={docLinks.synonymsApi}
>
<FormattedMessage
id="xpack.searchSynonyms.synonymsSetDetail.documentationLink"
defaultMessage="API Documentation"
/>
</EuiLink>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton
data-test-subj="searchSynonymsSearchSynonymsOverviewCreateButton"
fill
iconType="plusInCircle"
onClick={() => {
setIsCreateModalVisible(true);
}}
>
<FormattedMessage
id="xpack.searchSynonyms.synonymsSetDetail.createButton"
defaultMessage="Create"
/>
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>,
]}
>
<EuiText>
<FormattedMessage
id="xpack.searchSynonyms.synonymsSetDetail.description"
defaultMessage="Create and manage synonym sets and synonym rules."
/>
</EuiText>
</KibanaPageTemplate.Header>
)}
<KibanaPageTemplate.Section restrictWidth>
{isCreateModalVisible && (
<CreateSynonymsSetModal
Expand All @@ -97,6 +101,9 @@ export const SearchSynonymsOverview = () => {
/>
)}
{isInitialLoading && <EuiLoadingSpinner />}
{isError && (
<ErrorPrompt errorType={isPermissionError(error) ? 'missingPermissions' : 'generic'} />
)}

{!isInitialLoading && synonymsData && synonymsData._meta.totalItemCount > 0 && (
<SynonymSets />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

import { useQuery } from '@tanstack/react-query';
import type { SynonymsGetSynonymsSetsSynonymsSetItem } from '@elastic/elasticsearch/lib/api/types';
import { KibanaServerError } from '@kbn/kibana-utils-plugin/common';
import { SYNONYMS_SETS_QUERY_KEY } from '../../common/constants';
import { DEFAULT_PAGE_VALUE, Page, Paginate } from '../../common/pagination';
import { APIRoutes } from '../../common/api_routes';
Expand All @@ -16,7 +17,7 @@ export const useFetchSynonymsSets = (page: Page = DEFAULT_PAGE_VALUE) => {
const {
services: { http },
} = useKibana();
return useQuery({
return useQuery<Paginate<SynonymsGetSynonymsSetsSynonymsSetItem>, { body: KibanaServerError }>({
queryKey: [SYNONYMS_SETS_QUERY_KEY, page.from, page.size],
queryFn: async () => {
return await http.get<Paginate<SynonymsGetSynonymsSetsSynonymsSetItem>>(
Expand All @@ -26,5 +27,7 @@ export const useFetchSynonymsSets = (page: Page = DEFAULT_PAGE_VALUE) => {
}
);
},
refetchOnWindowFocus: false,
retry: false,
});
};
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
*/

import { EuiComboBoxOptionOption } from '@elastic/eui';
import { KibanaServerError } from '@kbn/kibana-utils-plugin/common';

export const isExplicitSynonym = (synonym: string) => {
return synonym.trim().includes('=>');
Expand Down Expand Up @@ -41,3 +42,7 @@ export const synonymsOptionToString = ({
`${fromTerms.map((s) => s.label).join(',')}${
isExplicit ? ' => ' + toTerms.map((s) => s.label).join(',') : ''
}`;

export const isPermissionError = (error: { body: KibanaServerError }) => {
return error.body.statusCode === 403;
};

0 comments on commit e7fa8a2

Please sign in to comment.