From 476b9f5ba767d391236cd026da13fd939863abe1 Mon Sep 17 00:00:00 2001 From: Katerina Date: Tue, 24 Sep 2024 20:15:26 +0300 Subject: [PATCH] [Inventory][ECO] Show empty state when no entities are found. (#193755) ## Summary closes #193336 - Introduce route `GET /internal/inventory/has_data` that check if there are any entities for hosts, services and containers - Show empty state - Refactor a bit the button to add data to reuse it **Note**: - I used the illustration we had for the new service inventory as it had the dark version. - The callout will be shown by default. (I dismissed before starting the recording) ### without entities https://github.com/user-attachments/assets/6c5f7f38-a31c-4801-9e3d-3c91edd88f1d ### with entities https://github.com/user-attachments/assets/19de8215-1c11-4c36-bae4-00432f205855 --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- .../add_data_action_menu.tsx | 20 +-- .../components/empty_states/empty_state.tsx | 137 ++++++++++++++++++ .../inventory_page_template/index.tsx | 60 +++++--- .../shared/add_data_buttons/buttons.tsx | 72 +++++++++ .../public/services/telemetry/types.ts | 2 +- .../get_global_inventory_route_repository.ts | 2 + .../server/routes/has_data/get_has_data.ts | 39 +++++ .../inventory/server/routes/has_data/route.ts | 34 +++++ .../inventory/tsconfig.json | 3 +- 9 files changed, 336 insertions(+), 33 deletions(-) create mode 100644 x-pack/plugins/observability_solution/inventory/public/components/empty_states/empty_state.tsx create mode 100644 x-pack/plugins/observability_solution/inventory/public/components/shared/add_data_buttons/buttons.tsx create mode 100644 x-pack/plugins/observability_solution/inventory/server/routes/has_data/get_has_data.ts create mode 100644 x-pack/plugins/observability_solution/inventory/server/routes/has_data/route.ts diff --git a/x-pack/plugins/observability_solution/inventory/public/components/app_root/header_action_menu/add_data_action_menu.tsx b/x-pack/plugins/observability_solution/inventory/public/components/app_root/header_action_menu/add_data_action_menu.tsx index ca4bc06df648a..cec6188a1553f 100644 --- a/x-pack/plugins/observability_solution/inventory/public/components/app_root/header_action_menu/add_data_action_menu.tsx +++ b/x-pack/plugins/observability_solution/inventory/public/components/app_root/header_action_menu/add_data_action_menu.tsx @@ -15,26 +15,18 @@ import { EuiIcon, EuiPopover, } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; import { OBSERVABILITY_ONBOARDING_LOCATOR, ObservabilityOnboardingLocatorParams, } from '@kbn/deeplinks-observability'; import { useKibana } from '../../../hooks/use_kibana'; import type { InventoryAddDataParams } from '../../../services/telemetry/types'; - -const addDataTitle = i18n.translate('xpack.inventory.addDataContextMenu.link', { - defaultMessage: 'Add data', -}); -const addDataItem = i18n.translate('xpack.inventory.add.apm.agent.button.', { - defaultMessage: 'Add data', -}); - -const associateServiceLogsItem = i18n.translate('xpack.inventory.associate.service.logs.button', { - defaultMessage: 'Associate existing service logs', -}); - -const ASSOCIATE_LOGS_LINK = 'https://ela.st/new-experience-associate-service-logs'; +import { + ASSOCIATE_LOGS_LINK, + addDataItem, + addDataTitle, + associateServiceLogsItem, +} from '../../shared/add_data_buttons/buttons'; export function AddDataContextMenu() { const [popoverOpen, setPopoverOpen] = useState(false); diff --git a/x-pack/plugins/observability_solution/inventory/public/components/empty_states/empty_state.tsx b/x-pack/plugins/observability_solution/inventory/public/components/empty_states/empty_state.tsx new file mode 100644 index 0000000000000..587812aa6c86e --- /dev/null +++ b/x-pack/plugins/observability_solution/inventory/public/components/empty_states/empty_state.tsx @@ -0,0 +1,137 @@ +/* + * 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 { + COLOR_MODES_STANDARD, + EuiCallOut, + EuiEmptyPrompt, + EuiFlexGroup, + EuiFlexItem, + EuiHorizontalRule, + EuiImage, + EuiLink, + EuiText, + EuiTextColor, + useEuiTheme, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import React from 'react'; +import { dashboardsLight, dashboardsDark } from '@kbn/shared-svg'; +import useLocalStorage from 'react-use/lib/useLocalStorage'; +import { AddData, AssociateServiceLogs } from '../shared/add_data_buttons/buttons'; +import { useKibana } from '../../hooks/use_kibana'; +import { InventoryAddDataParams } from '../../services/telemetry/types'; + +export function EmptyState() { + const { services } = useKibana(); + + const [isDismissed, setDismissed] = useLocalStorage( + 'inventory.emptyStateDismissed', + false + ); + + function reportButtonClick(journey: InventoryAddDataParams['journey']) { + services.telemetry.reportInventoryAddData({ + view: 'empty_state', + journey, + }); + } + + const { colorMode } = useEuiTheme(); + + return ( + + {!isDismissed && ( + + setDismissed(true)} + title={i18n.translate('xpack.inventory.noEntitiesEmptyState.callout.title', { + defaultMessage: 'Trying for the first time?', + })} + > +

+ {i18n.translate('xpack.inventory.noEntitiesEmptyState.description', { + defaultMessage: + 'It can take a couple of minutes for your entities to show. Try refreshing in a minute or two.', + })} +

+ + {i18n.translate('xpack.inventory.noEntitiesEmptyState.learnMore.link', { + defaultMessage: 'Learn more', + })} + +
+
+ )} + + + } + title={ +

+ {i18n.translate('xpack.inventory.noEntitiesEmptyState.title', { + defaultMessage: 'No entities available', + })} +

+ } + layout={'horizontal'} + color="plain" + body={ + <> +

+ {i18n.translate('xpack.inventory.noEntitiesEmptyState.body.description', { + defaultMessage: + 'See all of your observed entities in one place by collecting some data.', + })} +

+ + +
+ + {i18n.translate('xpack.inventory.noEntitiesEmptyState.actions.title', { + defaultMessage: 'Start observing your entities:', + })} + +
+
+ + } + actions={ + + + { + reportButtonClick('add_data'); + }} + /> + { + reportButtonClick('associate_existing_service_logs'); + }} + /> + + + } + /> +
+
+ ); +} diff --git a/x-pack/plugins/observability_solution/inventory/public/components/inventory_page_template/index.tsx b/x-pack/plugins/observability_solution/inventory/public/components/inventory_page_template/index.tsx index fc0f3c03660a9..c2734d6643553 100644 --- a/x-pack/plugins/observability_solution/inventory/public/components/inventory_page_template/index.tsx +++ b/x-pack/plugins/observability_solution/inventory/public/components/inventory_page_template/index.tsx @@ -7,15 +7,24 @@ import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React from 'react'; +import { EuiEmptyPrompt, EuiLoadingLogo } from '@elastic/eui'; import { useKibana } from '../../hooks/use_kibana'; import { SearchBar } from '../search_bar'; import { getEntityManagerEnablement } from './no_data_config'; import { useEntityManager } from '../../hooks/use_entity_manager'; import { Welcome } from '../entity_enablement/welcome_modal'; +import { useInventoryAbortableAsync } from '../../hooks/use_inventory_abortable_async'; +import { EmptyState } from '../empty_states/empty_state'; + +const pageTitle = { + pageTitle: i18n.translate('xpack.inventory.inventoryPageHeaderLabel', { + defaultMessage: 'Inventory', + }), +}; export function InventoryPageTemplate({ children }: { children: React.ReactNode }) { const { - services: { observabilityShared }, + services: { observabilityShared, inventoryAPIClient }, } = useKibana(); const { PageTemplate: ObservabilityPageTemplate } = observabilityShared.navigation; @@ -32,6 +41,23 @@ export function InventoryPageTemplate({ children }: { children: React.ReactNode toggleWelcomedModal(); }; + const { value = { hasData: false }, loading: hasDataLoading } = useInventoryAbortableAsync( + ({ signal }) => { + return inventoryAPIClient.fetch('GET /internal/inventory/has_data', { + signal, + }); + }, + [inventoryAPIClient] + ); + + if (isEnablementLoading || hasDataLoading) { + return ( + + } /> + + ); + } + return ( - - - - - - {children} - {showWelcomedModal ? ( - - ) : null} - - + {value.hasData ? ( + + + + + + {children} + {showWelcomedModal ? ( + + ) : null} + + + ) : ( + + )} ); } diff --git a/x-pack/plugins/observability_solution/inventory/public/components/shared/add_data_buttons/buttons.tsx b/x-pack/plugins/observability_solution/inventory/public/components/shared/add_data_buttons/buttons.tsx new file mode 100644 index 0000000000000..90f8cdbba0946 --- /dev/null +++ b/x-pack/plugins/observability_solution/inventory/public/components/shared/add_data_buttons/buttons.tsx @@ -0,0 +1,72 @@ +/* + * 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. + */ + +// Disabling it for now until the EUI team fixes it + +/* eslint-disable @elastic/eui/href-or-on-click */ +import React from 'react'; +import { + OBSERVABILITY_ONBOARDING_LOCATOR, + ObservabilityOnboardingLocatorParams, +} from '@kbn/deeplinks-observability'; +import { EuiButton } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { useKibana } from '../../../hooks/use_kibana'; + +export const addDataTitle = i18n.translate('xpack.inventory.addDataContextMenu.link', { + defaultMessage: 'Add data', +}); +export const addDataItem = i18n.translate('xpack.inventory.add.apm.agent.button.', { + defaultMessage: 'Add data', +}); + +export const associateServiceLogsItem = i18n.translate( + 'xpack.inventory.associate.service.logs.button', + { + defaultMessage: 'Associate existing service logs', + } +); + +export const ASSOCIATE_LOGS_LINK = 'https://ela.st/new-experience-associate-service-logs'; + +export function AssociateServiceLogs({ onClick }: { onClick?: () => void }) { + return ( + + {associateServiceLogsItem} + + ); +} + +export function AddData({ onClick }: { onClick?: () => void }) { + const { + services: { share }, + } = useKibana(); + const onboardingLocator = share.url.locators.get( + OBSERVABILITY_ONBOARDING_LOCATOR + ); + return ( + + {addDataItem} + + ); +} diff --git a/x-pack/plugins/observability_solution/inventory/public/services/telemetry/types.ts b/x-pack/plugins/observability_solution/inventory/public/services/telemetry/types.ts index 494391aa1a7c1..e5fdf162b750c 100644 --- a/x-pack/plugins/observability_solution/inventory/public/services/telemetry/types.ts +++ b/x-pack/plugins/observability_solution/inventory/public/services/telemetry/types.ts @@ -12,7 +12,7 @@ export interface TelemetryServiceSetupParams { } export interface InventoryAddDataParams { - view: 'add_data_button'; + view: 'add_data_button' | 'empty_state'; journey?: 'add_data' | 'associate_existing_service_logs'; } diff --git a/x-pack/plugins/observability_solution/inventory/server/routes/get_global_inventory_route_repository.ts b/x-pack/plugins/observability_solution/inventory/server/routes/get_global_inventory_route_repository.ts index 190178cb25a95..598b69db90e5a 100644 --- a/x-pack/plugins/observability_solution/inventory/server/routes/get_global_inventory_route_repository.ts +++ b/x-pack/plugins/observability_solution/inventory/server/routes/get_global_inventory_route_repository.ts @@ -6,10 +6,12 @@ */ import { entitiesRoutes } from './entities/route'; +import { hasDataRoutes } from './has_data/route'; export function getGlobalInventoryServerRouteRepository() { return { ...entitiesRoutes, + ...hasDataRoutes, }; } diff --git a/x-pack/plugins/observability_solution/inventory/server/routes/has_data/get_has_data.ts b/x-pack/plugins/observability_solution/inventory/server/routes/has_data/get_has_data.ts new file mode 100644 index 0000000000000..465e720938b32 --- /dev/null +++ b/x-pack/plugins/observability_solution/inventory/server/routes/has_data/get_has_data.ts @@ -0,0 +1,39 @@ +/* + * 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 { Logger } from '@kbn/core/server'; +import { esqlResultToPlainObjects } from '@kbn/observability-utils/es/utils/esql_result_to_plain_objects'; +import { type ObservabilityElasticsearchClient } from '@kbn/observability-utils/es/client/create_observability_es_client'; +import { + getEntityDefinitionIdWhereClause, + getEntityTypesWhereClause, +} from '../entities/query_helper'; +import { ENTITIES_LATEST_ALIAS } from '../../../common/entities'; + +export async function getHasData({ + inventoryEsClient, + logger, +}: { + inventoryEsClient: ObservabilityElasticsearchClient; + logger: Logger; +}) { + try { + const esqlResults = await inventoryEsClient.esql('get_has_data', { + query: `FROM ${ENTITIES_LATEST_ALIAS} + | ${getEntityDefinitionIdWhereClause()} + | ${getEntityTypesWhereClause()} + | STATS _count = COUNT(*) + | LIMIT 1`, + }); + + const totalCount = esqlResultToPlainObjects(esqlResults)?.[0]._count ?? 0; + + return { hasData: totalCount > 0 }; + } catch (e) { + logger.error(e); + return { hasData: false }; + } +} diff --git a/x-pack/plugins/observability_solution/inventory/server/routes/has_data/route.ts b/x-pack/plugins/observability_solution/inventory/server/routes/has_data/route.ts new file mode 100644 index 0000000000000..aae8be7f846f8 --- /dev/null +++ b/x-pack/plugins/observability_solution/inventory/server/routes/has_data/route.ts @@ -0,0 +1,34 @@ +/* + * 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 { createObservabilityEsClient } from '@kbn/observability-utils/es/client/create_observability_es_client'; +import { INVENTORY_APP_ID } from '@kbn/deeplinks-observability/constants'; +import { createInventoryServerRoute } from '../create_inventory_server_route'; +import { getHasData } from './get_has_data'; + +export const hasDataRoute = createInventoryServerRoute({ + endpoint: 'GET /internal/inventory/has_data', + options: { + tags: ['access:inventory'], + }, + handler: async ({ context, logger }) => { + const coreContext = await context.core; + const inventoryEsClient = createObservabilityEsClient({ + client: coreContext.elasticsearch.client.asCurrentUser, + logger, + plugin: `@kbn/${INVENTORY_APP_ID}-plugin`, + }); + + return getHasData({ + inventoryEsClient, + logger, + }); + }, +}); + +export const hasDataRoutes = { + ...hasDataRoute, +}; diff --git a/x-pack/plugins/observability_solution/inventory/tsconfig.json b/x-pack/plugins/observability_solution/inventory/tsconfig.json index 009221ad0c0ca..a391737762c52 100644 --- a/x-pack/plugins/observability_solution/inventory/tsconfig.json +++ b/x-pack/plugins/observability_solution/inventory/tsconfig.json @@ -39,6 +39,7 @@ "@kbn/unified-search-plugin", "@kbn/data-plugin", "@kbn/core-analytics-browser", - "@kbn/core-http-browser" + "@kbn/core-http-browser", + "@kbn/shared-svg" ] }