Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
7 changes: 6 additions & 1 deletion x-pack/plugins/infra/.storybook/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,9 @@
* you may not use this file except in compliance with the Elastic License.
*/

module.exports = require('@kbn/storybook').defaultConfig;
const defaultConfig = require('@kbn/storybook').defaultConfig;

module.exports = {
...defaultConfig,
stories: ['../**/*.stories.mdx', ...defaultConfig.stories],
};
22 changes: 19 additions & 3 deletions x-pack/plugins/infra/common/search_strategies/common/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,22 @@

import * as rt from 'io-ts';

const genericErrorRT = rt.type({
const abortedRequestSearchStrategyErrorRT = rt.type({
type: rt.literal('aborted'),
});

export type AbortedRequestSearchStrategyError = rt.TypeOf<
typeof abortedRequestSearchStrategyErrorRT
>;

const genericSearchStrategyErrorRT = rt.type({
type: rt.literal('generic'),
message: rt.string,
});

const shardFailureErrorRT = rt.type({
export type GenericSearchStrategyError = rt.TypeOf<typeof genericSearchStrategyErrorRT>;

const shardFailureSearchStrategyErrorRT = rt.type({
type: rt.literal('shardFailure'),
shardInfo: rt.type({
shard: rt.number,
Expand All @@ -21,6 +31,12 @@ const shardFailureErrorRT = rt.type({
message: rt.string,
});

export const searchStrategyErrorRT = rt.union([genericErrorRT, shardFailureErrorRT]);
export type ShardFailureSearchStrategyError = rt.TypeOf<typeof shardFailureSearchStrategyErrorRT>;

export const searchStrategyErrorRT = rt.union([
abortedRequestSearchStrategyErrorRT,
genericSearchStrategyErrorRT,
shardFailureSearchStrategyErrorRT,
]);

export type SearchStrategyError = rt.TypeOf<typeof searchStrategyErrorRT>;
25 changes: 25 additions & 0 deletions x-pack/plugins/infra/public/components/centered_flyout_body.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/*
* 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 { EuiFlyoutBody } from '@elastic/eui';
import { euiStyled } from '../../../observability/public';

export const CenteredEuiFlyoutBody = euiStyled(EuiFlyoutBody)`
& .euiFlyoutBody__overflow {
display: flex;
flex-direction: column;
}

& .euiFlyoutBody__overflowContent {
align-items: center;
align-self: stretch;
display: flex;
flex-direction: column;
flex-grow: 1;
justify-content: center;
overflow: hidden;
}
`;
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
/*
* 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 { PropsOf } from '@elastic/eui';
import { Meta, Story } from '@storybook/react/types-6-0';
import React from 'react';
import { EuiThemeProvider } from '../../../observability/public';
import { DataSearchErrorCallout } from './data_search_error_callout';

export default {
title: 'infra/dataSearch/DataSearchErrorCallout',
decorators: [
(wrappedStory) => (
<EuiThemeProvider>
<div style={{ width: 600 }}>{wrappedStory()}</div>
</EuiThemeProvider>
),
],
parameters: {
layout: 'padded',
},
argTypes: {
errors: {
control: {
type: 'object',
},
},
},
} as Meta;

type DataSearchErrorCalloutProps = PropsOf<typeof DataSearchErrorCallout>;

const DataSearchErrorCalloutTemplate: Story<DataSearchErrorCalloutProps> = (args) => (
<DataSearchErrorCallout {...args} />
);

const commonArgs = {
title: 'Failed to load data',
errors: [
{
type: 'generic' as const,
message: 'A generic error message',
},
{
type: 'shardFailure' as const,
shardInfo: {
index: 'filebeat-7.9.3-2020.12.01-000003',
node: 'a45hJUm3Tba4U8MkvkCU_g',
shard: 0,
},
message: 'No mapping found for [@timestamp] in order to sort on',
},
],
};

export const ErrorCallout = DataSearchErrorCalloutTemplate.bind({});

ErrorCallout.args = {
...commonArgs,
};

export const ErrorCalloutWithRetry = DataSearchErrorCalloutTemplate.bind({});

ErrorCalloutWithRetry.args = {
...commonArgs,
};
ErrorCalloutWithRetry.argTypes = {
onRetry: { action: 'retrying' },
};

export const AbortedErrorCallout = DataSearchErrorCalloutTemplate.bind({});

AbortedErrorCallout.args = {
...commonArgs,
errors: [
{
type: 'aborted',
},
],
};
AbortedErrorCallout.argTypes = {
onRetry: { action: 'retrying' },
};
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;
* you may not use this file except in compliance with the Elastic License.
*/

import { EuiButton, EuiCallOut } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import React from 'react';
import {
AbortedRequestSearchStrategyError,
GenericSearchStrategyError,
SearchStrategyError,
ShardFailureSearchStrategyError,
} from '../../common/search_strategies/common/errors';

export const DataSearchErrorCallout: React.FC<{
title: React.ReactNode;
errors: SearchStrategyError[];
onRetry?: () => void;
}> = ({ errors, onRetry, title }) => {
const calloutColor = errors.some((error) => error.type !== 'aborted') ? 'danger' : 'warning';

return (
<EuiCallOut color={calloutColor} iconType="alert" title={title}>
{errors?.map((error, errorIndex) => (
<DataSearchErrorMessage key={errorIndex} error={error} />
))}
{onRetry ? (
<EuiButton color={calloutColor} size="s" onClick={onRetry}>
<FormattedMessage
id="xpack.infra.dataSearch.loadingErrorRetryButtonLabel"
defaultMessage="Retry"
/>
</EuiButton>
) : null}
</EuiCallOut>
);
};

const DataSearchErrorMessage: React.FC<{ error: SearchStrategyError }> = ({ error }) => {
if (error.type === 'aborted') {
return <AbortedRequestErrorMessage error={error} />;
} else if (error.type === 'shardFailure') {
return <ShardFailureErrorMessage error={error} />;
} else {
return <GenericErrorMessage error={error} />;
}
};

const AbortedRequestErrorMessage: React.FC<{
error?: AbortedRequestSearchStrategyError;
}> = ({}) => (
<FormattedMessage
tagName="p"
id="xpack.infra.dataSearch.abortedRequestErrorMessage"
defaultMessage="The request was aborted."
/>
);

const GenericErrorMessage: React.FC<{ error: GenericSearchStrategyError }> = ({ error }) => (
<p>{error.message ?? `${error}`}</p>
);

const ShardFailureErrorMessage: React.FC<{ error: ShardFailureSearchStrategyError }> = ({
error,
}) => (
<FormattedMessage
tagName="p"
id="xpack.infra.dataSearch.shardFailureErrorMessage"
defaultMessage="Index {indexName}: {errorMessage}"
values={{
indexName: error.shardInfo.index,
errorMessage: error.message,
}}
/>
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/*
* 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 { PropsOf } from '@elastic/eui';
import { Meta, Story } from '@storybook/react/types-6-0';
import React from 'react';
import { EuiThemeProvider } from '../../../observability/public';
import { DataSearchProgress } from './data_search_progress';

export default {
title: 'infra/dataSearch/DataSearchProgress',
decorators: [
(wrappedStory) => (
<EuiThemeProvider>
<div style={{ width: 400 }}>{wrappedStory()}</div>
</EuiThemeProvider>
),
],
parameters: {
layout: 'padded',
},
} as Meta;

type DataSearchProgressProps = PropsOf<typeof DataSearchProgress>;

const DataSearchProgressTemplate: Story<DataSearchProgressProps> = (args) => (
<DataSearchProgress {...args} />
);

export const UndeterminedProgress = DataSearchProgressTemplate.bind({});

export const DeterminedProgress = DataSearchProgressTemplate.bind({});

DeterminedProgress.args = {
label: 'Searching',
maxValue: 10,
value: 3,
};

export const CancelableDeterminedProgress = DataSearchProgressTemplate.bind({});

CancelableDeterminedProgress.args = {
label: 'Searching',
maxValue: 10,
value: 3,
};
CancelableDeterminedProgress.argTypes = {
onCancel: { action: 'canceled' },
};
45 changes: 45 additions & 0 deletions x-pack/plugins/infra/public/components/data_search_progress.tsx
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;
* you may not use this file except in compliance with the Elastic License.
*/

import { EuiButtonIcon, EuiFlexGroup, EuiFlexItem, EuiProgress } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import React, { useMemo } from 'react';

export const DataSearchProgress: React.FC<{
label?: React.ReactNode;
maxValue?: number;
onCancel?: () => void;
value?: number;
}> = ({ label, maxValue, onCancel, value }) => {
const valueText = useMemo(
() =>
Number.isFinite(maxValue) && Number.isFinite(value) ? `${value} / ${maxValue}` : undefined,
[value, maxValue]
);

return (
<EuiFlexGroup alignItems="center">
<EuiFlexItem>
<EuiProgress label={label} size="s" max={maxValue} value={value} valueText={valueText} />
</EuiFlexItem>
{onCancel ? (
<EuiFlexItem grow={false}>
<EuiButtonIcon
color="danger"
iconType="cross"
onClick={onCancel}
title={cancelButtonLabel}
aria-label={cancelButtonLabel}
/>
</EuiFlexItem>
) : null}
</EuiFlexGroup>
);
};

const cancelButtonLabel = i18n.translate('xpack.infra.dataSearch.cancelButtonLabel', {
defaultMessage: 'Cancel request',
});
Loading