Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[App Search] Convert API Logs page to new page template + empty state polish #102820

Merged
merged 6 commits into from
Jun 22, 2021
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,7 @@ import React from 'react';

import { shallow } from 'enzyme';

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

import { Loading } from '../../../shared/loading';
import { rerender } from '../../../test_helpers';
import { rerender, getPageTitle } from '../../../test_helpers';
import { LogRetentionCallout, LogRetentionTooltip } from '../log_retention';

import { ApiLogsTable, NewApiEventsPrompt } from './components';
Expand All @@ -42,19 +39,28 @@ describe('ApiLogs', () => {

it('renders', () => {
const wrapper = shallow(<ApiLogs />);
expect(wrapper.find(EuiPageHeader).prop('pageTitle')).toEqual('API Logs');
expect(getPageTitle(wrapper)).toEqual('API Logs');
expect(wrapper.find(ApiLogsTable)).toHaveLength(1);
expect(wrapper.find(NewApiEventsPrompt)).toHaveLength(1);

expect(wrapper.find(LogRetentionCallout).prop('type')).toEqual('api');
expect(wrapper.find(LogRetentionTooltip).prop('type')).toEqual('api');
});

it('renders a loading screen', () => {
setMockValues({ ...values, dataLoading: true, apiLogs: [] });
const wrapper = shallow(<ApiLogs />);
describe('loading state', () => {
it('renders a full-page loading state on initial page load (no logs exist yet)', () => {
setMockValues({ ...values, dataLoading: true, apiLogs: [] });
const wrapper = shallow(<ApiLogs />);

expect(wrapper.prop('isLoading')).toEqual(true);
});

it('does not re-render a full-page loading state after initial page load (uses component-level loading state instead)', () => {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that the component level loading here is the EuiBasicTable's default loading prop. The only time data reloads on this page is when users are paginating between pages of logs, so it makes sense to handle that loading UX at the component level instead of re-flashing a full-page loading state.

The test name was already getting really long though so I didn't want to add all that extra context to it 🤪

setMockValues({ ...values, dataLoading: true, apiLogs: [{}] });
const wrapper = shallow(<ApiLogs />);

expect(wrapper.find(Loading)).toHaveLength(1);
expect(wrapper.prop('isLoading')).toEqual(false);
});
});

describe('effects', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,25 +9,14 @@ import React, { useEffect } from 'react';

import { useValues, useActions } from 'kea';

import {
EuiPageHeader,
EuiTitle,
EuiPageContent,
EuiPageContentBody,
EuiFlexGroup,
EuiFlexItem,
EuiSpacer,
} from '@elastic/eui';

import { FlashMessages } from '../../../shared/flash_messages';
import { SetAppSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome';
import { Loading } from '../../../shared/loading';
import { EuiTitle, EuiFlexGroup, EuiFlexItem, EuiPanel, EuiSpacer } from '@elastic/eui';

import { getEngineBreadcrumbs } from '../engine';
import { AppSearchPageTemplate } from '../layout';
import { LogRetentionCallout, LogRetentionTooltip, LogRetentionOptions } from '../log_retention';

import { ApiLogFlyout } from './api_log';
import { ApiLogsTable, NewApiEventsPrompt } from './components';
import { ApiLogsTable, NewApiEventsPrompt, EmptyState } from './components';
import { API_LOGS_TITLE, RECENT_API_EVENTS } from './constants';

import { ApiLogsLogic } from './';
Expand All @@ -44,38 +33,36 @@ export const ApiLogs: React.FC = () => {
pollForApiLogs();
}, []);

if (dataLoading && !apiLogs.length) return <Loading />;

return (
<>
<SetPageChrome trail={getEngineBreadcrumbs([API_LOGS_TITLE])} />
<EuiPageHeader pageTitle={API_LOGS_TITLE} />

<FlashMessages />
<AppSearchPageTemplate
pageChrome={getEngineBreadcrumbs([API_LOGS_TITLE])}
pageHeader={{ pageTitle: API_LOGS_TITLE }}
isLoading={dataLoading && !apiLogs.length}
isEmptyState={!apiLogs.length}
emptyState={<EmptyState />}
>
<LogRetentionCallout type={LogRetentionOptions.API} />

<EuiPageContent hasBorder>
<EuiPageContentBody>
<EuiFlexGroup gutterSize="m" alignItems="center" responsive={false} wrap>
<EuiFlexItem grow={false}>
<EuiTitle size="s">
<h2>{RECENT_API_EVENTS}</h2>
</EuiTitle>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<LogRetentionTooltip type={LogRetentionOptions.API} />
</EuiFlexItem>
<EuiFlexItem />
<EuiFlexItem grow={false}>
<NewApiEventsPrompt />
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer size="m" />
<EuiPanel hasBorder>
<EuiFlexGroup gutterSize="m" alignItems="center" responsive={false} wrap>
<EuiFlexItem grow={false}>
<EuiTitle size="s">
<h2>{RECENT_API_EVENTS}</h2>
</EuiTitle>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<LogRetentionTooltip type={LogRetentionOptions.API} />
</EuiFlexItem>
<EuiFlexItem />
<EuiFlexItem grow={false}>
<NewApiEventsPrompt />
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer size="m" />

<ApiLogsTable hasPagination />
<ApiLogFlyout />
</EuiPageContentBody>
</EuiPageContent>
</>
<ApiLogsTable hasPagination />
<ApiLogFlyout />
</EuiPanel>
</AppSearchPageTemplate>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import React from 'react';

import { shallow } from 'enzyme';

import { EuiBasicTable, EuiBadge, EuiHealth, EuiButtonEmpty, EuiEmptyPrompt } from '@elastic/eui';
import { EuiBasicTable, EuiBadge, EuiHealth, EuiButtonEmpty } from '@elastic/eui';

import { DEFAULT_META } from '../../../../shared/constants';
import { mountWithIntl } from '../../../../test_helpers';
Expand Down Expand Up @@ -91,14 +91,6 @@ describe('ApiLogsTable', () => {
expect(actions.openFlyout).toHaveBeenCalled();
});

it('renders an empty prompt if no items are passed', () => {
setMockValues({ ...values, apiLogs: [] });
const wrapper = mountWithIntl(<ApiLogsTable />);
const promptContent = wrapper.find(EuiEmptyPrompt).text();

expect(promptContent).toContain('Perform your first API call');
});

describe('hasPagination', () => {
it('does not render with pagination by default', () => {
const wrapper = shallow(<ApiLogsTable />);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ import {
EuiBadge,
EuiHealth,
EuiButtonEmpty,
EuiEmptyPrompt,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedRelative } from '@kbn/i18n/react';
Expand Down Expand Up @@ -109,25 +108,6 @@ export const ApiLogsTable: React.FC<Props> = ({ hasPagination }) => {
items={apiLogs}
responsive
loading={dataLoading}
noItemsMessage={
<EuiEmptyPrompt
iconType="clock"
title={
<h3>
{i18n.translate('xpack.enterpriseSearch.appSearch.engine.apiLogs.emptyTitle', {
defaultMessage: 'Perform your first API call',
})}
</h3>
}
body={
<p>
{i18n.translate('xpack.enterpriseSearch.appSearch.engine.apiLogs.emptyDescription', {
defaultMessage: "Check back after you've performed some API calls.",
})}
</p>
}
/>
}
{...paginationProps}
/>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/*
* 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 { EuiEmptyPrompt, EuiButton } from '@elastic/eui';

import { EmptyState } from './';

describe('EmptyState', () => {
it('renders', () => {
const wrapper = shallow(<EmptyState />)
.find(EuiEmptyPrompt)
.dive();

expect(wrapper.find('h2').text()).toEqual('Perform your first API call');
expect(wrapper.find(EuiButton).prop('href')).toEqual(
expect.stringContaining('/api-reference.html')
);
});
});
Original file line number Diff line number Diff line change
@@ -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; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import React from 'react';

import { EuiButton, EuiEmptyPrompt } from '@elastic/eui';
import { i18n } from '@kbn/i18n';

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

export const EmptyState: React.FC = () => (
<EuiEmptyPrompt
iconType="clock"
title={
<h2>
{i18n.translate('xpack.enterpriseSearch.appSearch.engine.apiLogs.emptyTitle', {
defaultMessage: 'Perform your first API call',
})}
</h2>
}
body={
<p>
{i18n.translate('xpack.enterpriseSearch.appSearch.engine.apiLogs.emptyDescription', {
defaultMessage: "Check back after you've performed some API calls.",
})}
</p>
}
actions={
<EuiButton
size="s"
target="_blank"
iconType="popout"
href={`${DOCS_PREFIX}/api-reference.html`}
>
{i18n.translate('xpack.enterpriseSearch.appSearch.engine.searchUI.empty.buttonLabel', {
defaultMessage: 'View the API reference',
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This document link is new, I thought it made sense to provide a link teaching people how to make API calls if needed. Feel free to leave copy feedback

})}
</EuiButton>
}
/>
);
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@

export { ApiLogsTable } from './api_logs_table';
export { NewApiEventsPrompt } from './new_api_events_prompt';
export { EmptyState } from './empty_state';
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,11 @@ export const EngineRouter: React.FC = () => {
<Documents />
</Route>
)}
{canViewEngineApiLogs && (
<Route path={ENGINE_API_LOGS_PATH}>
<ApiLogs />
</Route>
)}
{/* TODO: Remove layout once page template migration is over */}
<Layout navigation={<AppSearchNav />}>
{canViewEngineAnalytics && (
Expand Down Expand Up @@ -136,11 +141,6 @@ export const EngineRouter: React.FC = () => {
<ResultSettings />
</Route>
)}
{canViewEngineApiLogs && (
<Route path={ENGINE_API_LOGS_PATH}>
<ApiLogs />
</Route>
)}
{canManageEngineSearchUi && (
<Route path={ENGINE_SEARCH_UI_PATH}>
<SearchUI />
Expand Down