Skip to content

Commit

Permalink
[App Search] Added an EntryPointsTable (#108316)
Browse files Browse the repository at this point in the history
  • Loading branch information
JasonStoltz authored Aug 12, 2021
1 parent caaa76f commit 4d7fd0a
Show file tree
Hide file tree
Showing 14 changed files with 679 additions and 4 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
/*
* 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 { shallow } from 'enzyme';

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

import { GenericEndpointInlineEditableTable } from '../../../../shared/tables/generic_endpoint_inline_editable_table';

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

import { EntryPointsTable } from './entry_points_table';

describe('EntryPointsTable', () => {
const engineName = 'my-engine';
const entryPoints = [
{ id: '1', value: '/whatever' },
{ id: '2', value: '/foo' },
];
const domain = {
createdOn: '2018-01-01T00:00:00.000Z',
documentCount: 10,
id: '6113e1407a2f2e6f42489794',
url: 'https://www.elastic.co',
crawlRules: [],
entryPoints,
sitemaps: [],
};

beforeEach(() => {
jest.clearAllMocks();
});

it('renders', () => {
const wrapper = shallow(
<EntryPointsTable domain={domain} engineName={engineName} items={domain.entryPoints} />
);

expect(wrapper.find(GenericEndpointInlineEditableTable).exists()).toBe(true);
});

describe('the first and only column in the table', () => {
it('shows the value of an entry point', () => {
const entryPoint = { id: '1', value: '/whatever' };

const wrapper = shallow(
<EntryPointsTable domain={domain} engineName={engineName} items={domain.entryPoints} />
);

const columns = wrapper.find(GenericEndpointInlineEditableTable).prop('columns');
const column = shallow(<div>{columns[0].render(entryPoint)}</div>);
expect(column.html()).toContain('/whatever');
});

it('can show the value of an entry point as editable', () => {
const entryPoint = { id: '1', value: '/whatever' };
const onChange = jest.fn();

const wrapper = shallow(
<EntryPointsTable domain={domain} engineName={engineName} items={domain.entryPoints} />
);

const columns = wrapper.find(GenericEndpointInlineEditableTable).prop('columns');
const column = shallow(
<div>
{columns[0].editingRender(entryPoint, onChange, { isInvalid: false, isLoading: false })}
</div>
);

const textField = column.find(EuiFieldText);
expect(textField.props()).toEqual(
expect.objectContaining({
value: '/whatever',
disabled: false, // It would be disabled if isLoading is true
isInvalid: false,
prepend: 'https://www.elastic.co',
})
);

textField.simulate('change', { target: { value: '/foo' } });
expect(onChange).toHaveBeenCalledWith('/foo');
});
});

describe('routes', () => {
it('can calculate an update and delete route correctly', () => {
const wrapper = shallow(
<EntryPointsTable domain={domain} engineName={engineName} items={domain.entryPoints} />
);

const table = wrapper.find(GenericEndpointInlineEditableTable);

const entryPoint = { id: '1', value: '/whatever' };
expect(table.prop('deleteRoute')(entryPoint)).toEqual(
'/api/app_search/engines/my-engine/crawler/domains/6113e1407a2f2e6f42489794/entry_points/1'
);
expect(table.prop('updateRoute')(entryPoint)).toEqual(
'/api/app_search/engines/my-engine/crawler/domains/6113e1407a2f2e6f42489794/entry_points/1'
);
});
});

it('shows a no items message whem there are no entry points to show', () => {
const wrapper = shallow(
<EntryPointsTable domain={domain} engineName={engineName} items={domain.entryPoints} />
);

const editNewItems = jest.fn();
const table = wrapper.find(GenericEndpointInlineEditableTable);
const message = mountWithIntl(<div>{table.prop('noItemsMessage')!(editNewItems)}</div>);
expect(message.html()).toContain('There are no existing entry points.');
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
/*
* 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 { useActions } from 'kea';

import { EuiFieldText, EuiLink, EuiSpacer, EuiText, EuiTitle } from '@elastic/eui';

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

import { GenericEndpointInlineEditableTable } from '../../../../shared/tables/generic_endpoint_inline_editable_table';
import { InlineEditableTableColumn } from '../../../../shared/tables/inline_editable_table/types';
import { ItemWithAnID } from '../../../../shared/tables/types';
import { DOCS_PREFIX } from '../../../routes';
import { CrawlerDomain, EntryPoint } from '../types';

import { EntryPointsTableLogic } from './entry_points_table_logic';

interface EntryPointsTableProps {
domain: CrawlerDomain;
engineName: string;
items: EntryPoint[];
}

export const EntryPointsTable: React.FC<EntryPointsTableProps> = ({
domain,
engineName,
items,
}) => {
const { onAdd, onDelete, onUpdate } = useActions(EntryPointsTableLogic);
const field = 'value';

const columns: Array<InlineEditableTableColumn<ItemWithAnID>> = [
{
editingRender: (entryPoint, onChange, { isInvalid, isLoading }) => (
<EuiFieldText
fullWidth
value={(entryPoint as EntryPoint)[field]}
onChange={(e) => onChange(e.target.value)}
disabled={isLoading}
isInvalid={isInvalid}
prepend={domain.url}
/>
),
render: (entryPoint) => (
<EuiText size="s">
{domain.url}
{(entryPoint as EntryPoint)[field]}
</EuiText>
),
name: i18n.translate(
'xpack.enterpriseSearch.appSearch.crawler.entryPointsTable.urlTableHead',
{ defaultMessage: 'URL' }
),
field,
},
];

const entryPointsRoute = `/api/app_search/engines/${engineName}/crawler/domains/${domain.id}/entry_points`;

const getEntryPointRoute = (entryPoint: EntryPoint) =>
`/api/app_search/engines/${engineName}/crawler/domains/${domain.id}/entry_points/${entryPoint.id}`;

return (
<GenericEndpointInlineEditableTable
addButtonText={i18n.translate(
'xpack.enterpriseSearch.appSearch.crawler.entryPointsTable.addButtonLabel',
{ defaultMessage: 'Add entry point' }
)}
columns={columns}
description={
<p>
{i18n.translate('xpack.enterpriseSearch.appSearch.crawler.entryPointsTable.description', {
defaultMessage:
'Include the most important URLs for your website here. Entry point URLs will be the first pages to be indexed and processed for links to other pages.',
})}{' '}
<EuiLink
href={`${DOCS_PREFIX}/crawl-web-content.html#crawl-web-content-manage-entry-points`}
target="_blank"
external
>
{i18n.translate(
'xpack.enterpriseSearch.appSearch.crawler.entryPointsTable.learnMoreLinkText',
{ defaultMessage: 'Learn more about entry points.' }
)}
</EuiLink>
</p>
}
instanceId="EntryPointsTable"
items={items}
lastItemWarning={i18n.translate(
'xpack.enterpriseSearch.appSearch.crawler.entryPointsTable.lastItemMessage',
{ defaultMessage: 'The crawler requires at least one entry point.' }
)}
// Since canRemoveLastItem is false, the only time noItemsMessage would be displayed is if the last entry point was deleted via the API.
noItemsMessage={(editNewItem) => (
<>
<EuiSpacer />
<EuiTitle size="m">
<h4>
{i18n.translate(
'xpack.enterpriseSearch.appSearch.crawler.entryPointsTable.emptyMessageTitle',
{
defaultMessage: 'There are no existing entry points.',
}
)}
</h4>
</EuiTitle>
<EuiSpacer />
<EuiText>
<FormattedMessage
id="xpack.enterpriseSearch.appSearch.crawler.entryPointsTable.emptyMessageDescription"
defaultMessage="{link} to specify an entry point
for the crawler"
values={{
link: (
<EuiLink onClick={editNewItem}>
{i18n.translate(
'xpack.enterpriseSearch.appSearch.crawler.entryPointsTable.emptyMessageLinkText',
{ defaultMessage: 'Add an entry point' }
)}
</EuiLink>
),
}}
/>
</EuiText>
<EuiSpacer />
</>
)}
addRoute={entryPointsRoute}
canRemoveLastItem={false}
deleteRoute={getEntryPointRoute}
updateRoute={getEntryPointRoute}
dataProperty="entry_points"
onAdd={onAdd}
onDelete={onDelete}
onUpdate={onUpdate}
title={i18n.translate('xpack.enterpriseSearch.appSearch.crawler.entryPointsTable.title', {
defaultMessage: 'Entry points',
})}
disableReordering
/>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/*
* 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.
*/

jest.mock('../crawler_single_domain_logic', () => ({
CrawlerSingleDomainLogic: {
actions: {
updateEntryPoints: jest.fn(),
},
},
}));

import { LogicMounter, mockFlashMessageHelpers } from '../../../../__mocks__/kea_logic';

import { CrawlerSingleDomainLogic } from '../crawler_single_domain_logic';

import { EntryPointsTableLogic } from './entry_points_table_logic';

describe('EntryPointsTableLogic', () => {
const { mount } = new LogicMounter(EntryPointsTableLogic);
const { clearFlashMessages, flashSuccessToast } = mockFlashMessageHelpers;

beforeEach(() => {
jest.clearAllMocks();
});

describe('listeners', () => {
describe('onAdd', () => {
it('should update the entry points for the current domain, and clear flash messages', () => {
const entryThatWasAdded = { id: '2', value: 'bar' };
const updatedEntries = [
{ id: '1', value: 'foo' },
{ id: '2', value: 'bar' },
];
mount();
EntryPointsTableLogic.actions.onAdd(entryThatWasAdded, updatedEntries);
expect(CrawlerSingleDomainLogic.actions.updateEntryPoints).toHaveBeenCalledWith(
updatedEntries
);
expect(clearFlashMessages).toHaveBeenCalled();
});
});

describe('onDelete', () => {
it('should update the entry points for the current domain, clear flash messages, and show a success toast', () => {
const entryThatWasDeleted = { id: '2', value: 'bar' };
const updatedEntries = [{ id: '1', value: 'foo' }];
mount();
EntryPointsTableLogic.actions.onDelete(entryThatWasDeleted, updatedEntries);
expect(CrawlerSingleDomainLogic.actions.updateEntryPoints).toHaveBeenCalledWith(
updatedEntries
);
expect(clearFlashMessages).toHaveBeenCalled();
expect(flashSuccessToast).toHaveBeenCalled();
});
});

describe('onUpdate', () => {
it('should update the entry points for the current domain, clear flash messages, and show a success toast', () => {
const entryThatWasUpdated = { id: '2', value: 'baz' };
const updatedEntries = [
{ id: '1', value: 'foo' },
{ id: '2', value: 'baz' },
];
mount();
EntryPointsTableLogic.actions.onUpdate(entryThatWasUpdated, updatedEntries);
expect(CrawlerSingleDomainLogic.actions.updateEntryPoints).toHaveBeenCalledWith(
updatedEntries
);
expect(clearFlashMessages).toHaveBeenCalled();
});
});
});
});
Loading

0 comments on commit 4d7fd0a

Please sign in to comment.