diff --git a/.eslintrc.js b/.eslintrc.js index 8d979dc0f8645..4425ad3a12659 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -906,6 +906,18 @@ module.exports = { }, }, + /** + * Enterprise Search overrides + */ + { + files: ['x-pack/plugins/enterprise_search/**/*.{ts,tsx}'], + excludedFiles: ['x-pack/plugins/enterprise_search/**/*.{test,mock}.{ts,tsx}'], + rules: { + 'react-hooks/exhaustive-deps': 'off', + '@typescript-eslint/no-explicit-any': 'error', + }, + }, + /** * disable jsx-a11y for kbn-ui-framework */ diff --git a/x-pack/plugins/enterprise_search/public/applications/__mocks__/mount_with_context.mock.tsx b/x-pack/plugins/enterprise_search/public/applications/__mocks__/mount_with_context.mock.tsx index 7d0716ce0cdd0..dfcda544459d4 100644 --- a/x-pack/plugins/enterprise_search/public/applications/__mocks__/mount_with_context.mock.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/__mocks__/mount_with_context.mock.tsx @@ -22,7 +22,7 @@ import { mockLicenseContext } from './license_context.mock'; * * const wrapper = mountWithContext(, { enterpriseSearchUrl: 'someOverride', license: {} }); */ -export const mountWithContext = (children, context) => { +export const mountWithContext = (children: React.ReactNode, context?: object) => { return mount( @@ -40,7 +40,7 @@ export const mountWithContext = (children, context) => { * * Same usage/override functionality as mountWithContext */ -export const mountWithKibanaContext = (children, context) => { +export const mountWithKibanaContext = (children: React.ReactNode, context?: object) => { return mount( {children} diff --git a/x-pack/plugins/enterprise_search/public/applications/__mocks__/shallow_usecontext.mock.ts b/x-pack/plugins/enterprise_search/public/applications/__mocks__/shallow_usecontext.mock.ts index 20add45e16b58..767a52a75d1fb 100644 --- a/x-pack/plugins/enterprise_search/public/applications/__mocks__/shallow_usecontext.mock.ts +++ b/x-pack/plugins/enterprise_search/public/applications/__mocks__/shallow_usecontext.mock.ts @@ -12,7 +12,7 @@ import { mockKibanaContext } from './kibana_context.mock'; import { mockLicenseContext } from './license_context.mock'; jest.mock('react', () => ({ - ...jest.requireActual('react'), + ...(jest.requireActual('react') as object), useContext: jest.fn(() => ({ ...mockKibanaContext, ...mockLicenseContext })), })); diff --git a/x-pack/plugins/enterprise_search/public/applications/__mocks__/shallow_with_i18n.mock.tsx b/x-pack/plugins/enterprise_search/public/applications/__mocks__/shallow_with_i18n.mock.tsx index 7815bb71fa50e..ae7d0b09f9872 100644 --- a/x-pack/plugins/enterprise_search/public/applications/__mocks__/shallow_with_i18n.mock.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/__mocks__/shallow_with_i18n.mock.tsx @@ -20,12 +20,11 @@ const { intl } = intlProvider.getChildContext(); * * const wrapper = shallowWithIntl(); */ -export const shallowWithIntl = (children) => { - return shallow({children}, { - context: { intl }, - childContextTypes: { intl }, - }) +export const shallowWithIntl = (children: React.ReactNode) => { + const context = { context: { intl } }; + + return shallow({children}, context) .childAt(0) - .dive() + .dive(context) .shallow(); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/empty_states/empty_state.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/empty_states/empty_state.tsx index 91b1a4319cbd7..26ed01cc614dc 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/empty_states/empty_state.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/empty_states/empty_state.tsx @@ -16,7 +16,7 @@ import { EngineOverviewHeader } from '../engine_overview_header'; import './empty_states.scss'; -export const EmptyState: React.FC<> = () => { +export const EmptyState: React.FC = () => { const { enterpriseSearchUrl, http } = useContext(KibanaContext) as IKibanaContext; const buttonProps = { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/empty_states/empty_states.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/empty_states/empty_states.test.tsx index bb37229998ed2..2d2f92c2f7b1f 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/empty_states/empty_states.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/empty_states/empty_states.test.tsx @@ -39,7 +39,8 @@ describe('NoUserState', () => { }); it('renders with username', () => { - getUserName.mockImplementationOnce(() => 'dolores-abernathy'); + (getUserName as jest.Mock).mockImplementationOnce(() => 'dolores-abernathy'); + const wrapper = shallowWithIntl(); const prompt = wrapper.find(EuiEmptyPrompt).dive(); const description1 = prompt.find(FormattedMessage).at(1).dive(); @@ -62,7 +63,7 @@ describe('EmptyState', () => { button.simulate('click'); expect(sendTelemetry).toHaveBeenCalled(); - sendTelemetry.mockClear(); + (sendTelemetry as jest.Mock).mockClear(); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/empty_states/error_state.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/empty_states/error_state.tsx index 039e645a27126..5891c89c3a022 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/empty_states/error_state.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/empty_states/error_state.tsx @@ -16,7 +16,7 @@ import { EngineOverviewHeader } from '../engine_overview_header'; import './empty_states.scss'; -export const ErrorState: ReactFC<> = () => { +export const ErrorState: React.FC = () => { const { enterpriseSearchUrl } = useContext(KibanaContext) as IKibanaContext; return ( diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/empty_states/loading_state.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/empty_states/loading_state.tsx index 5c1d0c744f743..3d69fe6126273 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/empty_states/loading_state.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/empty_states/loading_state.tsx @@ -12,7 +12,7 @@ import { EngineOverviewHeader } from '../engine_overview_header'; import './empty_states.scss'; -export const LoadingState: React.FC<> = () => { +export const LoadingState: React.FC = () => { return ( diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/empty_states/no_user_state.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/empty_states/no_user_state.tsx index c1d6c2bcffe41..bf728bd43ead0 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/empty_states/no_user_state.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/empty_states/no_user_state.tsx @@ -15,7 +15,7 @@ import { getUserName } from '../../utils/get_username'; import './empty_states.scss'; -export const NoUserState: React.FC<> = () => { +export const NoUserState: React.FC = () => { const username = getUserName(); return ( diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.test.tsx index a9670163e76b8..18cf3dade2056 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.test.tsx @@ -8,7 +8,7 @@ import '../../../__mocks__/react_router_history.mock'; import React from 'react'; import { act } from 'react-dom/test-utils'; -import { render } from 'enzyme'; +import { render, ReactWrapper } from 'enzyme'; import { I18nProvider } from '@kbn/i18n/react'; import { KibanaContext } from '../../../'; @@ -16,7 +16,7 @@ import { LicenseContext } from '../../../shared/licensing'; import { mountWithContext, mockKibanaContext } from '../../../__mocks__'; import { EmptyState, ErrorState, NoUserState } from '../empty_states'; -import { EngineTable } from './engine_table'; +import { EngineTable, IEngineTablePagination } from './engine_table'; import { EngineOverview } from './'; @@ -25,7 +25,7 @@ describe('EngineOverview', () => { it('isLoading', () => { // We use render() instead of mount() here to not trigger lifecycle methods (i.e., useEffect) // TODO: Consider pulling this out to a renderWithContext mock/helper - const wrapper = render( + const wrapper: Cheerio = render( @@ -85,7 +85,7 @@ describe('EngineOverview', () => { }, }; const mockApi = jest.fn(() => mockedApiResponse); - let wrapper; + let wrapper: ReactWrapper; beforeAll(async () => { wrapper = await mountWithApiMock({ get: mockApi }); @@ -105,7 +105,8 @@ describe('EngineOverview', () => { }); describe('pagination', () => { - const getTablePagination = () => wrapper.find(EngineTable).first().prop('pagination'); + const getTablePagination: () => IEngineTablePagination = () => + wrapper.find(EngineTable).first().prop('pagination'); it('passes down page data from the API', () => { const pagination = getTablePagination(); @@ -156,8 +157,8 @@ describe('EngineOverview', () => { * Test helpers */ - const mountWithApiMock = async ({ get, license }) => { - let wrapper; + const mountWithApiMock = async ({ get, license }: { get(): any; license?: object }) => { + let wrapper: ReactWrapper | undefined; const httpMock = { ...mockKibanaContext.http, get }; // We get a lot of act() warning/errors in the terminal without this. @@ -166,8 +167,12 @@ describe('EngineOverview', () => { await act(async () => { wrapper = mountWithContext(, { http: httpMock, license }); }); - wrapper.update(); // This seems to be required for the DOM to actually update + if (wrapper) { + wrapper.update(); // This seems to be required for the DOM to actually update - return wrapper; + return wrapper; + } else { + throw new Error('Could not mount wrapper'); + } }; }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.tsx index a1e4a11dc2daf..479dfe8e61513 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.tsx @@ -30,7 +30,7 @@ import { EngineTable } from './engine_table'; import './engine_overview.scss'; -export const EngineOverview: ReactFC<> = () => { +export const EngineOverview: React.FC = () => { const { http } = useContext(KibanaContext) as IKibanaContext; const { license } = useContext(LicenseContext) as ILicenseContext; @@ -45,12 +45,12 @@ export const EngineOverview: ReactFC<> = () => { const [metaEnginesPage, setMetaEnginesPage] = useState(1); const [metaEnginesTotal, setMetaEnginesTotal] = useState(0); - const getEnginesData = async ({ type, pageIndex }) => { + const getEnginesData = async ({ type, pageIndex }: IGetEnginesParams) => { return await http.get('/api/app_search/engines', { query: { type, pageIndex }, }); }; - const setEnginesData = async (params, callbacks) => { + const setEnginesData = async (params: IGetEnginesParams, callbacks: ISetEnginesCallbacks) => { try { const response = await getEnginesData(params); @@ -72,7 +72,7 @@ export const EngineOverview: ReactFC<> = () => { const callbacks = { setResults: setEngines, setResultsTotal: setEnginesTotal }; setEnginesData(params, callbacks); - }, [enginesPage]); // eslint-disable-line react-hooks/exhaustive-deps + }, [enginesPage]); useEffect(() => { if (hasPlatinumLicense(license)) { @@ -81,7 +81,7 @@ export const EngineOverview: ReactFC<> = () => { setEnginesData(params, callbacks); } - }, [license, metaEnginesPage]); // eslint-disable-line react-hooks/exhaustive-deps + }, [license, metaEnginesPage]); if (hasErrorConnecting) return ; if (hasNoAccount) return ; @@ -150,3 +150,16 @@ export const EngineOverview: ReactFC<> = () => { ); }; + +/** + * Type definitions + */ + +interface IGetEnginesParams { + type: string; + pageIndex: number; +} +interface ISetEnginesCallbacks { + setResults: React.Dispatch>; + setResultsTotal: React.Dispatch>; +} diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_table.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_table.test.tsx index 1665726251bd6..46b6e61e352de 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_table.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_table.test.tsx @@ -72,9 +72,9 @@ describe('EngineTable', () => { it('handles empty data', () => { const emptyWrapper = mountWithContext( - + {} }} /> ); - const emptyTable = wrapper.find(EuiBasicTable); + const emptyTable = emptyWrapper.find(EuiBasicTable); expect(emptyTable.prop('pagination').pageIndex).toEqual(0); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_table.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_table.tsx index d565856f9675d..1e58d820dc83b 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_table.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_table.tsx @@ -5,7 +5,7 @@ */ import React, { useContext } from 'react'; -import { EuiBasicTable, EuiLink } from '@elastic/eui'; +import { EuiBasicTable, EuiBasicTableColumn, EuiLink } from '@elastic/eui'; import { FormattedMessage, FormattedDate, FormattedNumber } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; @@ -14,31 +14,33 @@ import { KibanaContext, IKibanaContext } from '../../../index'; import { ENGINES_PAGE_SIZE } from '../../../../../common/constants'; -interface IEngineTableProps { - data: Array<{ - name: string; - created_at: string; - document_count: number; - field_count: number; - }>; - pagination: { - totalEngines: number; - pageIndex: number; - onPaginate(pageIndex: number); - }; +export interface IEngineTableData { + name: string; + created_at: string; + document_count: number; + field_count: number; +} +export interface IEngineTablePagination { + totalEngines: number; + pageIndex: number; + onPaginate(pageIndex: number): void; +} +export interface IEngineTableProps { + data: IEngineTableData[]; + pagination: IEngineTablePagination; } -interface IOnChange { +export interface IOnChange { page: { index: number; }; } -export const EngineTable: ReactFC = ({ +export const EngineTable: React.FC = ({ data, - pagination: { totalEngines, pageIndex = 0, onPaginate }, + pagination: { totalEngines, pageIndex, onPaginate }, }) => { const { enterpriseSearchUrl, http } = useContext(KibanaContext) as IKibanaContext; - const engineLinkProps = (name) => ({ + const engineLinkProps = (name: string) => ({ href: `${enterpriseSearchUrl}/as/engines/${name}`, target: '_blank', onClick: () => @@ -50,13 +52,13 @@ export const EngineTable: ReactFC = ({ }), }); - const columns = [ + const columns: Array> = [ { field: 'name', name: i18n.translate('xpack.enterpriseSearch.appSearch.enginesOverview.table.column.name', { defaultMessage: 'Name', }), - render: (name) => ( + render: (name: string) => ( {name} @@ -65,6 +67,8 @@ export const EngineTable: ReactFC = ({ truncateText: true, mobileOptions: { header: true, + // Note: the below props are valid props per https://elastic.github.io/eui/#/tabular-content/tables (Responsive tables), but EUI's types have a bug reporting it as an error + // @ts-ignore enlarge: true, fullWidth: true, truncateText: false, @@ -79,7 +83,7 @@ export const EngineTable: ReactFC = ({ } ), dataType: 'string', - render: (dateString) => ( + render: (dateString: string) => ( // e.g., January 1, 1970 ), @@ -93,7 +97,7 @@ export const EngineTable: ReactFC = ({ } ), dataType: 'number', - render: (number) => , + render: (number: number) => , truncateText: true, }, { @@ -105,7 +109,7 @@ export const EngineTable: ReactFC = ({ } ), dataType: 'number', - render: (number) => , + render: (number: number) => , truncateText: true, }, { @@ -117,7 +121,7 @@ export const EngineTable: ReactFC = ({ } ), dataType: 'string', - render: (name) => ( + render: (name: string) => ( = ({ totalItemCount: totalEngines, hidePerPageOptions: true, }} - onChange={({ page }): IOnChange => { + onChange={({ page }: IOnChange) => { const { index } = page; onPaginate(index + 1); // Note on paging - App Search's API pages start at 1, EuiBasicTables' pages start at 0 }} diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview_header/engine_overview_header.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview_header/engine_overview_header.test.tsx index 9663eb4ef61af..2e49540270ef0 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview_header/engine_overview_header.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview_header/engine_overview_header.test.tsx @@ -24,8 +24,8 @@ describe('EngineOverviewHeader', () => { const wrapper = shallow(); const button = wrapper.find('[data-test-subj="launchButton"]'); - expect(button.props().href).toBe('http://localhost:3002/as'); - expect(button.props().isDisabled).toBeFalsy(); + expect(button.prop('href')).toBe('http://localhost:3002/as'); + expect(button.prop('isDisabled')).toBeFalsy(); button.simulate('click'); expect(sendTelemetry).toHaveBeenCalled(); @@ -35,7 +35,7 @@ describe('EngineOverviewHeader', () => { const wrapper = shallow(); const button = wrapper.find('[data-test-subj="launchButton"]'); - expect(button.props().isDisabled).toBe(true); - expect(button.props().href).toBeUndefined(); + expect(button.prop('isDisabled')).toBe(true); + expect(button.prop('href')).toBeUndefined(); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview_header/engine_overview_header.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview_header/engine_overview_header.tsx index 650a864f5e615..9aafa8ec0380c 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview_header/engine_overview_header.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview_header/engine_overview_header.tsx @@ -5,7 +5,14 @@ */ import React, { useContext } from 'react'; -import { EuiPageHeader, EuiPageHeaderSection, EuiTitle, EuiButton } from '@elastic/eui'; +import { + EuiPageHeader, + EuiPageHeaderSection, + EuiTitle, + EuiButton, + EuiButtonProps, + EuiLinkProps, +} from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { sendTelemetry } from '../../../shared/telemetry'; @@ -24,7 +31,8 @@ export const EngineOverviewHeader: React.FC = ({ fill: true, iconType: 'popout', 'data-test-subj': 'launchButton', - }; + } as EuiButtonProps & EuiLinkProps; + if (isButtonDisabled) { buttonProps.isDisabled = true; } else { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/setup_guide/setup_guide.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/setup_guide/setup_guide.tsx index 855449b0c0bcd..3e290a7777f1c 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/setup_guide/setup_guide.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/setup_guide/setup_guide.tsx @@ -15,7 +15,6 @@ import { EuiFlexItem, EuiTitle, EuiText, - EuiImage, EuiIcon, EuiSteps, EuiCode, @@ -32,7 +31,7 @@ import { SendAppSearchTelemetry as SendTelemetry } from '../../../shared/telemet import GettingStarted from '../../assets/getting_started.png'; import './setup_guide.scss'; -export const SetupGuide: React.FC<> = () => { +export const SetupGuide: React.FC = () => { return ( diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/index.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/index.test.tsx index d11c47475089d..45e318ca0f9d9 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/index.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/index.test.tsx @@ -18,7 +18,7 @@ import { AppSearch } from './'; describe('App Search Routes', () => { describe('/', () => { it('redirects to Setup Guide when enterpriseSearchUrl is not set', () => { - useContext.mockImplementationOnce(() => ({ enterpriseSearchUrl: '' })); + (useContext as jest.Mock).mockImplementationOnce(() => ({ enterpriseSearchUrl: '' })); const wrapper = shallow(); expect(wrapper.find(Redirect)).toHaveLength(1); @@ -26,7 +26,9 @@ describe('App Search Routes', () => { }); it('renders Engine Overview when enterpriseSearchUrl is set', () => { - useContext.mockImplementationOnce(() => ({ enterpriseSearchUrl: 'https://foo.bar' })); + (useContext as jest.Mock).mockImplementationOnce(() => ({ + enterpriseSearchUrl: 'https://foo.bar', + })); const wrapper = shallow(); expect(wrapper.find(EngineOverview)).toHaveLength(1); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/index.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/index.tsx index 9afc3c9fd9761..8f7142f1631a9 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/index.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/index.tsx @@ -12,7 +12,7 @@ import { KibanaContext, IKibanaContext } from '../index'; import { SetupGuide } from './components/setup_guide'; import { EngineOverview } from './components/engine_overview'; -export const AppSearch: React.FC<> = () => { +export const AppSearch: React.FC = () => { const { enterpriseSearchUrl } = useContext(KibanaContext) as IKibanaContext; return ( diff --git a/x-pack/plugins/enterprise_search/public/applications/index.test.tsx b/x-pack/plugins/enterprise_search/public/applications/index.test.tsx index fd88fc32ff4ae..ef69ba7e40cf3 100644 --- a/x-pack/plugins/enterprise_search/public/applications/index.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/index.test.tsx @@ -18,14 +18,14 @@ describe('renderApp', () => { const config = {}; const plugins = { licensing: licensingMock.createSetup(), - }; + } as any; beforeEach(() => { jest.clearAllMocks(); }); it('mounts and unmounts UI', () => { - const MockApp: React.FC = () =>
Hello world!
; + const MockApp = () =>
Hello world!
; const unmount = renderApp(MockApp, core, params, config, plugins); expect(params.element.querySelector('.hello-world')).not.toBeNull(); diff --git a/x-pack/plugins/enterprise_search/public/applications/index.tsx b/x-pack/plugins/enterprise_search/public/applications/index.tsx index ae7079befb8c9..4ef7aca8260a2 100644 --- a/x-pack/plugins/enterprise_search/public/applications/index.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/index.tsx @@ -9,18 +9,17 @@ import ReactDOM from 'react-dom'; import { Router } from 'react-router-dom'; import { I18nProvider } from '@kbn/i18n/react'; -import { CoreStart, AppMountParams, HttpHandler } from 'src/core/public'; +import { CoreStart, AppMountParameters, HttpSetup, ChromeBreadcrumb } from 'src/core/public'; import { ClientConfigType, PluginsSetup } from '../plugin'; -import { TSetBreadcrumbs } from './shared/kibana_breadcrumbs'; import { LicenseProvider } from './shared/licensing'; export interface IKibanaContext { enterpriseSearchUrl?: string; - http(): HttpHandler; - setBreadCrumbs(): TSetBreadcrumbs; + http: HttpSetup; + setBreadcrumbs(crumbs: ChromeBreadcrumb[]): void; } -export const KibanaContext = React.createContext(); +export const KibanaContext = React.createContext({}); /** * This file serves as a reusable wrapper to share Kibana-level context and other helpers @@ -29,9 +28,9 @@ export const KibanaContext = React.createContext(); */ export const renderApp = ( - App: React.Element, + App: React.FC, core: CoreStart, - params: AppMountParams, + params: AppMountParameters, config: ClientConfigType, plugins: PluginsSetup ) => { diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/kibana_breadcrumbs/generate_breadcrumbs.test.ts b/x-pack/plugins/enterprise_search/public/applications/shared/kibana_breadcrumbs/generate_breadcrumbs.test.ts index b07aacf443abb..5a5cce6ec23b6 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/kibana_breadcrumbs/generate_breadcrumbs.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/kibana_breadcrumbs/generate_breadcrumbs.test.ts @@ -7,7 +7,8 @@ import { generateBreadcrumb } from './generate_breadcrumbs'; import { appSearchBreadcrumbs, enterpriseSearchBreadcrumbs } from './'; -import { mockHistory } from '../../__mocks__'; +import { mockHistory as mockHistoryUntyped } from '../../__mocks__'; +const mockHistory = mockHistoryUntyped as any; jest.mock('../react_router_helpers', () => ({ letBrowserHandleEvent: jest.fn(() => false) })); import { letBrowserHandleEvent } from '../react_router_helpers'; @@ -31,7 +32,7 @@ describe('generateBreadcrumb', () => { }); it('prevents default navigation and uses React Router history on click', () => { - const breadcrumb = generateBreadcrumb({ text: '', path: '/', history: mockHistory }); + const breadcrumb = generateBreadcrumb({ text: '', path: '/', history: mockHistory }) as any; const event = { preventDefault: jest.fn() }; breadcrumb.onClick(event); @@ -40,9 +41,9 @@ describe('generateBreadcrumb', () => { }); it('does not prevents default browser behavior on new tab/window clicks', () => { - const breadcrumb = generateBreadcrumb({ text: '', path: '/', history: mockHistory }); + const breadcrumb = generateBreadcrumb({ text: '', path: '/', history: mockHistory }) as any; - letBrowserHandleEvent.mockImplementationOnce(() => true); + (letBrowserHandleEvent as jest.Mock).mockImplementationOnce(() => true); breadcrumb.onClick(); expect(mockHistory.push).not.toHaveBeenCalled(); @@ -103,19 +104,19 @@ describe('enterpriseSearchBreadcrumbs', () => { describe('links', () => { const eventMock = { preventDefault: jest.fn(), - }; + } as any; it('has Enterprise Search text first', () => { expect(subject()[0].onClick).toBeUndefined(); }); it('has a link to page 1 second', () => { - subject()[1].onClick(eventMock); + (subject()[1] as any).onClick(eventMock); expect(mockHistory.push).toHaveBeenCalledWith('/page1'); }); it('has a link to page 2 last', () => { - subject()[2].onClick(eventMock); + (subject()[2] as any).onClick(eventMock); expect(mockHistory.push).toHaveBeenCalledWith('/page2'); }); }); @@ -136,7 +137,7 @@ describe('appSearchBreadcrumbs', () => { beforeEach(() => { jest.clearAllMocks(); mockHistory.createHref.mockImplementation( - ({ pathname }) => `/enterprise_search/app_search${pathname}` + ({ pathname }: any) => `/enterprise_search/app_search${pathname}` ); }); @@ -181,24 +182,24 @@ describe('appSearchBreadcrumbs', () => { describe('links', () => { const eventMock = { preventDefault: jest.fn(), - }; + } as any; it('has Enterprise Search text first', () => { expect(subject()[0].onClick).toBeUndefined(); }); it('has a link to App Search second', () => { - subject()[1].onClick(eventMock); + (subject()[1] as any).onClick(eventMock); expect(mockHistory.push).toHaveBeenCalledWith('/'); }); it('has a link to page 1 third', () => { - subject()[2].onClick(eventMock); + (subject()[2] as any).onClick(eventMock); expect(mockHistory.push).toHaveBeenCalledWith('/page1'); }); it('has a link to page 2 last', () => { - subject()[3].onClick(eventMock); + (subject()[3] as any).onClick(eventMock); expect(mockHistory.push).toHaveBeenCalledWith('/page2'); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/kibana_breadcrumbs/generate_breadcrumbs.ts b/x-pack/plugins/enterprise_search/public/applications/shared/kibana_breadcrumbs/generate_breadcrumbs.ts index 659a113dc31de..0e1bb796cbf2e 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/kibana_breadcrumbs/generate_breadcrumbs.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/kibana_breadcrumbs/generate_breadcrumbs.ts @@ -21,7 +21,7 @@ interface IGenerateBreadcrumbProps { } export const generateBreadcrumb = ({ text, path, history }: IGenerateBreadcrumbProps) => { - const breadcrumb = { text }; + const breadcrumb = { text } as EuiBreadcrumb; if (path && history) { breadcrumb.href = history.createHref({ pathname: path }); @@ -39,13 +39,15 @@ export const generateBreadcrumb = ({ text, path, history }: IGenerateBreadcrumbP * Product-specific breadcrumb helpers */ -type TBreadcrumbs = EuiBreadcrumb[] | []; +export type TBreadcrumbs = IGenerateBreadcrumbProps[]; export const enterpriseSearchBreadcrumbs = (history: History) => ( breadcrumbs: TBreadcrumbs = [] ) => [ generateBreadcrumb({ text: 'Enterprise Search' }), - ...breadcrumbs.map(({ text, path }) => generateBreadcrumb({ text, path, history })), + ...breadcrumbs.map(({ text, path }: IGenerateBreadcrumbProps) => + generateBreadcrumb({ text, path, history }) + ), ]; export const appSearchBreadcrumbs = (history: History) => (breadcrumbs: TBreadcrumbs = []) => diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/kibana_breadcrumbs/set_breadcrumbs.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/kibana_breadcrumbs/set_breadcrumbs.test.tsx index aeaa38a5ad38f..974ca54277c51 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/kibana_breadcrumbs/set_breadcrumbs.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/kibana_breadcrumbs/set_breadcrumbs.test.tsx @@ -14,16 +14,16 @@ import { appSearchBreadcrumbs, SetAppSearchBreadcrumbs } from './'; describe('SetAppSearchBreadcrumbs', () => { const setBreadcrumbs = jest.fn(); - const builtBreadcrumbs = []; + const builtBreadcrumbs = [] as any; const appSearchBreadCrumbsInnerCall = jest.fn().mockReturnValue(builtBreadcrumbs); const appSearchBreadCrumbsOuterCall = jest.fn().mockReturnValue(appSearchBreadCrumbsInnerCall); - appSearchBreadcrumbs.mockImplementation(appSearchBreadCrumbsOuterCall); + (appSearchBreadcrumbs as jest.Mock).mockImplementation(appSearchBreadCrumbsOuterCall); afterEach(() => { jest.clearAllMocks(); }); - const mountSetAppSearchBreadcrumbs = (props) => { + const mountSetAppSearchBreadcrumbs = (props: any) => { return mountWithKibanaContext(, { http: {}, enterpriseSearchUrl: 'http://localhost:3002', diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/kibana_breadcrumbs/set_breadcrumbs.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/kibana_breadcrumbs/set_breadcrumbs.tsx index aaa54febcc20b..ad3cd65c09516 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/kibana_breadcrumbs/set_breadcrumbs.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/kibana_breadcrumbs/set_breadcrumbs.tsx @@ -8,7 +8,7 @@ import React, { useContext, useEffect } from 'react'; import { useHistory } from 'react-router-dom'; import { Breadcrumb as EuiBreadcrumb } from '@elastic/eui'; import { KibanaContext, IKibanaContext } from '../../index'; -import { appSearchBreadcrumbs } from './generate_breadcrumbs'; +import { appSearchBreadcrumbs, TBreadcrumbs } from './generate_breadcrumbs'; /** * Small on-mount helper for setting Kibana's chrome breadcrumbs on any App Search view @@ -17,20 +17,27 @@ import { appSearchBreadcrumbs } from './generate_breadcrumbs'; export type TSetBreadcrumbs = (breadcrumbs: EuiBreadcrumb[]) => void; -interface ISetBreadcrumbsProps { +interface IBreadcrumbProps { text: string; - isRoot?: boolean; + isRoot?: never; +} +interface IRootBreadcrumbProps { + isRoot: true; + text?: never; } -export const SetAppSearchBreadcrumbs: React.FC = ({ text, isRoot }) => { +export const SetAppSearchBreadcrumbs: React.FC = ({ + text, + isRoot, +}) => { const history = useHistory(); const { setBreadcrumbs } = useContext(KibanaContext) as IKibanaContext; const crumb = isRoot ? [] : [{ text, path: history.location.pathname }]; useEffect(() => { - setBreadcrumbs(appSearchBreadcrumbs(history)(crumb)); - }, []); // eslint-disable-line react-hooks/exhaustive-deps + setBreadcrumbs(appSearchBreadcrumbs(history)(crumb as TBreadcrumbs | [])); + }, []); return null; }; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/licensing/license_checks.test.ts b/x-pack/plugins/enterprise_search/public/applications/shared/licensing/license_checks.test.ts index e21bf004b39a2..ad134e7d36b10 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/licensing/license_checks.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/licensing/license_checks.test.ts @@ -8,26 +8,26 @@ import { hasPlatinumLicense } from './license_checks'; describe('hasPlatinumLicense', () => { it('is true for platinum licenses', () => { - expect(hasPlatinumLicense({ isActive: true, type: 'platinum' })).toEqual(true); + expect(hasPlatinumLicense({ isActive: true, type: 'platinum' } as any)).toEqual(true); }); it('is true for enterprise licenses', () => { - expect(hasPlatinumLicense({ isActive: true, type: 'enterprise' })).toEqual(true); + expect(hasPlatinumLicense({ isActive: true, type: 'enterprise' } as any)).toEqual(true); }); it('is true for trial licenses', () => { - expect(hasPlatinumLicense({ isActive: true, type: 'platinum' })).toEqual(true); + expect(hasPlatinumLicense({ isActive: true, type: 'platinum' } as any)).toEqual(true); }); it('is false if the current license is expired', () => { - expect(hasPlatinumLicense({ isActive: false, type: 'platinum' })).toEqual(false); - expect(hasPlatinumLicense({ isActive: false, type: 'enterprise' })).toEqual(false); - expect(hasPlatinumLicense({ isActive: false, type: 'trial' })).toEqual(false); + expect(hasPlatinumLicense({ isActive: false, type: 'platinum' } as any)).toEqual(false); + expect(hasPlatinumLicense({ isActive: false, type: 'enterprise' } as any)).toEqual(false); + expect(hasPlatinumLicense({ isActive: false, type: 'trial' } as any)).toEqual(false); }); it('is false for licenses below platinum', () => { - expect(hasPlatinumLicense({ isActive: true, type: 'basic' })).toEqual(false); - expect(hasPlatinumLicense({ isActive: false, type: 'standard' })).toEqual(false); - expect(hasPlatinumLicense({ isActive: true, type: 'gold' })).toEqual(false); + expect(hasPlatinumLicense({ isActive: true, type: 'basic' } as any)).toEqual(false); + expect(hasPlatinumLicense({ isActive: false, type: 'standard' } as any)).toEqual(false); + expect(hasPlatinumLicense({ isActive: true, type: 'gold' } as any)).toEqual(false); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/licensing/license_checks.ts b/x-pack/plugins/enterprise_search/public/applications/shared/licensing/license_checks.ts index 7d0de8a093b31..363ae39ab0da4 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/licensing/license_checks.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/licensing/license_checks.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ILicense } from '../../../../../../licensing/public'; +import { ILicense } from '../../../../../licensing/public'; -export const hasPlatinumLicense = (license: ILicenseContext) => { - return license?.isActive && ['platinum', 'enterprise', 'trial'].includes(license?.type); +export const hasPlatinumLicense = (license: ILicense) => { + return license?.isActive && ['platinum', 'enterprise', 'trial'].includes(license?.type as string); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/licensing/license_context.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/licensing/license_context.test.tsx index 01d976bf49c19..c65474ec1f590 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/licensing/license_context.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/licensing/license_context.test.tsx @@ -10,9 +10,9 @@ import { mountWithContext } from '../../__mocks__'; import { LicenseContext, ILicenseContext } from './'; describe('LicenseProvider', () => { - const MockComponent: React.FC<> = () => { + const MockComponent: React.FC = () => { const { license } = useContext(LicenseContext) as ILicenseContext; - return
{license.type}
; + return
{license?.type}
; }; it('renders children', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/licensing/license_context.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/licensing/license_context.tsx index 03787031bc075..9b47959ff7544 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/licensing/license_context.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/licensing/license_context.tsx @@ -8,17 +8,17 @@ import React from 'react'; import useObservable from 'react-use/lib/useObservable'; import { Observable } from 'rxjs'; -import { ILicense } from '../../../../licensing/public'; +import { ILicense } from '../../../../../licensing/public'; export interface ILicenseContext { - license?: ILicense; + license: ILicense; } interface ILicenseContextProps { license$: Observable; children: React.ReactNode; } -export const LicenseContext = React.createContext(); +export const LicenseContext = React.createContext({}); export const LicenseProvider: React.FC = ({ license$, children }) => { // Listen for changes to license subscription diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/eui_link.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/eui_link.test.tsx index eb9b9f3e35e06..7d4c068b21155 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/eui_link.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/eui_link.test.tsx @@ -5,7 +5,7 @@ */ import React from 'react'; -import { shallow } from 'enzyme'; +import { shallow, mount } from 'enzyme'; import { EuiLink, EuiButton } from '@elastic/eui'; import '../../__mocks__/react_router_history.mock'; @@ -25,23 +25,21 @@ describe('EUI & React Router Component Helpers', () => { }); it('renders an EuiButton', () => { - const wrapper = shallow() - .find(EuiReactRouterLink) - .dive(); + const wrapper = shallow(); expect(wrapper.find(EuiButton)).toHaveLength(1); }); it('passes down all ...rest props', () => { - const wrapper = shallow(); + const wrapper = shallow(); const link = wrapper.find(EuiLink); - expect(link.prop('disabled')).toEqual(true); + expect(link.prop('external')).toEqual(true); expect(link.prop('data-test-subj')).toEqual('foo'); }); it('renders with the correct href and onClick props', () => { - const wrapper = shallow(); + const wrapper = mount(); const link = wrapper.find(EuiLink); expect(link.prop('onClick')).toBeInstanceOf(Function); @@ -51,7 +49,7 @@ describe('EUI & React Router Component Helpers', () => { describe('onClick', () => { it('prevents default navigation and uses React Router history', () => { - const wrapper = shallow(); + const wrapper = mount(); const simulatedEvent = { button: 0, @@ -65,7 +63,7 @@ describe('EUI & React Router Component Helpers', () => { }); it('does not prevent default browser behavior on new tab/window clicks', () => { - const wrapper = shallow(); + const wrapper = mount(); const simulatedEvent = { shiftKey: true, diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/eui_link.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/eui_link.tsx index 3c410584cc49d..f486e432bae76 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/eui_link.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/eui_link.tsx @@ -6,7 +6,7 @@ import React from 'react'; import { useHistory } from 'react-router-dom'; -import { EuiLink, EuiButton } from '@elastic/eui'; +import { EuiLink, EuiButton, EuiButtonProps, EuiLinkAnchorProps } from '@elastic/eui'; import { letBrowserHandleEvent } from './link_events'; @@ -19,13 +19,12 @@ import { letBrowserHandleEvent } from './link_events'; interface IEuiReactRouterProps { to: string; - isButton?: boolean; } -export const EuiReactRouterLink: React.FC = ({ to, isButton, ...rest }) => { +export const EuiReactRouterHelper: React.FC = ({ to, children }) => { const history = useHistory(); - const onClick = (event) => { + const onClick = (event: React.MouseEvent) => { if (letBrowserHandleEvent(event)) return; // Prevent regular link behavior, which causes a browser refresh. @@ -38,10 +37,21 @@ export const EuiReactRouterLink: React.FC = ({ to, isButto // Generate the correct link href (with basename etc. accounted for) const href = history.createHref({ pathname: to }); - const props = { ...rest, href, onClick }; - return isButton ? : ; + const reactRouterProps = { href, onClick }; + return React.cloneElement(children as React.ReactElement, reactRouterProps); }; -export const EuiReactRouterButton: React.FC = (props) => ( - +type TEuiReactRouterLinkProps = EuiLinkAnchorProps & IEuiReactRouterProps; +type TEuiReactRouterButtonProps = EuiButtonProps & IEuiReactRouterProps; + +export const EuiReactRouterLink: React.FC = ({ to, ...rest }) => ( + + + +); + +export const EuiReactRouterButton: React.FC = ({ to, ...rest }) => ( + + + ); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/link_events.test.ts b/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/link_events.test.ts index 0845e5562776b..3682946b63a13 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/link_events.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/link_events.test.ts @@ -17,7 +17,7 @@ describe('letBrowserHandleEvent', () => { target: { getAttribute: () => '_self', }, - }; + } as any; describe('the browser should handle the link when', () => { it('default is prevented', () => { @@ -95,7 +95,7 @@ describe('letBrowserHandleEvent', () => { }); }); -const targetValue = (value) => { +const targetValue = (value: string | null) => { return { getAttribute: () => value, }; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/link_events.ts b/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/link_events.ts index 67e987623c2c1..93da2ab71d952 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/link_events.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/link_events.ts @@ -4,14 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SyntheticEvent } from 'react'; +import { MouseEvent } from 'react'; /** * Helper functions for determining which events we should * let browsers handle natively, e.g. new tabs/windows */ -type THandleEvent = (event: SyntheticEvent) => boolean; +type THandleEvent = (event: MouseEvent) => boolean; export const letBrowserHandleEvent: THandleEvent = (event) => event.defaultPrevented || @@ -25,6 +25,7 @@ const isModifiedEvent: THandleEvent = (event) => const isLeftClickEvent: THandleEvent = (event) => event.button === 0; const isTargetBlank: THandleEvent = (event) => { - const target = event.target.getAttribute('target'); + const element = event.target as HTMLElement; + const target = element.getAttribute('target'); return !!target && target !== '_self'; }; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/telemetry/send_telemetry.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/telemetry/send_telemetry.test.tsx index da8fd25b9194b..e08fe6c06b0f1 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/telemetry/send_telemetry.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/telemetry/send_telemetry.test.tsx @@ -33,18 +33,19 @@ describe('Shared Telemetry Helpers', () => { }); it('throws an error if the telemetry endpoint fails', () => { - const httpRejectMock = { put: () => Promise.reject() }; + const httpRejectMock = sendTelemetry({ + http: { put: () => Promise.reject() }, + } as any); - expect(sendTelemetry({ http: httpRejectMock })).rejects.toThrow('Unable to send telemetry'); + expect(httpRejectMock).rejects.toThrow('Unable to send telemetry'); }); }); describe('React component helpers', () => { it('SendAppSearchTelemetry component', () => { - const wrapper = mountWithKibanaContext( - , - { http: httpMock } - ); + mountWithKibanaContext(, { + http: httpMock, + }); expect(httpMock.put).toHaveBeenCalledWith('/api/app_search/telemetry', { headers: { 'content-type': 'application/json; charset=utf-8' }, diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/telemetry/send_telemetry.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/telemetry/send_telemetry.tsx index 00c521303d269..0be26b2bf0459 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/telemetry/send_telemetry.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/telemetry/send_telemetry.tsx @@ -6,7 +6,7 @@ import React, { useContext, useEffect } from 'react'; -import { HttpHandler } from 'src/core/public'; +import { HttpSetup } from 'src/core/public'; import { KibanaContext, IKibanaContext } from '../../index'; interface ISendTelemetryProps { @@ -15,7 +15,7 @@ interface ISendTelemetryProps { } interface ISendTelemetry extends ISendTelemetryProps { - http(): HttpHandler; + http: HttpSetup; product: 'app_search' | 'workplace_search' | 'enterprise_search'; } diff --git a/x-pack/plugins/enterprise_search/public/plugin.ts b/x-pack/plugins/enterprise_search/public/plugin.ts index 5863df5ccba25..1ebfdd779a791 100644 --- a/x-pack/plugins/enterprise_search/public/plugin.ts +++ b/x-pack/plugins/enterprise_search/public/plugin.ts @@ -32,7 +32,7 @@ export interface PluginsSetup { export class EnterpriseSearchPlugin implements Plugin { private config: ClientConfigType; - constructor(private readonly initializerContext: PluginInitializerContext) { + constructor(initializerContext: PluginInitializerContext) { this.config = initializerContext.config.get(); } diff --git a/x-pack/plugins/enterprise_search/server/collectors/app_search/telemetry.test.ts b/x-pack/plugins/enterprise_search/server/collectors/app_search/telemetry.test.ts index 4be2c220024bc..9e82a7f8da9ee 100644 --- a/x-pack/plugins/enterprise_search/server/collectors/app_search/telemetry.test.ts +++ b/x-pack/plugins/enterprise_search/server/collectors/app_search/telemetry.test.ts @@ -17,7 +17,7 @@ describe('App Search Telemetry Usage Collector', () => { const usageCollectionMock = { makeUsageCollector: makeUsageCollectorStub, registerCollector: registerStub, - }; + } as any; const savedObjectsRepoStub = { get: () => ({ @@ -35,7 +35,7 @@ describe('App Search Telemetry Usage Collector', () => { }; const savedObjectsMock = { createInternalRepository: jest.fn(() => savedObjectsRepoStub), - }; + } as any; beforeEach(() => { jest.clearAllMocks(); @@ -48,6 +48,7 @@ describe('App Search Telemetry Usage Collector', () => { expect(registerStub).toHaveBeenCalledTimes(1); expect(makeUsageCollectorStub).toHaveBeenCalledTimes(1); expect(makeUsageCollectorStub.mock.calls[0][0].type).toBe('app_search'); + expect(makeUsageCollectorStub.mock.calls[0][0].isReady()).toBe(true); }); }); @@ -74,7 +75,7 @@ describe('App Search Telemetry Usage Collector', () => { }); it('should not error & should return a default telemetry object if no saved data exists', async () => { - const emptySavedObjectsMock = { createInternalRepository: () => ({}) }; + const emptySavedObjectsMock = { createInternalRepository: () => ({}) } as any; registerTelemetryUsageCollector(usageCollectionMock, emptySavedObjectsMock); const savedObjectsCounts = await makeUsageCollectorStub.mock.calls[0][0].fetch(); diff --git a/x-pack/plugins/enterprise_search/server/collectors/app_search/telemetry.ts b/x-pack/plugins/enterprise_search/server/collectors/app_search/telemetry.ts index 12b5a165bf1ac..2a396ead2f718 100644 --- a/x-pack/plugins/enterprise_search/server/collectors/app_search/telemetry.ts +++ b/x-pack/plugins/enterprise_search/server/collectors/app_search/telemetry.ts @@ -5,7 +5,11 @@ */ import { set } from 'lodash'; -import { ISavedObjectsRepository, SavedObjectsServiceStart } from 'src/core/server'; +import { + ISavedObjectsRepository, + SavedObjectsServiceStart, + SavedObjectAttributes, +} from 'src/core/server'; import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; import { AS_TELEMETRY_NAME, ITelemetrySavedObject } from '../../saved_objects/app_search/telemetry'; @@ -21,6 +25,7 @@ export const registerTelemetryUsageCollector = ( const telemetryUsageCollector = usageCollection.makeUsageCollector({ type: 'app_search', fetch: async () => fetchTelemetryMetrics(savedObjects), + isReady: () => true, }); usageCollection.registerCollector(telemetryUsageCollector); }; @@ -31,7 +36,9 @@ export const registerTelemetryUsageCollector = ( const fetchTelemetryMetrics = async (savedObjects: SavedObjectsServiceStart) => { const savedObjectsRepository = savedObjects.createInternalRepository(); - const savedObjectAttributes = await getSavedObjectAttributesFromRepo(savedObjectsRepository); + const savedObjectAttributes = (await getSavedObjectAttributesFromRepo( + savedObjectsRepository + )) as SavedObjectAttributes; const defaultTelemetrySavedObject: ITelemetrySavedObject = { ui_viewed: { @@ -68,10 +75,6 @@ const fetchTelemetryMetrics = async (savedObjects: SavedObjectsServiceStart) => * Helper function - fetches saved objects attributes */ -interface ISavedObjectAttributes { - [key: string]: any; -} - const getSavedObjectAttributesFromRepo = async ( savedObjectsRepository: ISavedObjectsRepository ) => { diff --git a/x-pack/plugins/enterprise_search/server/plugin.ts b/x-pack/plugins/enterprise_search/server/plugin.ts index 077d900a8d8cf..a8430ad8f56af 100644 --- a/x-pack/plugins/enterprise_search/server/plugin.ts +++ b/x-pack/plugins/enterprise_search/server/plugin.ts @@ -12,6 +12,7 @@ import { CoreSetup, Logger, SavedObjectsServiceStart, + IRouter, } from 'src/core/server'; import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; @@ -28,10 +29,16 @@ export interface ServerConfigType { host?: string; } +export interface IRouteDependencies { + router: IRouter; + config: ServerConfigType; + log: Logger; + getSavedObjectsService?(): SavedObjectsServiceStart; +} + export class EnterpriseSearchPlugin implements Plugin { private config: Observable; private logger: Logger; - private savedObjects?: SavedObjectsServiceStart; constructor(initializerContext: PluginInitializerContext) { this.config = initializerContext.config.create(); diff --git a/x-pack/plugins/enterprise_search/server/routes/__mocks__/router.mock.ts b/x-pack/plugins/enterprise_search/server/routes/__mocks__/router.mock.ts index 1cec5da055140..332d1ad1062f2 100644 --- a/x-pack/plugins/enterprise_search/server/routes/__mocks__/router.mock.ts +++ b/x-pack/plugins/enterprise_search/server/routes/__mocks__/router.mock.ts @@ -5,7 +5,12 @@ */ import { httpServiceMock, httpServerMock } from 'src/core/server/mocks'; -import { IRouter, RequestHandlerContext, RouteValidatorConfig } from 'src/core/server'; +import { + IRouter, + KibanaRequest, + RequestHandlerContext, + RouteValidatorConfig, +} from 'src/core/server'; /** * Test helper that mocks Kibana's router and DRYs out various helper (callRoute, schema validation) @@ -14,13 +19,24 @@ import { IRouter, RequestHandlerContext, RouteValidatorConfig } from 'src/core/s type methodType = 'get' | 'post' | 'put' | 'patch' | 'delete'; type payloadType = 'params' | 'query' | 'body'; +interface IMockRouterProps { + method: methodType; + payload: payloadType; +} +interface IMockRouterRequest { + body?: object; + query?: object; + params?: object; +} +type TMockRouterRequest = KibanaRequest | IMockRouterRequest; + export class MockRouter { - public router: jest.Mocked; + public router!: jest.Mocked; public method: methodType; public payload: payloadType; public response = httpServerMock.createResponseFactory(); - private constructor({ method, payload }) { + constructor({ method, payload }: IMockRouterProps) { this.createRouter(); this.method = method; this.payload = payload; @@ -30,29 +46,32 @@ export class MockRouter { this.router = httpServiceMock.createRouter(); }; - public callRoute = async (request) => { - const [_, handler] = this.router[this.method].mock.calls[0]; + public callRoute = async (request: TMockRouterRequest) => { + const [, handler] = this.router[this.method].mock.calls[0]; const context = {} as jest.Mocked; - await handler(context, httpServerMock.createKibanaRequest(request), this.response); + await handler(context, httpServerMock.createKibanaRequest(request as any), this.response); }; /** * Schema validation helpers */ - public validateRoute = (request) => { + public validateRoute = (request: TMockRouterRequest) => { const [config] = this.router[this.method].mock.calls[0]; const validate = config.validate as RouteValidatorConfig<{}, {}, {}>; - validate[this.payload].validate(request[this.payload]); + const payloadValidation = validate[this.payload] as { validate(request: KibanaRequest): void }; + const payloadRequest = request[this.payload] as KibanaRequest; + + payloadValidation.validate(payloadRequest); }; - public shouldValidate = (request) => { + public shouldValidate = (request: TMockRouterRequest) => { expect(() => this.validateRoute(request)).not.toThrow(); }; - public shouldThrow = (request) => { + public shouldThrow = (request: TMockRouterRequest) => { expect(() => this.validateRoute(request)).toThrow(); }; } diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/engines.test.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/engines.test.ts index 722ad0d9269d3..c45514ae537fe 100644 --- a/x-pack/plugins/enterprise_search/server/routes/app_search/engines.test.ts +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/engines.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { loggingServiceMock } from 'src/core/server/mocks'; +import { loggingSystemMock } from 'src/core/server/mocks'; import { MockRouter } from '../__mocks__/router.mock'; import { registerEnginesRoute } from './engines'; @@ -28,7 +28,7 @@ describe('engine routes', () => { }; const mockRouter = new MockRouter({ method: 'get', payload: 'query' }); - const mockLogger = loggingServiceMock.create().get(); + const mockLogger = loggingSystemMock.create().get(); beforeEach(() => { jest.clearAllMocks(); @@ -172,7 +172,7 @@ describe('engine routes', () => { return Promise.resolve(new Response(JSON.stringify(response))); }); }, - andReturnInvalidData(response: object) { + andReturnInvalidData() { fetchMock.mockImplementation((url: string, params: object) => { expect(url).toEqual(expectedUrl); expect(params).toEqual(expectedParams); @@ -180,7 +180,7 @@ describe('engine routes', () => { return Promise.resolve(new Response(JSON.stringify({ foo: 'bar' }))); }); }, - andReturnError(response: object) { + andReturnError() { fetchMock.mockImplementation((url: string, params: object) => { expect(url).toEqual(expectedUrl); expect(params).toEqual(expectedParams); diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/engines.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/engines.ts index ebe2252b24eef..ffc7a0228454f 100644 --- a/x-pack/plugins/enterprise_search/server/routes/app_search/engines.ts +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/engines.ts @@ -8,9 +8,10 @@ import fetch from 'node-fetch'; import querystring from 'querystring'; import { schema } from '@kbn/config-schema'; +import { IRouteDependencies } from '../../plugin'; import { ENGINES_PAGE_SIZE } from '../../../common/constants'; -export function registerEnginesRoute({ router, config, log }) { +export function registerEnginesRoute({ router, config, log }: IRouteDependencies) { router.get( { path: '/api/app_search/engines', @@ -23,7 +24,7 @@ export function registerEnginesRoute({ router, config, log }) { }, async (context, request, response) => { try { - const appSearchUrl = config.host; + const appSearchUrl = config.host as string; const { type, pageIndex } = request.query; const params = querystring.stringify({ @@ -34,7 +35,7 @@ export function registerEnginesRoute({ router, config, log }) { const url = `${encodeURI(appSearchUrl)}/as/engines/collection?${params}`; const enginesResponse = await fetch(url, { - headers: { Authorization: request.headers.authorization }, + headers: { Authorization: request.headers.authorization as string }, }); if (enginesResponse.url.endsWith('/login')) { @@ -58,7 +59,7 @@ export function registerEnginesRoute({ router, config, log }) { } } catch (e) { log.error(`Cannot connect to App Search: ${e.toString()}`); - if (e instanceof Error) log.debug(e.stack); + if (e instanceof Error) log.debug(e.stack as string); return response.notFound({ body: 'cannot-connect' }); } diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/telemetry.test.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/telemetry.test.ts index 7644f3019de80..9e4ca2459ebd5 100644 --- a/x-pack/plugins/enterprise_search/server/routes/app_search/telemetry.test.ts +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/telemetry.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { loggingServiceMock, savedObjectsServiceMock } from 'src/core/server/mocks'; +import { loggingSystemMock, savedObjectsServiceMock } from 'src/core/server/mocks'; import { MockRouter } from '../__mocks__/router.mock'; import { registerTelemetryRoute } from './telemetry'; @@ -16,7 +16,7 @@ import { incrementUICounter } from '../../collectors/app_search/telemetry'; describe('App Search Telemetry API', () => { const mockRouter = new MockRouter({ method: 'put', payload: 'body' }); - const mockLogger = loggingServiceMock.create().get(); + const mockLogger = loggingSystemMock.create().get(); beforeEach(() => { jest.clearAllMocks(); @@ -26,13 +26,13 @@ describe('App Search Telemetry API', () => { router: mockRouter.router, getSavedObjectsService: () => savedObjectsServiceMock.create(), log: mockLogger, - }); + } as any); }); describe('PUT /api/app_search/telemetry', () => { it('increments the saved objects counter', async () => { const successResponse = { success: true }; - incrementUICounter.mockImplementation(jest.fn(() => successResponse)); + (incrementUICounter as jest.Mock).mockImplementation(jest.fn(() => successResponse)); await mockRouter.callRoute({ body: { action: 'viewed', metric: 'setup_guide' } }); @@ -45,7 +45,7 @@ describe('App Search Telemetry API', () => { }); it('throws an error when incrementing fails', async () => { - incrementUICounter.mockImplementation(jest.fn(() => Promise.reject('Failed'))); + (incrementUICounter as jest.Mock).mockImplementation(jest.fn(() => Promise.reject('Failed'))); await mockRouter.callRoute({ body: { action: 'error', metric: 'error' } }); @@ -54,6 +54,20 @@ describe('App Search Telemetry API', () => { expect(mockRouter.response.internalError).toHaveBeenCalled(); }); + it('throws an error if the Saved Objects service is unavailable', async () => { + jest.clearAllMocks(); + registerTelemetryRoute({ + router: mockRouter.router, + getSavedObjectsService: null, + log: mockLogger, + } as any); + await mockRouter.callRoute({}); + + expect(incrementUICounter).not.toHaveBeenCalled(); + expect(mockLogger.error).toHaveBeenCalled(); + expect(mockRouter.response.internalError).toHaveBeenCalled(); + }); + describe('validates', () => { it('correctly', () => { const request = { body: { action: 'viewed', metric: 'setup_guide' } }; diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/telemetry.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/telemetry.ts index 6b7657a384e9f..4cc9b64adc092 100644 --- a/x-pack/plugins/enterprise_search/server/routes/app_search/telemetry.ts +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/telemetry.ts @@ -6,9 +6,14 @@ import { schema } from '@kbn/config-schema'; +import { IRouteDependencies } from '../../plugin'; import { incrementUICounter } from '../../collectors/app_search/telemetry'; -export function registerTelemetryRoute({ router, getSavedObjectsService, log }) { +export function registerTelemetryRoute({ + router, + getSavedObjectsService, + log, +}: IRouteDependencies) { router.put( { path: '/api/app_search/telemetry', @@ -27,6 +32,8 @@ export function registerTelemetryRoute({ router, getSavedObjectsService, log }) const { action, metric } = request.body; try { + if (!getSavedObjectsService) throw new Error('Could not find Saved Objects service'); + return response.ok({ body: await incrementUICounter({ savedObjects: getSavedObjectsService(), diff --git a/x-pack/plugins/enterprise_search/server/saved_objects/app_search/telemetry.ts b/x-pack/plugins/enterprise_search/server/saved_objects/app_search/telemetry.ts index 20c03b6aece8a..02bfe450ce7eb 100644 --- a/x-pack/plugins/enterprise_search/server/saved_objects/app_search/telemetry.ts +++ b/x-pack/plugins/enterprise_search/server/saved_objects/app_search/telemetry.ts @@ -27,7 +27,7 @@ export interface ITelemetrySavedObject { export const appSearchTelemetryType: SavedObjectsType = { name: AS_TELEMETRY_NAME, hidden: false, - namespaceAgnostic: true, + namespaceType: 'single', mappings: { properties: { ui_viewed: {