[Profiling] Empty state (#172295)

The empty state will show up when `has_setup` from the Profiling Status
API returns `false`.

<img width="1276" alt="Screenshot 2023-11-30 at 15 06 30"
src="97a313be-db4f-4a5a-af36-df574f9793d5">
<img width="1036" alt="Screenshot 2023-11-30 at 14 47 48"
src="2622cad6-6763-4abc-9469-fa292137efda">
This commit is contained in:
Cauê Marcondes 2023-12-05 09:36:04 +00:00 committed by GitHub
parent a29b0f4573
commit 4a308c60fd
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 182 additions and 22 deletions

View file

@ -11,13 +11,18 @@ import {
EuiFlexGroup,
EuiFlexItem,
EuiLink,
EuiLoadingSpinner,
EuiSpacer,
EuiTabbedContent,
EuiTabbedContentProps,
} from '@elastic/eui';
import { css } from '@emotion/react';
import { i18n } from '@kbn/i18n';
import { useKibana } from '@kbn/kibana-react-plugin/public';
import { EmbeddableProfilingSearchBar } from '@kbn/observability-shared-plugin/public';
import {
EmbeddableProfilingSearchBar,
ProfilingEmptyState,
} from '@kbn/observability-shared-plugin/public';
import React, { useMemo } from 'react';
import { useHistory } from 'react-router-dom';
import { ApmDocumentType } from '../../../../common/document_type';
@ -38,7 +43,7 @@ export function ProfilingOverview() {
path: { serviceName },
query: { rangeFrom, rangeTo, environment, kuery },
} = useApmParams('/services/{serviceName}/profiling');
const { isProfilingAvailable } = useProfilingPlugin();
const { isProfilingAvailable, isLoading } = useProfilingPlugin();
const { start, end, refreshTimeRange } = useTimeRange({ rangeFrom, rangeTo });
const preferred = usePreferredDataSourceAndBucketSize({
start,
@ -101,8 +106,21 @@ export function ProfilingOverview() {
];
}, [end, environment, kuery, preferred?.source, serviceName, start]);
if (!isProfilingAvailable) {
return null;
if (isLoading) {
return (
<div
css={css`
display: flex;
justify-content: center;
`}
>
<EuiLoadingSpinner size="m" />
</div>
);
}
if (isProfilingAvailable === false) {
return <ProfilingEmptyState />;
}
return (

View file

@ -6,6 +6,7 @@
*/
import {
EuiBadge,
EuiFlexGroup,
EuiFlexItem,
EuiLoadingLogo,
@ -13,27 +14,28 @@ import {
EuiSpacer,
EuiTitle,
EuiToolTip,
EuiBadge,
} from '@elastic/eui';
import { useLocation } from 'react-router-dom';
import { i18n } from '@kbn/i18n';
import { enableAwsLambdaMetrics } from '@kbn/observability-plugin/common';
import { omit } from 'lodash';
import React from 'react';
import { useHistory } from 'react-router-dom';
import { useHistory, useLocation } from 'react-router-dom';
import {
isMobileAgentName,
isRumAgentName,
isAWSLambdaAgentName,
isAzureFunctionsAgentName,
isServerlessAgentName,
isMobileAgentName,
isRumAgentName,
isRumOrMobileAgentName,
isServerlessAgentName,
} from '../../../../../common/agent_name';
import { ApmFeatureFlagName } from '../../../../../common/apm_feature_flags';
import { ServerlessType } from '../../../../../common/serverless';
import { useApmPluginContext } from '../../../../context/apm_plugin/use_apm_plugin_context';
import { ApmServiceContextProvider } from '../../../../context/apm_service/apm_service_context';
import { useApmServiceContext } from '../../../../context/apm_service/use_apm_service_context';
import { useBreadcrumb } from '../../../../context/breadcrumbs/use_breadcrumb';
import { ServiceAnomalyTimeseriesContextProvider } from '../../../../context/service_anomaly_timeseries/service_anomaly_timeseries_context';
import { useApmFeatureFlag } from '../../../../hooks/use_apm_feature_flag';
import { useApmParams } from '../../../../hooks/use_apm_params';
import { useApmRouter } from '../../../../hooks/use_apm_router';
import { isPending, useFetcher } from '../../../../hooks/use_fetcher';
@ -46,10 +48,6 @@ import { ServiceIcons } from '../../../shared/service_icons';
import { TechnicalPreviewBadge } from '../../../shared/technical_preview_badge';
import { ApmMainTemplate } from '../apm_main_template';
import { AnalyzeDataButton } from './analyze_data_button';
import { ServerlessType } from '../../../../../common/serverless';
import { useApmFeatureFlag } from '../../../../hooks/use_apm_feature_flag';
import { ApmFeatureFlagName } from '../../../../../common/apm_feature_flags';
import { useProfilingPlugin } from '../../../../hooks/use_profiling_plugin';
type Tab = NonNullable<EuiPageHeaderProps['tabs']>[0] & {
key:
@ -220,7 +218,6 @@ function useTabs({ selectedTab }: { selectedTab: Tab['key'] }) {
plugins,
capabilities
);
const { isProfilingAvailable } = useProfilingPlugin();
const router = useApmRouter();
const isInfraTabAvailable = useApmFeatureFlag(
@ -407,7 +404,6 @@ function useTabs({ selectedTab }: { selectedTab: Tab['key'] }) {
defaultMessage: 'Universal Profiling',
}),
hidden:
!isProfilingAvailable ||
isRumOrMobileAgentName(agentName) ||
isAWSLambdaAgentName(serverlessType),
append: (

View file

@ -7,7 +7,7 @@
import { apmEnableProfilingIntegration } from '@kbn/observability-plugin/common';
import { useApmPluginContext } from '../context/apm_plugin/use_apm_plugin_context';
import { useFetcher } from './use_fetcher';
import { isPending, useFetcher } from './use_fetcher';
export function useProfilingPlugin() {
const { plugins, core } = useApmPluginContext();
@ -16,7 +16,7 @@ export function useProfilingPlugin() {
true
);
const { data } = useFetcher((callApmApi) => {
const { data, status } = useFetcher((callApmApi) => {
return callApmApi('GET /internal/apm/profiling/status');
}, []);
@ -30,5 +30,6 @@ export function useProfilingPlugin() {
isProfilingPluginInitialized: data?.initialized,
isProfilingIntegrationEnabled,
isProfilingAvailable,
isLoading: isPending(status),
};
}

View file

@ -9,7 +9,7 @@ import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import React from 'react';
import { DatePicker } from '../date_picker/date_picker';
import { useTabSwitcherContext } from '../hooks/use_tab_switcher';
import { Anomalies, Metadata, Processes, Osquery, Logs, Overview, Profiling } from '../tabs';
import { Anomalies, Logs, Metadata, Osquery, Overview, Processes, Profiling } from '../tabs';
import { ContentTabIds } from '../types';
export const Content = () => {
@ -22,7 +22,6 @@ export const Content = () => {
ContentTabIds.LOGS,
ContentTabIds.METADATA,
ContentTabIds.PROCESSES,
ContentTabIds.PROFILING,
ContentTabIds.ANOMALIES,
]}
/>

View file

@ -0,0 +1,42 @@
/*
* 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 type { ProfilingStatus } from '@kbn/profiling-utils';
import { useEffect } from 'react';
import { useHTTPRequest } from '../../../hooks/use_http_request';
import { useRequestObservable } from './use_request_observable';
interface Props {
isActive: boolean;
}
export function useProfilingStatusData({ isActive }: Props) {
const { request$ } = useRequestObservable<ProfilingStatus>();
const { loading, error, response, makeRequest } = useHTTPRequest<ProfilingStatus>(
`/api/infra/profiling/status`,
'GET',
undefined,
undefined,
undefined,
undefined,
true
);
useEffect(() => {
if (!isActive) {
return;
}
request$.next(makeRequest);
}, [isActive, makeRequest, request$]);
return {
loading,
error,
response,
};
}

View file

@ -8,10 +8,23 @@ import { i18n } from '@kbn/i18n';
import { EuiSpacer, EuiTabbedContent, type EuiTabbedContentProps } from '@elastic/eui';
import React from 'react';
import { ProfilingEmptyState } from '@kbn/observability-shared-plugin/public';
import { EuiLoadingSpinner } from '@elastic/eui';
import { css } from '@emotion/react';
import { Flamegraph } from './flamegraph';
import { Functions } from './functions';
import { DatePicker } from '../../date_picker/date_picker';
import { useProfilingStatusData } from '../../hooks/use_profiling_status_data';
import { useTabSwitcherContext } from '../../hooks/use_tab_switcher';
import { ContentTabIds } from '../../types';
import { ErrorPrompt } from './error_prompt';
export function Profiling() {
const { activeTabId } = useTabSwitcherContext();
const { error, loading, response } = useProfilingStatusData({
isActive: activeTabId === ContentTabIds.PROFILING,
});
const tabs: EuiTabbedContentProps['tabs'] = [
{
id: 'flamegraph',
@ -39,9 +52,33 @@ export function Profiling() {
},
];
if (loading) {
return (
<div
css={css`
display: flex;
justify-content: center;
`}
>
<EuiLoadingSpinner size="m" />
</div>
);
}
if (error !== null) {
return <ErrorPrompt />;
}
return (
<>
<EuiTabbedContent tabs={tabs} initialSelectedTab={tabs[0]} />
{response?.has_setup === false ? (
<ProfilingEmptyState />
) : (
<>
<DatePicker />
<EuiTabbedContent tabs={tabs} initialSelectedTab={tabs[0]} />
</>
)}
</>
);
}

View file

@ -118,7 +118,7 @@ export const config: PluginConfigDescriptor<InfraConfig> = {
* before enabling this flag.
*/
profilingEnabled: offeringBasedSchema({
traditional: schema.boolean({ defaultValue: false }),
traditional: schema.boolean({ defaultValue: true }),
serverless: schema.boolean({ defaultValue: false }),
}),
}),

View file

@ -0,0 +1,65 @@
/*
* 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 { EuiButton, EuiEmptyPrompt, EuiImage, EuiLink } from '@elastic/eui';
import { useKibana } from '@kbn/kibana-react-plugin/public';
import { i18n } from '@kbn/i18n';
import React from 'react';
import profilingImg from '../../images/profiling.png';
export function ProfilingEmptyState() {
const { services } = useKibana();
return (
<EuiEmptyPrompt
icon={<EuiImage size="fullWidth" src={profilingImg} alt="" />}
title={
<h2>
{i18n.translate('xpack.observabilityShared.profilingEmptyState.title', {
defaultMessage:
'Improve computational efficiency. Debug performance regressions. Reduce cloud spend.',
})}
</h2>
}
layout="horizontal"
color="plain"
hasBorder
hasShadow={false}
body={
<>
<p>
{i18n.translate('xpack.observabilityShared.profilingEmptyState.body', {
defaultMessage:
'Elastic Universal Profiling is a whole-system, always-on, continuous profiling solution that eliminates the need for code instrumentation, recompilation, on-host debug symbols, or service restarts. Leveraging eBPF, Universal Profiling operates within the Linux kernel space, capturing only the needed data with minimal overhead in an unobtrusive manner.',
})}
</p>
</>
}
actions={[
<EuiButton
href={services.http?.basePath.prepend(`/app/profiling`)}
data-test-subj="infraProfilingEmptyStateAddProfilingButton"
color="primary"
fill
>
{i18n.translate('xpack.observabilityShared.profilingEmptyState.addProfiling', {
defaultMessage: 'Add profiling',
})}
</EuiButton>,
<EuiLink
href={`${services.docLinks?.ELASTIC_WEBSITE_URL}/guide/en/observability/${services.docLinks?.DOC_LINK_VERSION}/profiling-get-started.html`}
data-test-subj="infraProfilingEmptyStateGoToDocsButton"
target="_blank"
external
>
{i18n.translate('xpack.observabilityShared.profilingEmptyState.goToDocs', {
defaultMessage: 'Go to docs',
})}
</EuiLink>,
]}
/>
);
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

View file

@ -88,3 +88,5 @@ export {
EmbeddableProfilingSearchBar,
type EmbeddableProfilingSearchBarProps,
} from './components/profiling/embeddables';
export { ProfilingEmptyState } from './components/profiling/profiling_empty_state';