[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:
parent
641cef7ca6
commit
4e9e7a8671
|
@ -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 |
|
@ -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>
|
||||
)}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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
|
||||
|
|
|
@ -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};
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Reference in a new issue