Skip to content

Commit

Permalink
Display search unavailable error if search is not configured (stolost…
Browse files Browse the repository at this point in the history
…ron#3311)

Signed-off-by: zlayne <[email protected]>
  • Loading branch information
zlayne authored Feb 21, 2024
1 parent 1ae0a92 commit 24c24f0
Show file tree
Hide file tree
Showing 3 changed files with 148 additions and 22 deletions.
9 changes: 7 additions & 2 deletions frontend/src/lib/nock-util.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
/* Copyright Contributors to the Open Cluster Management project */

/* istanbul ignore file */
import { diff } from 'deep-diff'
import isEqual from 'lodash/isEqual'
import set from 'lodash/set'
import { diff } from 'deep-diff'
import nock from 'nock'
import StackTrace from 'stacktrace-js'
import { Url } from 'url'
Expand All @@ -22,8 +22,8 @@ import {
} from '../resources'
import { AnsibleTowerInventoryList } from '../resources/ansible-inventory'
import { APIResourceNames } from './api-resource-list'
import { apiSearchUrl, ISearchResult, SearchQuery } from './search'
import { OperatorCheckResponse, SupportedOperator } from './operatorCheck'
import { apiSearchUrl, ISearchResult, SearchQuery } from './search'

export type ISearchRelatedResult = {
data: {
Expand Down Expand Up @@ -618,6 +618,11 @@ const mockApiPathList: APIResourceNames = {
pluralName: 'applications',
},
},
'search.open-cluster-management.io/v1alpha1': {
Search: {
pluralName: 'searches',
},
},
}

const mockOperatorCheckResponse: OperatorCheckResponse = {
Expand Down
87 changes: 87 additions & 0 deletions frontend/src/routes/Home/Overview/SavedSearchesCard.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,36 @@

import { MockedProvider } from '@apollo/client/testing'
import { render, waitFor } from '@testing-library/react'
import { GraphQLError } from 'graphql'
import { createBrowserHistory } from 'history'
import { Router } from 'react-router-dom'
import { RecoilRoot } from 'recoil'
import { Settings, settingsState } from '../../../../src/atoms'
import { nockIgnoreApiPaths } from '../../../lib/nock-util'
import { SavedSearch } from '../../../resources'
import { SearchResultCountDocument } from '../Search/search-sdk/search-sdk'
import SavedSearchesCard from './SavedSearchesCard'

jest.mock('../../../resources', () => ({
listResources: jest.fn(() => ({
promise: Promise.resolve([
{
status: {
conditions: [
{
lastTransitionTime: '2024-02-21T19:34:39Z',
message: 'Check status of deployment: search-api',
reason: 'NoPodsFound',
status: 'False',
type: 'Ready--search-api',
},
],
},
},
]),
})),
}))

const mockSettings: Settings = {
SEARCH_RESULT_LIMIT: '1000',
}
Expand Down Expand Up @@ -83,6 +105,49 @@ const mocks = [
},
]

const errorMock = [
{
request: {
query: SearchResultCountDocument,
variables: {
input: [
{
keywords: [],
filters: [
{
property: 'kind',
values: ['Pod'],
},
],
limit: 1000,
},
{
keywords: [],
filters: [
{
property: 'label',
values: ['app=search'],
},
{
property: 'kind',
values: ['Pod'],
},
{
property: 'namespace',
values: ['open-cluster-management'],
},
],
limit: 1000,
},
],
},
},
result: {
errors: [new GraphQLError('Error getting overview data')],
},
},
]

describe('SavedSearchesCard', () => {
test('Renders valid SavedSearchesCard with no saved searches', async () => {
const { getByText } = render(
Expand Down Expand Up @@ -137,4 +202,26 @@ describe('SavedSearchesCard', () => {
await waitFor(() => expect(getByText('10')).toBeTruthy())
await waitFor(() => expect(getByText('2')).toBeTruthy())
})

test('Renders erro correctly when search is disabled', async () => {
nockIgnoreApiPaths()
const { getByText } = render(
<RecoilRoot
initializeState={(snapshot) => {
snapshot.set(settingsState, mockSettings)
}}
>
<Router history={createBrowserHistory()}>
<MockedProvider mocks={errorMock}>
<SavedSearchesCard isUserPreferenceLoading={false} savedSearches={savedSearches} />
</MockedProvider>
</Router>
</RecoilRoot>
)

// Check header strings
await waitFor(() => expect(getByText('Saved searches')).toBeTruthy())
await waitFor(() => expect(getByText('This view is disabled with the current configuration.')).toBeTruthy())
await waitFor(() => expect(getByText('Enable the search service to see this view.')).toBeTruthy())
})
})
74 changes: 54 additions & 20 deletions frontend/src/routes/Home/Overview/SavedSearchesCard.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
/* Copyright Contributors to the Open Cluster Management project */
import { V1CustomResourceDefinitionCondition } from '@kubernetes/client-node'
import {
Card,
CardBody,
Expand All @@ -14,28 +15,31 @@ import {
Title,
} from '@patternfly/react-core'
import { CogIcon, ExclamationCircleIcon } from '@patternfly/react-icons'
import { Fragment } from 'react'
import { Fragment, useEffect, useState } from 'react'
import { Link } from 'react-router-dom'
import { useTranslation } from '../../../lib/acm-i18next'
import { NavigationPath } from '../../../NavigationPath'
import { SavedSearch } from '../../../resources'
import { IResource, listResources, SavedSearch } from '../../../resources'
import { useSharedAtoms } from '../../../shared-recoil'
import { convertStringToQuery } from '../Search/search-helper'
import { searchClient } from '../Search/search-sdk/search-client'
import { useSearchResultCountQuery } from '../Search/search-sdk/search-sdk'

const CardHeader = () => {
const CardHeader = (props: { isSearchDisabled: boolean }) => {
const { isSearchDisabled } = props
const { t } = useTranslation()
return (
<CardTitle>
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<span>{t('Saved searches')}</span>
<Link style={{ display: 'flex', alignItems: 'center' }} to={NavigationPath.search}>
<CogIcon />
<Text style={{ paddingLeft: '.25rem' }} component={TextVariants.small}>
{t('Manage')}
</Text>
</Link>
{!isSearchDisabled && (
<Link style={{ display: 'flex', alignItems: 'center' }} to={NavigationPath.search}>
<CogIcon />
<Text style={{ paddingLeft: '.25rem' }} component={TextVariants.small}>
{t('Manage')}
</Text>
</Link>
)}
</div>
</CardTitle>
)
Expand All @@ -48,18 +52,44 @@ export default function SavedSearchesCard(
const { t } = useTranslation()
const { useSearchResultLimit } = useSharedAtoms()
const searchResultLimit = useSearchResultLimit()
const [isSearchDisabledLoading, setIsSearchDisabledLoading] = useState<boolean>(false)
const [isSearchDisabled, setIsSearchDisabled] = useState<boolean>()

const { data, error, loading } = useSearchResultCountQuery({
variables: { input: savedSearches.map((query) => convertStringToQuery(query.searchText, searchResultLimit)) },
skip: isUserPreferenceLoading || savedSearches.length === 0,
client: process.env.NODE_ENV === 'test' ? undefined : searchClient,
})

if (isUserPreferenceLoading || loading) {
useEffect(() => {
if (error) {
setIsSearchDisabledLoading(true)
listResources<IResource>({
apiVersion: 'search.open-cluster-management.io/v1alpha1',
kind: 'Search',
})
.promise.then((response) => {
const operatorConditions: V1CustomResourceDefinitionCondition[] = response[0]?.status?.conditions ?? []
const searchApiCondition = operatorConditions.find((c: V1CustomResourceDefinitionCondition) => {
return c.type.toLowerCase() === 'ready--search-api' && c.status === 'False'
})
if (searchApiCondition) {
setIsSearchDisabled(true)
}
setIsSearchDisabledLoading(false)
})
.catch((err) => {
console.error('Error getting resource: ', err)
setIsSearchDisabledLoading(false)
})
}
}, [error])

if (isUserPreferenceLoading || loading || isSearchDisabledLoading) {
return (
<div>
<Card isRounded>
<CardHeader />
<CardHeader isSearchDisabled={false} />
<CardBody>
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<Skeleton width="45%" />
Expand All @@ -75,19 +105,23 @@ export default function SavedSearchesCard(
</div>
)
} else if (error) {
const searchDisabledTitle = 'This view is disabled with the current configuration.'
const searchDisabledMessage = 'Enable the search service to see this view.'
return (
<Card isRounded>
<CardHeader />
<CardHeader isSearchDisabled />
<EmptyState style={{ paddingTop: 0, marginTop: 'auto' }}>
<EmptyStateIcon
style={{ fontSize: '36px', marginBottom: '1rem' }}
icon={ExclamationCircleIcon}
color={'var(--pf-global--danger-color--100)'}
/>
{!isSearchDisabled && (
<EmptyStateIcon
style={{ fontSize: '36px', marginBottom: '1rem' }}
icon={ExclamationCircleIcon}
color={'var(--pf-global--danger-color--100)'}
/>
)}
<Title size="md" headingLevel="h4">
{t('Error occurred while getting the result count.')}
{isSearchDisabled ? searchDisabledTitle : t('Error occurred while getting the result count.')}
</Title>
<EmptyStateBody>{error.message}</EmptyStateBody>
<EmptyStateBody>{isSearchDisabled ? searchDisabledMessage : error.message}</EmptyStateBody>
</EmptyState>
</Card>
)
Expand Down Expand Up @@ -116,7 +150,7 @@ export default function SavedSearchesCard(
return (
<div>
<Card isRounded>
<CardHeader />
<CardHeader isSearchDisabled={false} />
<CardBody>
<div style={{ display: 'grid', gridTemplateColumns: 'auto auto auto auto auto', columnGap: 16 }}>
{savedSearches.map((savedSearch: SavedSearch, index: number) => {
Expand Down

0 comments on commit 24c24f0

Please sign in to comment.