[APM] Distinguish between loading state and empty state (#40651) (#40743)

* [APM] Distinguish between loading state and empty state

* Remove async describe

* Fix tests

* Show empty state outside table for agent configuration list

* Fix translations
This commit is contained in:
Søren Louv-Jansen 2019-07-10 17:33:00 +02:00 committed by GitHub
parent f44890030a
commit 96c5901fe0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 134 additions and 71 deletions

View file

@ -9,13 +9,19 @@ import { i18n } from '@kbn/i18n';
import React from 'react';
import { KibanaLink } from '../../shared/Links/KibanaLink';
import { SetupInstructionsLink } from '../../shared/Links/SetupInstructionsLink';
import { LoadingStatePrompt } from '../../shared/LoadingStatePrompt';
interface Props {
// any data submitted from APM agents found (not just in the given time range)
historicalDataFound: boolean;
isLoading: boolean;
}
export function NoServicesMessage({ historicalDataFound }: Props) {
export function NoServicesMessage({ historicalDataFound, isLoading }: Props) {
if (isLoading) {
return <LoadingStatePrompt />;
}
if (historicalDataFound) {
return (
<EuiEmptyPrompt
@ -29,48 +35,42 @@ export function NoServicesMessage({ historicalDataFound }: Props) {
titleSize="s"
/>
);
} else {
return (
<EuiEmptyPrompt
title={
<div>
{i18n.translate('xpack.apm.servicesTable.noServicesLabel', {
defaultMessage: `Looks like you don't have any APM services installed. Let's add some!`
})}
</div>
}
titleSize="s"
body={
<React.Fragment>
<p>
{i18n.translate(
'xpack.apm.servicesTable.7xUpgradeServerMessage',
{
defaultMessage: `Upgrading from a pre-7.x version? Make sure you've also upgraded
your APM server instance(s) to at least 7.0.`
}
)}
</p>
<p>
{i18n.translate('xpack.apm.servicesTable.7xOldDataMessage', {
defaultMessage:
'You may also have old data that needs to be migrated.'
})}{' '}
<KibanaLink path="/management/elasticsearch/upgrade_assistant">
{i18n.translate(
'xpack.apm.servicesTable.UpgradeAssistantLink',
{
defaultMessage:
'Learn more by visiting the Kibana Upgrade Assistant'
}
)}
</KibanaLink>
.
</p>
</React.Fragment>
}
actions={<SetupInstructionsLink buttonFill={true} />}
/>
);
}
return (
<EuiEmptyPrompt
title={
<div>
{i18n.translate('xpack.apm.servicesTable.noServicesLabel', {
defaultMessage: `Looks like you don't have any APM services installed. Let's add some!`
})}
</div>
}
titleSize="s"
body={
<React.Fragment>
<p>
{i18n.translate('xpack.apm.servicesTable.7xUpgradeServerMessage', {
defaultMessage: `Upgrading from a pre-7.x version? Make sure you've also upgraded
your APM server instance(s) to at least 7.0.`
})}
</p>
<p>
{i18n.translate('xpack.apm.servicesTable.7xOldDataMessage', {
defaultMessage:
'You may also have old data that needs to be migrated.'
})}{' '}
<KibanaLink path="/management/elasticsearch/upgrade_assistant">
{i18n.translate('xpack.apm.servicesTable.UpgradeAssistantLink', {
defaultMessage:
'Learn more by visiting the Kibana Upgrade Assistant'
})}
</KibanaLink>
.
</p>
</React.Fragment>
}
actions={<SetupInstructionsLink buttonFill={true} />}
/>
);
}

View file

@ -10,12 +10,16 @@ import { NoServicesMessage } from '../NoServicesMessage';
describe('NoServicesMessage', () => {
it('should show only a "not found" message when historical data is found', () => {
const wrapper = shallow(<NoServicesMessage historicalDataFound={true} />);
const wrapper = shallow(
<NoServicesMessage isLoading={false} historicalDataFound={true} />
);
expect(wrapper).toMatchSnapshot();
});
it('should show a "no services installed" message, a link to the set up instructions page, a message about upgrading APM server, and a link to the upgrade assistant when NO historical data is found', () => {
const wrapper = shallow(<NoServicesMessage historicalDataFound={false} />);
const wrapper = shallow(
<NoServicesMessage isLoading={false} historicalDataFound={false} />
);
expect(wrapper).toMatchSnapshot();
});
});

View file

@ -30,7 +30,7 @@ export function ServiceOverview() {
urlParams: { start, end },
uiFilters
} = useUrlParams();
const { data = initalData } = useFetcher(() => {
const { data = initalData, status } = useFetcher(() => {
if (start && end) {
return loadServiceList({ start, end, uiFilters });
}
@ -75,7 +75,10 @@ export function ServiceOverview() {
<ServiceList
items={data.items}
noItemsMessage={
<NoServicesMessage historicalDataFound={data.hasHistoricalData} />
<NoServicesMessage
historicalDataFound={data.hasHistoricalData}
isLoading={status === 'loading'}
/>
}
/>
</EuiPanel>

View file

@ -28,11 +28,15 @@ import { ITableColumn, ManagedTable } from '../../shared/ManagedTable';
import { AgentConfigurationListAPIResponse } from '../../../../server/lib/settings/agent_configuration/list_configurations';
import { AddSettingsFlyout } from './AddSettings/AddSettingFlyout';
import { APMLink } from '../../shared/Links/APMLink';
import { LoadingStatePrompt } from '../../shared/LoadingStatePrompt';
export type Config = AgentConfigurationListAPIResponse[0];
export function SettingsList() {
const { data = [], refresh } = useFetcher(loadAgentConfigurationList, []);
const { data = [], status, refresh } = useFetcher(
loadAgentConfigurationList,
[]
);
const [selectedConfig, setSelectedConfig] = useState<Config | null>(null);
const [isFlyoutOpen, setIsFlyoutOpen] = useState(false);
@ -129,7 +133,7 @@ export function SettingsList() {
const hasConfigurations = !isEmpty(data);
const emptyState = (
const emptyStatePrompt = (
<EuiEmptyPrompt
iconType="controlsHorizontal"
title={
@ -288,16 +292,17 @@ export function SettingsList() {
<EuiSpacer size="m" />
{hasConfigurations ? (
{status === 'success' && !hasConfigurations ? (
emptyStatePrompt
) : (
<ManagedTable
noItemsMessage={<LoadingStatePrompt />}
columns={COLUMNS}
items={data}
initialSortField="service.name"
initialSortDirection="asc"
initialPageSize={50}
/>
) : (
emptyState
)}
</EuiPanel>
</>

View file

@ -15,6 +15,7 @@ import { EmptyMessage } from '../../shared/EmptyMessage';
import { ImpactBar } from '../../shared/ImpactBar';
import { TransactionLink } from '../../shared/Links/TransactionLink';
import { ITableColumn, ManagedTable } from '../../shared/ManagedTable';
import { LoadingStatePrompt } from '../../shared/LoadingStatePrompt';
const StyledTransactionLink = styled(TransactionLink)`
font-size: ${fontSizes.large};
@ -97,7 +98,7 @@ const noItemsMessage = (
);
export function TraceList({ items = [], isLoading }: Props) {
const noItems = isLoading ? null : noItemsMessage;
const noItems = isLoading ? <LoadingStatePrompt /> : noItemsMessage;
return (
<ManagedTable
columns={traceListColumns}

View file

@ -18,6 +18,7 @@ import Histogram from '../../../shared/charts/Histogram';
import { EmptyMessage } from '../../../shared/EmptyMessage';
import { fromQuery, toQuery } from '../../../shared/Links/url_helpers';
import { history } from '../../../../utils/history';
import { LoadingStatePrompt } from '../../../shared/LoadingStatePrompt';
interface IChartPoint {
sample?: IBucket['sample'];
@ -90,7 +91,7 @@ const getFormatYLong = (transactionType: string | undefined) => (t: number) => {
interface Props {
distribution?: ITransactionDistributionAPIResponse;
urlParams: IUrlParams;
loading: boolean;
isLoading: boolean;
}
export const TransactionDistribution: FunctionComponent<Props> = (
@ -99,7 +100,7 @@ export const TransactionDistribution: FunctionComponent<Props> = (
const {
distribution,
urlParams: { transactionId, traceId, transactionType },
loading
isLoading
} = props;
const formatYShort = useCallback(getFormatYShort(transactionType), [
@ -125,10 +126,10 @@ export const TransactionDistribution: FunctionComponent<Props> = (
...defaultSample
})
});
}, [distribution, loading]);
}, [distribution, isLoading]);
useEffect(() => {
if (loading) {
if (isLoading) {
return;
}
const selectedSampleIsAvailable = distribution
@ -145,7 +146,17 @@ export const TransactionDistribution: FunctionComponent<Props> = (
if (!selectedSampleIsAvailable && !!distribution) {
redirectToDefaultSample();
}
}, [distribution, transactionId, traceId, redirectToDefaultSample, loading]);
}, [
distribution,
transactionId,
traceId,
redirectToDefaultSample,
isLoading
]);
if (isLoading) {
return <LoadingStatePrompt />;
}
if (!distribution || !distribution.totalHits || !traceId || !transactionId) {
return (

View file

@ -61,7 +61,7 @@ export function TransactionDetails() {
<EuiPanel>
<TransactionDistribution
distribution={distributionData}
loading={
isLoading={
distributionStatus === FETCH_STATUS.LOADING ||
distributionStatus === undefined
}

View file

@ -16,6 +16,8 @@ import { ImpactBar } from '../../../shared/ImpactBar';
import { APMLink } from '../../../shared/Links/APMLink';
import { legacyEncodeURIComponent } from '../../../shared/Links/url_helpers';
import { ITableColumn, ManagedTable } from '../../../shared/ManagedTable';
import { LoadingStatePrompt } from '../../../shared/LoadingStatePrompt';
import { EmptyMessage } from '../../../shared/EmptyMessage';
const TransactionNameLink = styled(APMLink)`
${truncate('100%')};
@ -25,9 +27,10 @@ const TransactionNameLink = styled(APMLink)`
interface Props {
items: ITransactionGroup[];
serviceName: string;
isLoading: boolean;
}
export function TransactionList({ items, serviceName, ...rest }: Props) {
export function TransactionList({ items, serviceName, isLoading }: Props) {
const columns: Array<ITableColumn<ITransactionGroup>> = useMemo(
() => [
{
@ -111,14 +114,22 @@ export function TransactionList({ items, serviceName, ...rest }: Props) {
[serviceName]
);
const noItemsMessage = (
<EmptyMessage
heading={i18n.translate('xpack.apm.transactionsTable.notFoundLabel', {
defaultMessage: 'No transactions were found.'
})}
/>
);
return (
<ManagedTable
noItemsMessage={isLoading ? <LoadingStatePrompt /> : noItemsMessage}
columns={columns}
items={items}
initialSortField="impact"
initialSortDirection="desc"
initialPageSize={25}
{...rest}
/>
);
}

View file

@ -78,7 +78,10 @@ export function TransactionOverview({
return null;
}
const { data: transactionListData } = useTransactionList(urlParams);
const {
data: transactionListData,
status: transactionListStatus
} = useTransactionList(urlParams);
const { data: hasMLJob = false } = useFetcher(
() => getHasMLJob({ serviceName, transactionType }),
[serviceName, transactionType]
@ -134,6 +137,7 @@ export function TransactionOverview({
</EuiTitle>
<EuiSpacer size="s" />
<TransactionList
isLoading={transactionListStatus === 'loading'}
items={transactionListData}
serviceName={serviceName}
/>

View file

@ -0,0 +1,24 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import { i18n } from '@kbn/i18n';
import { EuiEmptyPrompt } from '@elastic/eui';
export function LoadingStatePrompt() {
return (
<EuiEmptyPrompt
title={
<div>
{i18n.translate('xpack.apm.loading.prompt', {
defaultMessage: 'Loading...'
})}
</div>
}
titleSize="s"
/>
);
}

View file

@ -13,7 +13,7 @@ import { initTracesApi } from '../traces';
describe('route handlers should fail with a Boom error', () => {
let consoleErrorSpy: any;
async function testRouteFailures(init: (core: InternalCoreSetup) => void) {
function testRouteFailures(init: (core: InternalCoreSetup) => void) {
const mockServer = { route: jest.fn() };
const mockCore = ({
http: {
@ -44,7 +44,7 @@ describe('route handlers should fail with a Boom error', () => {
};
const routes = flatten(mockServer.route.mock.calls);
routes.forEach(async (route, i) => {
routes.forEach((route, i) => {
test(`${route.method} ${route.path}"`, async () => {
await expect(route.handler(mockReq)).rejects.toMatchObject({
message: 'request failed',
@ -65,15 +65,15 @@ describe('route handlers should fail with a Boom error', () => {
consoleErrorSpy.mockRestore();
});
describe('error routes', async () => {
await testRouteFailures(initErrorsApi);
describe('error routes', () => {
testRouteFailures(initErrorsApi);
});
describe('service routes', async () => {
await testRouteFailures(initServicesApi);
describe('service routes', () => {
testRouteFailures(initServicesApi);
});
describe('trace routes', async () => {
await testRouteFailures(initTracesApi);
describe('trace routes', () => {
testRouteFailures(initTracesApi);
});
});