[TIP] Align Threat Intel plugin loading states with the designs (#142200)

This commit is contained in:
Luke Gmys 2022-09-29 22:31:01 +02:00 committed by GitHub
parent 8ab92b206a
commit 0cfaff4deb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 430 additions and 313 deletions

View file

@ -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>
`;

View file

@ -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>];

View file

@ -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();
});
});
});

View file

@ -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>
);
}
);

View file

@ -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>

View file

@ -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();
});
});

View file

@ -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>
);
};

View file

@ -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 [],

View file

@ -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,
};
};

View file

@ -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}
/>