Skip to content

Commit

Permalink
Create a new menu for observability links (#54847) (#56133)
Browse files Browse the repository at this point in the history
* Create a new menu for observability links. Use it on inentory page.

* Change the order of props for clarity

* Fix default message

* Composition over configuration

* Show ids and ips. PR feedback.

* Don't wrap subtitle. Use fields in inventory model for name

* Tooltip was becoming hacky. Keep it simple and wrap the id.

* Create observability plugin. Add action menu to it.

* Fix path

* Satisfy linter and fix test

* Please the linter

* Update translastions

* Update test for disabled links

* Update more tests

Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>

Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
  • Loading branch information
phillipb and elasticmachine committed Jan 28, 2020
1 parent 512b9eb commit 1b96001
Show file tree
Hide file tree
Showing 17 changed files with 243 additions and 64 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ export const awsEC2: InventoryModel = {
displayName: i18n.translate('xpack.infra.inventoryModels.awsEC2.displayName', {
defaultMessage: 'EC2 Instances',
}),
singularDisplayName: i18n.translate('xpack.infra.inventoryModels.awsEC2.singularDisplayName', {
defaultMessage: 'EC2 Instance',
}),
requiredModule: 'aws',
crosslinkSupport: {
details: true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ export const awsRDS: InventoryModel = {
displayName: i18n.translate('xpack.infra.inventoryModels.awsRDS.displayName', {
defaultMessage: 'RDS Databases',
}),
singularDisplayName: i18n.translate('xpack.infra.inventoryModels.awsRDS.singularDisplayName', {
defaultMessage: 'RDS Database',
}),
requiredModule: 'aws',
crosslinkSupport: {
details: true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ export const awsS3: InventoryModel = {
displayName: i18n.translate('xpack.infra.inventoryModels.awsS3.displayName', {
defaultMessage: 'S3 Buckets',
}),
singularDisplayName: i18n.translate('xpack.infra.inventoryModels.awsS3.singularDisplayName', {
defaultMessage: 'S3 Bucket',
}),
requiredModule: 'aws',
crosslinkSupport: {
details: true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ export const awsSQS: InventoryModel = {
displayName: i18n.translate('xpack.infra.inventoryModels.awsSQS.displayName', {
defaultMessage: 'SQS Queues',
}),
singularDisplayName: i18n.translate('xpack.infra.inventoryModels.awsSQS.singularDisplayName', {
defaultMessage: 'SQS Queue',
}),
requiredModule: 'aws',
crosslinkSupport: {
details: true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ export const container: InventoryModel = {
displayName: i18n.translate('xpack.infra.inventoryModel.container.displayName', {
defaultMessage: 'Docker Containers',
}),
singularDisplayName: i18n.translate('xpack.infra.inventoryModel.container.singularDisplayName', {
defaultMessage: 'Docker Container',
}),
requiredModule: 'docker',
crosslinkSupport: {
details: true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ export const host: InventoryModel = {
displayName: i18n.translate('xpack.infra.inventoryModel.host.displayName', {
defaultMessage: 'Hosts',
}),
singularDisplayName: i18n.translate('xpack.infra.inventoryModels.host.singularDisplayName', {
defaultMessage: 'Host',
}),
requiredModule: 'system',
crosslinkSupport: {
details: true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ export const pod: InventoryModel = {
displayName: i18n.translate('xpack.infra.inventoryModel.pod.displayName', {
defaultMessage: 'Kubernetes Pods',
}),
singularDisplayName: i18n.translate('xpack.infra.inventoryModels.pod.singularDisplayName', {
defaultMessage: 'Kubernetes Pod',
}),
requiredModule: 'kubernetes',
crosslinkSupport: {
details: true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,7 @@ export interface InventoryMetrics {
export interface InventoryModel {
id: string;
displayName: string;
singularDisplayName: string;
requiredModule: string;
fields: {
id: string;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,26 @@
* you may not use this file except in compliance with the Elastic License.
*/

import {
EuiContextMenu,
EuiContextMenuPanelDescriptor,
EuiPopover,
EuiPopoverProps,
} from '@elastic/eui';
import { EuiPopoverProps, EuiCode } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';

import React from 'react';
import React, { useMemo } from 'react';
import { InfraWaffleMapNode, InfraWaffleMapOptions } from '../../lib/lib';
import { getNodeDetailUrl, getNodeLogsUrl } from '../../pages/link_to';
import { createUptimeLink } from './lib/create_uptime_link';
import { findInventoryModel } from '../../../common/inventory_models';
import { findInventoryModel, findInventoryFields } from '../../../common/inventory_models';
import { useKibana } from '../../../../../../../src/plugins/kibana_react/public';
import { InventoryItemType } from '../../../common/inventory_models/types';
import {
Section,
SectionLinkProps,
ActionMenu,
SectionTitle,
SectionSubtitle,
SectionLinks,
SectionLink,
} from '../../../../../../plugins/observability/public';

interface Props {
options: InfraWaffleMapOptions;
Expand All @@ -43,83 +48,139 @@ export const NodeContextMenu = ({
}: Props) => {
const uiCapabilities = useKibana().services.application?.capabilities;
const inventoryModel = findInventoryModel(nodeType);
const nodeDetailFrom = currentTime - inventoryModel.metrics.defaultTimeRangeInSeconds * 1000;
// Due to the changing nature of the fields between APM and this UI,
// We need to have some exceptions until 7.0 & ECS is finalized. Reference
// #26620 for the details for these fields.
// TODO: This is tech debt, remove it after 7.0 & ECS migration.
const apmField = nodeType === 'host' ? 'host.hostname' : inventoryModel.fields.id;

const nodeLogsMenuItem = {
name: i18n.translate('xpack.infra.nodeContextMenu.viewLogsName', {
defaultMessage: 'View logs',
const showDetail = inventoryModel.crosslinkSupport.details;
const showLogsLink =
inventoryModel.crosslinkSupport.logs && node.id && uiCapabilities?.logs?.show;
const showAPMTraceLink =
inventoryModel.crosslinkSupport.apm && uiCapabilities?.apm && uiCapabilities?.apm.show;
const showUptimeLink =
inventoryModel.crosslinkSupport.uptime && (['pod', 'container'].includes(nodeType) || node.ip);

const inventoryId = useMemo(() => {
if (nodeType === 'host') {
if (node.ip) {
return { label: <EuiCode>host.ip</EuiCode>, value: node.ip };
}
} else {
if (options.fields) {
const { id } = findInventoryFields(nodeType, options.fields);
return {
label: <EuiCode>{id}</EuiCode>,
value: node.id,
};
}
}
return { label: '', value: '' };
}, [nodeType, node.ip, node.id, options.fields]);

const nodeLogsMenuItem: SectionLinkProps = {
label: i18n.translate('xpack.infra.nodeContextMenu.viewLogsName', {
defaultMessage: '{inventoryName} logs',
values: { inventoryName: inventoryModel.singularDisplayName },
}),
href: getNodeLogsUrl({
nodeType,
nodeId: node.id,
time: currentTime,
}),
'data-test-subj': 'viewLogsContextMenuItem',
isDisabled: !showLogsLink,
};

const nodeDetailFrom = currentTime - inventoryModel.metrics.defaultTimeRangeInSeconds * 1000;
const nodeDetailMenuItem = {
name: i18n.translate('xpack.infra.nodeContextMenu.viewMetricsName', {
defaultMessage: 'View metrics',
const nodeDetailMenuItem: SectionLinkProps = {
label: i18n.translate('xpack.infra.nodeContextMenu.viewMetricsName', {
defaultMessage: '{inventoryName} metrics',
values: { inventoryName: inventoryModel.singularDisplayName },
}),
href: getNodeDetailUrl({
nodeType,
nodeId: node.id,
from: nodeDetailFrom,
to: currentTime,
}),
isDisabled: !showDetail,
};

const apmTracesMenuItem = {
name: i18n.translate('xpack.infra.nodeContextMenu.viewAPMTraces', {
defaultMessage: 'View APM traces',
const apmTracesMenuItem: SectionLinkProps = {
label: i18n.translate('xpack.infra.nodeContextMenu.viewAPMTraces', {
defaultMessage: '{inventoryName} APM traces',
values: { inventoryName: inventoryModel.singularDisplayName },
}),
href: `../app/apm#/traces?_g=()&kuery=${apmField}:"${node.id}"`,
'data-test-subj': 'viewApmTracesContextMenuItem',
isDisabled: !showAPMTraceLink,
};

const uptimeMenuItem = {
name: i18n.translate('xpack.infra.nodeContextMenu.viewUptimeLink', {
defaultMessage: 'View in Uptime',
const uptimeMenuItem: SectionLinkProps = {
label: i18n.translate('xpack.infra.nodeContextMenu.viewUptimeLink', {
defaultMessage: '{inventoryName} in Uptime',
values: { inventoryName: inventoryModel.singularDisplayName },
}),
href: createUptimeLink(options, nodeType, node),
isDisabled: !showUptimeLink,
};

const showDetail = inventoryModel.crosslinkSupport.details;
const showLogsLink =
inventoryModel.crosslinkSupport.logs && node.id && uiCapabilities?.logs?.show;
const showAPMTraceLink =
inventoryModel.crosslinkSupport.apm && uiCapabilities?.apm && uiCapabilities?.apm.show;
const showUptimeLink =
inventoryModel.crosslinkSupport.uptime && (['pod', 'container'].includes(nodeType) || node.ip);

const items = [
...(showLogsLink ? [nodeLogsMenuItem] : []),
...(showDetail ? [nodeDetailMenuItem] : []),
...(showAPMTraceLink ? [apmTracesMenuItem] : []),
...(showUptimeLink ? [uptimeMenuItem] : []),
];
const panels: EuiContextMenuPanelDescriptor[] = [{ id: 0, title: '', items }];

// If there is nothing to show then we need to return the child as is
if (items.length === 0) {
return <>{children}</>;
}

return (
<EuiPopover
<ActionMenu
closePopover={closePopover}
id={`${node.pathId}-popover`}
isOpen={isPopoverOpen}
button={children}
panelPaddingSize="none"
anchorPosition={popoverPosition}
>
<EuiContextMenu initialPanelId={0} panels={panels} data-test-subj="nodeContextMenu" />
</EuiPopover>
<div style={{ maxWidth: 300 }} data-test-subj="nodeContextMenu">
<Section>
<SectionTitle>
<FormattedMessage
id="xpack.infra.nodeContextMenu.title"
defaultMessage="{inventoryName} details"
values={{ inventoryName: inventoryModel.singularDisplayName }}
/>
</SectionTitle>
{inventoryId.label && (
<SectionSubtitle>
<div style={{ wordBreak: 'break-all' }}>
<FormattedMessage
id="xpack.infra.nodeContextMenu.description"
defaultMessage="View details for {label} {value}"
values={{ label: inventoryId.label, value: inventoryId.value }}
/>
</div>
</SectionSubtitle>
)}
<SectionLinks>
<SectionLink
data-test-subj="viewLogsContextMenuItem"
label={nodeLogsMenuItem.label}
href={nodeLogsMenuItem.href}
isDisabled={nodeLogsMenuItem.isDisabled}
/>
<SectionLink
label={nodeDetailMenuItem.label}
href={nodeDetailMenuItem.href}
isDisabled={nodeDetailMenuItem.isDisabled}
/>
<SectionLink
label={apmTracesMenuItem.label}
href={apmTracesMenuItem.href}
data-test-subj="viewApmTracesContextMenuItem"
isDisabled={apmTracesMenuItem.isDisabled}
/>
<SectionLink
label={uptimeMenuItem.label}
href={uptimeMenuItem.href}
isDisabled={uptimeMenuItem.isDisabled}
/>
</SectionLinks>
</Section>
</div>
</ActionMenu>
);
};
6 changes: 6 additions & 0 deletions x-pack/plugins/observability/kibana.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"id": "observability",
"version": "8.0.0",
"kibanaVersion": "kibana",
"ui": true
}
57 changes: 57 additions & 0 deletions x-pack/plugins/observability/public/components/action_menu.tsx
Original file line number Diff line number Diff line change
@@ -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;
* you may not use this file except in compliance with the Elastic License.
*/

import {
EuiPopover,
EuiText,
EuiListGroup,
EuiSpacer,
EuiHorizontalRule,
EuiListGroupItem,
EuiPopoverProps,
} from '@elastic/eui';

import React, { HTMLAttributes } from 'react';
import { EuiListGroupItemProps } from '@elastic/eui/src/components/list_group/list_group_item';

type Props = EuiPopoverProps & HTMLAttributes<HTMLDivElement>;

export const SectionTitle: React.FC<{}> = props => (
<>
<EuiText size={'s'} grow={false}>
<h5>{props.children}</h5>
</EuiText>
<EuiSpacer size={'s'} />
</>
);

export const SectionSubtitle: React.FC<{}> = props => (
<>
<EuiText size={'xs'} color={'subdued'} grow={false}>
<small>{props.children}</small>
</EuiText>
<EuiSpacer size={'s'} />
</>
);

export const SectionLinks: React.FC<{}> = props => (
<EuiListGroup flush={true} bordered={false}>
{props.children}
</EuiListGroup>
);

export const SectionSpacer: React.FC<{}> = () => <EuiSpacer size={'l'} />;

export const Section: React.FC<{}> = props => <>{props.children}</>;

export type SectionLinkProps = EuiListGroupItemProps;
export const SectionLink: React.FC<EuiListGroupItemProps> = props => (
<EuiListGroupItem style={{ padding: 0 }} size={'s'} {...props} />
);

export const ActionMenuDivider: React.FC<{}> = props => <EuiHorizontalRule margin={'s'} />;

export const ActionMenu: React.FC<Props> = props => <EuiPopover {...props} ownFocus={true} />;
16 changes: 16 additions & 0 deletions x-pack/plugins/observability/public/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

import { PluginInitializerContext, PluginInitializer } from 'kibana/public';
import { Plugin, ClientSetup, ClientStart } from './plugin';

export const plugin: PluginInitializer<ClientSetup, ClientStart> = (
context: PluginInitializerContext
) => {
return new Plugin(context);
};

export * from './components/action_menu';
15 changes: 15 additions & 0 deletions x-pack/plugins/observability/public/plugin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { Plugin as PluginClass, PluginInitializerContext } from 'kibana/public';

export type ClientSetup = void;
export type ClientStart = void;

export class Plugin implements PluginClass {
constructor(context: PluginInitializerContext) {}
start() {}
setup() {}
}
Loading

0 comments on commit 1b96001

Please sign in to comment.