Refactor Observability Overview Page (#146182)

This commit is contained in:
Coen Warmer 2022-11-24 21:27:38 +01:00 committed by GitHub
parent 55ca52ddb7
commit dd86c7f25c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 350 additions and 276 deletions

View file

@ -31,7 +31,7 @@ export function NewsFeed({ items }: Props) {
// The news feed is manually added/edited, to prevent any errors caused by typos or missing fields,
// wraps the component with EuiErrorBoundary to avoid breaking the entire page.
<EuiErrorBoundary>
<EuiFlexGrid direction="column" gutterSize="s">
<EuiFlexGrid direction="row" gutterSize="s" alignItems="start">
<EuiFlexItem grow={false}>
<EuiTitle size="xs">
<h4>
@ -58,7 +58,7 @@ function NewsItem({ item }: { item: INewsItem }) {
return (
<EuiPanel hasBorder={true}>
<EuiFlexGrid direction="column" gutterSize="s">
<EuiFlexGrid direction="row" gutterSize="s">
<EuiFlexItem grow={false}>
<EuiTitle size="xxs">
<h4>{item.title.en}</h4>

View file

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

View file

@ -0,0 +1,57 @@
/*
* 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 {
EuiFlyout,
EuiFlyoutHeader,
EuiTitle,
EuiSpacer,
EuiText,
EuiFlyoutBody,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { ObservabilityStatus } from '../../../components/app/observability_status';
interface DataAsssistantFlyoutProps {
onClose: () => void;
}
export function DataAssistantFlyout({ onClose }: DataAsssistantFlyoutProps) {
return (
<EuiFlyout
ownFocus
aria-labelledby="statusVisualizationFlyoutTitle"
className="oblt__flyout"
size="s"
onClose={onClose}
>
<EuiFlyoutHeader hasBorder>
<EuiTitle size="m">
<h2 id="statusVisualizationFlyoutTitle" data-test-subj="statusVisualizationFlyoutTitle">
<FormattedMessage
id="xpack.observability.overview.statusVisualizationFlyoutTitle"
defaultMessage="Data assistant"
/>
</h2>
</EuiTitle>
<EuiSpacer size="s" />
<EuiText size="s">
<p>
<FormattedMessage
id="xpack.observability.overview.statusVisualizationFlyoutDescription"
defaultMessage="Track your progress towards adding observability integrations and features."
/>
</p>
</EuiText>
</EuiFlyoutHeader>
<EuiFlyoutBody>
<ObservabilityStatus />
</EuiFlyoutBody>
</EuiFlyout>
);
}

View file

@ -0,0 +1,110 @@
/*
* 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, { useRef } from 'react';
import {
EuiButton,
EuiButtonEmpty,
EuiFlexGroup,
EuiFlexItem,
EuiText,
EuiTourStep,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import { DatePicker } from '../../../components/shared/date_picker';
import { useDatePickerContext } from '../../../hooks/use_date_picker_context';
import { useObservabilityTourContext } from '../../../components/shared/tour';
export interface HeaderActionsProps {
showTour?: boolean;
onGuidedSetupClick: () => void;
onTimeRangeRefresh: () => void;
onTourDismiss: () => void;
}
export function HeaderActions({
showTour = false,
onGuidedSetupClick,
onTimeRangeRefresh,
onTourDismiss,
}: HeaderActionsProps) {
const buttonRef = useRef();
const { relativeStart, relativeEnd, refreshInterval, refreshPaused } = useDatePickerContext();
const { endTour, isTourVisible } = useObservabilityTourContext();
return (
<EuiFlexGroup direction="row" gutterSize="s" justifyContent="flexEnd">
<EuiFlexItem grow={false}>
<DatePicker
rangeFrom={relativeStart}
rangeTo={relativeEnd}
refreshInterval={refreshInterval}
refreshPaused={refreshPaused}
width="auto"
onTimeRangeRefresh={onTimeRangeRefresh}
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton
// @ts-expect-error the EUI verson that kibana uses right now doesn't have the correct types
buttonRef={buttonRef}
color="text"
data-test-subj="guidedSetupButton"
id="guidedSetupButton"
iconType="wrench"
onClick={() => {
if (isTourVisible) {
endTour();
}
onGuidedSetupClick();
}}
>
<FormattedMessage
id="xpack.observability.overview.guidedSetupButton"
defaultMessage="Data assistant"
/>
</EuiButton>
{showTour ? (
<EuiTourStep
// @ts-expect-error the EUI verson that kibana uses right now doesn't have the correct types
anchor={() => buttonRef.current}
step={1}
stepsTotal={1}
isStepOpen
maxWidth={400}
onFinish={onTourDismiss}
title={i18n.translate('xpack.observability.overview.guidedSetupTourTitle', {
defaultMessage: 'Data assistant is always available',
})}
content={
<EuiText size="s">
<FormattedMessage
id="xpack.observability.overview.guidedSetupTourContent"
defaultMessage="If you're ever in doubt you can always access the data assistant and view your next steps by clicking here."
/>
</EuiText>
}
footerAction={
<EuiButtonEmpty color="text" flush="right" size="xs" onClick={onTourDismiss}>
<FormattedMessage
id="xpack.observability.overview.guidedSetupTourDismissButton"
defaultMessage="Dismiss"
/>
</EuiButtonEmpty>
}
/>
) : null}
</EuiFlexItem>
</EuiFlexGroup>
);
}

View file

@ -5,6 +5,8 @@
* 2.0.
*/
export * from './data_assistant_flyout';
export * from './data_sections';
export * from './loading_observability';
export * from './empty_section';
export * from './header_actions';
export * from './loading_observability';

View file

@ -0,0 +1,43 @@
/*
* 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 { useEffect } from 'react';
import { useKibana } from '@kbn/kibana-react-plugin/public';
import { useTrackPageview } from '../../../../..';
import { useUiTracker } from '../../../../../hooks/use_track_metric';
import { ObservabilityAppServices } from '../../../../../application/types';
import { CAPABILITIES_KEYS } from '../constants';
export const useOverviewMetrics = ({ hasAnyData }: { hasAnyData: boolean | undefined }) => {
const {
application: { capabilities },
} = useKibana<ObservabilityAppServices>().services;
const trackMetric = useUiTracker({ app: 'observability-overview' });
useTrackPageview({ app: 'observability-overview', path: 'overview' });
useTrackPageview({ app: 'observability-overview', path: 'overview', delay: 15000 });
useEffect(() => {
if (hasAnyData !== true) {
return;
}
CAPABILITIES_KEYS.forEach((feature) => {
if (capabilities[feature].show === false) {
trackMetric({
metric: `oblt_disabled_feature_${feature === 'infrastructure' ? 'metrics' : feature}`,
});
}
});
}, [capabilities, hasAnyData, trackMetric]);
return {
trackMetric,
};
};

View file

@ -5,60 +5,56 @@
* 2.0.
*/
import {
EuiButton,
EuiButtonEmpty,
EuiFlexGroup,
EuiFlexItem,
EuiFlyout,
EuiFlyoutSize,
EuiFlyoutBody,
EuiFlyoutHeader,
EuiHorizontalRule,
EuiSpacer,
EuiText,
EuiTitle,
EuiTourStep,
} from '@elastic/eui';
import { EuiFlexGroup, EuiFlexItem, EuiHorizontalRule, EuiSpacer } from '@elastic/eui';
import { BoolQuery } from '@kbn/es-query';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import { useKibana } from '@kbn/kibana-react-plugin/public';
import { AlertConsumers } from '@kbn/rule-data-utils';
import React, { useMemo, useRef, useCallback, useState, useEffect } from 'react';
import React, { useMemo, useRef, useCallback, useState } from 'react';
import { calculateBucketSize } from './helpers';
import { HeaderActionsProps } from './types';
import { EmptySections } from '../../../../components/app/empty_sections';
import { observabilityFeatureId } from '../../../../../common';
import { useTrackPageview, useUiTracker } from '../../../..';
import { ObservabilityHeaderMenu } from '../../../../components/app/header';
import { NewsFeed } from '../../../../components/app/news_feed';
import { Resources } from '../../../../components/app/resources';
import { DatePicker } from '../../../../components/shared/date_picker';
import { useBreadcrumbs } from '../../../../hooks/use_breadcrumbs';
import { useFetcher } from '../../../../hooks/use_fetcher';
import { useHasData } from '../../../../hooks/use_has_data';
import { usePluginContext } from '../../../../hooks/use_plugin_context';
import { buildEsQuery } from '../../../../utils/build_es_query';
import { getNewsFeed } from '../../../../services/get_news_feed';
import { DataSections, LoadingObservability } from '../../components';
import {
DataSections,
LoadingObservability,
HeaderActions,
DataAssistantFlyout,
} from '../../components';
import { EmptySections } from '../../../../components/app/empty_sections';
import { ObservabilityHeaderMenu } from '../../../../components/app/header';
import { Resources } from '../../../../components/app/resources';
import { NewsFeed } from '../../../../components/app/news_feed';
import { SectionContainer } from '../../../../components/app/section';
import { ObservabilityAppServices } from '../../../../application/types';
import { useGetUserCasesPermissions } from '../../../../hooks/use_get_user_cases_permissions';
import { paths } from '../../../../config';
import { useDatePickerContext } from '../../../../hooks/use_date_picker_context';
import { ObservabilityStatusProgress } from '../../../../components/app/observability_status/observability_status_progress';
import { ObservabilityStatus } from '../../../../components/app/observability_status';
import { useBreadcrumbs } from '../../../../hooks/use_breadcrumbs';
import { useDatePickerContext } from '../../../../hooks/use_date_picker_context';
import { useFetcher } from '../../../../hooks/use_fetcher';
import { useGetUserCasesPermissions } from '../../../../hooks/use_get_user_cases_permissions';
import { useGuidedSetupProgress } from '../../../../hooks/use_guided_setup_progress';
import { useObservabilityTourContext } from '../../../../components/shared/tour';
import { CAPABILITIES_KEYS, ALERTS_PER_PAGE, ALERTS_TABLE_ID } from './constants';
import { useHasData } from '../../../../hooks/use_has_data';
import { useOverviewMetrics } from './helpers/use_metrics';
import { usePluginContext } from '../../../../hooks/use_plugin_context';
import { observabilityFeatureId } from '../../../../../common';
import { observabilityAlertFeatureIds, paths } from '../../../../config';
import { ALERTS_PER_PAGE, ALERTS_TABLE_ID } from './constants';
import type { ObservabilityAppServices } from '../../../../application/types';
export function OverviewPage() {
const trackMetric = useUiTracker({ app: 'observability-overview' });
useTrackPageview({ app: 'observability-overview', path: 'overview' });
useTrackPageview({ app: 'observability-overview', path: 'overview', delay: 15000 });
const {
cases: {
ui: { getCasesContext },
},
http,
triggersActionsUi: { alertsTableConfigurationRegistry, getAlertsStateTable: AlertsStateTable },
} = useKibana<ObservabilityAppServices>().services;
const { ObservabilityPageTemplate } = usePluginContext();
useBreadcrumbs([
{
text: i18n.translate('xpack.observability.breadcrumbs.overviewLinkText', {
@ -66,17 +62,23 @@ export function OverviewPage() {
}),
},
]);
const [isFlyoutVisible, setIsFlyoutVisible] = useState(false);
const {
cases,
http,
application: { capabilities },
triggersActionsUi: { alertsTableConfigurationRegistry, getAlertsStateTable: AlertsStateTable },
} = useKibana<ObservabilityAppServices>().services;
const { data: newsFeed } = useFetcher(() => getNewsFeed({ http }), [http]);
const { hasAnyData, isAllRequestsComplete } = useHasData();
const refetch = useRef<() => void>();
const { trackMetric } = useOverviewMetrics({ hasAnyData });
const CasesContext = getCasesContext();
const userCasesPermissions = useGetUserCasesPermissions();
const [isDataAssistantFlyoutVisible, setIsDataAssistantFlyoutVisible] = useState(false);
const { isGuidedSetupProgressDismissed } = useGuidedSetupProgress();
const [isGuidedSetupTourVisible, setGuidedSetupTourVisible] = useState(false);
const { ObservabilityPageTemplate } = usePluginContext();
const { relativeStart, relativeEnd, absoluteStart, absoluteEnd } = useDatePickerContext();
const [esQuery, setEsQuery] = useState<{ bool: BoolQuery }>(
buildEsQuery({
from: relativeStart,
@ -84,15 +86,6 @@ export function OverviewPage() {
})
);
const { data: newsFeed } = useFetcher(() => getNewsFeed({ http }), [http]);
const { hasAnyData, isAllRequestsComplete } = useHasData();
const refetch = useRef<() => void>();
const [isGuidedSetupTourVisible, setGuidedSetupTourVisible] = useState(false);
const hideGuidedSetupTour = useCallback(() => setGuidedSetupTourVisible(false), []);
const { isGuidedSetupProgressDismissed } = useGuidedSetupProgress();
const bucketSize = useMemo(
() =>
calculateBucketSize({
@ -102,15 +95,7 @@ export function OverviewPage() {
[absoluteStart, absoluteEnd]
);
const handleGuidedSetupClick = useCallback(() => {
if (isGuidedSetupProgressDismissed) {
trackMetric({ metric: 'guided_setup_view_details_after_dismiss' });
}
hideGuidedSetupTour();
setIsFlyoutVisible(true);
}, [trackMetric, isGuidedSetupProgressDismissed, hideGuidedSetupTour]);
const onTimeRangeRefresh = useCallback(() => {
const handleTimeRangeRefresh = useCallback(() => {
setEsQuery(
buildEsQuery({
from: relativeStart,
@ -120,29 +105,24 @@ export function OverviewPage() {
return refetch.current && refetch.current();
}, [relativeEnd, relativeStart]);
const CasesContext = cases.ui.getCasesContext();
const userCasesPermissions = useGetUserCasesPermissions();
const handleCloseGuidedSetupTour = () => {
setGuidedSetupTourVisible(false);
};
useEffect(() => {
if (hasAnyData !== true) {
return;
const handleGuidedSetupClick = useCallback(() => {
if (isGuidedSetupProgressDismissed) {
trackMetric({ metric: 'guided_setup_view_details_after_dismiss' });
}
CAPABILITIES_KEYS.forEach((feature) => {
if (capabilities[feature].show === false) {
trackMetric({
metric: `oblt_disabled_feature_${feature === 'infrastructure' ? 'metrics' : feature}`,
});
}
});
}, [capabilities, hasAnyData, trackMetric]);
handleCloseGuidedSetupTour();
setIsDataAssistantFlyoutVisible(true);
}, [trackMetric, isGuidedSetupProgressDismissed]);
if (hasAnyData === undefined) {
return <LoadingObservability />;
}
const alertsLink = paths.observability.alerts;
return (
<ObservabilityPageTemplate
isPageDataLoaded={isAllRequestsComplete}
@ -153,194 +133,83 @@ export function OverviewPage() {
rightSideItems: [
<HeaderActions
showTour={isGuidedSetupTourVisible}
onTourDismiss={hideGuidedSetupTour}
handleGuidedSetupClick={handleGuidedSetupClick}
onTimeRangeRefresh={onTimeRangeRefresh}
onGuidedSetupClick={handleGuidedSetupClick}
onTourDismiss={handleCloseGuidedSetupTour}
onTimeRangeRefresh={handleTimeRangeRefresh}
/>,
],
rightSideGroupProps: {
responsive: true,
},
}}
>
<>
<ObservabilityHeaderMenu />
<ObservabilityStatusProgress
onViewDetailsClick={() => setIsFlyoutVisible(true)}
onDismissClick={() => setGuidedSetupTourVisible(true)}
/>
<EuiFlexGroup direction="column" gutterSize="s">
<EuiFlexItem>
<SectionContainer
initialIsOpen={hasAnyData}
title={i18n.translate('xpack.observability.overview.alerts.title', {
defaultMessage: 'Alerts',
})}
hasError={false}
appLink={{
href: alertsLink,
label: i18n.translate('xpack.observability.overview.alerts.appLink', {
defaultMessage: 'Show alerts',
}),
}}
<ObservabilityHeaderMenu />
<ObservabilityStatusProgress
onDismissClick={() => setGuidedSetupTourVisible(true)}
onViewDetailsClick={() => setIsDataAssistantFlyoutVisible(true)}
/>
<EuiFlexGroup direction="column" gutterSize="s">
<EuiFlexItem>
<SectionContainer
title={i18n.translate('xpack.observability.overview.alerts.title', {
defaultMessage: 'Alerts',
})}
appLink={{
href: paths.observability.alerts,
label: i18n.translate('xpack.observability.overview.alerts.appLink', {
defaultMessage: 'Show alerts',
}),
}}
initialIsOpen={hasAnyData}
hasError={false}
>
<CasesContext
owner={[observabilityFeatureId]}
permissions={userCasesPermissions}
features={{ alerts: { sync: false } }}
>
<CasesContext
owner={[observabilityFeatureId]}
permissions={userCasesPermissions}
features={{ alerts: { sync: false } }}
>
<AlertsStateTable
alertsTableConfigurationRegistry={alertsTableConfigurationRegistry}
configurationId={AlertConsumers.OBSERVABILITY}
id={ALERTS_TABLE_ID}
flyoutSize={'s' as EuiFlyoutSize}
featureIds={[
AlertConsumers.APM,
AlertConsumers.INFRASTRUCTURE,
AlertConsumers.LOGS,
AlertConsumers.UPTIME,
]}
query={esQuery}
showExpandToDetails={false}
pageSize={ALERTS_PER_PAGE}
/>
</CasesContext>
</SectionContainer>
</EuiFlexItem>
<EuiFlexItem>
{/* Data sections */}
{<DataSections bucketSize={bucketSize} />}
<EmptySections />
</EuiFlexItem>
<EuiSpacer size="s" />
</EuiFlexGroup>
<EuiHorizontalRule />
<EuiFlexGroup>
<EuiFlexItem>
{/* Resources / What's New sections */}
<EuiFlexGroup>
<EuiFlexItem grow={4}>
{!!newsFeed?.items?.length && <NewsFeed items={newsFeed.items.slice(0, 3)} />}
</EuiFlexItem>
<EuiFlexItem grow={2}>
<Resources />
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
</EuiFlexGroup>
</>
{isFlyoutVisible && (
<EuiFlyout
className="oblt__flyout"
size="s"
ownFocus
onClose={() => setIsFlyoutVisible(false)}
aria-labelledby="statusVisualizationFlyoutTitle"
>
<EuiFlyoutHeader hasBorder>
<EuiTitle size="m">
<h2
id="statusVisualizationFlyoutTitle"
data-test-subj="statusVisualizationFlyoutTitle"
>
<FormattedMessage
id="xpack.observability.overview.statusVisualizationFlyoutTitle"
defaultMessage="Data assistant"
/>
</h2>
</EuiTitle>
<EuiSpacer size="s" />
<EuiText size="s">
<p>
<FormattedMessage
id="xpack.observability.overview.statusVisualizationFlyoutDescription"
defaultMessage="Track your progress towards adding observability integrations and features."
/>
</p>
</EuiText>
</EuiFlyoutHeader>
<EuiFlyoutBody>
<ObservabilityStatus />
</EuiFlyoutBody>
</EuiFlyout>
)}
<AlertsStateTable
alertsTableConfigurationRegistry={alertsTableConfigurationRegistry}
configurationId={AlertConsumers.OBSERVABILITY}
id={ALERTS_TABLE_ID}
flyoutSize="s"
featureIds={observabilityAlertFeatureIds}
pageSize={ALERTS_PER_PAGE}
query={esQuery}
showExpandToDetails={false}
/>
</CasesContext>
</SectionContainer>
</EuiFlexItem>
<EuiFlexItem>
{/* Data sections */}
<DataSections bucketSize={bucketSize} />
<EmptySections />
</EuiFlexItem>
<EuiSpacer size="s" />
</EuiFlexGroup>
<EuiHorizontalRule />
<EuiFlexGroup>
<EuiFlexItem>
{/* Resources / What's New sections */}
<EuiFlexGroup>
<EuiFlexItem grow={4}>
{!!newsFeed?.items?.length && <NewsFeed items={newsFeed.items.slice(0, 3)} />}
</EuiFlexItem>
<EuiFlexItem grow={2}>
<Resources />
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
</EuiFlexGroup>
{isDataAssistantFlyoutVisible ? (
<DataAssistantFlyout onClose={() => setIsDataAssistantFlyoutVisible(false)} />
) : null}
</ObservabilityPageTemplate>
);
}
function HeaderActions({
showTour = false,
onTourDismiss,
handleGuidedSetupClick,
onTimeRangeRefresh,
}: HeaderActionsProps) {
const { relativeStart, relativeEnd, refreshInterval, refreshPaused } = useDatePickerContext();
const { endTour: endObservabilityTour, isTourVisible: isObservabilityTourVisible } =
useObservabilityTourContext();
const buttonRef = useRef();
return (
<EuiFlexGroup wrap gutterSize="s" justifyContent="flexEnd">
<EuiFlexItem grow={false}>
<DatePicker
rangeFrom={relativeStart}
rangeTo={relativeEnd}
refreshInterval={refreshInterval}
refreshPaused={refreshPaused}
width="auto"
onTimeRangeRefresh={onTimeRangeRefresh}
/>
</EuiFlexItem>
<EuiFlexItem grow={false} style={{ alignItems: 'flex-end' }}>
<EuiButton
// @ts-expect-error the EUI verson that kibana uses right now doesn't have the correct types
buttonRef={buttonRef}
data-test-subj="guidedSetupButton"
id="guidedSetupButton"
color="text"
iconType="wrench"
onClick={() => {
// End the Observability tour if it's visible and the user clicks the data assistant button
if (isObservabilityTourVisible) {
endObservabilityTour();
}
handleGuidedSetupClick();
}}
>
<FormattedMessage
id="xpack.observability.overview.guidedSetupButton"
defaultMessage="Data assistant"
/>
</EuiButton>
{showTour ? (
<EuiTourStep
// @ts-expect-error the EUI verson that kibana uses right now doesn't have the correct types
anchor={() => buttonRef.current}
isStepOpen
title={i18n.translate('xpack.observability.overview.guidedSetupTourTitle', {
defaultMessage: 'Data assistant is always available',
})}
content={
<EuiText size="s">
<FormattedMessage
id="xpack.observability.overview.guidedSetupTourContent"
defaultMessage="If you're ever in doubt you can always access the data assistant and view your next steps by clicking here."
/>
</EuiText>
}
step={1}
stepsTotal={1}
maxWidth={400}
onFinish={onTourDismiss}
footerAction={
<EuiButtonEmpty color="text" flush="right" size="xs" onClick={onTourDismiss}>
<FormattedMessage
id="xpack.observability.overview.guidedSetupTourDismissButton"
defaultMessage="Dismiss"
/>
</EuiButtonEmpty>
}
/>
) : null}
</EuiFlexItem>
</EuiFlexGroup>
);
}

View file

@ -10,10 +10,3 @@ export interface Bucket {
end?: number;
}
export type BucketSize = { bucketSize: number; intervalString: string } | undefined;
export interface HeaderActionsProps {
showTour?: boolean;
onTourDismiss: () => void;
handleGuidedSetupClick: () => void;
onTimeRangeRefresh: () => void;
}