Skip to content

Commit

Permalink
[Inventory][ECO] Create header action menu (#193398)
Browse files Browse the repository at this point in the history
closes [#192326](#192326)

## Summary

This PR introduces the "Add data" item to the header menu:

https://github.com/user-attachments/assets/78ea3667-4ef1-4f02-a513-76e7ca896e67

<img width="600" alt="image"
src="https://github.com/user-attachments/assets/afd21f2d-da66-4d10-83c0-29500591cf3c">

>[!NOTE]
>I have refactored` plugin.ts`, moving the `ReactDOM.render` call to
`application.tsx`. I've also created a new component to render the
context providers.
>
>`useKibana` and `InventoryKibanaContext` were simplified.
>
>Besides, the analytics events created for the EEM Service Inventory
'Add data' button were replicated for this button.

### How to test

- Add `xpack.inventory.enabled: true` to kibana.dev.yml
- Start ES and Kibana locally
- Navigate to Observability -> Inventory

---------

Co-authored-by: kibanamachine <[email protected]>
(cherry picked from commit 1a192bc)
  • Loading branch information
crespocarlos committed Sep 20, 2024
1 parent 9c641cb commit d5566c6
Show file tree
Hide file tree
Showing 19 changed files with 462 additions and 107 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -8,24 +8,22 @@
import { coreMock } from '@kbn/core/public/mocks';
import type { ObservabilitySharedPluginStart } from '@kbn/observability-shared-plugin/public';
import type { InferencePublicStart } from '@kbn/inference-plugin/public';
import type { SharePluginStart } from '@kbn/share-plugin/public';
import type { InventoryKibanaContext } from '../public/hooks/use_kibana';
import type { ITelemetryClient } from '../public/services/telemetry/types';

export function getMockInventoryContext(): InventoryKibanaContext {
const core = coreMock.createStart();
const coreStart = coreMock.createStart();

return {
core,
dependencies: {
start: {
observabilityShared: {} as unknown as ObservabilitySharedPluginStart,
inference: {} as unknown as InferencePublicStart,
},
},
services: {
inventoryAPIClient: {
fetch: jest.fn(),
stream: jest.fn(),
},
...coreStart,
observabilityShared: {} as unknown as ObservabilitySharedPluginStart,
inference: {} as unknown as InferencePublicStart,
share: {} as unknown as SharePluginStart,
telemetry: {} as unknown as ITelemetryClient,
inventoryAPIClient: {
fetch: jest.fn(),
stream: jest.fn(),
},
};
}
3 changes: 2 additions & 1 deletion x-pack/plugins/observability_solution/inventory/kibana.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@
"observabilityShared",
"entityManager",
"inference",
"dataViews"
"dataViews",
"share"
],
"requiredBundles": [
"kibanaReact"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,64 +4,46 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import type { CoreStart, CoreTheme } from '@kbn/core/public';
import { RedirectAppLinks } from '@kbn/shared-ux-link-redirect-app';
import type { History } from 'history';
import React, { useMemo } from 'react';
import type { Observable } from 'rxjs';
import { RouteRenderer, RouterProvider } from '@kbn/typed-react-router-config';
import React from 'react';
import ReactDOM from 'react-dom';
import { APP_WRAPPER_CLASS, type AppMountParameters, type CoreStart } from '@kbn/core/public';
import { KibanaRenderContextProvider } from '@kbn/react-kibana-context-render';
import { css } from '@emotion/css';
import type { InventoryStartDependencies } from './types';
import { inventoryRouter } from './routes/config';
import { InventoryKibanaContext } from './hooks/use_kibana';
import { InventoryServices } from './services/types';
import { InventoryContextProvider } from './components/inventory_context_provider';
import { AppRoot } from './components/app_root';

function Application({
export const renderApp = ({
coreStart,
history,
pluginsStart,
theme$,
services,
appMountParameters,
}: {
coreStart: CoreStart;
history: History;
pluginsStart: InventoryStartDependencies;
theme$: Observable<CoreTheme>;
services: InventoryServices;
}) {
const theme = useMemo(() => {
return { theme$ };
}, [theme$]);
} & { appMountParameters: AppMountParameters }) => {
const { element } = appMountParameters;

const context: InventoryKibanaContext = useMemo(
() => ({
core: coreStart,
dependencies: {
start: pluginsStart,
},
services,
}),
[coreStart, pluginsStart, services]
);
const appWrapperClassName = css`
overflow: auto;
`;
const appWrapperElement = document.getElementsByClassName(APP_WRAPPER_CLASS)[1];
appWrapperElement.classList.add(appWrapperClassName);

return (
<KibanaRenderContextProvider
theme={theme}
i18n={coreStart.i18n}
analytics={coreStart.analytics}
>
<InventoryContextProvider context={context}>
<RedirectAppLinks coreStart={coreStart}>
<coreStart.i18n.Context>
<RouterProvider history={history} router={inventoryRouter as any}>
<RouteRenderer />
</RouterProvider>
</coreStart.i18n.Context>
</RedirectAppLinks>
</InventoryContextProvider>
</KibanaRenderContextProvider>
ReactDOM.render(
<KibanaRenderContextProvider {...coreStart}>
<AppRoot
appMountParameters={appMountParameters}
coreStart={coreStart}
pluginsStart={pluginsStart}
services={services}
/>
</KibanaRenderContextProvider>,
element
);
}

export { Application };
return () => {
ReactDOM.unmountComponentAtNode(element);
appWrapperElement.classList.remove(APP_WRAPPER_CLASS);
};
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
/*
* 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, { useState } from 'react';
import {
EuiContextMenu,
EuiContextMenuPanelDescriptor,
EuiFlexGroup,
EuiFlexItem,
EuiHeaderLink,
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';

export function AddDataContextMenu() {
const [popoverOpen, setPopoverOpen] = useState(false);
const {
services: { share, telemetry },
} = useKibana();

const onboardingLocator = share.url.locators.get<ObservabilityOnboardingLocatorParams>(
OBSERVABILITY_ONBOARDING_LOCATOR
);

const button = (
<EuiHeaderLink
color="primary"
iconType="indexOpen"
onClick={() => setPopoverOpen((prevState) => !prevState)}
data-test-subj="inventoryAddDataHeaderContextMenu"
>
<EuiFlexGroup gutterSize="s" alignItems="center" responsive={false}>
<EuiFlexItem grow={false}>{addDataTitle}</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiIcon type="arrowDown" />
</EuiFlexItem>
</EuiFlexGroup>
</EuiHeaderLink>
);

function reportButtonClick(journey: InventoryAddDataParams['journey']) {
telemetry.reportInventoryAddData({
view: 'add_data_button',
journey,
});
}

const panels: EuiContextMenuPanelDescriptor[] = [
{
id: 0,
title: addDataTitle,
items: [
{
name: (
<EuiFlexGroup gutterSize="s" alignItems="center" responsive={false}>
<EuiFlexItem grow={false}>{associateServiceLogsItem}</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiIcon type="popout" />
</EuiFlexItem>
</EuiFlexGroup>
),
key: 'associateServiceLogs',
href: ASSOCIATE_LOGS_LINK,
'data-test-subj': 'inventoryHeaderMenuAddDataAssociateServiceLogs',
target: '_blank',
onClick: () => {
reportButtonClick('associate_existing_service_logs');
},
},
{
name: addDataItem,
key: 'addData',
href: onboardingLocator?.getRedirectUrl({ category: '' }),
icon: 'plusInCircle',
'data-test-subj': 'inventoryHeaderMenuAddData',
onClick: () => {
reportButtonClick('add_data');
},
},
],
},
];

return (
<EuiPopover
id="inventoryHeaderMenuAddDataPopover"
button={button}
isOpen={popoverOpen}
closePopover={() => setPopoverOpen(false)}
panelPaddingSize="none"
anchorPosition="downCenter"
>
<EuiContextMenu initialPanelId={0} panels={panels} />
</EuiPopover>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/*
* 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 { EuiHeaderLinks } from '@elastic/eui';
import { AddDataContextMenu } from './add_data_action_menu';

export function HeaderActionMenuItems() {
return (
<EuiHeaderLinks gutterSize="xs">
<AddDataContextMenu />
</EuiHeaderLinks>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/*
* 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 { RedirectAppLinks } from '@kbn/shared-ux-link-redirect-app';
import React from 'react';
import { type AppMountParameters, type CoreStart } from '@kbn/core/public';
import { RouteRenderer, RouterProvider } from '@kbn/typed-react-router-config';
import { HeaderMenuPortal } from '@kbn/observability-shared-plugin/public';
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { InventoryContextProvider } from '../inventory_context_provider';
import { inventoryRouter } from '../../routes/config';
import { HeaderActionMenuItems } from './header_action_menu';
import { InventoryStartDependencies } from '../../types';
import { InventoryServices } from '../../services/types';

export function AppRoot({
coreStart,
pluginsStart,
services,
appMountParameters,
}: {
coreStart: CoreStart;
pluginsStart: InventoryStartDependencies;
services: InventoryServices;
} & { appMountParameters: AppMountParameters }) {
const { history } = appMountParameters;

const context = {
...coreStart,
...pluginsStart,
...services,
};

return (
<InventoryContextProvider context={context}>
<RedirectAppLinks coreStart={coreStart}>
<RouterProvider history={history} router={inventoryRouter}>
<RouteRenderer />
<InventoryHeaderActionMenu appMountParameters={appMountParameters} />
</RouterProvider>
</RedirectAppLinks>
</InventoryContextProvider>
);
}

export function InventoryHeaderActionMenu({
appMountParameters,
}: {
appMountParameters: AppMountParameters;
}) {
const { setHeaderActionMenu, theme$ } = appMountParameters;

return (
<HeaderMenuPortal setHeaderActionMenu={setHeaderActionMenu} theme$={theme$}>
<EuiFlexGroup responsive={false} gutterSize="s">
<EuiFlexItem>
<HeaderActionMenuItems />
</EuiFlexItem>
</EuiFlexGroup>
</HeaderMenuPortal>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,7 @@ import { useKibana } from '../../hooks/use_kibana';

export function InventoryPageTemplate({ children }: { children: React.ReactNode }) {
const {
dependencies: {
start: { observabilityShared },
},
services: { observabilityShared },
} = useKibana();

const { PageTemplate: ObservabilityPageTemplate } = observabilityShared.navigation;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ const getDetailsFromErrorResponse = (error: IHttpFetchError<ResponseErrorBody>)

export function useInventoryAbortableAsync<T>(...args: Parameters<typeof useAbortableAsync<T>>) {
const {
core: { notifications },
services: { notifications },
} = useKibana();
const response = useAbortableAsync(...args);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ interface StatefulInventoryRouter extends InventoryRouter {

export function useInventoryRouter(): StatefulInventoryRouter {
const {
core: {
services: {
http,
application: { navigateToApp },
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,13 @@
* 2.0.
*/

import { useKibana } from '@kbn/kibana-react-plugin/public';
import type { CoreStart } from '@kbn/core/public';
import { type KibanaReactContextValue, useKibana } from '@kbn/kibana-react-plugin/public';
import type { InventoryStartDependencies } from '../types';
import type { InventoryServices } from '../services/types';

export interface InventoryKibanaContext {
core: CoreStart;
dependencies: { start: InventoryStartDependencies };
services: InventoryServices;
}
export type InventoryKibanaContext = CoreStart & InventoryStartDependencies & InventoryServices;

const useTypedKibana = () => {
return useKibana<InventoryKibanaContext>().services;
};
const useTypedKibana = useKibana as () => KibanaReactContextValue<InventoryKibanaContext>;

export { useTypedKibana as useKibana };
Loading

0 comments on commit d5566c6

Please sign in to comment.