[SIEM] Fix Inspect query 'request timestamp' value changes when curso… (#54223)

This commit is contained in:
patrykkopycinski 2020-01-14 08:50:49 +01:00 committed by GitHub
parent f71142dcd3
commit 7c4a531ae7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 1325 additions and 1424 deletions

View file

@ -59,7 +59,6 @@ interface Props {
kqlMode: KqlMode;
onChangeItemsPerPage: OnChangeItemsPerPage;
query: Query;
showInspect: boolean;
start: number;
sort: Sort;
timelineTypeContext: TimelineTypeContextProps;
@ -67,171 +66,171 @@ interface Props {
utilityBar?: (totalCount: number) => React.ReactNode;
}
export const EventsViewer = React.memo<Props>(
({
browserFields,
columns,
const EventsViewerComponent: React.FC<Props> = ({
browserFields,
columns,
dataProviders,
deletedEventIds,
end,
filters,
headerFilterGroup,
height = DEFAULT_EVENTS_VIEWER_HEIGHT,
id,
indexPattern,
isLive,
itemsPerPage,
itemsPerPageOptions,
kqlMode,
onChangeItemsPerPage,
query,
start,
sort,
timelineTypeContext,
toggleColumn,
utilityBar,
}) => {
const columnsHeader = isEmpty(columns) ? defaultHeaders : columns;
const kibana = useKibana();
const combinedQueries = combineQueries({
config: esQuery.getEsQueryConfig(kibana.services.uiSettings),
dataProviders,
deletedEventIds,
end,
filters,
headerFilterGroup,
height = DEFAULT_EVENTS_VIEWER_HEIGHT,
id,
indexPattern,
isLive,
itemsPerPage,
itemsPerPageOptions,
browserFields,
filters,
kqlQuery: query,
kqlMode,
onChangeItemsPerPage,
query,
showInspect,
start,
sort,
timelineTypeContext,
toggleColumn,
utilityBar,
}) => {
const columnsHeader = isEmpty(columns) ? defaultHeaders : columns;
const kibana = useKibana();
const combinedQueries = combineQueries({
config: esQuery.getEsQueryConfig(kibana.services.uiSettings),
dataProviders,
indexPattern,
browserFields,
filters,
kqlQuery: query,
kqlMode,
start,
end,
isEventViewer: true,
});
const queryFields = useMemo(
() =>
union(
columnsHeader.map(c => c.id),
timelineTypeContext.queryFields ?? []
),
[columnsHeader, timelineTypeContext.queryFields]
);
end,
isEventViewer: true,
});
const queryFields = useMemo(
() =>
union(
columnsHeader.map(c => c.id),
timelineTypeContext.queryFields ?? []
),
[columnsHeader, timelineTypeContext.queryFields]
);
return (
<EuiPanel data-test-subj="events-viewer-panel" grow={false}>
<AutoSizer detectAnyWindowResize={true} content>
{({ measureRef, content: { width = 0 } }) => (
<>
<WrappedByAutoSizer ref={measureRef}>
<div
data-test-subj="events-viewer-measured"
style={{ height: '0px', width: '100%' }}
/>
</WrappedByAutoSizer>
return (
<EuiPanel data-test-subj="events-viewer-panel" grow={false}>
<AutoSizer detectAnyWindowResize={true} content>
{({ measureRef, content: { width = 0 } }) => (
<>
<WrappedByAutoSizer ref={measureRef}>
<div
data-test-subj="events-viewer-measured"
style={{ height: '0px', width: '100%' }}
/>
</WrappedByAutoSizer>
{combinedQueries != null ? (
<TimelineQuery
fields={queryFields}
filterQuery={combinedQueries.filterQuery}
id={id}
indexPattern={indexPattern}
limit={itemsPerPage}
sortField={{
sortFieldId: sort.columnId,
direction: sort.sortDirection as Direction,
}}
sourceId="default"
>
{({
events,
getUpdatedAt,
inspect,
loading,
loadMore,
pageInfo,
refetch,
totalCount = 0,
}) => {
const totalCountMinusDeleted =
totalCount > 0 ? totalCount - deletedEventIds.length : 0;
{combinedQueries != null ? (
<TimelineQuery
fields={queryFields}
filterQuery={combinedQueries.filterQuery}
id={id}
indexPattern={indexPattern}
limit={itemsPerPage}
sortField={{
sortFieldId: sort.columnId,
direction: sort.sortDirection as Direction,
}}
sourceId="default"
>
{({
events,
getUpdatedAt,
inspect,
loading,
loadMore,
pageInfo,
refetch,
totalCount = 0,
}) => {
const totalCountMinusDeleted =
totalCount > 0 ? totalCount - deletedEventIds.length : 0;
// TODO: Reset eventDeletedIds/eventLoadingIds on refresh/loadmore (getUpdatedAt)
return (
<>
<HeaderSection
id={id}
showInspect={showInspect}
subtitle={
utilityBar
? undefined
: `${
i18n.SHOWING
}: ${totalCountMinusDeleted.toLocaleString()} ${i18n.UNIT(
totalCountMinusDeleted
)}`
}
title={timelineTypeContext?.title ?? i18n.EVENTS}
// TODO: Reset eventDeletedIds/eventLoadingIds on refresh/loadmore (getUpdatedAt)
return (
<>
<HeaderSection
id={id}
subtitle={
utilityBar
? undefined
: `${
i18n.SHOWING
}: ${totalCountMinusDeleted.toLocaleString()} ${i18n.UNIT(
totalCountMinusDeleted
)}`
}
title={timelineTypeContext?.title ?? i18n.EVENTS}
>
{headerFilterGroup}
</HeaderSection>
{utilityBar?.(totalCountMinusDeleted)}
<div
data-test-subj={`events-container-loading-${loading}`}
style={{ width: `${width}px` }}
>
<ManageTimelineContext
loading={loading}
width={width}
type={timelineTypeContext}
>
{headerFilterGroup}
</HeaderSection>
{utilityBar?.(totalCountMinusDeleted)}
<div
data-test-subj={`events-container-loading-${loading}`}
style={{ width: `${width}px` }}
>
<ManageTimelineContext
<TimelineRefetch
id={id}
inputId="global"
inspect={inspect}
loading={loading}
width={width}
type={timelineTypeContext}
>
<TimelineRefetch
id={id}
inputId="global"
inspect={inspect}
loading={loading}
refetch={refetch}
/>
refetch={refetch}
/>
<StatefulBody
browserFields={browserFields}
data={events.filter(e => !deletedEventIds.includes(e._id))}
id={id}
isEventViewer={true}
height={height}
sort={sort}
toggleColumn={toggleColumn}
/>
<StatefulBody
browserFields={browserFields}
data={events.filter(e => !deletedEventIds.includes(e._id))}
id={id}
isEventViewer={true}
height={height}
sort={sort}
toggleColumn={toggleColumn}
/>
<Footer
compact={isCompactFooter(width)}
getUpdatedAt={getUpdatedAt}
hasNextPage={getOr(false, 'hasNextPage', pageInfo)!}
height={footerHeight}
isEventViewer={true}
isLive={isLive}
isLoading={loading}
itemsCount={events.length}
itemsPerPage={itemsPerPage}
itemsPerPageOptions={itemsPerPageOptions}
onChangeItemsPerPage={onChangeItemsPerPage}
onLoadMore={loadMore}
nextCursor={getOr(null, 'endCursor.value', pageInfo)!}
serverSideEventCount={totalCountMinusDeleted}
tieBreaker={getOr(null, 'endCursor.tiebreaker', pageInfo)}
/>
</ManageTimelineContext>
</div>
</>
);
}}
</TimelineQuery>
) : null}
</>
)}
</AutoSizer>
</EuiPanel>
);
},
<Footer
compact={isCompactFooter(width)}
getUpdatedAt={getUpdatedAt}
hasNextPage={getOr(false, 'hasNextPage', pageInfo)!}
height={footerHeight}
isEventViewer={true}
isLive={isLive}
isLoading={loading}
itemsCount={events.length}
itemsPerPage={itemsPerPage}
itemsPerPageOptions={itemsPerPageOptions}
onChangeItemsPerPage={onChangeItemsPerPage}
onLoadMore={loadMore}
nextCursor={getOr(null, 'endCursor.value', pageInfo)!}
serverSideEventCount={totalCountMinusDeleted}
tieBreaker={getOr(null, 'endCursor.tiebreaker', pageInfo)}
/>
</ManageTimelineContext>
</div>
</>
);
}}
</TimelineQuery>
) : null}
</>
)}
</AutoSizer>
</EuiPanel>
);
};
export const EventsViewer = React.memo(
EventsViewerComponent,
(prevProps, nextProps) =>
prevProps.browserFields === nextProps.browserFields &&
prevProps.columns === nextProps.columns &&
@ -247,10 +246,8 @@ export const EventsViewer = React.memo<Props>(
prevProps.itemsPerPageOptions === nextProps.itemsPerPageOptions &&
prevProps.kqlMode === nextProps.kqlMode &&
isEqual(prevProps.query, nextProps.query) &&
prevProps.showInspect === nextProps.showInspect &&
prevProps.start === nextProps.start &&
prevProps.sort === nextProps.sort &&
isEqual(prevProps.timelineTypeContext, nextProps.timelineTypeContext) &&
prevProps.utilityBar === nextProps.utilityBar
);
EventsViewer.displayName = 'EventsViewer';

View file

@ -57,7 +57,8 @@ describe('StatefulEventsViewer', () => {
).toBe(true);
});
test('it renders a transparent inspect button when it does NOT have mouse focus', async () => {
// InspectButtonContainer controls displaying InspectButton components
test('it renders InspectButtonContainer', async () => {
const wrapper = mount(
<TestProviders>
<MockedProvider mocks={mockEventViewerResponse} addTypename={false}>
@ -74,39 +75,6 @@ describe('StatefulEventsViewer', () => {
await wait();
wrapper.update();
expect(
wrapper
.find(`[data-test-subj="transparent-inspect-container"]`)
.first()
.exists()
).toBe(true);
});
test('it renders an opaque inspect button when it has mouse focus', async () => {
const wrapper = mount(
<TestProviders>
<MockedProvider mocks={mockEventViewerResponse} addTypename={false}>
<StatefulEventsViewer
defaultModel={eventsDefaultModel}
end={to}
id={'test-stateful-events-viewer'}
start={from}
/>
</MockedProvider>
</TestProviders>
);
await wait();
wrapper.update();
wrapper.simulate('mouseenter');
wrapper.update();
expect(
wrapper
.find(`[data-test-subj="opaque-inspect-container"]`)
.first()
.exists()
).toBe(true);
expect(wrapper.find(`InspectButtonContainer`).exists()).toBe(true);
});
});

View file

@ -5,7 +5,7 @@
*/
import { isEqual } from 'lodash/fp';
import React, { useCallback, useEffect, useState } from 'react';
import React, { useCallback, useEffect } from 'react';
import { connect } from 'react-redux';
import { ActionCreator } from 'typescript-fsa';
import { inputsModel, inputsSelectors, State, timelineSelectors } from '../../store';
@ -23,6 +23,7 @@ import { InputsModelId } from '../../store/inputs/constants';
import { useFetchIndexPatterns } from '../../containers/detection_engine/rules/fetch_index_patterns';
import { TimelineTypeContextProps } from '../timeline/timeline_context';
import { DEFAULT_INDEX_KEY } from '../../../common/constants';
import { InspectButtonContainer } from '../inspect';
import * as i18n from './translations';
export interface OwnProps {
@ -83,133 +84,102 @@ interface DispatchProps {
type Props = OwnProps & StateReduxProps & DispatchProps;
const StatefulEventsViewerComponent = React.memo<Props>(
({
createTimeline,
columns,
dataProviders,
defaultModel,
deletedEventIds,
defaultIndices,
deleteEventQuery,
end,
filters,
headerFilterGroup,
id,
isLive,
itemsPerPage,
itemsPerPageOptions,
kqlMode,
pageFilters = [],
query,
removeColumn,
start,
showCheckboxes,
showRowRenderers,
sort,
timelineTypeContext = {
loadingText: i18n.LOADING_EVENTS,
},
updateItemsPerPage,
upsertColumn,
utilityBar,
}) => {
const [showInspect, setShowInspect] = useState(false);
const [{ browserFields, indexPatterns }] = useFetchIndexPatterns(
defaultIndices ?? useUiSetting<string[]>(DEFAULT_INDEX_KEY)
);
useEffect(() => {
if (createTimeline != null) {
createTimeline({ id, columns, sort, itemsPerPage, showCheckboxes, showRowRenderers });
}
return () => {
deleteEventQuery({ id, inputId: 'global' });
};
}, []);
const onChangeItemsPerPage: OnChangeItemsPerPage = useCallback(
itemsChangedPerPage => updateItemsPerPage({ id, itemsPerPage: itemsChangedPerPage }),
[id, updateItemsPerPage]
);
const toggleColumn = useCallback(
(column: ColumnHeader) => {
const exists = columns.findIndex(c => c.id === column.id) !== -1;
if (!exists && upsertColumn != null) {
upsertColumn({
column,
id,
index: 1,
});
}
if (exists && removeColumn != null) {
removeColumn({
columnId: column.id,
id,
});
}
},
[columns, id, upsertColumn, removeColumn]
);
const handleOnMouseEnter = useCallback(() => setShowInspect(true), []);
const handleOnMouseLeave = useCallback(() => setShowInspect(false), []);
return (
<div onMouseEnter={handleOnMouseEnter} onMouseLeave={handleOnMouseLeave}>
<EventsViewer
browserFields={browserFields ?? {}}
columns={columns}
id={id}
dataProviders={dataProviders!}
deletedEventIds={deletedEventIds}
end={end}
filters={filters}
headerFilterGroup={headerFilterGroup}
indexPattern={indexPatterns ?? { fields: [], title: '' }}
isLive={isLive}
itemsPerPage={itemsPerPage!}
itemsPerPageOptions={itemsPerPageOptions!}
kqlMode={kqlMode}
onChangeItemsPerPage={onChangeItemsPerPage}
query={query}
showInspect={showInspect}
start={start}
sort={sort!}
timelineTypeContext={timelineTypeContext}
toggleColumn={toggleColumn}
utilityBar={utilityBar}
/>
</div>
);
const StatefulEventsViewerComponent: React.FC<Props> = ({
createTimeline,
columns,
dataProviders,
deletedEventIds,
defaultIndices,
deleteEventQuery,
end,
filters,
headerFilterGroup,
id,
isLive,
itemsPerPage,
itemsPerPageOptions,
kqlMode,
pageFilters = [],
query,
removeColumn,
start,
showCheckboxes,
showRowRenderers,
sort,
timelineTypeContext = {
loadingText: i18n.LOADING_EVENTS,
},
(prevProps, nextProps) =>
prevProps.id === nextProps.id &&
isEqual(prevProps.columns, nextProps.columns) &&
isEqual(prevProps.dataProviders, nextProps.dataProviders) &&
prevProps.deletedEventIds === nextProps.deletedEventIds &&
prevProps.end === nextProps.end &&
isEqual(prevProps.filters, nextProps.filters) &&
prevProps.isLive === nextProps.isLive &&
prevProps.itemsPerPage === nextProps.itemsPerPage &&
isEqual(prevProps.itemsPerPageOptions, nextProps.itemsPerPageOptions) &&
prevProps.kqlMode === nextProps.kqlMode &&
isEqual(prevProps.query, nextProps.query) &&
prevProps.pageCount === nextProps.pageCount &&
isEqual(prevProps.sort, nextProps.sort) &&
prevProps.start === nextProps.start &&
isEqual(prevProps.pageFilters, nextProps.pageFilters) &&
prevProps.showCheckboxes === nextProps.showCheckboxes &&
prevProps.showRowRenderers === nextProps.showRowRenderers &&
prevProps.start === nextProps.start &&
isEqual(prevProps.timelineTypeContext, nextProps.timelineTypeContext) &&
prevProps.utilityBar === nextProps.utilityBar
);
updateItemsPerPage,
upsertColumn,
utilityBar,
}) => {
const [{ browserFields, indexPatterns }] = useFetchIndexPatterns(
defaultIndices ?? useUiSetting<string[]>(DEFAULT_INDEX_KEY)
);
StatefulEventsViewerComponent.displayName = 'StatefulEventsViewerComponent';
useEffect(() => {
if (createTimeline != null) {
createTimeline({ id, columns, sort, itemsPerPage, showCheckboxes, showRowRenderers });
}
return () => {
deleteEventQuery({ id, inputId: 'global' });
};
}, []);
const onChangeItemsPerPage: OnChangeItemsPerPage = useCallback(
itemsChangedPerPage => updateItemsPerPage({ id, itemsPerPage: itemsChangedPerPage }),
[id, updateItemsPerPage]
);
const toggleColumn = useCallback(
(column: ColumnHeader) => {
const exists = columns.findIndex(c => c.id === column.id) !== -1;
if (!exists && upsertColumn != null) {
upsertColumn({
column,
id,
index: 1,
});
}
if (exists && removeColumn != null) {
removeColumn({
columnId: column.id,
id,
});
}
},
[columns, id, upsertColumn, removeColumn]
);
return (
<InspectButtonContainer>
<EventsViewer
browserFields={browserFields ?? {}}
columns={columns}
id={id}
dataProviders={dataProviders!}
deletedEventIds={deletedEventIds}
end={end}
filters={filters}
headerFilterGroup={headerFilterGroup}
indexPattern={indexPatterns ?? { fields: [], title: '' }}
isLive={isLive}
itemsPerPage={itemsPerPage!}
itemsPerPageOptions={itemsPerPageOptions!}
kqlMode={kqlMode}
onChangeItemsPerPage={onChangeItemsPerPage}
query={query}
start={start}
sort={sort!}
timelineTypeContext={timelineTypeContext}
toggleColumn={toggleColumn}
utilityBar={utilityBar}
/>
</InspectButtonContainer>
);
};
const makeMapStateToProps = () => {
const getInputsTimeline = inputsSelectors.getTimelineSelector();
@ -256,4 +226,29 @@ export const StatefulEventsViewer = connect(makeMapStateToProps, {
updateItemsPerPage: timelineActions.updateItemsPerPage,
removeColumn: timelineActions.removeColumn,
upsertColumn: timelineActions.upsertColumn,
})(StatefulEventsViewerComponent);
})(
React.memo(
StatefulEventsViewerComponent,
(prevProps, nextProps) =>
prevProps.id === nextProps.id &&
isEqual(prevProps.columns, nextProps.columns) &&
isEqual(prevProps.dataProviders, nextProps.dataProviders) &&
prevProps.deletedEventIds === nextProps.deletedEventIds &&
prevProps.end === nextProps.end &&
isEqual(prevProps.filters, nextProps.filters) &&
prevProps.isLive === nextProps.isLive &&
prevProps.itemsPerPage === nextProps.itemsPerPage &&
isEqual(prevProps.itemsPerPageOptions, nextProps.itemsPerPageOptions) &&
prevProps.kqlMode === nextProps.kqlMode &&
isEqual(prevProps.query, nextProps.query) &&
prevProps.pageCount === nextProps.pageCount &&
isEqual(prevProps.sort, nextProps.sort) &&
prevProps.start === nextProps.start &&
isEqual(prevProps.pageFilters, nextProps.pageFilters) &&
prevProps.showCheckboxes === nextProps.showCheckboxes &&
prevProps.showRowRenderers === nextProps.showRowRenderers &&
prevProps.start === nextProps.start &&
isEqual(prevProps.timelineTypeContext, nextProps.timelineTypeContext) &&
prevProps.utilityBar === nextProps.utilityBar
)
);

View file

@ -63,36 +63,6 @@ describe('HeaderSection', () => {
).toBe(false);
});
test('it renders a transparent inspect button when showInspect is false', () => {
const wrapper = mount(
<TestProviders>
<HeaderSection title="Test title" id="test" showInspect={false} />
</TestProviders>
);
expect(
wrapper
.find('[data-test-subj="transparent-inspect-container"]')
.first()
.exists()
).toBe(true);
});
test('it renders an opaque inspect button when showInspect is true', () => {
const wrapper = mount(
<TestProviders>
<HeaderSection title="Test title" id="test" showInspect={true} />
</TestProviders>
);
expect(
wrapper
.find('[data-test-subj="opaque-inspect-container"]')
.first()
.exists()
).toBe(true);
});
test('it renders supplements when children provided', () => {
const wrapper = mount(
<TestProviders>

View file

@ -35,48 +35,54 @@ export interface HeaderSectionProps extends HeaderProps {
id?: string;
split?: boolean;
subtitle?: string | React.ReactNode;
showInspect?: boolean;
title: string | React.ReactNode;
tooltip?: string;
}
export const HeaderSection = React.memo<HeaderSectionProps>(
({ border, children, id, showInspect = false, split, subtitle, title, tooltip }) => (
<Header border={border}>
<EuiFlexGroup alignItems="center">
<EuiFlexItem>
<EuiFlexGroup alignItems="center" responsive={false}>
<EuiFlexItem>
<EuiTitle>
<h2 data-test-subj="header-section-title">
{title}
{tooltip && (
<>
{' '}
<EuiIconTip color="subdued" content={tooltip} size="l" type="iInCircle" />
</>
)}
</h2>
</EuiTitle>
const HeaderSectionComponent: React.FC<HeaderSectionProps> = ({
border,
children,
id,
split,
subtitle,
title,
tooltip,
}) => (
<Header border={border}>
<EuiFlexGroup alignItems="center">
<EuiFlexItem>
<EuiFlexGroup alignItems="center" responsive={false}>
<EuiFlexItem>
<EuiTitle>
<h2 data-test-subj="header-section-title">
{title}
{tooltip && (
<>
{' '}
<EuiIconTip color="subdued" content={tooltip} size="l" type="iInCircle" />
</>
)}
</h2>
</EuiTitle>
{subtitle && <Subtitle data-test-subj="header-section-subtitle" items={subtitle} />}
</EuiFlexItem>
{id && (
<EuiFlexItem grow={false}>
<InspectButton queryId={id} inspectIndex={0} show={showInspect} title={title} />
</EuiFlexItem>
)}
</EuiFlexGroup>
</EuiFlexItem>
{children && (
<EuiFlexItem data-test-subj="header-section-supplements" grow={split ? true : false}>
{children}
{subtitle && <Subtitle data-test-subj="header-section-subtitle" items={subtitle} />}
</EuiFlexItem>
)}
</EuiFlexGroup>
</Header>
)
{id && (
<EuiFlexItem grow={false}>
<InspectButton queryId={id} inspectIndex={0} title={title} />
</EuiFlexItem>
)}
</EuiFlexGroup>
</EuiFlexItem>
{children && (
<EuiFlexItem data-test-subj="header-section-supplements" grow={split ? true : false}>
{children}
</EuiFlexItem>
)}
</EuiFlexGroup>
</Header>
);
HeaderSection.displayName = 'HeaderSection';
export const HeaderSection = React.memo(HeaderSectionComponent);

View file

@ -17,7 +17,7 @@ import {
import { createStore, State } from '../../store';
import { UpdateQueryParams, upsertQuery } from '../../store/inputs/helpers';
import { InspectButton } from '.';
import { InspectButton, InspectButtonContainer, BUTTON_CLASS } from '.';
import { cloneDeep } from 'lodash/fp';
describe('Inspect Button', () => {
@ -44,7 +44,7 @@ describe('Inspect Button', () => {
test('Eui Empty Button', () => {
const wrapper = mount(
<TestProviderWithoutDragAndDrop store={store}>
<InspectButton queryId={newQuery.id} inputId="timeline" show={true} title="My title" />
<InspectButton queryId={newQuery.id} inputId="timeline" title="My title" />
</TestProviderWithoutDragAndDrop>
);
expect(
@ -58,13 +58,7 @@ describe('Inspect Button', () => {
test('it does NOT render the Eui Empty Button when timeline is timeline and compact is true', () => {
const wrapper = mount(
<TestProviderWithoutDragAndDrop store={store}>
<InspectButton
compact={true}
queryId={newQuery.id}
inputId="timeline"
show={true}
title="My title"
/>
<InspectButton compact={true} queryId={newQuery.id} inputId="timeline" title="My title" />
</TestProviderWithoutDragAndDrop>
);
expect(
@ -78,7 +72,7 @@ describe('Inspect Button', () => {
test('Eui Icon Button', () => {
const wrapper = mount(
<TestProviderWithoutDragAndDrop store={store}>
<InspectButton queryId={newQuery.id} show={true} title="My title" />
<InspectButton queryId={newQuery.id} title="My title" />
</TestProviderWithoutDragAndDrop>
);
expect(
@ -92,13 +86,7 @@ describe('Inspect Button', () => {
test('renders the Icon Button when inputId does NOT equal global, but compact is true', () => {
const wrapper = mount(
<TestProviderWithoutDragAndDrop store={store}>
<InspectButton
compact={true}
inputId="timeline"
queryId={newQuery.id}
show={true}
title="My title"
/>
<InspectButton compact={true} inputId="timeline" queryId={newQuery.id} title="My title" />
</TestProviderWithoutDragAndDrop>
);
expect(
@ -112,7 +100,7 @@ describe('Inspect Button', () => {
test('Eui Empty Button disabled', () => {
const wrapper = mount(
<TestProviderWithoutDragAndDrop store={store}>
<InspectButton isDisabled={true} queryId={newQuery.id} show={true} title="My title" />
<InspectButton isDisabled={true} queryId={newQuery.id} title="My title" />
</TestProviderWithoutDragAndDrop>
);
expect(wrapper.find('.euiButtonIcon').get(0).props.disabled).toBe(true);
@ -121,11 +109,41 @@ describe('Inspect Button', () => {
test('Eui Icon Button disabled', () => {
const wrapper = mount(
<TestProviderWithoutDragAndDrop store={store}>
<InspectButton isDisabled={true} queryId={newQuery.id} show={true} title="My title" />
<InspectButton isDisabled={true} queryId={newQuery.id} title="My title" />
</TestProviderWithoutDragAndDrop>
);
expect(wrapper.find('.euiButtonIcon').get(0).props.disabled).toBe(true);
});
describe('InspectButtonContainer', () => {
test('it renders a transparent inspect button by default', async () => {
const wrapper = mount(
<TestProviderWithoutDragAndDrop store={store}>
<InspectButtonContainer>
<InspectButton queryId={newQuery.id} title="My title" />
</InspectButtonContainer>
</TestProviderWithoutDragAndDrop>
);
expect(wrapper.find(`InspectButtonContainer`)).toHaveStyleRule('opacity', '0', {
modifier: `.${BUTTON_CLASS}`,
});
});
test('it renders an opaque inspect button when it has mouse focus', async () => {
const wrapper = mount(
<TestProviderWithoutDragAndDrop store={store}>
<InspectButtonContainer>
<InspectButton queryId={newQuery.id} title="My title" />
</InspectButtonContainer>
</TestProviderWithoutDragAndDrop>
);
expect(wrapper.find(`InspectButtonContainer`)).toHaveStyleRule('opacity', '1', {
modifier: `:hover .${BUTTON_CLASS}`,
});
});
});
});
describe('Modal Inspect - happy path', () => {
@ -143,7 +161,7 @@ describe('Inspect Button', () => {
const wrapper = mount(
<ThemeProvider theme={theme}>
<TestProviderWithoutDragAndDrop store={store}>
<InspectButton queryId={newQuery.id} show={true} title="My title" />
<InspectButton queryId={newQuery.id} title="My title" />
</TestProviderWithoutDragAndDrop>
</ThemeProvider>
);
@ -167,7 +185,7 @@ describe('Inspect Button', () => {
const wrapper = mount(
<ThemeProvider theme={theme}>
<TestProviderWithoutDragAndDrop store={store}>
<InspectButton queryId={newQuery.id} show={true} title="My title" />
<InspectButton queryId={newQuery.id} title="My title" />
</TestProviderWithoutDragAndDrop>
</ThemeProvider>
);
@ -197,7 +215,7 @@ describe('Inspect Button', () => {
test('Do not Open Inspect Modal if it is loading', () => {
const wrapper = mount(
<TestProviderWithoutDragAndDrop store={store}>
<InspectButton queryId={newQuery.id} show={true} title="My title" />
<InspectButton queryId={newQuery.id} title="My title" />
</TestProviderWithoutDragAndDrop>
);
store.getState().inputs.global.queries[0].loading = true;

View file

@ -9,7 +9,7 @@ import { getOr } from 'lodash/fp';
import React, { useCallback } from 'react';
import { connect } from 'react-redux';
import { ActionCreator } from 'typescript-fsa';
import styled from 'styled-components';
import styled, { css } from 'styled-components';
import { inputsModel, inputsSelectors, State } from '../../store';
import { InputsModelId } from '../../store/inputs/constants';
@ -18,14 +18,31 @@ import { inputsActions } from '../../store/inputs';
import { ModalInspectQuery } from './modal';
import * as i18n from './translations';
const InspectContainer = styled.div<{ showInspect: boolean }>`
.euiButtonIcon {
${props => (props.showInspect ? 'opacity: 1;' : 'opacity: 0;')}
export const BUTTON_CLASS = 'inspectButtonComponent';
export const InspectButtonContainer = styled.div<{ show?: boolean }>`
display: flex;
flex-grow: 1;
.${BUTTON_CLASS} {
opacity: 0;
transition: opacity ${props => getOr(250, 'theme.eui.euiAnimSpeedNormal', props)} ease;
}
${({ show }) =>
show &&
css`
&:hover .${BUTTON_CLASS} {
opacity: 1;
}
`}
`;
InspectContainer.displayName = 'InspectContainer';
InspectButtonContainer.displayName = 'InspectButtonContainer';
InspectButtonContainer.defaultProps = {
show: true,
};
interface OwnProps {
compact?: boolean;
@ -34,7 +51,6 @@ interface OwnProps {
inspectIndex?: number;
isDisabled?: boolean;
onCloseInspect?: () => void;
show: boolean;
title: string | React.ReactElement | React.ReactNode;
}
@ -57,89 +73,84 @@ interface InspectButtonDispatch {
type InspectButtonProps = OwnProps & InspectButtonReducer & InspectButtonDispatch;
const InspectButtonComponent = React.memo<InspectButtonProps>(
({
compact = false,
inputId = 'global',
inspect,
isDisabled,
isInspected,
loading,
inspectIndex = 0,
onCloseInspect,
queryId = '',
selectedInspectIndex,
setIsInspected,
show,
title = '',
}: InspectButtonProps) => {
const handleClick = useCallback(() => {
setIsInspected({
id: queryId,
inputId,
isInspected: true,
selectedInspectIndex: inspectIndex,
});
}, [setIsInspected, queryId, inputId, inspectIndex]);
const InspectButtonComponent: React.FC<InspectButtonProps> = ({
compact = false,
inputId = 'global',
inspect,
isDisabled,
isInspected,
loading,
inspectIndex = 0,
onCloseInspect,
queryId = '',
selectedInspectIndex,
setIsInspected,
title = '',
}) => {
const isShowingModal = !loading && selectedInspectIndex === inspectIndex && isInspected;
const handleClick = useCallback(() => {
setIsInspected({
id: queryId,
inputId,
isInspected: true,
selectedInspectIndex: inspectIndex,
});
}, [setIsInspected, queryId, inputId, inspectIndex]);
const handleCloseModal = useCallback(() => {
if (onCloseInspect != null) {
onCloseInspect();
}
setIsInspected({
id: queryId,
inputId,
isInspected: false,
selectedInspectIndex: inspectIndex,
});
}, [onCloseInspect, setIsInspected, queryId, inputId, inspectIndex]);
const handleCloseModal = useCallback(() => {
if (onCloseInspect != null) {
onCloseInspect();
}
setIsInspected({
id: queryId,
inputId,
isInspected: false,
selectedInspectIndex: inspectIndex,
});
}, [onCloseInspect, setIsInspected, queryId, inputId, inspectIndex]);
return (
<InspectContainer
data-test-subj={`${show ? 'opaque' : 'transparent'}-inspect-container`}
showInspect={show}
>
{inputId === 'timeline' && !compact && (
<EuiButtonEmpty
aria-label={i18n.INSPECT}
data-test-subj="inspect-empty-button"
color="text"
iconSide="left"
iconType="inspect"
isDisabled={loading || isDisabled}
isLoading={loading}
onClick={handleClick}
>
{i18n.INSPECT}
</EuiButtonEmpty>
)}
{(inputId === 'global' || compact) && (
<EuiButtonIcon
aria-label={i18n.INSPECT}
data-test-subj="inspect-icon-button"
iconSize="m"
iconType="inspect"
isDisabled={loading || isDisabled}
title={i18n.INSPECT}
onClick={handleClick}
/>
)}
<ModalInspectQuery
closeModal={handleCloseModal}
isShowing={!loading && selectedInspectIndex === inspectIndex && isInspected}
request={inspect != null && inspect.dsl.length > 0 ? inspect.dsl[inspectIndex] : null}
response={
inspect != null && inspect.response.length > 0 ? inspect.response[inspectIndex] : null
}
title={title}
data-test-subj="inspect-modal"
return (
<>
{inputId === 'timeline' && !compact && (
<EuiButtonEmpty
className={BUTTON_CLASS}
aria-label={i18n.INSPECT}
data-test-subj="inspect-empty-button"
color="text"
iconSide="left"
iconType="inspect"
isDisabled={loading || isDisabled}
isLoading={loading}
onClick={handleClick}
>
{i18n.INSPECT}
</EuiButtonEmpty>
)}
{(inputId === 'global' || compact) && (
<EuiButtonIcon
className={BUTTON_CLASS}
aria-label={i18n.INSPECT}
data-test-subj="inspect-icon-button"
iconSize="m"
iconType="inspect"
isDisabled={loading || isDisabled}
title={i18n.INSPECT}
onClick={handleClick}
/>
</InspectContainer>
);
}
);
InspectButtonComponent.displayName = 'InspectButtonComponent';
)}
<ModalInspectQuery
closeModal={handleCloseModal}
isShowing={isShowingModal}
request={inspect != null && inspect.dsl.length > 0 ? inspect.dsl[inspectIndex] : null}
response={
inspect != null && inspect.response.length > 0 ? inspect.response[inspectIndex] : null
}
title={title}
data-test-subj="inspect-modal"
/>
</>
);
};
const makeMapStateToProps = () => {
const getGlobalQuery = inputsSelectors.globalQueryByIdSelector();
@ -150,6 +161,11 @@ const makeMapStateToProps = () => {
return mapStateToProps;
};
export const InspectButton = connect(makeMapStateToProps, {
const mapDispatchToProps = {
setIsInspected: inputsActions.setInspectionParameter,
})(InspectButtonComponent);
};
export const InspectButton = connect(
makeMapStateToProps,
mapDispatchToProps
)(React.memo(InspectButtonComponent));

View file

@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import React, { useState, useEffect, useCallback } from 'react';
import React, { useState, useEffect } from 'react';
import { ScaleType } from '@elastic/charts';
import darkTheme from '@elastic/eui/dist/eui_theme_dark.json';
@ -17,10 +17,11 @@ import { DEFAULT_DARK_MODE } from '../../../common/constants';
import { useUiSetting$ } from '../../lib/kibana';
import { Loader } from '../loader';
import { Panel } from '../panel';
import { InspectButtonContainer } from '../inspect';
import { getBarchartConfigs, getCustomChartData } from './utils';
import { MatrixHistogramProps, MatrixHistogramDataTypes } from './types';
export const MatrixHistogram = ({
export const MatrixHistogramComponent: React.FC<MatrixHistogramProps<MatrixHistogramDataTypes>> = ({
data,
dataKey,
endDate,
@ -35,7 +36,7 @@ export const MatrixHistogram = ({
updateDateRange,
yTickFormatter,
showLegend,
}: MatrixHistogramProps<MatrixHistogramDataTypes>) => {
}) => {
const barchartConfigs = getBarchartConfigs({
from: startDate,
to: endDate,
@ -44,7 +45,6 @@ export const MatrixHistogram = ({
yTickFormatter,
showLegend,
});
const [showInspect, setShowInspect] = useState(false);
const [darkMode] = useUiSetting$<boolean>(DEFAULT_DARK_MODE);
const [loadingInitial, setLoadingInitial] = useState(false);
@ -56,40 +56,31 @@ export const MatrixHistogram = ({
}
}, [loading, loadingInitial, totalCount]);
const handleOnMouseEnter = useCallback(() => setShowInspect(true), []);
const handleOnMouseLeave = useCallback(() => setShowInspect(false), []);
return (
<Panel
data-test-subj={`${dataKey}Panel`}
loading={loading}
onMouseEnter={handleOnMouseEnter}
onMouseLeave={handleOnMouseLeave}
>
<HeaderSection
id={id}
title={title}
showInspect={!loadingInitial && showInspect}
subtitle={!loadingInitial && subtitle}
/>
<InspectButtonContainer show={!loadingInitial}>
<Panel data-test-subj={`${dataKey}Panel`} loading={loading}>
<HeaderSection id={id} title={title} subtitle={!loadingInitial && subtitle} />
{loadingInitial ? (
<EuiLoadingContent data-test-subj="initialLoadingPanelMatrixOverTime" lines={10} />
) : (
<>
<BarChart barChart={barChartData} configs={barchartConfigs} />
{loadingInitial ? (
<EuiLoadingContent data-test-subj="initialLoadingPanelMatrixOverTime" lines={10} />
) : (
<>
<BarChart barChart={barChartData} configs={barchartConfigs} />
{loading && (
<Loader
overlay
overlayBackground={
darkMode ? darkTheme.euiPageBackgroundColor : lightTheme.euiPageBackgroundColor
}
size="xl"
/>
)}
</>
)}
</Panel>
{loading && (
<Loader
overlay
overlayBackground={
darkMode ? darkTheme.euiPageBackgroundColor : lightTheme.euiPageBackgroundColor
}
size="xl"
/>
)}
</>
)}
</Panel>
</InspectButtonContainer>
);
};
export const MatrixHistogram = React.memo(MatrixHistogramComponent);

View file

@ -8,14 +8,14 @@ import { EuiFlexItem } from '@elastic/eui';
import darkTheme from '@elastic/eui/dist/eui_theme_dark.json';
import lightTheme from '@elastic/eui/dist/eui_theme_light.json';
import { getOr } from 'lodash/fp';
import React, { useContext, useState, useCallback } from 'react';
import React, { useContext } from 'react';
import { DEFAULT_DARK_MODE } from '../../../../../common/constants';
import { DescriptionList } from '../../../../../common/utility_types';
import { useUiSetting$ } from '../../../../lib/kibana';
import { getEmptyTagValue } from '../../../empty_value';
import { DefaultFieldRenderer, hostIdRenderer } from '../../../field_renderers/field_renderers';
import { InspectButton } from '../../../inspect';
import { InspectButton, InspectButtonContainer } from '../../../inspect';
import { HostItem } from '../../../../graphql/types';
import { Loader } from '../../../loader';
import { IPDetailsLink } from '../../../links';
@ -56,7 +56,6 @@ export const HostOverview = React.memo<HostSummaryProps>(
anomaliesData,
narrowDateRange,
}) => {
const [showInspect, setShowInspect] = useState(false);
const capabilities = useContext(MlCapabilitiesContext);
const userPermissions = hasMlUserPermissions(capabilities);
const [darkMode] = useUiSetting$<boolean>(DEFAULT_DARK_MODE);
@ -165,32 +164,26 @@ export const HostOverview = React.memo<HostSummaryProps>(
],
];
const handleOnMouseEnter = useCallback(() => setShowInspect(true), []);
const handleOnMouseLeave = useCallback(() => setShowInspect(false), []);
return (
<OverviewWrapper onMouseEnter={handleOnMouseEnter} onMouseLeave={handleOnMouseLeave}>
<InspectButton
queryId={id}
show={showInspect}
title={i18n.INSPECT_TITLE}
inspectIndex={0}
/>
<InspectButtonContainer>
<OverviewWrapper>
<InspectButton queryId={id} title={i18n.INSPECT_TITLE} inspectIndex={0} />
{descriptionLists.map((descriptionList, index) =>
getDescriptionList(descriptionList, index)
)}
{descriptionLists.map((descriptionList, index) =>
getDescriptionList(descriptionList, index)
)}
{loading && (
<Loader
overlay
overlayBackground={
darkMode ? darkTheme.euiPageBackgroundColor : lightTheme.euiPageBackgroundColor
}
size="xl"
/>
)}
</OverviewWrapper>
{loading && (
<Loader
overlay
overlayBackground={
darkMode ? darkTheme.euiPageBackgroundColor : lightTheme.euiPageBackgroundColor
}
size="xl"
/>
)}
</OverviewWrapper>
</InspectButtonContainer>
);
}
);

View file

@ -7,7 +7,7 @@
import { EuiFlexItem } from '@elastic/eui';
import darkTheme from '@elastic/eui/dist/eui_theme_dark.json';
import lightTheme from '@elastic/eui/dist/eui_theme_light.json';
import React, { useContext, useState, useCallback } from 'react';
import React, { useContext } from 'react';
import { DEFAULT_DARK_MODE } from '../../../../../common/constants';
import { DescriptionList } from '../../../../../common/utility_types';
@ -32,7 +32,7 @@ import { Anomalies, NarrowDateRange } from '../../../ml/types';
import { AnomalyScores } from '../../../ml/score/anomaly_scores';
import { MlCapabilitiesContext } from '../../../ml/permissions/ml_capabilities_provider';
import { hasMlUserPermissions } from '../../../ml/permissions/has_ml_user_permissions';
import { InspectButton } from '../../../inspect';
import { InspectButton, InspectButtonContainer } from '../../../inspect';
interface OwnProps {
data: IpOverviewData;
@ -71,7 +71,6 @@ export const IpOverview = React.memo<IpOverviewProps>(
anomaliesData,
narrowDateRange,
}) => {
const [showInspect, setShowInspect] = useState(false);
const capabilities = useContext(MlCapabilitiesContext);
const userPermissions = hasMlUserPermissions(capabilities);
const [darkMode] = useUiSetting$<boolean>(DEFAULT_DARK_MODE);
@ -140,32 +139,26 @@ export const IpOverview = React.memo<IpOverviewProps>(
],
];
const handleOnMouseEnter = useCallback(() => setShowInspect(true), []);
const handleOnMouseLeave = useCallback(() => setShowInspect(false), []);
return (
<OverviewWrapper onMouseEnter={handleOnMouseEnter} onMouseLeave={handleOnMouseLeave}>
<InspectButton
queryId={id}
show={showInspect}
title={i18n.INSPECT_TITLE}
inspectIndex={0}
/>
<InspectButtonContainer>
<OverviewWrapper>
<InspectButton queryId={id} title={i18n.INSPECT_TITLE} inspectIndex={0} />
{descriptionLists.map((descriptionList, index) =>
getDescriptionList(descriptionList, index)
)}
{descriptionLists.map((descriptionList, index) =>
getDescriptionList(descriptionList, index)
)}
{loading && (
<Loader
overlay
overlayBackground={
darkMode ? darkTheme.euiPageBackgroundColor : lightTheme.euiPageBackgroundColor
}
size="xl"
/>
)}
</OverviewWrapper>
{loading && (
<Loader
overlay
overlayBackground={
darkMode ? darkTheme.euiPageBackgroundColor : lightTheme.euiPageBackgroundColor
}
size="xl"
/>
)}
</OverviewWrapper>
</InspectButtonContainer>
);
}
);

View file

@ -6,7 +6,7 @@
import { EuiButton, EuiFlexItem, EuiPanel } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import React, { useState, useCallback } from 'react';
import React from 'react';
import { HeaderSection } from '../../../header_section';
import { manageQuery } from '../../../page/manage_query';
@ -17,6 +17,7 @@ import {
import { inputsModel } from '../../../../store/inputs';
import { OverviewHostStats } from '../overview_host_stats';
import { getHostsUrl } from '../../../link_to';
import { InspectButtonContainer } from '../../../inspect';
export interface OwnProps {
startDate: number;
@ -36,18 +37,14 @@ export interface OwnProps {
const OverviewHostStatsManage = manageQuery(OverviewHostStats);
type OverviewHostProps = OwnProps;
export const OverviewHost = React.memo<OverviewHostProps>(({ endDate, startDate, setQuery }) => {
const [isHover, setIsHover] = useState(false);
const handleMouseEnter = useCallback(() => setIsHover(true), [setIsHover]);
const handleMouseLeave = useCallback(() => setIsHover(false), [setIsHover]);
return (
<EuiFlexItem>
<EuiPanel onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave}>
const OverviewHostComponent: React.FC<OverviewHostProps> = ({ endDate, startDate, setQuery }) => (
<EuiFlexItem>
<InspectButtonContainer>
<EuiPanel>
<HeaderSection
border
id={OverviewHostQueryId}
showInspect={isHover}
subtitle={
<FormattedMessage
id="xpack.siem.overview.hostsSubtitle"
@ -76,8 +73,8 @@ export const OverviewHost = React.memo<OverviewHostProps>(({ endDate, startDate,
)}
</OverviewHostQuery>
</EuiPanel>
</EuiFlexItem>
);
});
</InspectButtonContainer>
</EuiFlexItem>
);
OverviewHost.displayName = 'OverviewHost';
export const OverviewHost = React.memo(OverviewHostComponent);

View file

@ -6,7 +6,7 @@
import { EuiButton, EuiFlexItem, EuiPanel } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import React, { useState, useCallback } from 'react';
import React from 'react';
import { HeaderSection } from '../../../header_section';
import { manageQuery } from '../../../page/manage_query';
@ -17,6 +17,7 @@ import {
import { inputsModel } from '../../../../store/inputs';
import { OverviewNetworkStats } from '../overview_network_stats';
import { getNetworkUrl } from '../../../link_to';
import { InspectButtonContainer } from '../../../inspect';
export interface OwnProps {
startDate: number;
@ -36,18 +37,13 @@ export interface OwnProps {
const OverviewNetworkStatsManage = manageQuery(OverviewNetworkStats);
export const OverviewNetwork = React.memo<OwnProps>(({ endDate, startDate, setQuery }) => {
const [isHover, setIsHover] = useState(false);
const handleMouseEnter = useCallback(() => setIsHover(true), [setIsHover]);
const handleMouseLeave = useCallback(() => setIsHover(false), [setIsHover]);
return (
<EuiFlexItem>
<EuiPanel onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave}>
const OverviewNetworkComponent: React.FC<OwnProps> = ({ endDate, startDate, setQuery }) => (
<EuiFlexItem>
<InspectButtonContainer>
<EuiPanel>
<HeaderSection
border
id={OverviewNetworkQueryId}
showInspect={isHover}
subtitle={
<FormattedMessage
id="xpack.siem.overview.networkSubtitle"
@ -82,8 +78,8 @@ export const OverviewNetwork = React.memo<OwnProps>(({ endDate, startDate, setQu
)}
</OverviewNetworkQuery>
</EuiPanel>
</EuiFlexItem>
);
});
</InspectButtonContainer>
</EuiFlexItem>
);
OverviewNetwork.displayName = 'OverviewNetwork';
export const OverviewNetwork = React.memo(OverviewNetworkComponent);

View file

@ -520,7 +520,7 @@ exports[`Paginated Table Component rendering it renders the default load more ta
}
}
>
<PaginatedTable
<PaginatedTableComponent
activePage={0}
columns={
Array [

View file

@ -18,7 +18,7 @@ import {
Direction,
} from '@elastic/eui';
import { noop } from 'lodash/fp';
import React, { memo, useState, useEffect, useCallback, ComponentType } from 'react';
import React, { FC, memo, useState, useEffect, ComponentType } from 'react';
import styled from 'styled-components';
import { AuthTableColumns } from '../page/hosts/authentications_table';
@ -43,6 +43,7 @@ import { DEFAULT_MAX_TABLE_QUERY_SIZE } from '../../../common/constants';
import * as i18n from './translations';
import { Panel } from '../panel';
import { InspectButtonContainer } from '../inspect';
const DEFAULT_DATA_TEST_SUBJ = 'paginated-table';
@ -124,122 +125,113 @@ export interface Columns<T, U = T> {
width?: string;
}
export const PaginatedTable = memo<SiemTables>(
({
activePage,
columns,
dataTestSubj = DEFAULT_DATA_TEST_SUBJ,
headerCount,
headerSupplement,
headerTitle,
headerTooltip,
headerUnit,
id,
isInspect,
itemsPerRow,
limit,
loading,
loadPage,
onChange = noop,
pageOfItems,
showMorePagesIndicator,
sorting = null,
totalCount,
updateActivePage,
updateLimitPagination,
}) => {
const [myLoading, setMyLoading] = useState(loading);
const [myActivePage, setActivePage] = useState(activePage);
const [showInspect, setShowInspect] = useState(false);
const [loadingInitial, setLoadingInitial] = useState(headerCount === -1);
const [isPopoverOpen, setPopoverOpen] = useState(false);
const PaginatedTableComponent: FC<SiemTables> = ({
activePage,
columns,
dataTestSubj = DEFAULT_DATA_TEST_SUBJ,
headerCount,
headerSupplement,
headerTitle,
headerTooltip,
headerUnit,
id,
isInspect,
itemsPerRow,
limit,
loading,
loadPage,
onChange = noop,
pageOfItems,
showMorePagesIndicator,
sorting = null,
totalCount,
updateActivePage,
updateLimitPagination,
}) => {
const [myLoading, setMyLoading] = useState(loading);
const [myActivePage, setActivePage] = useState(activePage);
const [loadingInitial, setLoadingInitial] = useState(headerCount === -1);
const [isPopoverOpen, setPopoverOpen] = useState(false);
const pageCount = Math.ceil(totalCount / limit);
const dispatchToaster = useStateToaster()[1];
const pageCount = Math.ceil(totalCount / limit);
const dispatchToaster = useStateToaster()[1];
useEffect(() => {
setActivePage(activePage);
}, [activePage]);
useEffect(() => {
setActivePage(activePage);
}, [activePage]);
useEffect(() => {
if (headerCount >= 0 && loadingInitial) {
setLoadingInitial(false);
}
}, [loadingInitial, headerCount]);
useEffect(() => {
if (headerCount >= 0 && loadingInitial) {
setLoadingInitial(false);
}
}, [loadingInitial, headerCount]);
useEffect(() => {
setMyLoading(loading);
}, [loading]);
useEffect(() => {
setMyLoading(loading);
}, [loading]);
const onButtonClick = () => {
setPopoverOpen(!isPopoverOpen);
};
const onButtonClick = () => {
setPopoverOpen(!isPopoverOpen);
};
const closePopover = () => {
setPopoverOpen(false);
};
const closePopover = () => {
setPopoverOpen(false);
};
const goToPage = (newActivePage: number) => {
if ((newActivePage + 1) * limit >= DEFAULT_MAX_TABLE_QUERY_SIZE) {
const toast: Toast = {
id: 'PaginationWarningMsg',
title: headerTitle + i18n.TOAST_TITLE,
color: 'warning',
iconType: 'alert',
toastLifeTimeMs: 10000,
text: i18n.TOAST_TEXT,
};
return dispatchToaster({
type: 'addToaster',
toast,
});
}
setActivePage(newActivePage);
loadPage(newActivePage);
updateActivePage(newActivePage);
};
const goToPage = (newActivePage: number) => {
if ((newActivePage + 1) * limit >= DEFAULT_MAX_TABLE_QUERY_SIZE) {
const toast: Toast = {
id: 'PaginationWarningMsg',
title: headerTitle + i18n.TOAST_TITLE,
color: 'warning',
iconType: 'alert',
toastLifeTimeMs: 10000,
text: i18n.TOAST_TEXT,
};
return dispatchToaster({
type: 'addToaster',
toast,
});
}
setActivePage(newActivePage);
loadPage(newActivePage);
updateActivePage(newActivePage);
};
const button = (
<EuiButtonEmpty
size="xs"
color="text"
iconType="arrowDown"
iconSide="right"
onClick={onButtonClick}
const button = (
<EuiButtonEmpty
size="xs"
color="text"
iconType="arrowDown"
iconSide="right"
onClick={onButtonClick}
>
{`${i18n.ROWS}: ${limit}`}
</EuiButtonEmpty>
);
const rowItems =
itemsPerRow &&
itemsPerRow.map((item: ItemsPerRow) => (
<EuiContextMenuItem
key={item.text}
icon={limit === item.numberOfRow ? 'check' : 'empty'}
onClick={() => {
closePopover();
updateLimitPagination(item.numberOfRow);
updateActivePage(0); // reset results to first page
}}
>
{`${i18n.ROWS}: ${limit}`}
</EuiButtonEmpty>
);
{item.text}
</EuiContextMenuItem>
));
const PaginationWrapper = showMorePagesIndicator ? PaginationEuiFlexItem : EuiFlexItem;
const rowItems =
itemsPerRow &&
itemsPerRow.map((item: ItemsPerRow) => (
<EuiContextMenuItem
key={item.text}
icon={limit === item.numberOfRow ? 'check' : 'empty'}
onClick={() => {
closePopover();
updateLimitPagination(item.numberOfRow);
updateActivePage(0); // reset results to first page
}}
>
{item.text}
</EuiContextMenuItem>
));
const PaginationWrapper = showMorePagesIndicator ? PaginationEuiFlexItem : EuiFlexItem;
const handleOnMouseEnter = useCallback(() => setShowInspect(true), []);
const handleOnMouseLeave = useCallback(() => setShowInspect(false), []);
return (
<Panel
data-test-subj={`${dataTestSubj}-loading-${loading}`}
loading={loading}
onMouseEnter={handleOnMouseEnter}
onMouseLeave={handleOnMouseLeave}
>
return (
<InspectButtonContainer show={!loadingInitial}>
<Panel data-test-subj={`${dataTestSubj}-loading-${loading}`} loading={loading}>
<HeaderSection
id={id}
showInspect={!loadingInitial && showInspect}
subtitle={
!loadingInitial &&
`${i18n.SHOWING}: ${headerCount >= 0 ? headerCount.toLocaleString() : 0} ${headerUnit}`
@ -306,11 +298,11 @@ export const PaginatedTable = memo<SiemTables>(
</>
)}
</Panel>
);
}
);
</InspectButtonContainer>
);
};
PaginatedTable.displayName = 'PaginatedTable';
export const PaginatedTable = memo(PaginatedTableComponent);
type BasicTableType = ComponentType<EuiBasicTableProps<any>>; // eslint-disable-line @typescript-eslint/no-explicit-any
const BasicTable: typeof EuiBasicTable & { displayName: string } = styled(

View file

@ -21,7 +21,7 @@ import {
IconType,
} from '@elastic/eui';
import { get, getOr } from 'lodash/fp';
import React, { useState, useEffect, useCallback } from 'react';
import React, { useState, useEffect } from 'react';
import styled from 'styled-components';
import { KpiHostsData, KpiNetworkData } from '../../graphql/types';
@ -30,7 +30,7 @@ import { BarChart } from '../charts/barchart';
import { ChartSeriesData, ChartData, ChartSeriesConfigs, UpdateDateRange } from '../charts/common';
import { getEmptyTagValue } from '../empty_value';
import { InspectButton } from '../inspect';
import { InspectButton, InspectButtonContainer } from '../inspect';
const FlexItem = styled(EuiFlexItem)`
min-width: 0;
@ -209,9 +209,6 @@ export const StatItemsComponent = React.memo<StatItemsProps>(
statKey = 'item',
to,
}) => {
const [isHover, setIsHover] = useState(false);
const handleMouseEnter = useCallback(() => setIsHover(true), [setIsHover]);
const handleMouseLeave = useCallback(() => setIsHover(false), [setIsHover]);
const isBarChartDataAvailable =
barChart &&
barChart.length &&
@ -223,72 +220,69 @@ export const StatItemsComponent = React.memo<StatItemsProps>(
return (
<FlexItem grow={grow} data-test-subj={`stat-${statKey}`}>
<EuiPanel onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave}>
<EuiFlexGroup gutterSize={'none'}>
<EuiFlexItem>
<EuiTitle size="xxxs">
<h6>{description}</h6>
</EuiTitle>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<InspectButton
queryId={id}
title={`KPI ${description}`}
inspectIndex={index}
show={isHover}
/>
</EuiFlexItem>
</EuiFlexGroup>
<InspectButtonContainer>
<EuiPanel>
<EuiFlexGroup gutterSize={'none'}>
<EuiFlexItem>
<EuiTitle size="xxxs">
<h6>{description}</h6>
</EuiTitle>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<InspectButton queryId={id} title={`KPI ${description}`} inspectIndex={index} />
</EuiFlexItem>
</EuiFlexGroup>
<EuiFlexGroup>
{fields.map(field => (
<FlexItem key={`stat-items-field-${field.key}`}>
<EuiFlexGroup alignItems="center" gutterSize="m" responsive={false}>
{(isAreaChartDataAvailable || isBarChartDataAvailable) && field.icon && (
<FlexItem grow={false}>
<EuiIcon
type={field.icon}
color={field.color}
size="l"
data-test-subj="stat-icon"
/>
<EuiFlexGroup>
{fields.map(field => (
<FlexItem key={`stat-items-field-${field.key}`}>
<EuiFlexGroup alignItems="center" gutterSize="m" responsive={false}>
{(isAreaChartDataAvailable || isBarChartDataAvailable) && field.icon && (
<FlexItem grow={false}>
<EuiIcon
type={field.icon}
color={field.color}
size="l"
data-test-subj="stat-icon"
/>
</FlexItem>
)}
<FlexItem>
<StatValue>
<p data-test-subj="stat-title">
{field.value != null ? field.value.toLocaleString() : getEmptyTagValue()}{' '}
{field.description}
</p>
</StatValue>
</FlexItem>
)}
</EuiFlexGroup>
</FlexItem>
))}
</EuiFlexGroup>
<FlexItem>
<StatValue>
<p data-test-subj="stat-title">
{field.value != null ? field.value.toLocaleString() : getEmptyTagValue()}{' '}
{field.description}
</p>
</StatValue>
</FlexItem>
</EuiFlexGroup>
</FlexItem>
))}
</EuiFlexGroup>
{(enableAreaChart || enableBarChart) && <EuiHorizontalRule />}
<EuiFlexGroup>
{enableBarChart && (
<FlexItem>
<BarChart barChart={barChart} configs={barchartConfigs()} />
</FlexItem>
)}
{(enableAreaChart || enableBarChart) && <EuiHorizontalRule />}
<EuiFlexGroup>
{enableBarChart && (
<FlexItem>
<BarChart barChart={barChart} configs={barchartConfigs()} />
</FlexItem>
)}
{enableAreaChart && from != null && to != null && (
<FlexItem>
<AreaChart
areaChart={areaChart}
configs={areachartConfigs({
xTickFormatter: niceTimeFormatter([from, to]),
onBrushEnd: narrowDateRange,
})}
/>
</FlexItem>
)}
</EuiFlexGroup>
</EuiPanel>
{enableAreaChart && from != null && to != null && (
<FlexItem>
<AreaChart
areaChart={areaChart}
configs={areachartConfigs({
xTickFormatter: niceTimeFormatter([from, to]),
onBrushEnd: narrowDateRange,
})}
/>
</FlexItem>
)}
</EuiFlexGroup>
</EuiPanel>
</InspectButtonContainer>
</FlexItem>
);
}

View file

@ -17,7 +17,7 @@ import {
import { NewTimeline, Description, NotesButton } from './helpers';
import { OpenTimelineModalButton } from '../../open_timeline/open_timeline_modal/open_timeline_modal_button';
import { OpenTimelineModal } from '../../open_timeline/open_timeline_modal';
import { InspectButton } from '../../inspect';
import { InspectButton, InspectButtonContainer } from '../../inspect';
import * as i18n from './translations';
import { AssociateNote } from '../../notes/helpers';
@ -82,32 +82,32 @@ interface Props {
updateNote: UpdateNote;
}
export const PropertiesRight = React.memo<Props>(
({
onButtonClick,
showActions,
onClosePopover,
createTimeline,
timelineId,
isDataInTimeline,
showNotesFromWidth,
showNotes,
showDescription,
showUsersView,
usersViewing,
description,
updateDescription,
associateNote,
getNotesByIds,
noteIds,
onToggleShowNotes,
updateNote,
showTimelineModal,
onCloseTimelineModal,
onOpenTimelineModal,
}) => (
<PropertiesRightStyle alignItems="flexStart" data-test-subj="properties-right" gutterSize="s">
<EuiFlexItem grow={false}>
const PropertiesRightComponent: React.FC<Props> = ({
onButtonClick,
showActions,
onClosePopover,
createTimeline,
timelineId,
isDataInTimeline,
showNotesFromWidth,
showNotes,
showDescription,
showUsersView,
usersViewing,
description,
updateDescription,
associateNote,
getNotesByIds,
noteIds,
onToggleShowNotes,
updateNote,
showTimelineModal,
onCloseTimelineModal,
onOpenTimelineModal,
}) => (
<PropertiesRightStyle alignItems="flexStart" data-test-subj="properties-right" gutterSize="s">
<EuiFlexItem grow={false}>
<InspectButtonContainer>
<EuiPopover
anchorPosition="downRight"
button={
@ -142,7 +142,6 @@ export const PropertiesRight = React.memo<Props>(
inspectIndex={0}
isDisabled={!isDataInTimeline}
onCloseInspect={onClosePopover}
show={true}
title={i18n.INSPECT_TIMELINE_TITLE}
/>
</EuiFlexItem>
@ -177,26 +176,26 @@ export const PropertiesRight = React.memo<Props>(
) : null}
</EuiFlexGroup>
</EuiPopover>
</EuiFlexItem>
</InspectButtonContainer>
</EuiFlexItem>
{showUsersView
? usersViewing.map(user => (
// Hide the hard-coded elastic user avatar as the 7.2 release does not implement
// support for multi-user-collaboration as proposed in elastic/ingest-dev#395
<HiddenFlexItem key={user}>
<EuiToolTip
data-test-subj="timeline-action-pin-tool-tip"
content={`${user} ${i18n.IS_VIEWING}`}
>
<Avatar data-test-subj="avatar" size="s" name={user} />
</EuiToolTip>
</HiddenFlexItem>
))
: null}
{showUsersView
? usersViewing.map(user => (
// Hide the hard-coded elastic user avatar as the 7.2 release does not implement
// support for multi-user-collaboration as proposed in elastic/ingest-dev#395
<HiddenFlexItem key={user}>
<EuiToolTip
data-test-subj="timeline-action-pin-tool-tip"
content={`${user} ${i18n.IS_VIEWING}`}
>
<Avatar data-test-subj="avatar" size="s" name={user} />
</EuiToolTip>
</HiddenFlexItem>
))
: null}
{showTimelineModal ? <OpenTimelineModal onClose={onCloseTimelineModal} /> : null}
</PropertiesRightStyle>
)
{showTimelineModal ? <OpenTimelineModal onClose={onCloseTimelineModal} /> : null}
</PropertiesRightStyle>
);
PropertiesRight.displayName = 'PropertiesRight';
export const PropertiesRight = React.memo(PropertiesRightComponent);