[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;
}
const CHART_HEIGHT = 170;
const CHART_HEIGHT = 120;
export function ChartContainer({
isInitialLoad,
children,
iconSize = 'xl',
iconSize = 'l',
height = CHART_HEIGHT,
}: Props) {
if (isInitialLoad) {

View file

@ -9,7 +9,7 @@ import {
EuiErrorBoundary,
EuiFlexGroup,
EuiFlexItem,
EuiHorizontalRule,
EuiPanel,
EuiLink,
EuiText,
EuiTitle,
@ -56,48 +56,49 @@ function NewsItem({ item }: { item: INewsItem }) {
const theme = useContext(ThemeContext);
return (
<EuiFlexGroup direction="column" gutterSize="s">
<EuiFlexItem grow={false}>
<EuiTitle size="xxxs">
<h4>{item.title.en}</h4>
</EuiTitle>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiFlexGroup gutterSize="s">
<EuiFlexItem>
<EuiFlexGroup direction="column" gutterSize="s" alignItems="baseline">
<EuiFlexItem>
<EuiText grow={false} size="xs" color="subdued">
{limitString(item.description.en, 128)}
</EuiText>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiText size="xs">
<EuiLink href={item.link_url.en} target="_blank" external>
{i18n.translate('xpack.observability.news.readFullStory', {
defaultMessage: 'Read full story',
})}
</EuiLink>
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
{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"
/>
<EuiPanel hasBorder={true}>
<EuiFlexGroup direction="column" gutterSize="s">
<EuiFlexItem grow={false}>
<EuiTitle size="xxs">
<h4>{item.title.en}</h4>
</EuiTitle>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiFlexGroup gutterSize="s">
<EuiFlexItem>
<EuiFlexGroup direction="column" gutterSize="s" alignItems="baseline">
<EuiFlexItem>
<EuiText grow={false} size="s" color="subdued">
{limitString(item.description.en, 128)}
</EuiText>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiText size="s">
<EuiLink href={item.link_url.en} target="_blank" external>
{i18n.translate('xpack.observability.news.readFullStory', {
defaultMessage: 'Read full story',
})}
</EuiLink>
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
)}
</EuiFlexGroup>
</EuiFlexItem>
<EuiHorizontalRule margin="s" />
</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>
)}
</EuiFlexGroup>
</EuiFlexItem>
</EuiFlexGroup>
</EuiPanel>
);
}

View file

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

View file

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

View file

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

View file

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

View file

@ -5,7 +5,7 @@
* 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 { ErrorPanel } from './error_panel';
import { usePluginContext } from '../../../hooks/use_plugin_context';
@ -25,29 +25,32 @@ interface Props {
export function SectionContainer({ title, appLink, children, hasError }: Props) {
const { core } = usePluginContext();
return (
<EuiPanel hasBorder={true}>
<EuiPanel hasShadow={true} color="subdued">
<EuiAccordion
initialIsOpen
id={title}
buttonContentClassName="accordion-button"
buttonContent={
<EuiTitle size="s">
<EuiTitle size="xs">
<h5>{title}</h5>
</EuiTitle>
}
extraAction={
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}
</EuiButton>
</EuiButtonEmpty>
)
}
>
<>
<EuiSpacer size="s" />
<EuiPanel hasShadow={false} paddingSize="s">
{hasError ? <ErrorPanel /> : <>{children}</>}
</EuiPanel>
<EuiPanel hasShadow={true}>{hasError ? <ErrorPanel /> : <>{children}</>}</EuiPanel>
</>
</EuiAccordion>
</EuiPanel>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -5,7 +5,7 @@
* 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 React from 'react';
import { useTrackPageview } from '../..';
@ -100,30 +100,38 @@ export function OverviewPage({ routeParams }: Props) {
{hasData && (
<>
<ObservabilityHeaderMenu />
<EuiFlexGroup>
<EuiFlexItem grow={6}>
{/* Data sections */}
{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>
<EuiFlexGroup direction="column" gutterSize="s">
<EuiFlexItem>
<EuiFlexGroup direction="column" gutterSize="s">
{hasDataMap?.alert?.hasData && (
<EuiFlexItem>
<EuiPanel hasBorder={true}>
<EuiPanel color="subdued">
<AlertsSection />
</EuiPanel>
</EuiFlexItem>
)}
</EuiFlexGroup>
</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>
</>
)}