mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
[TIP] Align Threat Intel plugin loading states with the designs (#142200)
This commit is contained in:
parent
8ab92b206a
commit
0cfaff4deb
10 changed files with 430 additions and 313 deletions
|
@ -1,115 +1,10 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<IndicatorsBarChartWrapper /> should render barchart and field selector dropdown 1`] = `
|
||||
Object {
|
||||
"asFragment": [Function],
|
||||
"baseElement": <body>
|
||||
<div>
|
||||
<div
|
||||
class="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--justifyContentSpaceBetween euiFlexGroup--directionRow euiFlexGroup--responsive"
|
||||
>
|
||||
<div
|
||||
class="euiFlexItem"
|
||||
>
|
||||
<h2
|
||||
class="euiTitle emotion-euiTitle-s"
|
||||
>
|
||||
Trend
|
||||
</h2>
|
||||
</div>
|
||||
<div
|
||||
class="euiFlexItem euiFlexItem--flexGrowZero"
|
||||
>
|
||||
<div
|
||||
class="euiComboBox euiComboBox--prepended css-18y0wn9"
|
||||
data-test-subj="tiIndicatorFieldSelectorDropdown"
|
||||
>
|
||||
<div
|
||||
class="euiFormControlLayout euiFormControlLayout--group"
|
||||
>
|
||||
<label
|
||||
class="euiFormLabel euiFormControlLayout__prepend"
|
||||
for="generated-id__eui-combobox-id"
|
||||
>
|
||||
Stack by
|
||||
</label>
|
||||
<div
|
||||
class="euiFormControlLayout__childrenWrapper"
|
||||
>
|
||||
<div
|
||||
class="euiComboBox__inputWrap euiComboBox__inputWrap--noWrap euiComboBox__inputWrap--inGroup"
|
||||
data-test-subj="comboBoxInput"
|
||||
tabindex="-1"
|
||||
>
|
||||
<span
|
||||
class="euiComboBoxPill euiComboBoxPill--plainText"
|
||||
>
|
||||
threat.feed.name
|
||||
</span>
|
||||
<div
|
||||
class="euiComboBox__input"
|
||||
style="font-size: 14px; display: inline-block;"
|
||||
>
|
||||
<input
|
||||
aria-autocomplete="list"
|
||||
aria-controls=""
|
||||
aria-expanded="false"
|
||||
data-test-subj="comboBoxSearchInput"
|
||||
id="generated-id__eui-combobox-id"
|
||||
role="combobox"
|
||||
style="box-sizing: content-box; width: 2px;"
|
||||
value=""
|
||||
/>
|
||||
<div
|
||||
style="position: absolute; top: 0px; left: 0px; visibility: hidden; height: 0px; overflow: scroll; white-space: pre; font-family: -webkit-small-control; letter-spacing: normal; text-transform: none;"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="euiFormControlLayoutIcons euiFormControlLayoutIcons--right"
|
||||
>
|
||||
<button
|
||||
aria-label="Open list of options"
|
||||
class="euiFormControlLayoutCustomIcon euiFormControlLayoutCustomIcon--clickable"
|
||||
data-test-subj="comboBoxToggleListButton"
|
||||
tabindex="-1"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class="euiFormControlLayoutCustomIcon__icon"
|
||||
data-euiicon-type="arrowDown"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="echChart"
|
||||
style="width: 100%; height: 200px;"
|
||||
>
|
||||
<div
|
||||
class="echChartBackground"
|
||||
style="background-color: transparent;"
|
||||
/>
|
||||
<div
|
||||
class="echChartStatus"
|
||||
data-ech-render-complete="false"
|
||||
data-ech-render-count="0"
|
||||
/>
|
||||
<div
|
||||
class="echChartResizer"
|
||||
/>
|
||||
<div
|
||||
class="echContainer"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</body>,
|
||||
"container": <div>
|
||||
exports[`<IndicatorsBarChartWrapper /> when not loading or refetching should render barchart and field selector dropdown 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
style="position: relative;"
|
||||
>
|
||||
<div
|
||||
class="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--justifyContentSpaceBetween euiFlexGroup--directionRow euiFlexGroup--responsive"
|
||||
>
|
||||
|
@ -212,57 +107,6 @@ Object {
|
|||
class="echContainer"
|
||||
/>
|
||||
</div>
|
||||
</div>,
|
||||
"debug": [Function],
|
||||
"findAllByAltText": [Function],
|
||||
"findAllByDisplayValue": [Function],
|
||||
"findAllByLabelText": [Function],
|
||||
"findAllByPlaceholderText": [Function],
|
||||
"findAllByRole": [Function],
|
||||
"findAllByTestId": [Function],
|
||||
"findAllByText": [Function],
|
||||
"findAllByTitle": [Function],
|
||||
"findByAltText": [Function],
|
||||
"findByDisplayValue": [Function],
|
||||
"findByLabelText": [Function],
|
||||
"findByPlaceholderText": [Function],
|
||||
"findByRole": [Function],
|
||||
"findByTestId": [Function],
|
||||
"findByText": [Function],
|
||||
"findByTitle": [Function],
|
||||
"getAllByAltText": [Function],
|
||||
"getAllByDisplayValue": [Function],
|
||||
"getAllByLabelText": [Function],
|
||||
"getAllByPlaceholderText": [Function],
|
||||
"getAllByRole": [Function],
|
||||
"getAllByTestId": [Function],
|
||||
"getAllByText": [Function],
|
||||
"getAllByTitle": [Function],
|
||||
"getByAltText": [Function],
|
||||
"getByDisplayValue": [Function],
|
||||
"getByLabelText": [Function],
|
||||
"getByPlaceholderText": [Function],
|
||||
"getByRole": [Function],
|
||||
"getByTestId": [Function],
|
||||
"getByText": [Function],
|
||||
"getByTitle": [Function],
|
||||
"queryAllByAltText": [Function],
|
||||
"queryAllByDisplayValue": [Function],
|
||||
"queryAllByLabelText": [Function],
|
||||
"queryAllByPlaceholderText": [Function],
|
||||
"queryAllByRole": [Function],
|
||||
"queryAllByTestId": [Function],
|
||||
"queryAllByText": [Function],
|
||||
"queryAllByTitle": [Function],
|
||||
"queryByAltText": [Function],
|
||||
"queryByDisplayValue": [Function],
|
||||
"queryByLabelText": [Function],
|
||||
"queryByPlaceholderText": [Function],
|
||||
"queryByRole": [Function],
|
||||
"queryByTestId": [Function],
|
||||
"queryByText": [Function],
|
||||
"queryByTitle": [Function],
|
||||
"rerender": [Function],
|
||||
"unmount": [Function],
|
||||
}
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
|
|
@ -18,104 +18,108 @@ import { StoryProvidersComponent } from '../../../../common/mocks/story_provider
|
|||
import { mockKibanaTimelinesService } from '../../../../common/mocks/mock_kibana_timelines_service';
|
||||
import { DEFAULT_TIME_RANGE } from '../../../query_bar/hooks/use_filters/utils';
|
||||
import { IndicatorsBarChartWrapper } from './indicators_barchart_wrapper';
|
||||
import { Aggregation, AGGREGATION_NAME } from '../../services/fetch_aggregated_indicators';
|
||||
import {
|
||||
Aggregation,
|
||||
AGGREGATION_NAME,
|
||||
ChartSeries,
|
||||
} from '../../services/fetch_aggregated_indicators';
|
||||
|
||||
export default {
|
||||
component: IndicatorsBarChartWrapper,
|
||||
title: 'IndicatorsBarChartWrapper',
|
||||
};
|
||||
|
||||
export const Default: Story<void> = () => {
|
||||
const mockTimeRange: TimeRange = DEFAULT_TIME_RANGE;
|
||||
const mockTimeRange: TimeRange = DEFAULT_TIME_RANGE;
|
||||
|
||||
const mockIndexPattern: DataView = {
|
||||
fields: [
|
||||
const mockIndexPattern: DataView = {
|
||||
fields: [
|
||||
{
|
||||
name: '@timestamp',
|
||||
type: 'date',
|
||||
} as DataViewField,
|
||||
{
|
||||
name: 'threat.feed.name',
|
||||
type: 'string',
|
||||
} as DataViewField,
|
||||
],
|
||||
} as DataView;
|
||||
|
||||
const validDate: string = '1 Jan 2022 00:00:00 GMT';
|
||||
const numberOfDays: number = 1;
|
||||
const aggregation1: Aggregation = {
|
||||
events: {
|
||||
buckets: [
|
||||
{
|
||||
name: '@timestamp',
|
||||
type: 'date',
|
||||
} as DataViewField,
|
||||
doc_count: 0,
|
||||
key: 1641016800000,
|
||||
key_as_string: '1 Jan 2022 06:00:00 GMT',
|
||||
},
|
||||
{
|
||||
name: 'threat.feed.name',
|
||||
type: 'string',
|
||||
} as DataViewField,
|
||||
doc_count: 10,
|
||||
key: 1641038400000,
|
||||
key_as_string: '1 Jan 2022 12:00:00 GMT',
|
||||
},
|
||||
],
|
||||
} as DataView;
|
||||
},
|
||||
doc_count: 0,
|
||||
key: '[Filebeat] AbuseCH Malware',
|
||||
};
|
||||
const aggregation2: Aggregation = {
|
||||
events: {
|
||||
buckets: [
|
||||
{
|
||||
doc_count: 20,
|
||||
key: 1641016800000,
|
||||
key_as_string: '1 Jan 2022 06:00:00 GMT',
|
||||
},
|
||||
{
|
||||
doc_count: 8,
|
||||
key: 1641038400000,
|
||||
key_as_string: '1 Jan 2022 12:00:00 GMT',
|
||||
},
|
||||
],
|
||||
},
|
||||
doc_count: 0,
|
||||
key: '[Filebeat] AbuseCH MalwareBazaar',
|
||||
};
|
||||
|
||||
const validDate: string = '1 Jan 2022 00:00:00 GMT';
|
||||
const numberOfDays: number = 1;
|
||||
const aggregation1: Aggregation = {
|
||||
events: {
|
||||
buckets: [
|
||||
{
|
||||
doc_count: 0,
|
||||
key: 1641016800000,
|
||||
key_as_string: '1 Jan 2022 06:00:00 GMT',
|
||||
},
|
||||
{
|
||||
doc_count: 10,
|
||||
key: 1641038400000,
|
||||
key_as_string: '1 Jan 2022 12:00:00 GMT',
|
||||
},
|
||||
],
|
||||
},
|
||||
doc_count: 0,
|
||||
key: '[Filebeat] AbuseCH Malware',
|
||||
};
|
||||
const aggregation2: Aggregation = {
|
||||
events: {
|
||||
buckets: [
|
||||
{
|
||||
doc_count: 20,
|
||||
key: 1641016800000,
|
||||
key_as_string: '1 Jan 2022 06:00:00 GMT',
|
||||
},
|
||||
{
|
||||
doc_count: 8,
|
||||
key: 1641038400000,
|
||||
key_as_string: '1 Jan 2022 12:00:00 GMT',
|
||||
},
|
||||
],
|
||||
},
|
||||
doc_count: 0,
|
||||
key: '[Filebeat] AbuseCH MalwareBazaar',
|
||||
};
|
||||
|
||||
const dataServiceMock = {
|
||||
search: {
|
||||
search: () =>
|
||||
of({
|
||||
rawResponse: {
|
||||
aggregations: {
|
||||
[AGGREGATION_NAME]: {
|
||||
buckets: [aggregation1, aggregation2],
|
||||
},
|
||||
const dataServiceMock = {
|
||||
search: {
|
||||
search: () =>
|
||||
of({
|
||||
rawResponse: {
|
||||
aggregations: {
|
||||
[AGGREGATION_NAME]: {
|
||||
buckets: [aggregation1, aggregation2],
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
query: {
|
||||
timefilter: {
|
||||
timefilter: {
|
||||
calculateBounds: () => ({
|
||||
min: moment(validDate),
|
||||
max: moment(validDate).add(numberOfDays, 'days'),
|
||||
}),
|
||||
},
|
||||
},
|
||||
filterManager: {
|
||||
getFilters: () => {},
|
||||
setFilters: () => {},
|
||||
getUpdates$: () => of(),
|
||||
}),
|
||||
},
|
||||
query: {
|
||||
timefilter: {
|
||||
timefilter: {
|
||||
calculateBounds: () => ({
|
||||
min: moment(validDate),
|
||||
max: moment(validDate).add(numberOfDays, 'days'),
|
||||
}),
|
||||
},
|
||||
},
|
||||
} as unknown as DataPublicPluginStart;
|
||||
filterManager: {
|
||||
getFilters: () => {},
|
||||
setFilters: () => {},
|
||||
getUpdates$: () => of(),
|
||||
},
|
||||
},
|
||||
} as unknown as DataPublicPluginStart;
|
||||
|
||||
const uiSettingsMock = {
|
||||
get: () => {},
|
||||
} as unknown as IUiSettingsClient;
|
||||
const uiSettingsMock = {
|
||||
get: () => {},
|
||||
} as unknown as IUiSettingsClient;
|
||||
|
||||
const timelinesMock = mockKibanaTimelinesService;
|
||||
const timelinesMock = mockKibanaTimelinesService;
|
||||
|
||||
export const Default: Story<void> = () => {
|
||||
return (
|
||||
<StoryProvidersComponent
|
||||
kibana={{ data: dataServiceMock, uiSettings: uiSettingsMock, timelines: timelinesMock }}
|
||||
|
@ -133,4 +137,84 @@ export const Default: Story<void> = () => {
|
|||
</StoryProvidersComponent>
|
||||
);
|
||||
};
|
||||
|
||||
Default.decorators = [(story) => <MemoryRouter>{story()}</MemoryRouter>];
|
||||
|
||||
export const InitialLoad: Story<void> = () => {
|
||||
return (
|
||||
<StoryProvidersComponent
|
||||
kibana={{ data: dataServiceMock, uiSettings: uiSettingsMock, timelines: timelinesMock }}
|
||||
>
|
||||
<IndicatorsBarChartWrapper
|
||||
dateRange={{ min: moment(), max: moment() }}
|
||||
timeRange={mockTimeRange}
|
||||
indexPattern={mockIndexPattern}
|
||||
series={[]}
|
||||
isLoading={true}
|
||||
isFetching={false}
|
||||
field={''}
|
||||
onFieldChange={function (value: string): void {
|
||||
throw new Error('Function not implemented.');
|
||||
}}
|
||||
/>
|
||||
</StoryProvidersComponent>
|
||||
);
|
||||
};
|
||||
|
||||
InitialLoad.decorators = [(story) => <MemoryRouter>{story()}</MemoryRouter>];
|
||||
|
||||
export const UpdatingData: Story<void> = () => {
|
||||
const mockIndicators: ChartSeries[] = [
|
||||
{
|
||||
x: '1 Jan 2022 00:00:00 GMT',
|
||||
y: 2,
|
||||
g: '[Filebeat] AbuseCH Malware',
|
||||
},
|
||||
{
|
||||
x: '1 Jan 2022 00:00:00 GMT',
|
||||
y: 10,
|
||||
g: '[Filebeat] AbuseCH MalwareBazaar',
|
||||
},
|
||||
{
|
||||
x: '1 Jan 2022 06:00:00 GMT',
|
||||
y: 0,
|
||||
g: '[Filebeat] AbuseCH Malware',
|
||||
},
|
||||
{
|
||||
x: '1 Jan 2022 06:00:00 GMT',
|
||||
y: 0,
|
||||
g: '[Filebeat] AbuseCH MalwareBazaar',
|
||||
},
|
||||
{
|
||||
x: '1 Jan 2022 12:00:00 GMT',
|
||||
y: 25,
|
||||
g: '[Filebeat] AbuseCH Malware',
|
||||
},
|
||||
{
|
||||
x: '1 Jan 2022 18:00:00 GMT',
|
||||
y: 15,
|
||||
g: '[Filebeat] AbuseCH MalwareBazaar',
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<StoryProvidersComponent
|
||||
kibana={{ data: dataServiceMock, uiSettings: uiSettingsMock, timelines: timelinesMock }}
|
||||
>
|
||||
<IndicatorsBarChartWrapper
|
||||
dateRange={{ min: moment(), max: moment() }}
|
||||
timeRange={mockTimeRange}
|
||||
indexPattern={mockIndexPattern}
|
||||
series={mockIndicators}
|
||||
isLoading={false}
|
||||
isFetching={true}
|
||||
field={''}
|
||||
onFieldChange={function (value: string): void {
|
||||
throw new Error('Function not implemented.');
|
||||
}}
|
||||
/>
|
||||
</StoryProvidersComponent>
|
||||
);
|
||||
};
|
||||
|
||||
UpdatingData.decorators = [(story) => <MemoryRouter>{story()}</MemoryRouter>];
|
||||
|
|
|
@ -10,9 +10,11 @@ import { render } from '@testing-library/react';
|
|||
import { TimeRange } from '@kbn/es-query';
|
||||
import { DataView, DataViewField } from '@kbn/data-views-plugin/common';
|
||||
import { TestProvidersComponent } from '../../../../common/mocks/test_providers';
|
||||
import { IndicatorsBarChartWrapper } from './indicators_barchart_wrapper';
|
||||
import {
|
||||
CHART_UPDATE_PROGRESS_TEST_ID,
|
||||
IndicatorsBarChartWrapper,
|
||||
} from './indicators_barchart_wrapper';
|
||||
import { DEFAULT_TIME_RANGE } from '../../../query_bar/hooks/use_filters/utils';
|
||||
import { useFilters } from '../../../query_bar/hooks/use_filters';
|
||||
import moment from 'moment';
|
||||
|
||||
jest.mock('../../../query_bar/hooks/use_filters');
|
||||
|
@ -32,33 +34,67 @@ const mockIndexPattern: DataView = {
|
|||
|
||||
const mockTimeRange: TimeRange = DEFAULT_TIME_RANGE;
|
||||
|
||||
const stub = () => {};
|
||||
|
||||
describe('<IndicatorsBarChartWrapper />', () => {
|
||||
beforeEach(() => {
|
||||
(useFilters as jest.MockedFunction<typeof useFilters>).mockReturnValue({
|
||||
filters: [],
|
||||
filterQuery: { language: 'kuery', query: '' },
|
||||
filterManager: {} as any,
|
||||
handleSavedQuery: stub,
|
||||
handleSubmitQuery: stub,
|
||||
handleSubmitTimeRange: stub,
|
||||
describe('when not loading or refetching', () => {
|
||||
it('should render barchart and field selector dropdown', () => {
|
||||
const component = render(
|
||||
<TestProvidersComponent>
|
||||
<IndicatorsBarChartWrapper
|
||||
dateRange={{ max: moment(), min: moment() }}
|
||||
series={[]}
|
||||
field=""
|
||||
onFieldChange={jest.fn()}
|
||||
indexPattern={mockIndexPattern}
|
||||
timeRange={mockTimeRange}
|
||||
isFetching={false}
|
||||
isLoading={false}
|
||||
/>
|
||||
</TestProvidersComponent>
|
||||
);
|
||||
|
||||
expect(component.asFragment()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
it('should render barchart and field selector dropdown', () => {
|
||||
const component = render(
|
||||
<TestProvidersComponent>
|
||||
<IndicatorsBarChartWrapper
|
||||
dateRange={{ max: moment(), min: moment() }}
|
||||
series={[]}
|
||||
field=""
|
||||
onFieldChange={jest.fn()}
|
||||
indexPattern={mockIndexPattern}
|
||||
timeRange={mockTimeRange}
|
||||
/>
|
||||
</TestProvidersComponent>
|
||||
);
|
||||
|
||||
expect(component).toMatchSnapshot();
|
||||
describe('when loading for the first time', () => {
|
||||
it('should render progress indicator', () => {
|
||||
const component = render(
|
||||
<TestProvidersComponent>
|
||||
<IndicatorsBarChartWrapper
|
||||
dateRange={{ max: moment(), min: moment() }}
|
||||
series={[]}
|
||||
field=""
|
||||
onFieldChange={jest.fn()}
|
||||
indexPattern={mockIndexPattern}
|
||||
timeRange={mockTimeRange}
|
||||
isFetching={false}
|
||||
isLoading={true}
|
||||
/>
|
||||
</TestProvidersComponent>
|
||||
);
|
||||
|
||||
expect(component.queryByRole('progressbar')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when updating the data', () => {
|
||||
it('should render progress indicator', () => {
|
||||
const component = render(
|
||||
<TestProvidersComponent>
|
||||
<IndicatorsBarChartWrapper
|
||||
dateRange={{ max: moment(), min: moment() }}
|
||||
series={[]}
|
||||
field=""
|
||||
onFieldChange={jest.fn()}
|
||||
indexPattern={mockIndexPattern}
|
||||
timeRange={mockTimeRange}
|
||||
isFetching={true}
|
||||
isLoading={false}
|
||||
/>
|
||||
</TestProvidersComponent>
|
||||
);
|
||||
|
||||
expect(component.queryByTestId(CHART_UPDATE_PROGRESS_TEST_ID)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -6,7 +6,14 @@
|
|||
*/
|
||||
|
||||
import React, { memo } from 'react';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiTitle } from '@elastic/eui';
|
||||
import {
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiLoadingSpinner,
|
||||
EuiPanel,
|
||||
EuiProgress,
|
||||
EuiTitle,
|
||||
} from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { TimeRange } from '@kbn/es-query';
|
||||
import { TimeRangeBounds } from '@kbn/data-plugin/common';
|
||||
|
@ -18,6 +25,8 @@ import { ChartSeries } from '../../services/fetch_aggregated_indicators';
|
|||
|
||||
const DEFAULT_FIELD = RawIndicatorFieldId.Feed;
|
||||
|
||||
export const CHART_UPDATE_PROGRESS_TEST_ID = 'tiBarchartWrapper-updating';
|
||||
|
||||
export interface IndicatorsBarChartWrapperProps {
|
||||
/**
|
||||
* From and to values received from the KQL bar and passed down to the hook to query data.
|
||||
|
@ -35,6 +44,12 @@ export interface IndicatorsBarChartWrapperProps {
|
|||
field: string;
|
||||
|
||||
onFieldChange: (value: string) => void;
|
||||
|
||||
/** Is initial load in progress? */
|
||||
isLoading?: boolean;
|
||||
|
||||
/** Is data update in progress? */
|
||||
isFetching?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -42,9 +57,21 @@ export interface IndicatorsBarChartWrapperProps {
|
|||
* and handles retrieving aggregated indicator data.
|
||||
*/
|
||||
export const IndicatorsBarChartWrapper = memo<IndicatorsBarChartWrapperProps>(
|
||||
({ timeRange, indexPattern, series, dateRange, field, onFieldChange }) => {
|
||||
({ timeRange, indexPattern, isLoading, isFetching, series, dateRange, field, onFieldChange }) => {
|
||||
if (isLoading) {
|
||||
return (
|
||||
<EuiFlexGroup justifyContent="spaceAround">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiPanel hasShadow={false} hasBorder={false} paddingSize="xl">
|
||||
<EuiLoadingSpinner size="xl" />
|
||||
</EuiPanel>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div style={{ position: 'relative' }}>
|
||||
<EuiFlexGroup justifyContent={'spaceBetween'}>
|
||||
<EuiFlexItem>
|
||||
<EuiTitle size={'s'}>
|
||||
|
@ -64,12 +91,20 @@ export const IndicatorsBarChartWrapper = memo<IndicatorsBarChartWrapperProps>(
|
|||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
{timeRange ? (
|
||||
<IndicatorsBarChart indicators={series} dateRange={dateRange} field={field} />
|
||||
) : (
|
||||
<></>
|
||||
|
||||
{isFetching && (
|
||||
<EuiProgress
|
||||
data-test-subj={CHART_UPDATE_PROGRESS_TEST_ID}
|
||||
size="xs"
|
||||
color="accent"
|
||||
position="absolute"
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
||||
{timeRange && (
|
||||
<IndicatorsBarChart indicators={series} dateRange={dateRange} field={field} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
|
|
@ -36,7 +36,7 @@ const columnSettings = {
|
|||
onSort: stub,
|
||||
},
|
||||
};
|
||||
export function WithIndicators() {
|
||||
export function IndicatorsFullyLoaded() {
|
||||
const indicatorsFixture: Indicator[] = Array(10).fill(generateMockIndicator());
|
||||
|
||||
return (
|
||||
|
@ -62,6 +62,55 @@ export function WithIndicators() {
|
|||
);
|
||||
}
|
||||
|
||||
export function FirstLoad() {
|
||||
return (
|
||||
<StoryProvidersComponent>
|
||||
<IndicatorsTable
|
||||
browserFields={{}}
|
||||
pagination={{
|
||||
pageSize: 10,
|
||||
pageIndex: 0,
|
||||
pageSizeOptions: [10, 25, 50],
|
||||
}}
|
||||
indicators={[]}
|
||||
onChangePage={stub}
|
||||
onChangeItemsPerPage={stub}
|
||||
indicatorCount={0}
|
||||
isLoading={true}
|
||||
indexPattern={mockIndexPattern}
|
||||
columnSettings={columnSettings}
|
||||
/>
|
||||
</StoryProvidersComponent>
|
||||
);
|
||||
}
|
||||
|
||||
export function DataUpdateInProgress() {
|
||||
const indicatorsFixture: Indicator[] = Array(10).fill(generateMockIndicator());
|
||||
|
||||
return (
|
||||
<StoryProvidersComponent>
|
||||
<IndicatorsFiltersContext.Provider value={mockIndicatorsFiltersContext}>
|
||||
<IndicatorsTable
|
||||
browserFields={{}}
|
||||
isLoading={false}
|
||||
isFetching={true}
|
||||
pagination={{
|
||||
pageSize: 10,
|
||||
pageIndex: 0,
|
||||
pageSizeOptions: [10, 25, 50],
|
||||
}}
|
||||
indicators={indicatorsFixture}
|
||||
onChangePage={stub}
|
||||
onChangeItemsPerPage={stub}
|
||||
indicatorCount={indicatorsFixture.length * 2}
|
||||
indexPattern={mockIndexPattern}
|
||||
columnSettings={columnSettings}
|
||||
/>
|
||||
</IndicatorsFiltersContext.Provider>
|
||||
</StoryProvidersComponent>
|
||||
);
|
||||
}
|
||||
|
||||
export function WithNoIndicators() {
|
||||
return (
|
||||
<StoryProvidersComponent>
|
||||
|
|
|
@ -7,7 +7,11 @@
|
|||
|
||||
import { act, render, screen } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { IndicatorsTable, IndicatorsTableProps } from './indicators_table';
|
||||
import {
|
||||
IndicatorsTable,
|
||||
IndicatorsTableProps,
|
||||
TABLE_UPDATE_PROGRESS_TEST_ID,
|
||||
} from './indicators_table';
|
||||
import { TestProvidersComponent } from '../../../../common/mocks/test_providers';
|
||||
import { generateMockIndicator, Indicator } from '../../../../../common/types/indicator';
|
||||
import { BUTTON_TEST_ID } from '../open_indicator_flyout_button';
|
||||
|
@ -56,7 +60,7 @@ const indicatorsFixture: Indicator[] = [
|
|||
];
|
||||
|
||||
describe('<IndicatorsTable />', () => {
|
||||
it('should render loading spinner when loading', async () => {
|
||||
it('should render loading spinner when doing initial loading', async () => {
|
||||
await act(async () => {
|
||||
render(
|
||||
<TestProvidersComponent>
|
||||
|
@ -68,6 +72,25 @@ describe('<IndicatorsTable />', () => {
|
|||
expect(screen.queryByRole('progressbar')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render loading indicator when doing data update', async () => {
|
||||
await act(async () => {
|
||||
render(
|
||||
<TestProvidersComponent>
|
||||
<IndicatorsTable
|
||||
{...tableProps}
|
||||
indicatorCount={indicatorsFixture.length}
|
||||
indicators={indicatorsFixture}
|
||||
isFetching={true}
|
||||
/>
|
||||
</TestProvidersComponent>
|
||||
);
|
||||
});
|
||||
|
||||
screen.debug();
|
||||
|
||||
expect(screen.queryByTestId(TABLE_UPDATE_PROGRESS_TEST_ID)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render datagrid when loading is done', async () => {
|
||||
await act(async () => {
|
||||
render(
|
||||
|
@ -75,6 +98,7 @@ describe('<IndicatorsTable />', () => {
|
|||
<IndicatorsTable
|
||||
{...tableProps}
|
||||
isLoading={false}
|
||||
isFetching={false}
|
||||
indicatorCount={indicatorsFixture.length}
|
||||
indicators={indicatorsFixture}
|
||||
/>
|
||||
|
@ -92,5 +116,8 @@ describe('<IndicatorsTable />', () => {
|
|||
});
|
||||
|
||||
expect(screen.queryByTestId(TITLE_TEST_ID)).toBeInTheDocument();
|
||||
|
||||
expect(screen.queryByRole('progressbar')).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId(TABLE_UPDATE_PROGRESS_TEST_ID)).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -13,6 +13,8 @@ import {
|
|||
EuiFlexItem,
|
||||
EuiLoadingSpinner,
|
||||
EuiPanel,
|
||||
EuiProgress,
|
||||
EuiSpacer,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
|
@ -39,7 +41,8 @@ export interface IndicatorsTableProps {
|
|||
/**
|
||||
* If true, no data is available yet
|
||||
*/
|
||||
isLoading: boolean;
|
||||
isLoading?: boolean;
|
||||
isFetching?: boolean;
|
||||
indexPattern: SecuritySolutionDataViewBase;
|
||||
browserFields: BrowserFields;
|
||||
columnSettings: ColumnSettingsValue;
|
||||
|
@ -54,6 +57,8 @@ const gridStyle = {
|
|||
fontSize: 's',
|
||||
} as const;
|
||||
|
||||
export const TABLE_UPDATE_PROGRESS_TEST_ID = `${TABLE_TEST_ID}-updating` as const;
|
||||
|
||||
export const IndicatorsTable: VFC<IndicatorsTableProps> = ({
|
||||
indicators,
|
||||
indicatorCount,
|
||||
|
@ -61,6 +66,7 @@ export const IndicatorsTable: VFC<IndicatorsTableProps> = ({
|
|||
onChangeItemsPerPage,
|
||||
pagination,
|
||||
isLoading,
|
||||
isFetching,
|
||||
browserFields,
|
||||
columnSettings: { columns, columnVisibility, handleResetColumns, handleToggleColumn, sorting },
|
||||
}) => {
|
||||
|
@ -157,44 +163,57 @@ export const IndicatorsTable: VFC<IndicatorsTableProps> = ({
|
|||
}
|
||||
|
||||
return (
|
||||
<EuiDataGrid
|
||||
aria-labelledby="indicators-table"
|
||||
leadingControlColumns={leadingControlColumns}
|
||||
rowCount={indicatorCount}
|
||||
renderCellValue={renderCellValue}
|
||||
toolbarVisibility={toolbarOptions}
|
||||
pagination={{
|
||||
...pagination,
|
||||
onChangeItemsPerPage,
|
||||
onChangePage,
|
||||
}}
|
||||
gridStyle={gridStyle}
|
||||
data-test-subj={TABLE_TEST_ID}
|
||||
sorting={sorting}
|
||||
columnVisibility={columnVisibility}
|
||||
columns={mappedColumns}
|
||||
/>
|
||||
<>
|
||||
{isFetching && (
|
||||
<EuiProgress
|
||||
data-test-subj={TABLE_UPDATE_PROGRESS_TEST_ID}
|
||||
size="xs"
|
||||
color="accent"
|
||||
position="absolute"
|
||||
/>
|
||||
)}
|
||||
<EuiSpacer size="xs" />
|
||||
|
||||
<EuiDataGrid
|
||||
aria-labelledby="indicators-table"
|
||||
leadingControlColumns={leadingControlColumns}
|
||||
rowCount={indicatorCount}
|
||||
renderCellValue={renderCellValue}
|
||||
toolbarVisibility={toolbarOptions}
|
||||
pagination={{
|
||||
...pagination,
|
||||
onChangeItemsPerPage,
|
||||
onChangePage,
|
||||
}}
|
||||
gridStyle={gridStyle}
|
||||
data-test-subj={TABLE_TEST_ID}
|
||||
sorting={sorting}
|
||||
columnVisibility={columnVisibility}
|
||||
columns={mappedColumns}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}, [
|
||||
columnVisibility,
|
||||
mappedColumns,
|
||||
indicatorCount,
|
||||
leadingControlColumns,
|
||||
isLoading,
|
||||
indicatorCount,
|
||||
isFetching,
|
||||
leadingControlColumns,
|
||||
renderCellValue,
|
||||
toolbarOptions,
|
||||
pagination,
|
||||
onChangeItemsPerPage,
|
||||
onChangePage,
|
||||
pagination,
|
||||
renderCellValue,
|
||||
sorting,
|
||||
toolbarOptions,
|
||||
columnVisibility,
|
||||
mappedColumns,
|
||||
]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<IndicatorsTableContext.Provider value={indicatorTableContextValue}>
|
||||
<IndicatorsTableContext.Provider value={indicatorTableContextValue}>
|
||||
<div style={{ position: 'relative' }}>
|
||||
{flyoutFragment}
|
||||
{gridFragment}
|
||||
</IndicatorsTableContext.Provider>
|
||||
</div>
|
||||
</div>
|
||||
</IndicatorsTableContext.Provider>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -88,6 +88,8 @@ describe('useAggregatedIndicators()', () => {
|
|||
"max": "2022-01-02T00:00:00.000Z",
|
||||
"min": "2022-01-01T00:00:00.000Z",
|
||||
},
|
||||
"isFetching": true,
|
||||
"isLoading": true,
|
||||
"onFieldChange": [Function],
|
||||
"selectedField": "threat.feed.name",
|
||||
"series": Array [],
|
||||
|
|
|
@ -49,6 +49,12 @@ export interface UseAggregatedIndicatorsValue {
|
|||
* Indicator field used to query the aggregated Indicators.
|
||||
*/
|
||||
selectedField: string;
|
||||
|
||||
/** Is initial load in progress? */
|
||||
isLoading?: boolean;
|
||||
|
||||
/** Is data update in progress? */
|
||||
isFetching?: boolean;
|
||||
}
|
||||
|
||||
const DEFAULT_FIELD = RawIndicatorFieldId.Feed;
|
||||
|
@ -80,7 +86,7 @@ export const useAggregatedIndicators = ({
|
|||
[inspectorAdapters, queryService, searchService]
|
||||
);
|
||||
|
||||
const { data } = useQuery(
|
||||
const { data, isLoading, isFetching } = useQuery(
|
||||
[
|
||||
'indicatorsBarchart',
|
||||
{
|
||||
|
@ -97,7 +103,8 @@ export const useAggregatedIndicators = ({
|
|||
}: {
|
||||
signal?: AbortSignal;
|
||||
queryKey: [string, FetchAggregatedIndicatorsParams];
|
||||
}) => aggregatedIndicatorsQuery(queryParams, signal)
|
||||
}) => aggregatedIndicatorsQuery(queryParams, signal),
|
||||
{ keepPreviousData: true }
|
||||
);
|
||||
|
||||
const dateRange = useMemo(
|
||||
|
@ -110,5 +117,7 @@ export const useAggregatedIndicators = ({
|
|||
series: data || [],
|
||||
onFieldChange: setField,
|
||||
selectedField: field,
|
||||
isLoading,
|
||||
isFetching,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -53,10 +53,11 @@ const IndicatorsPageContent: VFC = () => {
|
|||
handleRefresh,
|
||||
indicatorCount,
|
||||
indicators,
|
||||
isLoading,
|
||||
onChangeItemsPerPage,
|
||||
onChangePage,
|
||||
pagination,
|
||||
isLoading: isLoadingIndicators,
|
||||
isFetching: isFetchingIndicators,
|
||||
} = useIndicators({
|
||||
filters,
|
||||
filterQuery,
|
||||
|
@ -64,7 +65,14 @@ const IndicatorsPageContent: VFC = () => {
|
|||
sorting: columnSettings.sorting.columns,
|
||||
});
|
||||
|
||||
const { dateRange, series, selectedField, onFieldChange } = useAggregatedIndicators({
|
||||
const {
|
||||
dateRange,
|
||||
series,
|
||||
selectedField,
|
||||
onFieldChange,
|
||||
isLoading: isLoadingAggregatedIndicators,
|
||||
isFetching: isFetchingAggregatedIndicators,
|
||||
} = useAggregatedIndicators({
|
||||
timeRange,
|
||||
filters,
|
||||
filterQuery,
|
||||
|
@ -97,7 +105,10 @@ const IndicatorsPageContent: VFC = () => {
|
|||
indexPattern={indexPattern}
|
||||
field={selectedField}
|
||||
onFieldChange={onFieldChange}
|
||||
isFetching={isFetchingAggregatedIndicators}
|
||||
isLoading={isLoadingAggregatedIndicators}
|
||||
/>
|
||||
|
||||
<IndicatorsTable
|
||||
browserFields={browserFields}
|
||||
indexPattern={indexPattern}
|
||||
|
@ -105,7 +116,8 @@ const IndicatorsPageContent: VFC = () => {
|
|||
pagination={pagination}
|
||||
indicatorCount={indicatorCount}
|
||||
indicators={indicators}
|
||||
isLoading={isLoading}
|
||||
isLoading={isLoadingIndicators}
|
||||
isFetching={isFetchingIndicators}
|
||||
onChangeItemsPerPage={onChangeItemsPerPage}
|
||||
onChangePage={onChangePage}
|
||||
/>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue