Skip to content

Commit

Permalink
[Enterprise Search][Engines] Engine overview - chrome (#148593)
Browse files Browse the repository at this point in the history
## Summary

Introduces the page chrome and side nav for viewing an engine.

### Screenshots
<img width="1392" alt="image"
src="https://user-images.githubusercontent.com/1972968/211667198-911fa25a-1359-4195-9b97-18d040490000.png">

404
<img width="1392" alt="image"
src="https://user-images.githubusercontent.com/1972968/211651023-0b883ff5-df9b-4a32-90d6-ced988185a37.png">
  • Loading branch information
TattdCodeMonkey committed Jan 12, 2023
1 parent 8052100 commit fb39c8b
Show file tree
Hide file tree
Showing 24 changed files with 841 additions and 19 deletions.
6 changes: 6 additions & 0 deletions x-pack/plugins/enterprise_search/common/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,12 @@ export const SEARCH_EXPERIENCES_PLUGIN = {
WORKPLACE_SEARCH_TUTORIAL_URL: 'https://docs.elastic.co/search-ui/tutorials/workplace-search',
};

export const ENGINES_PLUGIN = {
NAV_TITLE: i18n.translate('xpack.enterpriseSearch.engines.navTitle', {
defaultMessage: 'Engines',
}),
};

export const LICENSED_SUPPORT_URL = 'https://support.elastic.co';

export const JSON_HEADER = {
Expand Down
20 changes: 19 additions & 1 deletion x-pack/plugins/enterprise_search/common/types/engines.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
* 2.0.
*/

import { HealthStatus } from '@elastic/elasticsearch/lib/api/types';

export interface EnterpriseSearchEnginesResponse {
meta: {
from: number;
Expand All @@ -15,6 +17,22 @@ export interface EnterpriseSearchEnginesResponse {
}

export interface EnterpriseSearchEngine {
name: string;
created: string;
indices: string[];
name: string;
updated: string;
}

export interface EnterpriseSearchEngineDetails {
created: string;
indices: EnterpriseSearchEngineIndex[];
name: string;
updated: string;
}

export interface EnterpriseSearchEngineIndex {
count: number;
health: HealthStatus | 'unknown';
name: string;
source: 'api' | 'connector' | 'crawler';
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/*
* 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; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import { mockHttpValues } from '../../../__mocks__/kea_logic';

import { nextTick } from '@kbn/test-jest-helpers';

import { fetchEngine } from './fetch_engine_api_logic';

describe('FetchEngineApiLogic', () => {
const { http } = mockHttpValues;
beforeEach(() => {
jest.clearAllMocks();
});
describe('fetchEngine', () => {
it('calls correct api', async () => {
const promise = Promise.resolve('result');
http.get.mockReturnValue(promise);
const result = fetchEngine({ engineName: 'my-engine' });
await nextTick();
expect(http.get).toHaveBeenCalledWith('/internal/enterprise_search/engines/my-engine');
await expect(result).resolves.toEqual('result');
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/*
* 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; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import { EnterpriseSearchEngineDetails } from '../../../../../common/types/engines';
import { Actions, createApiLogic } from '../../../shared/api_logic/create_api_logic';
import { HttpLogic } from '../../../shared/http';

export interface FetchEngineApiParams {
engineName: string;
}

export type FetchEngineApiResponse = EnterpriseSearchEngineDetails;

export const fetchEngine = async ({
engineName,
}: FetchEngineApiParams): Promise<FetchEngineApiResponse> => {
const route = `/internal/enterprise_search/engines/${engineName}`;

return await HttpLogic.values.http.get<EnterpriseSearchEngineDetails>(route);
};

export const FetchEngineApiLogic = createApiLogic(['fetch_engine_api_logic'], fetchEngine);

export type FetchEngineApiLogicActions = Actions<FetchEngineApiParams, FetchEngineApiResponse>;
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,12 @@ import { nextTick } from '@kbn/test-jest-helpers';

import { fetchIndex } from './fetch_index_api_logic';

describe('generateConnectorApiKeyApiLogic', () => {
describe('FetchIndexApiLogic', () => {
const { http } = mockHttpValues;
beforeEach(() => {
jest.clearAllMocks();
});
describe('generateApiKey', () => {
describe('fetchIndex', () => {
it('calls correct api', async () => {
const promise = Promise.resolve('result');
http.get.mockReturnValue(promise);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/*
* 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; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { setMockValues } from '../../../__mocks__/kea_logic';

import React from 'react';

import { HttpError } from '../../../../../common/types/api';

import { ErrorStatePrompt } from '../../../shared/error_state';
import { NotFoundPrompt } from '../../../shared/not_found';
import { SendEnterpriseSearchTelemetry } from '../../../shared/telemetry';

import { mountWithIntl } from '../../../test_helpers';

import { EngineError } from './engine_error';

describe('EngineError', () => {
beforeEach(() => {
jest.clearAllMocks();
setMockValues({});
});

it('renders 404 prompt for 404 error', () => {
const error = {
body: {
error: 'NOT_FOUND',
message: 'Not Found',
statusCode: 404,
},
} as HttpError;
const wrapper = mountWithIntl(<EngineError error={error} />);

expect(wrapper.find(NotFoundPrompt)).toHaveLength(1);
expect(wrapper.find(SendEnterpriseSearchTelemetry)).toHaveLength(1);
expect(wrapper.find(ErrorStatePrompt)).toHaveLength(0);

const notFound = wrapper.find(NotFoundPrompt);
expect(notFound.prop('backToLink')).toEqual('/engines');
expect(notFound.prop('backToContent')).toEqual('Back to Engines');

const telemetry = wrapper.find(SendEnterpriseSearchTelemetry);
expect(telemetry.prop('action')).toEqual('error');
expect(telemetry.prop('metric')).toEqual('not_found');
});

it('renders error prompt for api errors', () => {
const error = {
body: {
error: 'ERROR',
message: 'Internal Server Error',
statusCode: 500,
},
} as HttpError;
const wrapper = mountWithIntl(<EngineError error={error} />);

expect(wrapper.find(ErrorStatePrompt)).toHaveLength(1);
expect(wrapper.find(SendEnterpriseSearchTelemetry)).toHaveLength(1);
expect(wrapper.find(NotFoundPrompt)).toHaveLength(0);

const telemetry = wrapper.find(SendEnterpriseSearchTelemetry);
expect(telemetry.prop('action')).toEqual('error');
expect(telemetry.prop('metric')).toEqual('cannot_connect');
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/*
* 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; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import React from 'react';

import { i18n } from '@kbn/i18n';

import { ENTERPRISE_SEARCH_CONTENT_PLUGIN } from '../../../../../common/constants';
import { HttpError } from '../../../../../common/types/api';

import { ErrorStatePrompt } from '../../../shared/error_state';
import { NotFoundPrompt } from '../../../shared/not_found';
import { SendEnterpriseSearchTelemetry } from '../../../shared/telemetry';

import { ENGINES_PATH } from '../../routes';

export const EngineError: React.FC<{ error: HttpError | undefined }> = ({ error }) => {
if (error?.body?.statusCode === 404) {
return (
<>
<SendEnterpriseSearchTelemetry action="error" metric="not_found" />
<NotFoundPrompt
backToContent={i18n.translate('xpack.enterpriseSearch.engines.engine.notFound.action1', {
defaultMessage: 'Back to Engines',
})}
backToLink={ENGINES_PATH}
productSupportUrl={ENTERPRISE_SEARCH_CONTENT_PLUGIN.SUPPORT_URL}
/>
</>
);
}
return (
<>
<SendEnterpriseSearchTelemetry action="error" metric="cannot_connect" />
<ErrorStatePrompt />
</>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/*
* 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; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import { kea, MakeLogicType } from 'kea';

export interface EngineNameProps {
engineName: string;
}

export type EngineNameValues = EngineNameProps;

export interface EngineNameActions {
setEngineName: (engineName: string) => { engineName: string };
}

export const EngineNameLogic = kea<
MakeLogicType<EngineNameValues, EngineNameActions, EngineNameProps>
>({
actions: {
setEngineName: (engineName) => ({ engineName }),
},
path: ['enterprise_search', 'content', 'engine_name'],
reducers: ({ props }) => ({
engineName: [
// Short-circuiting this to empty string is necessary to enable testing logics relying on this
props.engineName ?? '',
{
setEngineName: (_, { engineName }) => engineName,
},
],
}),
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/*
* 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; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import React, { useEffect } from 'react';
import { Redirect, Route, Switch, useParams } from 'react-router-dom';

import { useActions } from 'kea';

import { generateEncodedPath } from '../../../shared/encode_path_params';
import { ENGINE_PATH, ENGINE_TAB_PATH, EngineViewTabs } from '../../routes';

import { EngineNameLogic } from './engine_name_logic';
import { EngineView } from './engine_view';

export const EngineRouter: React.FC = () => {
const engineName = decodeURIComponent(useParams<{ engineName: string }>().engineName);
const engineNameLogic = EngineNameLogic({ engineName });
const { setEngineName } = useActions(engineNameLogic);

useEffect(() => {
const unmountName = engineNameLogic.mount();

return () => {
unmountName();
};
}, []);
useEffect(() => {
setEngineName(engineName);
}, [engineName]);

return (
<Switch>
<Redirect
from={ENGINE_PATH}
to={generateEncodedPath(ENGINE_TAB_PATH, {
engineName,
tabId: EngineViewTabs.OVERVIEW,
})}
exact
/>
<Route path={ENGINE_TAB_PATH}>
<EngineView />
</Route>
</Switch>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/*
* 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; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import React, { useEffect } from 'react';
import { useParams } from 'react-router-dom';

import { useActions, useValues } from 'kea';

import { EuiButtonEmpty } from '@elastic/eui';

import { Status } from '../../../../../common/types/api';

import { docLinks } from '../../../shared/doc_links';
import { EngineViewTabs } from '../../routes';

import { EnterpriseSearchEnginesPageTemplate } from '../layout/engines_page_template';

import { EngineError } from './engine_error';
import { EngineViewLogic } from './engine_view_logic';

export const EngineView: React.FC = () => {
const { engineName, fetchEngineApiError, fetchEngineApiStatus, isLoadingEngine } =
useValues(EngineViewLogic);
const { fetchEngine } = useActions(EngineViewLogic);
const { tabId = EngineViewTabs.OVERVIEW } = useParams<{
tabId?: string;
}>();
useEffect(() => {
fetchEngine({ engineName });
}, [engineName]);

if (fetchEngineApiStatus === Status.ERROR) {
return (
<EnterpriseSearchEnginesPageTemplate
isEmptyState
pageChrome={[engineName]}
pageViewTelemetry={tabId}
pageHeader={{
pageTitle: engineName,
rightSideItems: [],
}}
engineName={engineName}
emptyState={<EngineError error={fetchEngineApiError} />}
/>
);
}

return (
<EnterpriseSearchEnginesPageTemplate
pageChrome={[engineName]}
pageViewTelemetry={tabId}
isLoading={isLoadingEngine}
pageHeader={{
pageTitle: engineName,
rightSideItems: [
<EuiButtonEmpty
href={docLinks.appSearchElasticsearchIndexedEngines} // TODO: replace with real docLinks when it's created
target="_blank"
iconType="documents"
>
Engine Docs
</EuiButtonEmpty>,
],
}}
engineName={engineName}
>
<div />
</EnterpriseSearchEnginesPageTemplate>
);
};
Loading

0 comments on commit fb39c8b

Please sign in to comment.