[Discover] Improve query params validation for single doc view (#121036)

* [Discover] improve query params validation

* [Discover] update types

* [Discover] add unit tests

* [Discover] improve tests, add padding for error prompt

* [Discover] apply comments

* [Discover] fix unit tests, add period

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Dmitry Tomashevich 2021-12-17 15:17:27 +03:00 committed by GitHub
parent e0acc68655
commit 981bbd9497
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 175 additions and 53 deletions

View file

@ -10,26 +10,19 @@ import { useParams } from 'react-router-dom';
import { i18n } from '@kbn/i18n';
import { EuiEmptyPrompt } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { DiscoverServices } from '../../build_services';
import { ContextApp } from './context_app';
import { getRootBreadcrumbs } from '../../utils/breadcrumbs';
import { LoadingIndicator } from '../../components/common/loading_indicator';
import { useIndexPattern } from '../../utils/use_index_pattern';
import { DiscoverRouteProps } from '../types';
import { useMainRouteBreadcrumb } from '../../utils/use_navigation_props';
export interface ContextAppProps {
/**
* Kibana core services used by discover
*/
services: DiscoverServices;
}
export interface ContextUrlParams {
indexPatternId: string;
id: string;
}
export function ContextAppRoute(props: ContextAppProps) {
export function ContextAppRoute(props: DiscoverRouteProps) {
const { services } = props;
const { chrome } = services;

View file

@ -6,21 +6,22 @@
* Side Public License, v 1.
*/
import React, { useEffect } from 'react';
import { useLocation, useParams } from 'react-router-dom';
import { useParams } from 'react-router-dom';
import { EuiEmptyPrompt } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { DiscoverServices } from '../../build_services';
import { getRootBreadcrumbs } from '../../utils/breadcrumbs';
import { Doc } from './components/doc';
import { LoadingIndicator } from '../../components/common/loading_indicator';
import { useIndexPattern } from '../../utils/use_index_pattern';
import { withQueryParams } from '../../utils/with_query_params';
import { useMainRouteBreadcrumb } from '../../utils/use_navigation_props';
import { DiscoverRouteProps } from '../types';
import { Doc } from './components/doc';
export interface SingleDocRouteProps {
export interface SingleDocRouteProps extends DiscoverRouteProps {
/**
* Kibana core services used by discover
* Document id
*/
services: DiscoverServices;
id: string;
}
export interface DocUrlParams {
@ -28,28 +29,21 @@ export interface DocUrlParams {
index: string;
}
function useQuery() {
return new URLSearchParams(useLocation().search);
}
export function SingleDocRoute(props: SingleDocRouteProps) {
const { services } = props;
const SingleDoc = (props: SingleDocRouteProps) => {
const { id, services } = props;
const { chrome, timefilter } = services;
const { indexPatternId, index } = useParams<DocUrlParams>();
const breadcrumb = useMainRouteBreadcrumb();
const query = useQuery();
const docId = query.get('id') || '';
useEffect(() => {
chrome.setBreadcrumbs([
...getRootBreadcrumbs(breadcrumb),
{
text: `${index}#${docId}`,
text: `${index}#${id}`,
},
]);
}, [chrome, index, docId, breadcrumb]);
}, [chrome, index, id, breadcrumb]);
useEffect(() => {
timefilter.disableAutoRefreshSelector();
@ -86,7 +80,9 @@ export function SingleDocRoute(props: SingleDocRouteProps) {
return (
<div className="app-container">
<Doc id={docId} index={index} indexPattern={indexPattern} />
<Doc id={id} index={index} indexPattern={indexPattern} />
</div>
);
}
};
export const SingleDocRoute = withQueryParams(SingleDoc, ['id']);

View file

@ -8,11 +8,8 @@
import React, { useEffect, useState, memo } from 'react';
import { History } from 'history';
import { useParams } from 'react-router-dom';
import { i18n } from '@kbn/i18n';
import { EuiEmptyPrompt } from '@elastic/eui';
import { IndexPatternAttributes, ISearchSource, SavedObject } from 'src/plugins/data/common';
import { DiscoverServices } from '../../build_services';
import {
SavedSearch,
getSavedSearch,
@ -26,39 +23,22 @@ import { redirectWhenMissing } from '../../../../kibana_utils/public';
import { DataViewSavedObjectConflictError } from '../../../../data_views/common';
import { getUrlTracker } from '../../kibana_services';
import { LoadingIndicator } from '../../components/common/loading_indicator';
import { DiscoverError } from '../../components/common/error_alert';
import { DiscoverRouteProps } from '../types';
const DiscoverMainAppMemoized = memo(DiscoverMainApp);
export interface DiscoverMainProps {
export interface DiscoverMainProps extends DiscoverRouteProps {
/**
* Instance of browser history
*/
history: History;
/**
* Kibana core services used by discover
*/
services: DiscoverServices;
}
interface DiscoverLandingParams {
id: string;
}
const DiscoverError = ({ error }: { error: Error }) => (
<EuiEmptyPrompt
iconType="alert"
iconColor="danger"
title={
<h2>
{i18n.translate('discover.discoverError.title', {
defaultMessage: 'Error loading Discover',
})}
</h2>
}
body={<p>{error.message}</p>}
/>
);
export function DiscoverMainRoute({ services, history }: DiscoverMainProps) {
const {
core,

View file

@ -6,6 +6,7 @@
* Side Public License, v 1.
*/
import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import { DiscoverServices } from '../build_services';
export enum FetchStatus {
UNINITIALIZED = 'uninitialized',
@ -24,3 +25,10 @@ export type EsHitRecord = Required<
isAnchor?: boolean;
};
export type EsHitRecordList = EsHitRecord[];
export interface DiscoverRouteProps {
/**
* Kibana core services used by discover
*/
services: DiscoverServices;
}

View file

@ -0,0 +1,45 @@
/*
* 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 from 'react';
import { EuiButton, EuiEmptyPrompt } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import { useHistory } from 'react-router-dom';
export const DiscoverError = ({ error }: { error: Error }) => {
const history = useHistory();
const goToMain = () => {
history.push('/');
};
return (
<EuiEmptyPrompt
paddingSize="l"
iconType="alert"
iconColor="danger"
title={
<h2>
{i18n.translate('discover.discoverError.title', {
defaultMessage: 'Cannot load this page',
})}
</h2>
}
body={<p>{error.message}</p>}
actions={
<EuiButton color="primary" fill onClick={goToMain}>
<FormattedMessage
id="discover.goToDiscoverMainViewButtonText"
defaultMessage="Go to Discover main view"
/>
</EuiButton>
}
/>
);
};

View file

@ -0,0 +1,55 @@
/*
* 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, { ReactElement } from 'react';
import { Router } from 'react-router-dom';
import { createMemoryHistory } from 'history';
import { mountWithIntl } from '@kbn/test/jest';
import { withQueryParams } from './with_query_params';
import { DiscoverServices } from '../build_services';
import { DiscoverRouteProps } from '../application/types';
interface ComponentProps extends DiscoverRouteProps {
id: string;
query: string;
}
const mountComponent = (children: ReactElement, query = '') => {
const history = createMemoryHistory({
initialEntries: ['/' + query],
});
return mountWithIntl(<Router history={history}>{children}</Router>);
};
describe('withQueryParams', () => {
it('should display error message, when query does not contain required parameters', () => {
const Component = withQueryParams(() => <div />, ['id', 'query']);
const component = mountComponent(<Component services={{} as DiscoverServices} />);
expect(component.html()).toContain('Cannot load this page');
expect(component.html()).toContain('URL query string is missing id, query.');
});
it('should not display error message, when query contain required parameters', () => {
const Component = withQueryParams(
({ id, query }: ComponentProps) => (
<div>
{id} and {query} are presented
</div>
),
['id', 'query']
);
const component = mountComponent(
<Component services={{} as DiscoverServices} />,
'?id=one&query=another'
);
expect(component.html()).toContain('one and another are presented');
expect(component.html()).not.toContain('URL query string is missing id, query.');
});
});

View file

@ -0,0 +1,47 @@
/*
* 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, { useMemo } from 'react';
import { i18n } from '@kbn/i18n';
import { useLocation } from 'react-router-dom';
import { DiscoverRouteProps } from '../application/types';
import { DiscoverError } from '../components/common/error_alert';
function useQuery() {
const { search } = useLocation();
return useMemo(() => new URLSearchParams(search), [search]);
}
export const withQueryParams = <P extends DiscoverRouteProps>(
Component: React.ComponentType<P>,
requiredParams: string[]
) => {
return (routeProps: DiscoverRouteProps) => {
const query = useQuery();
const missingParamNames = useMemo(
() => requiredParams.filter((currentParamName) => !query.get(currentParamName)),
[query]
);
if (missingParamNames.length !== 0) {
const missingParamsList = missingParamNames.join(', ');
const errorMessage = i18n.translate('discover.discoverError.missingQueryParamsError', {
defaultMessage: 'URL query string is missing {missingParamsList}.',
values: { missingParamsList },
});
return <DiscoverError error={new Error(errorMessage)} />;
}
const queryProps = Object.fromEntries(
requiredParams.map((current) => [[current], query.get(current)])
);
return <Component {...queryProps} {...routeProps} />;
};
};

View file

@ -1623,7 +1623,6 @@
"discover.discoverBreadcrumbTitle": "Discover",
"discover.discoverDefaultSearchSessionName": "Discover",
"discover.discoverDescription": "ドキュメントにクエリをかけたりフィルターを適用することで、データをインタラクティブに閲覧できます。",
"discover.discoverError.title": "Discoverの読み込みエラー",
"discover.discoverSubtitle": "インサイトを検索して見つけます。",
"discover.discoverTitle": "Discover",
"discover.doc.couldNotFindDocumentsDescription": "そのIDに一致するドキュメントがありません。",

View file

@ -1635,7 +1635,6 @@
"discover.discoverBreadcrumbTitle": "Discover",
"discover.discoverDefaultSearchSessionName": "发现",
"discover.discoverDescription": "通过查询和筛选原始文档来以交互方式浏览您的数据。",
"discover.discoverError.title": "加载 Discover 时出错",
"discover.discoverSubtitle": "搜索和查找数据分析结果。",
"discover.discoverTitle": "Discover",
"discover.doc.couldNotFindDocumentsDescription": "无文档匹配该 ID。",