[RAC] Add loading and empty states to the alerts table - Take II (#110504)

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Alejandro Fernández Gómez 2021-09-03 12:59:56 +02:00 committed by GitHub
parent 641cef7ca6
commit 4e9e7a8671
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 195 additions and 144 deletions

View file

@ -5,7 +5,6 @@
* 2.0. * 2.0.
*/ */
import { EuiLoadingContent, EuiPanel } from '@elastic/eui';
import { isEmpty } from 'lodash/fp'; import { isEmpty } from 'lodash/fp';
import React, { useCallback, useEffect, useMemo, useState } from 'react'; import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { connect, ConnectedProps, useDispatch } from 'react-redux'; import { connect, ConnectedProps, useDispatch } from 'react-redux';
@ -369,11 +368,7 @@ export const AlertsTableComponent: React.FC<AlertsTableComponentProps> = ({
}, [dispatch, defaultTimelineModel, filterManager, tGridEnabled, timelineId]); }, [dispatch, defaultTimelineModel, filterManager, tGridEnabled, timelineId]);
if (loading || indexPatternsLoading || isEmpty(selectedPatterns)) { if (loading || indexPatternsLoading || isEmpty(selectedPatterns)) {
return ( return null;
<EuiPanel hasBorder={false} hasShadow={false} paddingSize="none">
<EuiLoadingContent data-test-subj="loading-alerts-panel" />
</EuiPanel>
);
} }
return ( return (

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 126 KiB

View file

@ -8,19 +8,12 @@
import type { AlertConsumers as AlertConsumersTyped } from '@kbn/rule-data-utils'; import type { AlertConsumers as AlertConsumersTyped } from '@kbn/rule-data-utils';
// @ts-expect-error // @ts-expect-error
import { AlertConsumers as AlertConsumersNonTyped } from '@kbn/rule-data-utils/target_node/alerts_as_data_rbac'; import { AlertConsumers as AlertConsumersNonTyped } from '@kbn/rule-data-utils/target_node/alerts_as_data_rbac';
import { import { EuiFlexGroup, EuiFlexItem, EuiPanel } from '@elastic/eui';
EuiEmptyPrompt,
EuiFlexGroup,
EuiFlexItem,
EuiPanel,
EuiLoadingContent,
} from '@elastic/eui';
import { isEmpty } from 'lodash/fp'; import { isEmpty } from 'lodash/fp';
import React, { useEffect, useMemo, useRef, useState } from 'react'; import React, { useEffect, useMemo, useRef, useState } from 'react';
import styled from 'styled-components'; import styled from 'styled-components';
import { useDispatch } from 'react-redux'; import { useDispatch } from 'react-redux';
import { FormattedMessage } from '@kbn/i18n/react';
import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; import { useKibana } from '../../../../../../../src/plugins/kibana_react/public';
import { Direction, EntityType } from '../../../../common/search_strategy'; import { Direction, EntityType } from '../../../../common/search_strategy';
import type { DocValueFields } from '../../../../common/search_strategy'; import type { DocValueFields } from '../../../../common/search_strategy';
@ -53,6 +46,7 @@ import { SELECTOR_TIMELINE_GLOBAL_CONTAINER, UpdatedFlexGroup, UpdatedFlexItem }
import { Sort } from '../body/sort'; import { Sort } from '../body/sort';
import { InspectButton, InspectButtonContainer } from '../../inspect'; import { InspectButton, InspectButtonContainer } from '../../inspect';
import { SummaryViewSelector, ViewSelection } from '../event_rendered_view/selector'; import { SummaryViewSelector, ViewSelection } from '../event_rendered_view/selector';
import { TGridLoading, TGridEmpty } from '../shared';
const AlertConsumers: typeof AlertConsumersTyped = AlertConsumersNonTyped; const AlertConsumers: typeof AlertConsumersTyped = AlertConsumersNonTyped;
@ -269,6 +263,8 @@ const TGridIntegratedComponent: React.FC<TGridIntegratedProps> = ({
[deletedEventIds.length, totalCount] [deletedEventIds.length, totalCount]
); );
const hasAlerts = totalCountMinusDeleted > 0;
const nonDeletedEvents = useMemo(() => events.filter((e) => !deletedEventIds.includes(e._id)), [ const nonDeletedEvents = useMemo(() => events.filter((e) => !deletedEventIds.includes(e._id)), [
deletedEventIds, deletedEventIds,
events, events,
@ -300,7 +296,7 @@ const TGridIntegratedComponent: React.FC<TGridIntegratedProps> = ({
data-test-subj="events-viewer-panel" data-test-subj="events-viewer-panel"
$isFullScreen={globalFullScreen} $isFullScreen={globalFullScreen}
> >
{isFirstUpdate.current && <EuiLoadingContent data-test-subj="loading-alerts-panel" />} {isFirstUpdate.current && <TGridLoading height="short" />}
{graphOverlay} {graphOverlay}
@ -325,61 +321,43 @@ const TGridIntegratedComponent: React.FC<TGridIntegratedProps> = ({
</UpdatedFlexGroup> </UpdatedFlexGroup>
{!graphEventId && graphOverlay == null && ( {!graphEventId && graphOverlay == null && (
<FullWidthFlexGroup <>
$visible={!graphEventId && graphOverlay == null} {!hasAlerts && !loading && <TGridEmpty height="short" />}
gutterSize="none" {hasAlerts && (
> <FullWidthFlexGroup
<ScrollableFlexItem grow={1}> $visible={!graphEventId && graphOverlay == null}
{totalCountMinusDeleted === 0 && loading === false && ( gutterSize="none"
<EuiEmptyPrompt >
title={ <ScrollableFlexItem grow={1}>
<h2> <StatefulBody
<FormattedMessage hasAlertsCrud={hasAlertsCrud}
id="xpack.timelines.tGrid.noResultsMatchSearchCriteriaTitle" activePage={pageInfo.activePage}
defaultMessage="No results match your search criteria" browserFields={browserFields}
/> filterQuery={filterQuery}
</h2> data={nonDeletedEvents}
} defaultCellActions={defaultCellActions}
titleSize="s" id={id}
body={ isEventViewer={true}
<p> itemsPerPageOptions={itemsPerPageOptions}
<FormattedMessage loadPage={loadPage}
id="xpack.timelines.tGrid.noResultsMatchSearchCriteriaDescription" onRuleChange={onRuleChange}
defaultMessage="Try searching over a longer period of time or modifying your search." pageSize={itemsPerPage}
/> renderCellValue={renderCellValue}
</p> rowRenderers={rowRenderers}
} tabType={TimelineTabs.query}
/> tableView={tableView}
)} totalItems={totalCountMinusDeleted}
{totalCountMinusDeleted > 0 && ( unit={unit}
<StatefulBody filterStatus={filterStatus}
hasAlertsCrud={hasAlertsCrud} leadingControlColumns={leadingControlColumns}
activePage={pageInfo.activePage} trailingControlColumns={trailingControlColumns}
browserFields={browserFields} refetch={refetch}
filterQuery={filterQuery} indexNames={indexNames}
data={nonDeletedEvents} />
defaultCellActions={defaultCellActions} </ScrollableFlexItem>
id={id} </FullWidthFlexGroup>
isEventViewer={true} )}
itemsPerPageOptions={itemsPerPageOptions} </>
loadPage={loadPage}
onRuleChange={onRuleChange}
pageSize={itemsPerPage}
renderCellValue={renderCellValue}
rowRenderers={rowRenderers}
tabType={TimelineTabs.query}
tableView={tableView}
totalItems={totalCountMinusDeleted}
unit={unit}
filterStatus={filterStatus}
leadingControlColumns={leadingControlColumns}
trailingControlColumns={trailingControlColumns}
refetch={refetch}
indexNames={indexNames}
/>
)}
</ScrollableFlexItem>
</FullWidthFlexGroup>
)} )}
</EventsContainerLoading> </EventsContainerLoading>
)} )}

View file

@ -0,0 +1,90 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import {
EuiPanel,
EuiFlexGroup,
EuiFlexItem,
EuiLoadingSpinner,
EuiImage,
EuiText,
EuiTitle,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { useKibana } from '../../../../../../../src/plugins/kibana_react/public';
import type { CoreStart } from '../../../../../../../src/core/public';
const heights = {
tall: 490,
short: 250,
};
export const TGridLoading: React.FC<{ height?: keyof typeof heights }> = ({ height = 'tall' }) => {
return (
<EuiPanel color="subdued">
<EuiFlexGroup
style={{ height: heights[height] }}
alignItems="center"
justifyContent="center"
data-test-subj="loading-alerts-panel"
>
<EuiFlexItem grow={false}>
<EuiLoadingSpinner size="xl" />
</EuiFlexItem>
</EuiFlexGroup>
</EuiPanel>
);
};
const panelStyle = {
maxWidth: 500,
};
export const TGridEmpty: React.FC<{ height?: keyof typeof heights }> = ({ height = 'tall' }) => {
const { http } = useKibana<CoreStart>().services;
return (
<EuiPanel color="subdued">
<EuiFlexGroup style={{ height: heights[height] }} alignItems="center" justifyContent="center">
<EuiFlexItem grow={false}>
<EuiPanel hasBorder={true} style={panelStyle}>
<EuiFlexGroup>
<EuiFlexItem>
<EuiText size="s">
<EuiTitle>
<h3>
<FormattedMessage
id="xpack.timelines.tgrid.empty.title"
defaultMessage="No results match your search criteria"
/>
</h3>
</EuiTitle>
<p>
<FormattedMessage
id="xpack.timelines.tgrid.empty.description"
defaultMessage="Try searching over a longer period of time or modifying your search"
/>
</p>
</EuiText>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiImage
size="200"
alt=""
url={http.basePath.prepend(
'/plugins/timelines/assets/illustration_product_no_results_magnifying_glass.svg'
)}
/>
</EuiFlexItem>
</EuiFlexGroup>
</EuiPanel>
</EuiFlexItem>
</EuiFlexGroup>
</EuiPanel>
);
};

View file

@ -4,8 +4,7 @@
* 2.0; you may not use this file except in compliance with the Elastic License * 2.0; you may not use this file except in compliance with the Elastic License
* 2.0. * 2.0.
*/ */
import { EuiEmptyPrompt, EuiFlexGroup, EuiFlexItem, EuiLoadingContent } from '@elastic/eui'; import { EuiFlexItem } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { isEmpty } from 'lodash/fp'; import { isEmpty } from 'lodash/fp';
import React, { useEffect, useMemo, useState, useRef } from 'react'; import React, { useEffect, useMemo, useState, useRef } from 'react';
import styled from 'styled-components'; import styled from 'styled-components';
@ -39,10 +38,16 @@ import type { State } from '../../../store/t_grid';
import { useTimelineEvents } from '../../../container'; import { useTimelineEvents } from '../../../container';
import { StatefulBody } from '../body'; import { StatefulBody } from '../body';
import { LastUpdatedAt } from '../..'; import { LastUpdatedAt } from '../..';
import { SELECTOR_TIMELINE_GLOBAL_CONTAINER, UpdatedFlexItem, UpdatedFlexGroup } from '../styles'; import {
SELECTOR_TIMELINE_GLOBAL_CONTAINER,
UpdatedFlexItem,
UpdatedFlexGroup,
FullWidthFlexGroup,
} from '../styles';
import { InspectButton, InspectButtonContainer } from '../../inspect'; import { InspectButton, InspectButtonContainer } from '../../inspect';
import { useFetchIndex } from '../../../container/source'; import { useFetchIndex } from '../../../container/source';
import { AddToCaseAction } from '../../actions/timeline/cases/add_to_case_action'; import { AddToCaseAction } from '../../actions/timeline/cases/add_to_case_action';
import { TGridLoading, TGridEmpty } from '../shared';
export const EVENTS_VIEWER_HEADER_HEIGHT = 90; // px export const EVENTS_VIEWER_HEADER_HEIGHT = 90; // px
const STANDALONE_ID = 'standalone-t-grid'; const STANDALONE_ID = 'standalone-t-grid';
@ -68,12 +73,6 @@ const EventsContainerLoading = styled.div.attrs(({ className = '' }) => ({
flex-direction: column; flex-direction: column;
`; `;
const FullWidthFlexGroup = styled(EuiFlexGroup)<{ $visible: boolean }>`
overflow: hidden;
margin: 0;
display: ${({ $visible }) => ($visible ? 'flex' : 'none')};
`;
const ScrollableFlexItem = styled(EuiFlexItem)` const ScrollableFlexItem = styled(EuiFlexItem)`
overflow: auto; overflow: auto;
`; `;
@ -255,6 +254,8 @@ const TGridStandaloneComponent: React.FC<TGridStandaloneProps> = ({
() => (totalCount > 0 ? totalCount - deletedEventIds.length : 0), () => (totalCount > 0 ? totalCount - deletedEventIds.length : 0),
[deletedEventIds.length, totalCount] [deletedEventIds.length, totalCount]
); );
const hasAlerts = totalCountMinusDeleted > 0;
const activeCaseFlowId = useSelector((state: State) => tGridSelectors.activeCaseFlowId(state)); const activeCaseFlowId = useSelector((state: State) => tGridSelectors.activeCaseFlowId(state));
const selectedEvent = useMemo(() => { const selectedEvent = useMemo(() => {
const matchedEvent = events.find((event) => event.ecs._id === activeCaseFlowId); const matchedEvent = events.find((event) => event.ecs._id === activeCaseFlowId);
@ -338,14 +339,14 @@ const TGridStandaloneComponent: React.FC<TGridStandaloneProps> = ({
return ( return (
<InspectButtonContainer data-test-subj="events-viewer-panel"> <InspectButtonContainer data-test-subj="events-viewer-panel">
<AlertsTableWrapper> <AlertsTableWrapper>
{isFirstUpdate.current && <EuiLoadingContent data-test-subj="loading-alerts-panel" />} {isFirstUpdate.current && <TGridLoading />}
{canQueryTimeline ? ( {canQueryTimeline ? (
<> <>
<EventsContainerLoading <EventsContainerLoading
data-timeline-id={STANDALONE_ID} data-timeline-id={STANDALONE_ID}
data-test-subj={`events-container-loading-${loading}`} data-test-subj={`events-container-loading-${loading}`}
> >
<UpdatedFlexGroup gutterSize="s" justifyContent="flexEnd" alignItems="baseline"> <UpdatedFlexGroup gutterSize="s" justifyContent="flexEnd" alignItems="center">
<UpdatedFlexItem grow={false} $show={!loading}> <UpdatedFlexItem grow={false} $show={!loading}>
<InspectButton title={justTitle} inspect={inspect} loading={loading} /> <InspectButton title={justTitle} inspect={inspect} loading={loading} />
</UpdatedFlexItem> </UpdatedFlexItem>
@ -354,28 +355,9 @@ const TGridStandaloneComponent: React.FC<TGridStandaloneProps> = ({
</UpdatedFlexItem> </UpdatedFlexItem>
</UpdatedFlexGroup> </UpdatedFlexGroup>
{totalCountMinusDeleted === 0 && loading === false && ( {!hasAlerts && !loading && <TGridEmpty />}
<EuiEmptyPrompt
title={ {hasAlerts && (
<h2>
<FormattedMessage
id="xpack.timelines.tGrid.noResultsMatchSearchCriteriaTitle"
defaultMessage="No results match your search criteria"
/>
</h2>
}
titleSize="s"
body={
<p>
<FormattedMessage
id="xpack.timelines.tGrid.noResultsMatchSearchCriteriaDescription"
defaultMessage="Try searching over a longer period of time or modifying your search."
/>
</p>
}
/>
)}
{totalCountMinusDeleted > 0 && (
<FullWidthFlexGroup direction="row" $visible={!graphEventId} gutterSize="none"> <FullWidthFlexGroup direction="row" $visible={!graphEventId} gutterSize="none">
<ScrollableFlexItem grow={1}> <ScrollableFlexItem grow={1}>
<StatefulBody <StatefulBody

View file

@ -459,6 +459,13 @@ export const HideShowContainer = styled.div.attrs<{ $isVisible: boolean }>(
}) })
)<{ $isVisible: boolean }>``; )<{ $isVisible: boolean }>``;
export const FullWidthFlexGroup = styled(EuiFlexGroup)<{ $visible?: boolean }>`
overflow: hidden;
margin: 0;
min-height: 490px;
display: ${({ $visible = true }) => ($visible ? 'flex' : 'none')};
`;
export const UpdatedFlexGroup = styled(EuiFlexGroup)` export const UpdatedFlexGroup = styled(EuiFlexGroup)`
position: absolute; position: absolute;
z-index: ${({ theme }) => theme.eui.euiZLevel1}; z-index: ${({ theme }) => theme.eui.euiZLevel1};

View file

@ -6,7 +6,7 @@
*/ */
import React, { lazy, Suspense } from 'react'; import React, { lazy, Suspense } from 'react';
import { EuiLoadingContent, EuiLoadingSpinner, EuiPanel } from '@elastic/eui'; import { EuiLoadingSpinner } from '@elastic/eui';
import { I18nProvider } from '@kbn/i18n/react'; import { I18nProvider } from '@kbn/i18n/react';
import type { Store } from 'redux'; import type { Store } from 'redux';
import { Provider } from 'react-redux'; import { Provider } from 'react-redux';
@ -17,6 +17,7 @@ import type { LastUpdatedAtProps, LoadingPanelProps, FieldBrowserProps } from '.
import type { AddToCaseActionProps } from '../components/actions/timeline/cases/add_to_case_action'; import type { AddToCaseActionProps } from '../components/actions/timeline/cases/add_to_case_action';
import { initialTGridState } from '../store/t_grid/reducer'; import { initialTGridState } from '../store/t_grid/reducer';
import { createStore } from '../store/t_grid'; import { createStore } from '../store/t_grid';
import { TGridLoading } from '../components/t_grid/shared';
const initializeStore = ({ const initializeStore = ({
store, store,
@ -51,13 +52,7 @@ export const getTGridLazy = (
) => { ) => {
initializeStore({ store, storage, setStore }); initializeStore({ store, storage, setStore });
return ( return (
<Suspense <Suspense fallback={<TGridLoading height={props.type === 'standalone' ? 'tall' : 'short'} />}>
fallback={
<EuiPanel hasBorder={false} hasShadow={false} paddingSize="none">
<EuiLoadingContent />
</EuiPanel>
}
>
<TimelineLazy {...props} store={store} storage={storage} data={data} setStore={setStore} /> <TimelineLazy {...props} store={store} storage={storage} data={data} setStore={setStore} />
</Suspense> </Suspense>
); );

View file

@ -11,6 +11,7 @@ import ReactDOM from 'react-dom';
import { AppMountParameters, CoreStart } from 'kibana/public'; import { AppMountParameters, CoreStart } from 'kibana/public';
import { I18nProvider } from '@kbn/i18n/react'; import { I18nProvider } from '@kbn/i18n/react';
import { KibanaContextProvider } from '../../../../../../../../src/plugins/kibana_react/public'; import { KibanaContextProvider } from '../../../../../../../../src/plugins/kibana_react/public';
import { EuiThemeProvider } from '../../../../../../../../src/plugins/kibana_react/common';
import { TimelinesUIStart } from '../../../../../../../plugins/timelines/public'; import { TimelinesUIStart } from '../../../../../../../plugins/timelines/public';
import { DataPublicPluginStart } from '../../../../../../../../src/plugins/data/public'; import { DataPublicPluginStart } from '../../../../../../../../src/plugins/data/public';
@ -60,39 +61,41 @@ const AppRoot = React.memo(
<I18nProvider> <I18nProvider>
<Router history={parameters.history}> <Router history={parameters.history}>
<KibanaContextProvider services={coreStart}> <KibanaContextProvider services={coreStart}>
{(timelinesPluginSetup && <EuiThemeProvider>
timelinesPluginSetup.getTGrid && {(timelinesPluginSetup &&
timelinesPluginSetup.getTGrid<'standalone'>({ timelinesPluginSetup.getTGrid &&
appId: 'securitySolution', timelinesPluginSetup.getTGrid<'standalone'>({
type: 'standalone', appId: 'securitySolution',
casePermissions: { type: 'standalone',
read: true, casePermissions: {
crud: true, read: true,
}, crud: true,
columns: [], },
indexNames: [], columns: [],
deletedEventIds: [], indexNames: [],
end: '', deletedEventIds: [],
footerText: 'Events', end: '',
filters: [], footerText: 'Events',
hasAlertsCrudPermissions, filters: [],
itemsPerPageOptions: [1, 2, 3], hasAlertsCrudPermissions,
loadingText: 'Loading events', itemsPerPageOptions: [1, 2, 3],
renderCellValue: () => <div data-test-subj="timeline-wrapper">test</div>, loadingText: 'Loading events',
sort: [], renderCellValue: () => <div data-test-subj="timeline-wrapper">test</div>,
leadingControlColumns: [], sort: [],
trailingControlColumns: [], leadingControlColumns: [],
query: { trailingControlColumns: [],
query: '', query: {
language: 'kuery', query: '',
}, language: 'kuery',
setRefetch, },
start: '', setRefetch,
rowRenderers: [], start: '',
filterStatus: 'open', rowRenderers: [],
unit: (n: number) => `${n}`, filterStatus: 'open',
})) ?? unit: (n: number) => `${n}`,
null} })) ??
null}
</EuiThemeProvider>
</KibanaContextProvider> </KibanaContextProvider>
</Router> </Router>
</I18nProvider> </I18nProvider>