mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[Discover] Add a way to quickly expand time range from "No results" screen (#147195)
Related to issue https://github.com/elastic/kibana/issues/12608 A part of Spacetime project https://github.com/elastic/kibana/pull/146729 but only for "No results" UI, excluding the time picker changes. ## Summary This PR extends the "No results matches your search criteria. Expand your time range..." message to allow users quickly expand the time range by clicking on a link. <img width="1492" alt="Screenshot 2022-12-07 at 14 38 45" src="https://user-images.githubusercontent.com/1415710/206221177-1a466b98-6cd3-494d-b7fe-09fdd43b1222.png"> --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
abfe96ff89
commit
6f3b29df5d
17 changed files with 738 additions and 208 deletions
|
@ -19,7 +19,6 @@ import {
|
|||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { METRIC_TYPE } from '@kbn/analytics';
|
||||
import { isOfQueryType } from '@kbn/es-query';
|
||||
import classNames from 'classnames';
|
||||
import { generateFilters } from '@kbn/data-plugin/public';
|
||||
import { DataView, DataViewField, DataViewType } from '@kbn/data-views-plugin/public';
|
||||
|
@ -42,7 +41,6 @@ import { DataMainMsg, RecordRawType } from '../../services/discover_data_state_c
|
|||
import { useColumns } from '../../../../hooks/use_data_grid_columns';
|
||||
import { FetchStatus } from '../../../types';
|
||||
import { useDataState } from '../../hooks/use_data_state';
|
||||
import { hasActiveFilter } from './utils';
|
||||
import { getRawRecordType } from '../../utils/get_raw_record_type';
|
||||
import { SavedSearchURLConflictCallout } from '../../../../components/saved_search_url_conflict_callout/saved_search_url_conflict_callout';
|
||||
import { DiscoverHistogramLayout } from './discover_histogram_layout';
|
||||
|
@ -84,10 +82,9 @@ export function DiscoverLayout({
|
|||
inspector,
|
||||
} = useDiscoverServices();
|
||||
const { main$ } = stateContainer.dataState.data$;
|
||||
const [query, savedQuery, filters, columns, sort] = useAppStateSelector((state) => [
|
||||
const [query, savedQuery, columns, sort] = useAppStateSelector((state) => [
|
||||
state.query,
|
||||
state.savedQuery,
|
||||
state.filters,
|
||||
state.columns,
|
||||
state.sort,
|
||||
]);
|
||||
|
@ -208,13 +205,16 @@ export function DiscoverLayout({
|
|||
|
||||
const mainDisplay = useMemo(() => {
|
||||
if (resultState === 'none') {
|
||||
const globalQueryState = data.query.getState();
|
||||
|
||||
return (
|
||||
<DiscoverNoResults
|
||||
isTimeBased={isTimeBased}
|
||||
query={globalQueryState.query}
|
||||
filters={globalQueryState.filters}
|
||||
data={data}
|
||||
error={dataState.error}
|
||||
hasQuery={isOfQueryType(query) && !!query?.query}
|
||||
hasFilters={hasActiveFilter(filters)}
|
||||
dataView={dataView}
|
||||
onDisableFilters={onDisableFilters}
|
||||
/>
|
||||
);
|
||||
|
@ -257,7 +257,6 @@ export function DiscoverLayout({
|
|||
dataState.error,
|
||||
dataView,
|
||||
expandedDoc,
|
||||
filters,
|
||||
inspectorAdapters,
|
||||
isPlainRecord,
|
||||
isTimeBased,
|
||||
|
@ -265,7 +264,6 @@ export function DiscoverLayout({
|
|||
onAddFilter,
|
||||
onDisableFilters,
|
||||
onFieldEdited,
|
||||
query,
|
||||
resetSavedSearch,
|
||||
resultState,
|
||||
savedSearch,
|
||||
|
|
|
@ -7,47 +7,83 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { ReactWrapper } from 'enzyme';
|
||||
import * as RxApi from 'rxjs';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { mountWithIntl } from '@kbn/test-jest-helpers';
|
||||
import { findTestSubject } from '@elastic/eui/lib/test';
|
||||
|
||||
import { DiscoverNoResults, DiscoverNoResultsProps } from './no_results';
|
||||
import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
|
||||
import {
|
||||
stubDataView,
|
||||
stubDataViewWithoutTimeField,
|
||||
} from '@kbn/data-views-plugin/common/data_view.stub';
|
||||
import { type Filter } from '@kbn/es-query';
|
||||
import { DiscoverNoResults, DiscoverNoResultsProps } from './no_results';
|
||||
import { createDiscoverServicesMock } from '../../../../__mocks__/services';
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
function mountAndFindSubjects(props: Omit<DiscoverNoResultsProps, 'onDisableFilters'>) {
|
||||
const services = {
|
||||
docLinks: {
|
||||
links: {
|
||||
query: {
|
||||
luceneQuerySyntax: 'documentation-link',
|
||||
},
|
||||
jest.spyOn(RxApi, 'lastValueFrom').mockImplementation(async () => ({
|
||||
rawResponse: {
|
||||
aggregations: {
|
||||
earliest_timestamp: {
|
||||
value_as_string: '2020-09-01T08:30:00.000Z',
|
||||
},
|
||||
latest_timestamp: {
|
||||
value_as_string: '2022-09-01T08:30:00.000Z',
|
||||
},
|
||||
},
|
||||
};
|
||||
const component = mountWithIntl(
|
||||
<KibanaContextProvider services={services}>
|
||||
<DiscoverNoResults onDisableFilters={() => {}} {...props} />
|
||||
</KibanaContextProvider>
|
||||
);
|
||||
},
|
||||
}));
|
||||
|
||||
async function mountAndFindSubjects(
|
||||
props: Omit<DiscoverNoResultsProps, 'onDisableFilters' | 'data' | 'isTimeBased'>
|
||||
) {
|
||||
const services = createDiscoverServicesMock();
|
||||
|
||||
let component: ReactWrapper;
|
||||
|
||||
await act(async () => {
|
||||
component = await mountWithIntl(
|
||||
<KibanaContextProvider services={services}>
|
||||
<DiscoverNoResults
|
||||
data={services.data}
|
||||
isTimeBased={props.dataView.isTimeBased()}
|
||||
onDisableFilters={() => {}}
|
||||
{...props}
|
||||
/>
|
||||
</KibanaContextProvider>
|
||||
);
|
||||
});
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
await act(async () => {
|
||||
await component!.update();
|
||||
});
|
||||
|
||||
return {
|
||||
mainMsg: findTestSubject(component, 'discoverNoResults').exists(),
|
||||
errorMsg: findTestSubject(component, 'discoverNoResultsError').exists(),
|
||||
adjustTimeRange: findTestSubject(component, 'discoverNoResultsTimefilter').exists(),
|
||||
adjustSearch: findTestSubject(component, 'discoverNoResultsAdjustSearch').exists(),
|
||||
adjustFilters: findTestSubject(component, 'discoverNoResultsAdjustFilters').exists(),
|
||||
checkIndices: findTestSubject(component, 'discoverNoResultsCheckIndices').exists(),
|
||||
disableFiltersButton: findTestSubject(component, 'discoverNoResultsDisableFilters').exists(),
|
||||
mainMsg: findTestSubject(component!, 'discoverNoResults').exists(),
|
||||
errorMsg: findTestSubject(component!, 'discoverNoResultsError').exists(),
|
||||
adjustTimeRange: findTestSubject(component!, 'discoverNoResultsTimefilter').exists(),
|
||||
adjustSearch: findTestSubject(component!, 'discoverNoResultsAdjustSearch').exists(),
|
||||
adjustFilters: findTestSubject(component!, 'discoverNoResultsAdjustFilters').exists(),
|
||||
checkIndices: findTestSubject(component!, 'discoverNoResultsCheckIndices').exists(),
|
||||
disableFiltersButton: findTestSubject(component!, 'discoverNoResultsDisableFilters').exists(),
|
||||
viewMatchesButton: findTestSubject(component!, 'discoverNoResultsViewAllMatches').exists(),
|
||||
};
|
||||
}
|
||||
|
||||
describe('DiscoverNoResults', () => {
|
||||
beforeEach(() => {
|
||||
(RxApi.lastValueFrom as jest.Mock).mockClear();
|
||||
});
|
||||
|
||||
describe('props', () => {
|
||||
describe('no props', () => {
|
||||
test('renders default feedback', () => {
|
||||
const result = mountAndFindSubjects({});
|
||||
test('renders default feedback', async () => {
|
||||
const result = await mountAndFindSubjects({
|
||||
dataView: stubDataViewWithoutTimeField,
|
||||
query: undefined,
|
||||
filters: undefined,
|
||||
});
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"adjustFilters": false,
|
||||
|
@ -57,14 +93,17 @@ describe('DiscoverNoResults', () => {
|
|||
"disableFiltersButton": false,
|
||||
"errorMsg": false,
|
||||
"mainMsg": true,
|
||||
"viewMatchesButton": false,
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
||||
describe('timeFieldName', () => {
|
||||
test('renders time range feedback', () => {
|
||||
const result = mountAndFindSubjects({
|
||||
isTimeBased: true,
|
||||
test('renders time range feedback', async () => {
|
||||
const result = await mountAndFindSubjects({
|
||||
dataView: stubDataView,
|
||||
query: { language: 'lucene', query: '' },
|
||||
filters: [],
|
||||
});
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
|
@ -75,30 +114,42 @@ describe('DiscoverNoResults', () => {
|
|||
"disableFiltersButton": false,
|
||||
"errorMsg": false,
|
||||
"mainMsg": true,
|
||||
"viewMatchesButton": true,
|
||||
}
|
||||
`);
|
||||
expect(RxApi.lastValueFrom).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('filter/query', () => {
|
||||
test('shows "adjust search" message when having query', () => {
|
||||
const result = mountAndFindSubjects({ hasQuery: true });
|
||||
test('shows "adjust search" message when having query', async () => {
|
||||
const result = await mountAndFindSubjects({
|
||||
dataView: stubDataView,
|
||||
query: { language: 'lucene', query: '*' },
|
||||
filters: undefined,
|
||||
});
|
||||
expect(result).toHaveProperty('adjustSearch', true);
|
||||
});
|
||||
|
||||
test('shows "adjust filters" message when having filters', () => {
|
||||
const result = mountAndFindSubjects({ hasFilters: true });
|
||||
test('shows "adjust filters" message when having filters', async () => {
|
||||
const result = await mountAndFindSubjects({
|
||||
dataView: stubDataView,
|
||||
query: { language: 'lucene', query: '' },
|
||||
filters: [{} as Filter],
|
||||
});
|
||||
expect(result).toHaveProperty('adjustFilters', true);
|
||||
expect(result).toHaveProperty('disableFiltersButton', true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('error message', () => {
|
||||
test('renders error message', () => {
|
||||
test('renders error message', async () => {
|
||||
const error = new Error('Fatal error');
|
||||
const result = mountAndFindSubjects({
|
||||
isTimeBased: true,
|
||||
const result = await mountAndFindSubjects({
|
||||
dataView: stubDataView,
|
||||
error,
|
||||
query: { language: 'lucene', query: '' },
|
||||
filters: [{} as Filter],
|
||||
});
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
|
@ -109,6 +160,7 @@ describe('DiscoverNoResults', () => {
|
|||
"disableFiltersButton": false,
|
||||
"errorMsg": true,
|
||||
"mainMsg": false,
|
||||
"viewMatchesButton": false,
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
|
|
@ -8,60 +8,41 @@
|
|||
|
||||
import React, { Fragment } from 'react';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import {
|
||||
EuiButton,
|
||||
EuiCallOut,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiSpacer,
|
||||
EuiTitle,
|
||||
} from '@elastic/eui';
|
||||
import { DataPublicPluginStart } from '@kbn/data-plugin/public';
|
||||
import { EuiButton, EuiCallOut, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
import type { DataView } from '@kbn/data-views-plugin/common';
|
||||
import type { AggregateQuery, Filter, Query } from '@kbn/es-query';
|
||||
import type { DataPublicPluginStart } from '@kbn/data-plugin/public';
|
||||
import { NoResultsSuggestions } from './no_results_suggestions';
|
||||
import './_no_results.scss';
|
||||
import { NoResultsIllustration } from './assets/no_results_illustration';
|
||||
|
||||
export interface DiscoverNoResultsProps {
|
||||
isTimeBased?: boolean;
|
||||
query: Query | AggregateQuery | undefined;
|
||||
filters: Filter[] | undefined;
|
||||
error?: Error;
|
||||
data?: DataPublicPluginStart;
|
||||
hasQuery?: boolean;
|
||||
hasFilters?: boolean;
|
||||
data: DataPublicPluginStart;
|
||||
dataView: DataView;
|
||||
onDisableFilters: () => void;
|
||||
}
|
||||
|
||||
export function DiscoverNoResults({
|
||||
isTimeBased,
|
||||
query,
|
||||
filters,
|
||||
error,
|
||||
data,
|
||||
hasFilters,
|
||||
hasQuery,
|
||||
dataView,
|
||||
onDisableFilters,
|
||||
}: DiscoverNoResultsProps) {
|
||||
const callOut = !error ? (
|
||||
<EuiFlexItem grow={false} className="dscNoResults">
|
||||
<EuiTitle className="dscNoResults__title">
|
||||
<h2 data-test-subj="discoverNoResults">
|
||||
<FormattedMessage
|
||||
id="discover.noResults.searchExamples.noResultsMatchSearchCriteriaTitle"
|
||||
defaultMessage="No results match your search criteria"
|
||||
/>
|
||||
</h2>
|
||||
</EuiTitle>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiFlexGroup gutterSize="xl" alignItems="center" direction="rowReverse" wrap>
|
||||
<EuiFlexItem className="dscNoResults__illustration" grow={1}>
|
||||
<NoResultsIllustration />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={2}>
|
||||
<NoResultsSuggestions
|
||||
isTimeBased={isTimeBased}
|
||||
hasFilters={hasFilters}
|
||||
hasQuery={hasQuery}
|
||||
onDisableFilters={onDisableFilters}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiFlexItem grow={false}>
|
||||
<NoResultsSuggestions
|
||||
isTimeBased={isTimeBased}
|
||||
query={query}
|
||||
filters={filters}
|
||||
dataView={dataView}
|
||||
onDisableFilters={onDisableFilters}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
) : (
|
||||
<EuiFlexItem grow={true} className="dscNoResults">
|
||||
|
|
|
@ -12,8 +12,8 @@ import React from 'react';
|
|||
export const NoResultsIllustration = () => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="226"
|
||||
height="166"
|
||||
width="290"
|
||||
height="213.01"
|
||||
fill="none"
|
||||
viewBox="0 0 226 166"
|
||||
>
|
|
@ -8,17 +8,15 @@
|
|||
|
||||
import React from 'react';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { EuiDescriptionList, EuiDescriptionListDescription } from '@elastic/eui';
|
||||
import { EuiText } from '@elastic/eui';
|
||||
|
||||
export function NoResultsSuggestionDefault() {
|
||||
return (
|
||||
<EuiDescriptionList compressed>
|
||||
<EuiDescriptionListDescription data-test-subj="discoverNoResultsCheckIndices">
|
||||
<FormattedMessage
|
||||
id="discover.noResults.noDocumentsOrCheckPermissionsDescription"
|
||||
defaultMessage="Make sure you have permission to view the indices and that they contain documents."
|
||||
/>
|
||||
</EuiDescriptionListDescription>
|
||||
</EuiDescriptionList>
|
||||
<EuiText data-test-subj="discoverNoResultsCheckIndices">
|
||||
<FormattedMessage
|
||||
id="discover.noResults.noDocumentsOrCheckPermissionsDescription"
|
||||
defaultMessage="Make sure you have permission to view the indices and that they contain documents."
|
||||
/>
|
||||
</EuiText>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -8,12 +8,7 @@
|
|||
|
||||
import React from 'react';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import {
|
||||
EuiDescriptionList,
|
||||
EuiDescriptionListTitle,
|
||||
EuiLink,
|
||||
EuiDescriptionListDescription,
|
||||
} from '@elastic/eui';
|
||||
import { EuiLink, EuiText } from '@elastic/eui';
|
||||
|
||||
export interface NoResultsSuggestionWhenFiltersProps {
|
||||
onDisableFilters: () => void;
|
||||
|
@ -23,29 +18,21 @@ export function NoResultsSuggestionWhenFilters({
|
|||
onDisableFilters,
|
||||
}: NoResultsSuggestionWhenFiltersProps) {
|
||||
return (
|
||||
<EuiDescriptionList compressed>
|
||||
<EuiDescriptionListTitle data-test-subj="discoverNoResultsAdjustFilters">
|
||||
<FormattedMessage
|
||||
id="discover.noResults.adjustFilters"
|
||||
defaultMessage="Adjust your filters"
|
||||
/>
|
||||
</EuiDescriptionListTitle>
|
||||
<EuiDescriptionListDescription>
|
||||
<FormattedMessage
|
||||
id="discover.noResults.tryRemovingOrDisablingFilters"
|
||||
defaultMessage="Try removing or {disablingFiltersLink}."
|
||||
values={{
|
||||
disablingFiltersLink: (
|
||||
<EuiLink data-test-subj="discoverNoResultsDisableFilters" onClick={onDisableFilters}>
|
||||
<FormattedMessage
|
||||
id="discover.noResults.temporaryDisablingFiltersLinkText"
|
||||
defaultMessage="temporarily disabling filters"
|
||||
/>
|
||||
</EuiLink>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</EuiDescriptionListDescription>
|
||||
</EuiDescriptionList>
|
||||
<EuiText data-test-subj="discoverNoResultsAdjustFilters">
|
||||
<FormattedMessage
|
||||
id="discover.noResults.suggestion.removeOrDisableFiltersText"
|
||||
defaultMessage="Remove or {disableFiltersLink}"
|
||||
values={{
|
||||
disableFiltersLink: (
|
||||
<EuiLink data-test-subj="discoverNoResultsDisableFilters" onClick={onDisableFilters}>
|
||||
<FormattedMessage
|
||||
id="discover.noResults.suggestion.disableFiltersLinkText"
|
||||
defaultMessage="disable filters"
|
||||
/>
|
||||
</EuiLink>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</EuiText>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -7,25 +7,199 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { css } from '@emotion/react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import {
|
||||
EuiDescriptionList,
|
||||
EuiDescriptionListTitle,
|
||||
EuiDescriptionListDescription,
|
||||
} from '@elastic/eui';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiText, EuiLink } from '@elastic/eui';
|
||||
import { SyntaxExamples, SyntaxSuggestionsPopover } from './syntax_suggestions_popover';
|
||||
import { type DiscoverServices } from '../../../../../build_services';
|
||||
import { useDiscoverServices } from '../../../../../hooks/use_discover_services';
|
||||
|
||||
export function NoResultsSuggestionWhenQuery() {
|
||||
return (
|
||||
<EuiDescriptionList compressed>
|
||||
<EuiDescriptionListTitle data-test-subj="discoverNoResultsAdjustSearch">
|
||||
<FormattedMessage id="discover.noResults.adjustSearch" defaultMessage="Adjust your query" />
|
||||
</EuiDescriptionListTitle>
|
||||
<EuiDescriptionListDescription>
|
||||
const getExamples = (
|
||||
querySyntax: string | undefined,
|
||||
docLinks: DiscoverServices['docLinks']
|
||||
): SyntaxExamples | null => {
|
||||
if (!querySyntax) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (querySyntax === 'lucene') {
|
||||
return {
|
||||
title: i18n.translate('discover.noResults.luceneExamples.title', {
|
||||
defaultMessage: 'Lucene examples',
|
||||
}),
|
||||
items: [
|
||||
{
|
||||
label: i18n.translate(
|
||||
'discover.noResults.luceneExamples.findRequestsThatContain200Text',
|
||||
{
|
||||
defaultMessage: 'Find requests that contain the number 200, in any field',
|
||||
}
|
||||
),
|
||||
example: '200',
|
||||
},
|
||||
{
|
||||
label: i18n.translate('discover.noResults.luceneExamples.find200InStatusFieldText', {
|
||||
defaultMessage: 'Find 200 in the status field',
|
||||
}),
|
||||
example: 'status:200',
|
||||
},
|
||||
{
|
||||
label: i18n.translate('discover.noResults.luceneExamples.findAllStatusCodesText', {
|
||||
defaultMessage: 'Find all status codes between 400-499',
|
||||
}),
|
||||
example: 'status:[400 TO 499]',
|
||||
},
|
||||
{
|
||||
label: i18n.translate('discover.noResults.luceneExamples.findStatusCodesWithPHPText', {
|
||||
defaultMessage: 'Find status codes 400-499 with the extension php',
|
||||
}),
|
||||
example: 'status:[400 TO 499] AND extension:PHP',
|
||||
},
|
||||
{
|
||||
label: i18n.translate(
|
||||
'discover.noResults.luceneExamples.findStatusCodesWithPhpOrHtmlText',
|
||||
{
|
||||
defaultMessage: 'Find status codes 400-499 with the extension php or html',
|
||||
}
|
||||
),
|
||||
example: 'status:[400 TO 499] AND (extension:php OR extension:html)',
|
||||
},
|
||||
],
|
||||
footer: (
|
||||
<FormattedMessage
|
||||
id="discover.noResults.trySearchingForDifferentCombination"
|
||||
defaultMessage="Try searching for a different combination of terms."
|
||||
id="discover.noResults.luceneExamples.footerDescription"
|
||||
defaultMessage="Learn more about {luceneLink}"
|
||||
values={{
|
||||
luceneLink: (
|
||||
<EuiLink href={docLinks.links.query.luceneQuerySyntax} target="_blank">
|
||||
<FormattedMessage
|
||||
id="discover.noResults.luceneExamples.footerLuceneLink"
|
||||
defaultMessage="query string syntax"
|
||||
/>
|
||||
</EuiLink>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</EuiDescriptionListDescription>
|
||||
</EuiDescriptionList>
|
||||
);
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
if (querySyntax === 'kuery') {
|
||||
return {
|
||||
title: i18n.translate('discover.noResults.kqlExamples.title', {
|
||||
defaultMessage: 'KQL examples',
|
||||
}),
|
||||
items: [
|
||||
{
|
||||
label: i18n.translate('discover.noResults.kqlExamples.filterForExistingFieldsText', {
|
||||
defaultMessage: 'Filter for documents where a field exists',
|
||||
}),
|
||||
example: 'http.request.method: *',
|
||||
},
|
||||
{
|
||||
label: i18n.translate('discover.noResults.kqlExamples.filterForDocsThatMatchValueText', {
|
||||
defaultMessage: 'Filter for documents that match a value',
|
||||
}),
|
||||
example: 'http.request.method: GET',
|
||||
},
|
||||
{
|
||||
label: i18n.translate('discover.noResults.kqlExamples.filterForDocsWithinRangeText', {
|
||||
defaultMessage: 'Filter for documents within a range',
|
||||
}),
|
||||
example: 'http.response.bytes > 10000 and http.response.bytes <= 20000',
|
||||
},
|
||||
{
|
||||
label: i18n.translate('discover.noResults.kqlExamples.filterForDocsWithWildcardsText', {
|
||||
defaultMessage: 'Filter for documents using wildcards',
|
||||
}),
|
||||
example: 'http.response.status_code: 4*',
|
||||
},
|
||||
{
|
||||
label: i18n.translate('discover.noResults.kqlExamples.negatingQueryText', {
|
||||
defaultMessage: 'Negating a query',
|
||||
}),
|
||||
example: 'NOT http.request.method: GET',
|
||||
},
|
||||
{
|
||||
label: i18n.translate('discover.noResults.kqlExamples.combineMultipleText', {
|
||||
defaultMessage: 'Combining multiple queries with AND/OR',
|
||||
}),
|
||||
example: 'http.request.method: GET AND http.response.status_code: 400',
|
||||
},
|
||||
{
|
||||
label: i18n.translate('discover.noResults.kqlExamples.queryMultipleText', {
|
||||
defaultMessage: 'Querying multiple values for the same field',
|
||||
}),
|
||||
example: 'http.request.method: (GET OR POST OR DELETE)',
|
||||
},
|
||||
],
|
||||
footer: (
|
||||
<FormattedMessage
|
||||
id="discover.noResults.kqlExamples.kqlDescription"
|
||||
defaultMessage="Learn more about {kqlLink}"
|
||||
values={{
|
||||
kqlLink: (
|
||||
<EuiLink href={docLinks.links.query.kueryQuerySyntax} target="_blank">
|
||||
<FormattedMessage
|
||||
id="discover.noResults.kqlExamples.footerKQLLink"
|
||||
defaultMessage="KQL"
|
||||
/>
|
||||
</EuiLink>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export interface NoResultsSuggestionWhenQueryProps {
|
||||
querySyntax: string | undefined;
|
||||
}
|
||||
|
||||
export const NoResultsSuggestionWhenQuery: React.FC<NoResultsSuggestionWhenQueryProps> = ({
|
||||
querySyntax,
|
||||
}) => {
|
||||
const services = useDiscoverServices();
|
||||
const { docLinks } = services;
|
||||
const examplesMeta = getExamples(querySyntax, docLinks);
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiFlexGroup
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
gutterSize="xs"
|
||||
responsive={false}
|
||||
wrap={false}
|
||||
css={css`
|
||||
display: inline-flex;
|
||||
`}
|
||||
>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiText data-test-subj="discoverNoResultsAdjustSearch">
|
||||
{examplesMeta ? (
|
||||
<FormattedMessage
|
||||
id="discover.noResults.suggestion.adjustYourQueryWithExamplesText"
|
||||
defaultMessage="Try a different query"
|
||||
/>
|
||||
) : (
|
||||
<FormattedMessage
|
||||
id="discover.noResults.suggestion.adjustYourQueryText"
|
||||
defaultMessage="Adjust your query"
|
||||
/>
|
||||
)}
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
{!!examplesMeta && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<SyntaxSuggestionsPopover meta={examplesMeta} />
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -8,27 +8,15 @@
|
|||
|
||||
import React from 'react';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import {
|
||||
EuiDescriptionList,
|
||||
EuiDescriptionListTitle,
|
||||
EuiDescriptionListDescription,
|
||||
} from '@elastic/eui';
|
||||
import { EuiText } from '@elastic/eui';
|
||||
|
||||
export function NoResultsSuggestionWhenTimeRange() {
|
||||
export const NoResultsSuggestionWhenTimeRange: React.FC = () => {
|
||||
return (
|
||||
<EuiDescriptionList compressed>
|
||||
<EuiDescriptionListTitle data-test-subj="discoverNoResultsTimefilter">
|
||||
<FormattedMessage
|
||||
id="discover.noResults.expandYourTimeRangeTitle"
|
||||
defaultMessage="Expand your time range"
|
||||
/>
|
||||
</EuiDescriptionListTitle>
|
||||
<EuiDescriptionListDescription>
|
||||
<FormattedMessage
|
||||
id="discover.noResults.queryMayNotMatchTitle"
|
||||
defaultMessage="Try searching over a longer period of time."
|
||||
/>
|
||||
</EuiDescriptionListDescription>
|
||||
</EuiDescriptionList>
|
||||
<EuiText data-test-subj="discoverNoResultsTimefilter">
|
||||
<FormattedMessage
|
||||
id="discover.noResults.suggestion.expandTimeRangeText"
|
||||
defaultMessage="Expand the time range"
|
||||
/>
|
||||
</EuiText>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -6,8 +6,18 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { EuiSpacer } from '@elastic/eui';
|
||||
import React, { useState } from 'react';
|
||||
import { css } from '@emotion/react';
|
||||
import { EuiEmptyPrompt, EuiButton, EuiLoadingSpinner, EuiSpacer, useEuiTheme } from '@elastic/eui';
|
||||
import type { DataView } from '@kbn/data-views-plugin/common';
|
||||
import {
|
||||
isOfQueryType,
|
||||
isOfAggregateQueryType,
|
||||
type Query,
|
||||
type AggregateQuery,
|
||||
type Filter,
|
||||
} from '@kbn/es-query';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { NoResultsSuggestionDefault } from './no_results_suggestion_default';
|
||||
import {
|
||||
NoResultsSuggestionWhenFilters,
|
||||
|
@ -15,41 +25,133 @@ import {
|
|||
} from './no_results_suggestion_when_filters';
|
||||
import { NoResultsSuggestionWhenQuery } from './no_results_suggestion_when_query';
|
||||
import { NoResultsSuggestionWhenTimeRange } from './no_results_suggestion_when_time_range';
|
||||
import { hasActiveFilter } from '../../layout/utils';
|
||||
import { useDiscoverServices } from '../../../../../hooks/use_discover_services';
|
||||
import { useFetchOccurrencesRange } from './use_fetch_occurances_range';
|
||||
import { NoResultsIllustration } from './assets/no_results_illustration';
|
||||
|
||||
interface NoResultsSuggestionProps {
|
||||
hasFilters?: boolean;
|
||||
hasQuery?: boolean;
|
||||
dataView: DataView;
|
||||
isTimeBased?: boolean;
|
||||
query: Query | AggregateQuery | undefined;
|
||||
filters: Filter[] | undefined;
|
||||
onDisableFilters: NoResultsSuggestionWhenFiltersProps['onDisableFilters'];
|
||||
}
|
||||
|
||||
export function NoResultsSuggestions({
|
||||
export const NoResultsSuggestions: React.FC<NoResultsSuggestionProps> = ({
|
||||
dataView,
|
||||
isTimeBased,
|
||||
hasFilters,
|
||||
hasQuery,
|
||||
query,
|
||||
filters,
|
||||
onDisableFilters,
|
||||
}: NoResultsSuggestionProps) {
|
||||
}) => {
|
||||
const { euiTheme } = useEuiTheme();
|
||||
const services = useDiscoverServices();
|
||||
const { data, uiSettings, timefilter } = services;
|
||||
const hasQuery =
|
||||
(isOfQueryType(query) && !!query?.query) || (!!query && isOfAggregateQueryType(query));
|
||||
const hasFilters = hasActiveFilter(filters);
|
||||
|
||||
const [isExtending, setIsExtending] = useState<boolean>(false);
|
||||
const { range: occurrencesRange, refetch } = useFetchOccurrencesRange({
|
||||
dataView,
|
||||
query,
|
||||
filters,
|
||||
services: {
|
||||
data,
|
||||
uiSettings,
|
||||
},
|
||||
});
|
||||
|
||||
const extendTimeRange = async () => {
|
||||
setIsExtending(true);
|
||||
const range = await refetch();
|
||||
if (range?.from && range?.to) {
|
||||
timefilter.setTime({
|
||||
from: range.from,
|
||||
to: range.to,
|
||||
});
|
||||
} else {
|
||||
setIsExtending(false);
|
||||
}
|
||||
};
|
||||
|
||||
const canExtendTimeRange = Boolean(occurrencesRange?.from && occurrencesRange.to);
|
||||
const canAdjustSearchCriteria = isTimeBased || hasFilters || hasQuery;
|
||||
|
||||
if (canAdjustSearchCriteria) {
|
||||
return (
|
||||
<>
|
||||
{isTimeBased && <NoResultsSuggestionWhenTimeRange />}
|
||||
const body = canAdjustSearchCriteria ? (
|
||||
<>
|
||||
<FormattedMessage
|
||||
id="discover.noResults.suggestion.tryText"
|
||||
defaultMessage="Here are some things to try:"
|
||||
/>
|
||||
<EuiSpacer size="xs" />
|
||||
<ul
|
||||
css={css`
|
||||
display: inline-block;
|
||||
`}
|
||||
>
|
||||
{isTimeBased && (
|
||||
<li>
|
||||
<NoResultsSuggestionWhenTimeRange />
|
||||
</li>
|
||||
)}
|
||||
{hasQuery && (
|
||||
<>
|
||||
<EuiSpacer size="s" />
|
||||
<NoResultsSuggestionWhenQuery />
|
||||
</>
|
||||
<li>
|
||||
<NoResultsSuggestionWhenQuery
|
||||
querySyntax={isOfQueryType(query) ? query.language : undefined}
|
||||
/>
|
||||
</li>
|
||||
)}
|
||||
{hasFilters && (
|
||||
<>
|
||||
<EuiSpacer size="s" />
|
||||
<li>
|
||||
<NoResultsSuggestionWhenFilters onDisableFilters={onDisableFilters} />
|
||||
</>
|
||||
</li>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
</ul>
|
||||
</>
|
||||
) : (
|
||||
<NoResultsSuggestionDefault />
|
||||
);
|
||||
|
||||
return <NoResultsSuggestionDefault />;
|
||||
}
|
||||
return (
|
||||
<EuiEmptyPrompt
|
||||
layout="horizontal"
|
||||
color="plain"
|
||||
icon={<NoResultsIllustration />}
|
||||
title={
|
||||
<h2 data-test-subj="discoverNoResults">
|
||||
<FormattedMessage
|
||||
id="discover.noResults.searchExamples.noResultsMatchSearchCriteriaTitle"
|
||||
defaultMessage="No results match your search criteria"
|
||||
/>
|
||||
</h2>
|
||||
}
|
||||
body={body}
|
||||
actions={
|
||||
<div
|
||||
css={css`
|
||||
min-block-size: ${euiTheme.size.xxl};
|
||||
`}
|
||||
>
|
||||
{typeof occurrencesRange === 'undefined' ? (
|
||||
<EuiLoadingSpinner />
|
||||
) : canExtendTimeRange ? (
|
||||
<EuiButton
|
||||
color="primary"
|
||||
fill
|
||||
onClick={extendTimeRange}
|
||||
isLoading={isExtending}
|
||||
data-test-subj="discoverNoResultsViewAllMatches"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="discover.noResults.suggestion.viewAllMatchesButtonText"
|
||||
defaultMessage="View all matches"
|
||||
/>
|
||||
</EuiButton>
|
||||
) : null}
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -0,0 +1,102 @@
|
|||
/*
|
||||
* 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 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 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { css } from '@emotion/react';
|
||||
import {
|
||||
EuiBasicTable,
|
||||
EuiButtonIcon,
|
||||
EuiPanel,
|
||||
EuiPopover,
|
||||
EuiPopoverTitle,
|
||||
EuiCode,
|
||||
EuiText,
|
||||
EuiSpacer,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
interface SyntaxExample {
|
||||
label: string;
|
||||
example: string;
|
||||
}
|
||||
|
||||
export interface SyntaxExamples {
|
||||
title: string;
|
||||
footer: React.ReactElement;
|
||||
items: SyntaxExample[];
|
||||
}
|
||||
|
||||
export interface SyntaxSuggestionsPopoverProps {
|
||||
meta: SyntaxExamples;
|
||||
}
|
||||
|
||||
export const SyntaxSuggestionsPopover: React.FC<SyntaxSuggestionsPopoverProps> = ({ meta }) => {
|
||||
const [isOpen, setIsOpen] = useState<boolean>(false);
|
||||
const { title, items, footer } = meta;
|
||||
|
||||
const helpButton = (
|
||||
<EuiButtonIcon
|
||||
onClick={() => setIsOpen((prev) => !prev)}
|
||||
iconType="documentation"
|
||||
aria-label={title}
|
||||
/>
|
||||
);
|
||||
|
||||
const columns = [
|
||||
{
|
||||
field: 'label',
|
||||
name: i18n.translate('discover.noResults.suggestion.syntaxPopoverDescriptionHeader', {
|
||||
defaultMessage: 'Description',
|
||||
}),
|
||||
width: '200px',
|
||||
},
|
||||
{
|
||||
field: 'example',
|
||||
name: i18n.translate('discover.noResults.suggestion.syntaxPopoverExampleHeader', {
|
||||
defaultMessage: 'Example',
|
||||
}),
|
||||
render: (example: string) => <EuiCode>{example}</EuiCode>,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<EuiPopover
|
||||
button={helpButton}
|
||||
isOpen={isOpen}
|
||||
display="inlineBlock"
|
||||
panelPaddingSize="none"
|
||||
closePopover={() => setIsOpen(false)}
|
||||
initialFocus="#querySyntaxBasicTableId"
|
||||
>
|
||||
<EuiPopoverTitle paddingSize="s">{title}</EuiPopoverTitle>
|
||||
<EuiPanel
|
||||
className="eui-yScroll"
|
||||
css={css`
|
||||
max-height: 40vh;
|
||||
max-width: 500px;
|
||||
`}
|
||||
color="transparent"
|
||||
paddingSize="s"
|
||||
>
|
||||
<EuiBasicTable<SyntaxExample>
|
||||
id="querySyntaxBasicTableId"
|
||||
tableCaption={title}
|
||||
items={items}
|
||||
compressed={true}
|
||||
rowHeader="label"
|
||||
columns={columns}
|
||||
responsive
|
||||
/>
|
||||
</EuiPanel>
|
||||
<EuiPanel color="transparent" paddingSize="s">
|
||||
<EuiText size="s">{footer}</EuiText>
|
||||
<EuiSpacer size="xs" />
|
||||
</EuiPanel>
|
||||
</EuiPopover>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,153 @@
|
|||
/*
|
||||
* 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 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 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { lastValueFrom } from 'rxjs';
|
||||
import type { DataView } from '@kbn/data-plugin/common';
|
||||
import type { AggregateQuery, Filter, Query } from '@kbn/es-query';
|
||||
import type { DataPublicPluginStart } from '@kbn/data-plugin/public';
|
||||
import type { IUiSettingsClient } from '@kbn/core-ui-settings-browser';
|
||||
import type { AggregationsSingleMetricAggregateBase } from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
|
||||
import { buildEsQuery } from '@kbn/es-query';
|
||||
import { getEsQueryConfig } from '@kbn/data-plugin/common';
|
||||
|
||||
export interface Params {
|
||||
dataView?: DataView;
|
||||
query?: Query | AggregateQuery;
|
||||
filters?: Filter[];
|
||||
services: {
|
||||
data: DataPublicPluginStart;
|
||||
uiSettings: IUiSettingsClient;
|
||||
};
|
||||
}
|
||||
|
||||
export interface OccurrencesRange {
|
||||
from: string;
|
||||
to: string;
|
||||
}
|
||||
|
||||
export interface Result {
|
||||
range: OccurrencesRange | null | undefined;
|
||||
refetch: () => Promise<OccurrencesRange | null | undefined>;
|
||||
}
|
||||
|
||||
export const useFetchOccurrencesRange = (params: Params): Result => {
|
||||
const data = params.services.data;
|
||||
const uiSettings = params.services.uiSettings;
|
||||
const [range, setRange] = useState<OccurrencesRange | null | undefined>(undefined);
|
||||
const abortControllerRef = useRef<AbortController | null>(null);
|
||||
const mountedRef = useRef<boolean>(true);
|
||||
|
||||
const fetchOccurrences = useCallback(
|
||||
async (dataView?: DataView, query?: Query | AggregateQuery, filters?: Filter[]) => {
|
||||
let occurrencesRange = null;
|
||||
if (!dataView?.timeFieldName || !query || !mountedRef.current) {
|
||||
return null;
|
||||
}
|
||||
|
||||
abortControllerRef.current?.abort();
|
||||
abortControllerRef.current = new AbortController();
|
||||
|
||||
try {
|
||||
const dslQuery = buildEsQuery(
|
||||
dataView,
|
||||
query ?? [],
|
||||
filters ?? [],
|
||||
getEsQueryConfig(uiSettings)
|
||||
);
|
||||
occurrencesRange = await fetchDocumentsTimeRange({
|
||||
data,
|
||||
dataView,
|
||||
dslQuery,
|
||||
abortSignal: abortControllerRef.current?.signal,
|
||||
});
|
||||
} catch (error) {
|
||||
//
|
||||
}
|
||||
|
||||
if (mountedRef.current) {
|
||||
setRange(occurrencesRange);
|
||||
}
|
||||
|
||||
return occurrencesRange;
|
||||
},
|
||||
[abortControllerRef, setRange, mountedRef, data, uiSettings]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
mountedRef.current = false;
|
||||
abortControllerRef.current?.abort();
|
||||
};
|
||||
}, [abortControllerRef, mountedRef]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchOccurrences(params.dataView, params.query, params.filters);
|
||||
}, [fetchOccurrences, params.query, params.filters, params.dataView]);
|
||||
|
||||
return {
|
||||
range,
|
||||
refetch: () => fetchOccurrences(params.dataView, params.query, params.filters),
|
||||
};
|
||||
};
|
||||
|
||||
async function fetchDocumentsTimeRange({
|
||||
data,
|
||||
dataView,
|
||||
dslQuery,
|
||||
abortSignal,
|
||||
}: {
|
||||
data: DataPublicPluginStart;
|
||||
dataView: DataView;
|
||||
dslQuery?: object;
|
||||
abortSignal?: AbortSignal;
|
||||
}): Promise<OccurrencesRange | null> {
|
||||
if (!dataView?.timeFieldName) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const result = await lastValueFrom(
|
||||
data.search.search(
|
||||
{
|
||||
params: {
|
||||
index: dataView.title,
|
||||
size: 0,
|
||||
body: {
|
||||
query: dslQuery ?? { match_all: {} },
|
||||
aggs: {
|
||||
earliest_timestamp: {
|
||||
min: {
|
||||
field: dataView.timeFieldName,
|
||||
},
|
||||
},
|
||||
latest_timestamp: {
|
||||
max: {
|
||||
field: dataView.timeFieldName,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
abortSignal,
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
const earliestTimestamp = (
|
||||
result.rawResponse?.aggregations?.earliest_timestamp as AggregationsSingleMetricAggregateBase
|
||||
)?.value_as_string;
|
||||
const latestTimestamp = (
|
||||
result.rawResponse?.aggregations?.latest_timestamp as AggregationsSingleMetricAggregateBase
|
||||
)?.value_as_string;
|
||||
|
||||
return earliestTimestamp && latestTimestamp
|
||||
? { from: earliestTimestamp, to: latestTimestamp }
|
||||
: null;
|
||||
}
|
|
@ -159,6 +159,15 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
const isVisible = await PageObjects.discover.hasNoResultsTimepicker();
|
||||
expect(isVisible).to.be(true);
|
||||
});
|
||||
|
||||
it('should show matches when time range is expanded', async () => {
|
||||
await PageObjects.discover.expandTimeRangeAsSuggestedInNoResultsMessage();
|
||||
await PageObjects.discover.waitUntilSearchingHasFinished();
|
||||
await retry.try(async function () {
|
||||
expect(await PageObjects.discover.hasNoResults()).to.be(false);
|
||||
expect(await PageObjects.discover.getHitCountInt()).to.be.above(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('nested query', () => {
|
||||
|
|
|
@ -457,6 +457,13 @@ export class DiscoverPageObject extends FtrService {
|
|||
return await this.testSubjects.exists('discoverNoResultsTimefilter');
|
||||
}
|
||||
|
||||
public async expandTimeRangeAsSuggestedInNoResultsMessage() {
|
||||
await this.retry.waitFor('the button before pressing it', async () => {
|
||||
return await this.testSubjects.exists('discoverNoResultsViewAllMatches');
|
||||
});
|
||||
return await this.testSubjects.click('discoverNoResultsViewAllMatches');
|
||||
}
|
||||
|
||||
public async getSidebarAriaDescription(): Promise<string> {
|
||||
return await (
|
||||
await this.testSubjects.find('fieldListGrouped__ariaDescription')
|
||||
|
|
|
@ -2079,7 +2079,6 @@
|
|||
"discover.gridSampleSize.description": "Vous voyez les {sampleSize} premiers échantillons de documents qui correspondent à votre recherche. Pour modifier cette valeur, accédez à {advancedSettingsLink}.",
|
||||
"discover.howToSeeOtherMatchingDocumentsDescription": "Voici les {sampleSize} premiers documents correspondant à votre recherche. Veuillez affiner celle-ci pour en voir plus.",
|
||||
"discover.noMatchRoute.bannerText": "L'application Discover ne reconnaît pas cet itinéraire : {route}",
|
||||
"discover.noResults.tryRemovingOrDisablingFilters": "Essayez de supprimer ou de {disablingFiltersLink}.",
|
||||
"discover.pageTitleWithSavedSearch": "Discover - {savedSearchTitle}",
|
||||
"discover.savedSearchAliasMatchRedirect.objectNoun": "Recherche {savedSearch}",
|
||||
"discover.savedSearchURLConflictCallout.objectNoun": "Recherche {savedSearch}",
|
||||
|
@ -2321,15 +2320,9 @@
|
|||
"discover.localMenu.shareSearchDescription": "Partager la recherche",
|
||||
"discover.localMenu.shareTitle": "Partager",
|
||||
"discover.noMatchRoute.bannerTitleText": "Page introuvable",
|
||||
"discover.noResults.adjustFilters": "Modifiez les filtres.",
|
||||
"discover.noResults.adjustSearch": "Modifiez la requête.",
|
||||
"discover.noResults.expandYourTimeRangeTitle": "Étendre la plage temporelle",
|
||||
"discover.noResults.noDocumentsOrCheckPermissionsDescription": "Assurez-vous de disposer de l'autorisation d'afficher les index et vérifiez qu'ils contiennent des documents.",
|
||||
"discover.noResults.queryMayNotMatchTitle": "Essayez de rechercher sur une période plus longue.",
|
||||
"discover.noResults.searchExamples.noResultsBecauseOfError": "Une erreur s’est produite lors de la récupération des résultats de recherche.",
|
||||
"discover.noResults.searchExamples.noResultsMatchSearchCriteriaTitle": "Aucun résultat ne correspond à vos critères de recherche.",
|
||||
"discover.noResults.temporaryDisablingFiltersLinkText": "désactiver temporairement les filtres",
|
||||
"discover.noResults.trySearchingForDifferentCombination": "Essayez de rechercher une autre combinaison de termes.",
|
||||
"discover.noResultsFound": "Résultat introuvable",
|
||||
"discover.notifications.invalidTimeRangeText": "La plage temporelle spécifiée n'est pas valide (de : \"{from}\" à \"{to}\").",
|
||||
"discover.notifications.invalidTimeRangeTitle": "Plage temporelle non valide",
|
||||
|
|
|
@ -2077,7 +2077,6 @@
|
|||
"discover.gridSampleSize.description": "検索と一致する最初の{sampleSize}ドキュメントを表示しています。この値を変更するには、{advancedSettingsLink}に移動してください。",
|
||||
"discover.howToSeeOtherMatchingDocumentsDescription": "これらは検索条件に一致した初めの {sampleSize} 件のドキュメントです。他の結果を表示するには検索条件を絞ってください。",
|
||||
"discover.noMatchRoute.bannerText": "Discoverアプリケーションはこのルート{route}を認識できません",
|
||||
"discover.noResults.tryRemovingOrDisablingFilters": "削除または{disablingFiltersLink}してください。",
|
||||
"discover.pageTitleWithSavedSearch": "Discover - {savedSearchTitle}",
|
||||
"discover.savedSearchAliasMatchRedirect.objectNoun": "{savedSearch}検索",
|
||||
"discover.savedSearchURLConflictCallout.objectNoun": "{savedSearch}検索",
|
||||
|
@ -2319,15 +2318,9 @@
|
|||
"discover.localMenu.shareSearchDescription": "検索を共有します",
|
||||
"discover.localMenu.shareTitle": "共有",
|
||||
"discover.noMatchRoute.bannerTitleText": "ページが見つかりません",
|
||||
"discover.noResults.adjustFilters": "フィルターを調整",
|
||||
"discover.noResults.adjustSearch": "クエリを調整",
|
||||
"discover.noResults.expandYourTimeRangeTitle": "時間範囲を拡大",
|
||||
"discover.noResults.noDocumentsOrCheckPermissionsDescription": "インデックスと含まれるドキュメントを表示する権限がありません。",
|
||||
"discover.noResults.queryMayNotMatchTitle": "期間を長くして検索を試してください。",
|
||||
"discover.noResults.searchExamples.noResultsBecauseOfError": "検索結果の取得中にエラーが発生しました",
|
||||
"discover.noResults.searchExamples.noResultsMatchSearchCriteriaTitle": "検索条件と一致する結果がありません。",
|
||||
"discover.noResults.temporaryDisablingFiltersLinkText": "フィルターを一時的に無効にしています",
|
||||
"discover.noResults.trySearchingForDifferentCombination": "別の用語の組み合わせを検索してください。",
|
||||
"discover.noResultsFound": "結果が見つかりませんでした",
|
||||
"discover.notifications.invalidTimeRangeText": "指定された時間範囲が無効です。(開始:'{from}'、終了:'{to}')",
|
||||
"discover.notifications.invalidTimeRangeTitle": "無効な時間範囲",
|
||||
|
|
|
@ -2081,7 +2081,6 @@
|
|||
"discover.gridSampleSize.description": "您正查看与您的搜索相匹配的前 {sampleSize} 个文档。要更改此值,请转到{advancedSettingsLink}。",
|
||||
"discover.howToSeeOtherMatchingDocumentsDescription": "下面是与您的搜索匹配的前 {sampleSize} 个文档,请优化您的搜索以查看其他文档。",
|
||||
"discover.noMatchRoute.bannerText": "Discover 应用程序无法识别此路由:{route}",
|
||||
"discover.noResults.tryRemovingOrDisablingFilters": "尝试删除或{disablingFiltersLink}。",
|
||||
"discover.pageTitleWithSavedSearch": "Discover - {savedSearchTitle}",
|
||||
"discover.savedSearchAliasMatchRedirect.objectNoun": "{savedSearch} 搜索",
|
||||
"discover.savedSearchURLConflictCallout.objectNoun": "{savedSearch} 搜索",
|
||||
|
@ -2323,15 +2322,9 @@
|
|||
"discover.localMenu.shareSearchDescription": "共享搜索",
|
||||
"discover.localMenu.shareTitle": "共享",
|
||||
"discover.noMatchRoute.bannerTitleText": "未找到页面",
|
||||
"discover.noResults.adjustFilters": "调整您的筛选",
|
||||
"discover.noResults.adjustSearch": "调整您的查询",
|
||||
"discover.noResults.expandYourTimeRangeTitle": "展开时间范围",
|
||||
"discover.noResults.noDocumentsOrCheckPermissionsDescription": "确保您有权查看索引并且它们包含文档。",
|
||||
"discover.noResults.queryMayNotMatchTitle": "尝试搜索更长的时间段。",
|
||||
"discover.noResults.searchExamples.noResultsBecauseOfError": "检索搜索结果时遇到问题",
|
||||
"discover.noResults.searchExamples.noResultsMatchSearchCriteriaTitle": "没有任何结果匹配您的搜索条件",
|
||||
"discover.noResults.temporaryDisablingFiltersLinkText": "正临时禁用筛选",
|
||||
"discover.noResults.trySearchingForDifferentCombination": "尝试搜索不同的词组合。",
|
||||
"discover.noResultsFound": "找不到结果",
|
||||
"discover.notifications.invalidTimeRangeText": "提供的时间范围无效。(自:“{from}”,至:“{to}”)",
|
||||
"discover.notifications.invalidTimeRangeTitle": "时间范围无效",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue