[Unified Observability] Overview style updates (#124702)

* Big chunk of style updates

* New layout and position for news and resources

* Alerts updated

* Rename headings and links

* Removed unncessary prop

* More fixes

* Remove active status

* Fixing tests

* fix tests

* fix checks

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: Ester Marti <ester.martivilaseca@elastic.co>
This commit is contained in:
Casper Hübertz 2022-02-09 19:00:26 +01:00 committed by GitHub
parent 809246721d
commit 3c73b605aa
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 166 additions and 146 deletions

View file

@ -18,12 +18,12 @@ interface Props {
children: React.ReactNode; children: React.ReactNode;
} }
const CHART_HEIGHT = 170; const CHART_HEIGHT = 120;
export function ChartContainer({ export function ChartContainer({
isInitialLoad, isInitialLoad,
children, children,
iconSize = 'xl', iconSize = 'l',
height = CHART_HEIGHT, height = CHART_HEIGHT,
}: Props) { }: Props) {
if (isInitialLoad) { if (isInitialLoad) {

View file

@ -9,7 +9,7 @@ import {
EuiErrorBoundary, EuiErrorBoundary,
EuiFlexGroup, EuiFlexGroup,
EuiFlexItem, EuiFlexItem,
EuiHorizontalRule, EuiPanel,
EuiLink, EuiLink,
EuiText, EuiText,
EuiTitle, EuiTitle,
@ -56,48 +56,49 @@ function NewsItem({ item }: { item: INewsItem }) {
const theme = useContext(ThemeContext); const theme = useContext(ThemeContext);
return ( return (
<EuiFlexGroup direction="column" gutterSize="s"> <EuiPanel hasBorder={true}>
<EuiFlexItem grow={false}> <EuiFlexGroup direction="column" gutterSize="s">
<EuiTitle size="xxxs"> <EuiFlexItem grow={false}>
<h4>{item.title.en}</h4> <EuiTitle size="xxs">
</EuiTitle> <h4>{item.title.en}</h4>
</EuiFlexItem> </EuiTitle>
<EuiFlexItem grow={false}> </EuiFlexItem>
<EuiFlexGroup gutterSize="s"> <EuiFlexItem grow={false}>
<EuiFlexItem> <EuiFlexGroup gutterSize="s">
<EuiFlexGroup direction="column" gutterSize="s" alignItems="baseline"> <EuiFlexItem>
<EuiFlexItem> <EuiFlexGroup direction="column" gutterSize="s" alignItems="baseline">
<EuiText grow={false} size="xs" color="subdued"> <EuiFlexItem>
{limitString(item.description.en, 128)} <EuiText grow={false} size="s" color="subdued">
</EuiText> {limitString(item.description.en, 128)}
</EuiFlexItem> </EuiText>
<EuiFlexItem grow={false}> </EuiFlexItem>
<EuiText size="xs"> <EuiFlexItem grow={false}>
<EuiLink href={item.link_url.en} target="_blank" external> <EuiText size="s">
{i18n.translate('xpack.observability.news.readFullStory', { <EuiLink href={item.link_url.en} target="_blank" external>
defaultMessage: 'Read full story', {i18n.translate('xpack.observability.news.readFullStory', {
})} defaultMessage: 'Read full story',
</EuiLink> })}
</EuiText> </EuiLink>
</EuiFlexItem> </EuiText>
</EuiFlexGroup> </EuiFlexItem>
</EuiFlexItem> </EuiFlexGroup>
{item.image_url?.en && (
<EuiFlexItem grow={false}>
<img
data-test-subj="newsImage"
style={{ border: theme.eui.euiBorderThin }}
width={48}
height={48}
alt={item.title.en}
src={item.image_url.en}
className="obsNewsFeed__itemImg"
/>
</EuiFlexItem> </EuiFlexItem>
)} {item.image_url?.en && (
</EuiFlexGroup> <EuiFlexItem grow={false}>
</EuiFlexItem> <img
<EuiHorizontalRule margin="s" /> data-test-subj="newsImage"
</EuiFlexGroup> style={{ border: theme.eui.euiBorderThin }}
width={48}
height={48}
alt={item.title.en}
src={item.image_url.en}
className="obsNewsFeed__itemImg"
/>
</EuiFlexItem>
)}
</EuiFlexGroup>
</EuiFlexItem>
</EuiFlexGroup>
</EuiPanel>
); );
} }

View file

@ -42,7 +42,7 @@ const resources = [
export function Resources() { export function Resources() {
return ( return (
<EuiFlexGroup direction="column" alignItems="flexStart" gutterSize="xs"> <EuiFlexGroup direction="column" alignItems="flexStart">
<EuiFlexItem grow={false}> <EuiFlexItem grow={false}>
<EuiTitle size="xs"> <EuiTitle size="xs">
<h4> <h4>

View file

@ -9,15 +9,14 @@ import {
EuiBadge, EuiBadge,
EuiFlexGroup, EuiFlexGroup,
EuiFlexItem, EuiFlexItem,
EuiHorizontalRule,
EuiIconTip,
EuiLink, EuiLink,
EuiText, EuiText,
EuiSpacer, EuiSpacer,
EuiTitle, EuiTitle,
EuiButton, EuiButtonEmpty,
EuiLoadingSpinner, EuiLoadingSpinner,
EuiCallOut, EuiCallOut,
EuiPanel,
} from '@elastic/eui'; } from '@elastic/eui';
import { i18n } from '@kbn/i18n'; import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react'; import { FormattedMessage } from '@kbn/i18n-react';
@ -68,7 +67,7 @@ export function AlertsSection() {
return ( return (
<EuiFlexGroup alignItems="center" justifyContent="center" responsive={false}> <EuiFlexGroup alignItems="center" justifyContent="center" responsive={false}>
<EuiFlexItem grow={false}> <EuiFlexItem grow={false}>
<EuiLoadingSpinner size="xl" /> <EuiLoadingSpinner size="l" />
</EuiFlexItem> </EuiFlexItem>
</EuiFlexGroup> </EuiFlexGroup>
); );
@ -99,7 +98,12 @@ export function AlertsSection() {
return ( return (
<div> <div>
<EuiFlexGroup alignItems="center" justifyContent="spaceBetween" responsive={false}> <EuiFlexGroup
alignItems="center"
justifyContent="spaceBetween"
responsive={false}
gutterSize="s"
>
<EuiFlexItem grow={false}> <EuiFlexItem grow={false}>
<EuiTitle size="xs"> <EuiTitle size="xs">
<h4> <h4>
@ -110,73 +114,77 @@ export function AlertsSection() {
</EuiTitle> </EuiTitle>
</EuiFlexItem> </EuiFlexItem>
<EuiFlexItem grow={false}> <EuiFlexItem grow={false}>
<EuiButton size="s" href={manageLink}> <EuiButtonEmpty iconType="sortRight" color="text" size="xs" href={manageLink}>
{i18n.translate('xpack.observability.overview.alert.appLink', { {i18n.translate('xpack.observability.overview.alert.appLink', {
defaultMessage: 'Manage alerts', defaultMessage: 'Show all alerts',
})} })}
</EuiButton> </EuiButtonEmpty>
</EuiFlexItem> </EuiFlexItem>
</EuiFlexGroup> </EuiFlexGroup>
<> <>
<EuiFlexItem grow={false}> <EuiFlexItem grow={false}>
<EuiSpacer /> <EuiSpacer size="s" />
<EuiSelect <EuiSelect
compressed compressed
fullWidth={true}
id="filterAlerts" id="filterAlerts"
options={[allTypes, ...filterOptions]} options={[allTypes, ...filterOptions]}
value={filter} value={filter}
onChange={(e) => setFilter(e.target.value)} onChange={(e) => setFilter(e.target.value)}
prepend="Show"
/> />
<EuiSpacer /> <EuiSpacer size="s" />
</EuiFlexItem> </EuiFlexItem>
{alerts {alerts
.filter((alert) => filter === ALL_TYPES || alert.consumer === filter) .filter((alert) => filter === ALL_TYPES || alert.consumer === filter)
.map((alert, index) => { .map((alert, index) => {
const isLastElement = index === alerts.length - 1;
return ( return (
<EuiFlexGroup direction="column" gutterSize="s" key={alert.id}> <EuiFlexGroup>
<EuiFlexItem grow={false}> <EuiFlexItem>
<EuiLink <EuiPanel>
href={core.http.basePath.prepend(paths.management.alertDetails(alert.id))} <EuiFlexGroup direction="column" gutterSize="s" key={alert.id}>
>
<EuiText size="s">{alert.name}</EuiText>
</EuiLink>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiFlexGroup gutterSize="xs">
<EuiFlexItem grow={false}>
<EuiBadge color="hollow">{alert.alertTypeId}</EuiBadge>
</EuiFlexItem>
{alert.tags.map((tag, idx) => {
return (
<EuiFlexItem key={idx} grow={false}>
<EuiBadge color="default">{tag}</EuiBadge>
</EuiFlexItem>
);
})}
</EuiFlexGroup>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiFlexGroup gutterSize="s">
<EuiFlexItem grow={false}>
<EuiText color="subdued" size="xs">
Updated {moment.duration(moment().diff(alert.updatedAt)).humanize()} ago
</EuiText>
</EuiFlexItem>
{alert.muteAll && (
<EuiFlexItem grow={false}> <EuiFlexItem grow={false}>
<EuiIconTip <EuiLink
type="minusInCircle" href={core.http.basePath.prepend(paths.management.alertDetails(alert.id))}
content={i18n.translate('xpack.observability.overview.alerts.muted', { >
defaultMessage: 'Muted', <EuiText size="s">{alert.name}</EuiText>
})} </EuiLink>
/>
</EuiFlexItem> </EuiFlexItem>
)} <EuiFlexItem grow={false}>
</EuiFlexGroup> <EuiFlexGroup gutterSize="xs">
<EuiFlexItem grow={false}>
<EuiBadge color="hollow">{alert.alertTypeId}</EuiBadge>
</EuiFlexItem>
{alert.tags.map((tag, idx) => {
return (
<EuiFlexItem key={idx} grow={false}>
<EuiBadge color="default">{tag}</EuiBadge>
</EuiFlexItem>
);
})}
</EuiFlexGroup>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiFlexGroup gutterSize="s" alignItems="center">
{alert.muteAll && (
<EuiFlexItem grow={false}>
<EuiBadge color="hollow">
{i18n.translate('xpack.observability.overview.alerts.muted', {
defaultMessage: 'Muted',
})}
</EuiBadge>
</EuiFlexItem>
)}
<EuiFlexItem grow={false}>
<EuiText color="subdued" size="xs">
Last updated{' '}
{moment.duration(moment().diff(alert.updatedAt)).humanize()} ago
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
</EuiFlexGroup>
</EuiPanel>
</EuiFlexItem> </EuiFlexItem>
{!isLastElement && <EuiHorizontalRule margin="xs" />}
</EuiFlexGroup> </EuiFlexGroup>
); );
})} })}

View file

@ -84,12 +84,12 @@ describe('APMSection', () => {
status: fetcherHook.FETCH_STATUS.SUCCESS, status: fetcherHook.FETCH_STATUS.SUCCESS,
refetch: jest.fn(), refetch: jest.fn(),
}); });
const { getByText, queryAllByTestId } = render( const { getByRole, getByText, queryAllByTestId } = render(
<APMSection bucketSize={{ intervalString: '60s', bucketSize: 60 }} /> <APMSection bucketSize={{ intervalString: '60s', bucketSize: 60 }} />
); );
expect(getByText('APM')).toBeInTheDocument(); expect(getByRole('heading')).toHaveTextContent('Services');
expect(getByText('View in app')).toBeInTheDocument(); expect(getByText('Show service inventory')).toBeInTheDocument();
expect(getByText('Services 11')).toBeInTheDocument(); expect(getByText('Services 11')).toBeInTheDocument();
expect(getByText('Throughput 900.0 tpm')).toBeInTheDocument(); expect(getByText('Throughput 900.0 tpm')).toBeInTheDocument();
expect(queryAllByTestId('loading')).toEqual([]); expect(queryAllByTestId('loading')).toEqual([]);
@ -101,12 +101,12 @@ describe('APMSection', () => {
status: fetcherHook.FETCH_STATUS.SUCCESS, status: fetcherHook.FETCH_STATUS.SUCCESS,
refetch: jest.fn(), refetch: jest.fn(),
}); });
const { getByText, queryAllByTestId } = render( const { getByRole, getByText, queryAllByTestId } = render(
<APMSection bucketSize={{ intervalString: '60s', bucketSize: 60 }} /> <APMSection bucketSize={{ intervalString: '60s', bucketSize: 60 }} />
); );
expect(getByText('APM')).toBeInTheDocument(); expect(getByRole('heading')).toHaveTextContent('Services');
expect(getByText('View in app')).toBeInTheDocument(); expect(getByText('Show service inventory')).toBeInTheDocument();
expect(getByText('Services 11')).toBeInTheDocument(); expect(getByText('Services 11')).toBeInTheDocument();
expect(getByText('Throughput 312.00k tpm')).toBeInTheDocument(); expect(getByText('Throughput 312.00k tpm')).toBeInTheDocument();
expect(queryAllByTestId('loading')).toEqual([]); expect(queryAllByTestId('loading')).toEqual([]);
@ -117,13 +117,13 @@ describe('APMSection', () => {
status: fetcherHook.FETCH_STATUS.LOADING, status: fetcherHook.FETCH_STATUS.LOADING,
refetch: jest.fn(), refetch: jest.fn(),
}); });
const { getByText, queryAllByText, getByTestId } = render( const { getByRole, queryAllByText, getByTestId } = render(
<APMSection bucketSize={{ intervalString: '60s', bucketSize: 60 }} /> <APMSection bucketSize={{ intervalString: '60s', bucketSize: 60 }} />
); );
expect(getByText('APM')).toBeInTheDocument(); expect(getByRole('heading')).toHaveTextContent('Services');
expect(getByTestId('loading')).toBeInTheDocument(); expect(getByTestId('loading')).toBeInTheDocument();
expect(queryAllByText('View in app')).toEqual([]); expect(queryAllByText('Show service inventory')).toEqual([]);
expect(queryAllByText('Services 11')).toEqual([]); expect(queryAllByText('Services 11')).toEqual([]);
expect(queryAllByText('Throughput 312.00k tpm')).toEqual([]); expect(queryAllByText('Throughput 312.00k tpm')).toEqual([]);
}); });

View file

@ -93,12 +93,12 @@ export function APMSection({ bucketSize }: Props) {
return ( return (
<SectionContainer <SectionContainer
title={i18n.translate('xpack.observability.overview.apm.title', { title={i18n.translate('xpack.observability.overview.apm.title', {
defaultMessage: 'APM', defaultMessage: 'Services',
})} })}
appLink={{ appLink={{
href: appLink, href: appLink,
label: i18n.translate('xpack.observability.overview.apm.appLink', { label: i18n.translate('xpack.observability.overview.apm.appLink', {
defaultMessage: 'View in app', defaultMessage: 'Show service inventory',
}), }),
}} }}
hasError={status === FETCH_STATUS.FAILURE} hasError={status === FETCH_STATUS.FAILURE}

View file

@ -5,7 +5,7 @@
* 2.0. * 2.0.
*/ */
import { EuiAccordion, EuiPanel, EuiSpacer, EuiTitle, EuiButton } from '@elastic/eui'; import { EuiAccordion, EuiPanel, EuiSpacer, EuiTitle, EuiButtonEmpty } from '@elastic/eui';
import React from 'react'; import React from 'react';
import { ErrorPanel } from './error_panel'; import { ErrorPanel } from './error_panel';
import { usePluginContext } from '../../../hooks/use_plugin_context'; import { usePluginContext } from '../../../hooks/use_plugin_context';
@ -25,29 +25,32 @@ interface Props {
export function SectionContainer({ title, appLink, children, hasError }: Props) { export function SectionContainer({ title, appLink, children, hasError }: Props) {
const { core } = usePluginContext(); const { core } = usePluginContext();
return ( return (
<EuiPanel hasBorder={true}> <EuiPanel hasShadow={true} color="subdued">
<EuiAccordion <EuiAccordion
initialIsOpen initialIsOpen
id={title} id={title}
buttonContentClassName="accordion-button" buttonContentClassName="accordion-button"
buttonContent={ buttonContent={
<EuiTitle size="s"> <EuiTitle size="xs">
<h5>{title}</h5> <h5>{title}</h5>
</EuiTitle> </EuiTitle>
} }
extraAction={ extraAction={
appLink?.href && ( appLink?.href && (
<EuiButton size="s" href={core.http.basePath.prepend(appLink.href)}> <EuiButtonEmpty
iconType={'sortRight'}
size="xs"
color="text"
href={core.http.basePath.prepend(appLink.href)}
>
{appLink.label} {appLink.label}
</EuiButton> </EuiButtonEmpty>
) )
} }
> >
<> <>
<EuiSpacer size="s" /> <EuiSpacer size="s" />
<EuiPanel hasShadow={false} paddingSize="s"> <EuiPanel hasShadow={true}>{hasError ? <ErrorPanel /> : <>{children}</>}</EuiPanel>
{hasError ? <ErrorPanel /> : <>{children}</>}
</EuiPanel>
</> </>
</EuiAccordion> </EuiAccordion>
</EuiPanel> </EuiPanel>

View file

@ -92,17 +92,17 @@ export function LogsSection({ bucketSize }: Props) {
return ( return (
<SectionContainer <SectionContainer
title={i18n.translate('xpack.observability.overview.logs.title', { title={i18n.translate('xpack.observability.overview.logs.title', {
defaultMessage: 'Logs', defaultMessage: 'Log Events',
})} })}
appLink={{ appLink={{
href: appLink, href: appLink,
label: i18n.translate('xpack.observability.overview.logs.appLink', { label: i18n.translate('xpack.observability.overview.logs.appLink', {
defaultMessage: 'View in app', defaultMessage: 'Show log stream',
}), }),
}} }}
hasError={status === FETCH_STATUS.FAILURE} hasError={status === FETCH_STATUS.FAILURE}
> >
<EuiTitle size="xs"> <EuiTitle size="xxs">
<h4> <h4>
{i18n.translate('xpack.observability.overview.logs.subtitle', { {i18n.translate('xpack.observability.overview.logs.subtitle', {
defaultMessage: 'Logs rate per minute', defaultMessage: 'Logs rate per minute',

View file

@ -202,12 +202,12 @@ export function MetricsSection({ bucketSize }: Props) {
return ( return (
<SectionContainer <SectionContainer
title={i18n.translate('xpack.observability.overview.metrics.title', { title={i18n.translate('xpack.observability.overview.metrics.title', {
defaultMessage: 'Metrics', defaultMessage: 'Hosts',
})} })}
appLink={{ appLink={{
href: appLink, href: appLink,
label: i18n.translate('xpack.observability.overview.metrics.appLink', { label: i18n.translate('xpack.observability.overview.metrics.appLink', {
defaultMessage: 'View in app', defaultMessage: 'Show inventory',
}), }),
}} }}
hasError={status === FETCH_STATUS.FAILURE} hasError={status === FETCH_STATUS.FAILURE}

View file

@ -74,17 +74,17 @@ export function UptimeSection({ bucketSize }: Props) {
const { appLink, stats, series } = data || {}; const { appLink, stats, series } = data || {};
const downColor = theme.eui.euiColorVis2; const downColor = theme.eui.euiColorVis2;
const upColor = theme.eui.euiColorLightShade; const upColor = theme.eui.euiColorMediumShade;
return ( return (
<SectionContainer <SectionContainer
title={i18n.translate('xpack.observability.overview.uptime.title', { title={i18n.translate('xpack.observability.overview.uptime.title', {
defaultMessage: 'Uptime', defaultMessage: 'Monitors',
})} })}
appLink={{ appLink={{
href: appLink, href: appLink,
label: i18n.translate('xpack.observability.overview.uptime.appLink', { label: i18n.translate('xpack.observability.overview.uptime.appLink', {
defaultMessage: 'View in app', defaultMessage: 'Show monitors',
}), }),
}} }}
hasError={status === FETCH_STATUS.FAILURE} hasError={status === FETCH_STATUS.FAILURE}

View file

@ -78,7 +78,7 @@ describe('UXSection', () => {
); );
expect(getByText('User Experience')).toBeInTheDocument(); expect(getByText('User Experience')).toBeInTheDocument();
expect(getByText('View in app')).toBeInTheDocument(); expect(getByText('Show dashboard')).toBeInTheDocument();
expect(getByText('elastic-co-frontend')).toBeInTheDocument(); expect(getByText('elastic-co-frontend')).toBeInTheDocument();
expect(getByText('Largest contentful paint')).toBeInTheDocument(); expect(getByText('Largest contentful paint')).toBeInTheDocument();
expect(getByText('1.94 s')).toBeInTheDocument(); expect(getByText('1.94 s')).toBeInTheDocument();
@ -113,7 +113,7 @@ describe('UXSection', () => {
expect(getByText('User Experience')).toBeInTheDocument(); expect(getByText('User Experience')).toBeInTheDocument();
expect(getAllByText('--')).toHaveLength(3); expect(getAllByText('--')).toHaveLength(3);
expect(queryAllByText('View in app')).toEqual([]); expect(queryAllByText('Show dashboard')).toEqual([]);
expect(getByText('elastic-co-frontend')).toBeInTheDocument(); expect(getByText('elastic-co-frontend')).toBeInTheDocument();
}); });
it('shows empty state', () => { it('shows empty state', () => {
@ -128,7 +128,7 @@ describe('UXSection', () => {
expect(getByText('User Experience')).toBeInTheDocument(); expect(getByText('User Experience')).toBeInTheDocument();
expect(getAllByText('No data is available.')).toHaveLength(3); expect(getAllByText('No data is available.')).toHaveLength(3);
expect(queryAllByText('View in app')).toEqual([]); expect(queryAllByText('Show dashboard')).toEqual([]);
expect(getByText('elastic-co-frontend')).toBeInTheDocument(); expect(getByText('elastic-co-frontend')).toBeInTheDocument();
}); });
}); });

View file

@ -57,7 +57,7 @@ export function UXSection({ bucketSize }: Props) {
appLink={{ appLink={{
href: appLink, href: appLink,
label: i18n.translate('xpack.observability.overview.ux.appLink', { label: i18n.translate('xpack.observability.overview.ux.appLink', {
defaultMessage: 'View in app', defaultMessage: 'Show dashboard',
}), }),
}} }}
hasError={status === FETCH_STATUS.FAILURE} hasError={status === FETCH_STATUS.FAILURE}

View file

@ -23,7 +23,7 @@ interface Props {
export function DataSections({ bucketSize }: Props) { export function DataSections({ bucketSize }: Props) {
return ( return (
<EuiFlexItem grow={false}> <EuiFlexItem grow={false}>
<EuiFlexGroup direction="column"> <EuiFlexGroup direction="column" gutterSize="s">
<EuiFlexItem grow={false}> <EuiFlexItem grow={false}>
<LogsSection bucketSize={bucketSize} /> <LogsSection bucketSize={bucketSize} />
</EuiFlexItem> </EuiFlexItem>

View file

@ -5,7 +5,7 @@
* 2.0. * 2.0.
*/ */
import { EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiPanel } from '@elastic/eui'; import { EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiPanel, EuiHorizontalRule } from '@elastic/eui';
import { i18n } from '@kbn/i18n'; import { i18n } from '@kbn/i18n';
import React from 'react'; import React from 'react';
import { useTrackPageview } from '../..'; import { useTrackPageview } from '../..';
@ -100,30 +100,38 @@ export function OverviewPage({ routeParams }: Props) {
{hasData && ( {hasData && (
<> <>
<ObservabilityHeaderMenu /> <ObservabilityHeaderMenu />
<EuiFlexGroup> <EuiFlexGroup direction="column" gutterSize="s">
<EuiFlexItem grow={6}> <EuiFlexItem>
{/* Data sections */} <EuiFlexGroup direction="column" gutterSize="s">
{hasAnyData && <DataSections bucketSize={bucketSize} />}
<EmptySections />
<EuiSpacer size="l" />
<EuiFlexGroup>
<EuiFlexItem>
{/* Resources / What's New sections */}
<EuiPanel hasBorder={true}>
<Resources />
<EuiSpacer size="l" />
{!!newsFeed?.items?.length && <NewsFeed items={newsFeed.items.slice(0, 5)} />}
</EuiPanel>
</EuiFlexItem>
{hasDataMap?.alert?.hasData && ( {hasDataMap?.alert?.hasData && (
<EuiFlexItem> <EuiFlexItem>
<EuiPanel hasBorder={true}> <EuiPanel color="subdued">
<AlertsSection /> <AlertsSection />
</EuiPanel> </EuiPanel>
</EuiFlexItem> </EuiFlexItem>
)} )}
</EuiFlexGroup> </EuiFlexGroup>
</EuiFlexItem> </EuiFlexItem>
<EuiFlexItem>
{/* Data sections */}
{hasAnyData && <DataSections bucketSize={bucketSize} />}
<EmptySections />
</EuiFlexItem>
<EuiSpacer size="s" />
</EuiFlexGroup>
<EuiHorizontalRule />
<EuiFlexGroup>
<EuiFlexItem>
{/* Resources / What's New sections */}
<EuiFlexGroup direction="row">
<EuiFlexItem grow={4}>
{!!newsFeed?.items?.length && <NewsFeed items={newsFeed.items.slice(0, 3)} />}
</EuiFlexItem>
<EuiFlexItem grow={2}>
<Resources />
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
</EuiFlexGroup> </EuiFlexGroup>
</> </>
)} )}