mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[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:
parent
e0acc68655
commit
981bbd9497
9 changed files with 175 additions and 53 deletions
|
@ -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;
|
||||
|
||||
|
|
|
@ -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']);
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
55
src/plugins/discover/public/utils/with_query_params.test.tsx
Normal file
55
src/plugins/discover/public/utils/with_query_params.test.tsx
Normal 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.');
|
||||
});
|
||||
});
|
47
src/plugins/discover/public/utils/with_query_params.tsx
Normal file
47
src/plugins/discover/public/utils/with_query_params.tsx
Normal 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} />;
|
||||
};
|
||||
};
|
|
@ -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に一致するドキュメントがありません。",
|
||||
|
|
|
@ -1635,7 +1635,6 @@
|
|||
"discover.discoverBreadcrumbTitle": "Discover",
|
||||
"discover.discoverDefaultSearchSessionName": "发现",
|
||||
"discover.discoverDescription": "通过查询和筛选原始文档来以交互方式浏览您的数据。",
|
||||
"discover.discoverError.title": "加载 Discover 时出错",
|
||||
"discover.discoverSubtitle": "搜索和查找数据分析结果。",
|
||||
"discover.discoverTitle": "Discover",
|
||||
"discover.doc.couldNotFindDocumentsDescription": "无文档匹配该 ID。",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue