[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.
*/
import { EuiLoadingContent, EuiPanel } from '@elastic/eui';
import { isEmpty } from 'lodash/fp';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { connect, ConnectedProps, useDispatch } from 'react-redux';
@ -369,11 +368,7 @@ export const AlertsTableComponent: React.FC<AlertsTableComponentProps> = ({
}, [dispatch, defaultTimelineModel, filterManager, tGridEnabled, timelineId]);
if (loading || indexPatternsLoading || isEmpty(selectedPatterns)) {
return (
<EuiPanel hasBorder={false} hasShadow={false} paddingSize="none">
<EuiLoadingContent data-test-subj="loading-alerts-panel" />
</EuiPanel>
);
return null;
}
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';
// @ts-expect-error
import { AlertConsumers as AlertConsumersNonTyped } from '@kbn/rule-data-utils/target_node/alerts_as_data_rbac';
import {
EuiEmptyPrompt,
EuiFlexGroup,
EuiFlexItem,
EuiPanel,
EuiLoadingContent,
} from '@elastic/eui';
import { EuiFlexGroup, EuiFlexItem, EuiPanel } from '@elastic/eui';
import { isEmpty } from 'lodash/fp';
import React, { useEffect, useMemo, useRef, useState } from 'react';
import styled from 'styled-components';
import { useDispatch } from 'react-redux';
import { FormattedMessage } from '@kbn/i18n/react';
import { useKibana } from '../../../../../../../src/plugins/kibana_react/public';
import { Direction, EntityType } 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 { InspectButton, InspectButtonContainer } from '../../inspect';
import { SummaryViewSelector, ViewSelection } from '../event_rendered_view/selector';
import { TGridLoading, TGridEmpty } from '../shared';
const AlertConsumers: typeof AlertConsumersTyped = AlertConsumersNonTyped;
@ -269,6 +263,8 @@ const TGridIntegratedComponent: React.FC<TGridIntegratedProps> = ({
[deletedEventIds.length, totalCount]
);
const hasAlerts = totalCountMinusDeleted > 0;
const nonDeletedEvents = useMemo(() => events.filter((e) => !deletedEventIds.includes(e._id)), [
deletedEventIds,
events,
@ -300,7 +296,7 @@ const TGridIntegratedComponent: React.FC<TGridIntegratedProps> = ({
data-test-subj="events-viewer-panel"
$isFullScreen={globalFullScreen}
>
{isFirstUpdate.current && <EuiLoadingContent data-test-subj="loading-alerts-panel" />}
{isFirstUpdate.current && <TGridLoading height="short" />}
{graphOverlay}
@ -325,61 +321,43 @@ const TGridIntegratedComponent: React.FC<TGridIntegratedProps> = ({
</UpdatedFlexGroup>
{!graphEventId && graphOverlay == null && (
<FullWidthFlexGroup
$visible={!graphEventId && graphOverlay == null}
gutterSize="none"
>
<ScrollableFlexItem grow={1}>
{totalCountMinusDeleted === 0 && loading === false && (
<EuiEmptyPrompt
title={
<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 && (
<StatefulBody
hasAlertsCrud={hasAlertsCrud}
activePage={pageInfo.activePage}
browserFields={browserFields}
filterQuery={filterQuery}
data={nonDeletedEvents}
defaultCellActions={defaultCellActions}
id={id}
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>
<>
{!hasAlerts && !loading && <TGridEmpty height="short" />}
{hasAlerts && (
<FullWidthFlexGroup
$visible={!graphEventId && graphOverlay == null}
gutterSize="none"
>
<ScrollableFlexItem grow={1}>
<StatefulBody
hasAlertsCrud={hasAlertsCrud}
activePage={pageInfo.activePage}
browserFields={browserFields}
filterQuery={filterQuery}
data={nonDeletedEvents}
defaultCellActions={defaultCellActions}
id={id}
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>
)}

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.
*/
import { EuiEmptyPrompt, EuiFlexGroup, EuiFlexItem, EuiLoadingContent } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { EuiFlexItem } from '@elastic/eui';
import { isEmpty } from 'lodash/fp';
import React, { useEffect, useMemo, useState, useRef } from 'react';
import styled from 'styled-components';
@ -39,10 +38,16 @@ import type { State } from '../../../store/t_grid';
import { useTimelineEvents } from '../../../container';
import { StatefulBody } from '../body';
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 { useFetchIndex } from '../../../container/source';
import { AddToCaseAction } from '../../actions/timeline/cases/add_to_case_action';
import { TGridLoading, TGridEmpty } from '../shared';
export const EVENTS_VIEWER_HEADER_HEIGHT = 90; // px
const STANDALONE_ID = 'standalone-t-grid';
@ -68,12 +73,6 @@ const EventsContainerLoading = styled.div.attrs(({ className = '' }) => ({
flex-direction: column;
`;
const FullWidthFlexGroup = styled(EuiFlexGroup)<{ $visible: boolean }>`
overflow: hidden;
margin: 0;
display: ${({ $visible }) => ($visible ? 'flex' : 'none')};
`;
const ScrollableFlexItem = styled(EuiFlexItem)`
overflow: auto;
`;
@ -255,6 +254,8 @@ const TGridStandaloneComponent: React.FC<TGridStandaloneProps> = ({
() => (totalCount > 0 ? totalCount - deletedEventIds.length : 0),
[deletedEventIds.length, totalCount]
);
const hasAlerts = totalCountMinusDeleted > 0;
const activeCaseFlowId = useSelector((state: State) => tGridSelectors.activeCaseFlowId(state));
const selectedEvent = useMemo(() => {
const matchedEvent = events.find((event) => event.ecs._id === activeCaseFlowId);
@ -338,14 +339,14 @@ const TGridStandaloneComponent: React.FC<TGridStandaloneProps> = ({
return (
<InspectButtonContainer data-test-subj="events-viewer-panel">
<AlertsTableWrapper>
{isFirstUpdate.current && <EuiLoadingContent data-test-subj="loading-alerts-panel" />}
{isFirstUpdate.current && <TGridLoading />}
{canQueryTimeline ? (
<>
<EventsContainerLoading
data-timeline-id={STANDALONE_ID}
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}>
<InspectButton title={justTitle} inspect={inspect} loading={loading} />
</UpdatedFlexItem>
@ -354,28 +355,9 @@ const TGridStandaloneComponent: React.FC<TGridStandaloneProps> = ({
</UpdatedFlexItem>
</UpdatedFlexGroup>
{totalCountMinusDeleted === 0 && loading === false && (
<EuiEmptyPrompt
title={
<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 && (
{!hasAlerts && !loading && <TGridEmpty />}
{hasAlerts && (
<FullWidthFlexGroup direction="row" $visible={!graphEventId} gutterSize="none">
<ScrollableFlexItem grow={1}>
<StatefulBody

View file

@ -459,6 +459,13 @@ export const HideShowContainer = styled.div.attrs<{ $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)`
position: absolute;
z-index: ${({ theme }) => theme.eui.euiZLevel1};

View file

@ -6,7 +6,7 @@
*/
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 type { Store } from '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 { initialTGridState } from '../store/t_grid/reducer';
import { createStore } from '../store/t_grid';
import { TGridLoading } from '../components/t_grid/shared';
const initializeStore = ({
store,
@ -51,13 +52,7 @@ export const getTGridLazy = (
) => {
initializeStore({ store, storage, setStore });
return (
<Suspense
fallback={
<EuiPanel hasBorder={false} hasShadow={false} paddingSize="none">
<EuiLoadingContent />
</EuiPanel>
}
>
<Suspense fallback={<TGridLoading height={props.type === 'standalone' ? 'tall' : 'short'} />}>
<TimelineLazy {...props} store={store} storage={storage} data={data} setStore={setStore} />
</Suspense>
);

View file

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