diff --git a/packages/kbn-apm-config-loader/src/config.ts b/packages/kbn-apm-config-loader/src/config.ts index a611e205ec83..6e5a830d04b1 100644 --- a/packages/kbn-apm-config-loader/src/config.ts +++ b/packages/kbn-apm-config-loader/src/config.ts @@ -27,22 +27,27 @@ import { ApmAgentConfig } from './types'; const getDefaultConfig = (isDistributable: boolean): ApmAgentConfig => { // https://www.elastic.co/guide/en/apm/agent/nodejs/current/configuration.html + return { - active: process.env.ELASTIC_APM_ACTIVE || false, + active: process.env.ELASTIC_APM_ACTIVE === 'true' || false, environment: process.env.ELASTIC_APM_ENVIRONMENT || process.env.NODE_ENV || 'development', - serverUrl: 'https://b1e3b4b4233e44cdad468c127d0af8d8.apm.europe-west1.gcp.cloud.es.io:443', + serverUrl: 'https://38b80fbd79fb4c91bae06b4642d4d093.apm.us-east-1.aws.cloud.es.io', // The secretToken below is intended to be hardcoded in this file even though // it makes it public. This is not a security/privacy issue. Normally we'd // instead disable the need for a secretToken in the APM Server config where // the data is transmitted to, but due to how it's being hosted, it's easier, // for now, to simply leave it in. - secretToken: '2OyjjaI6RVkzx2O5CV', + secretToken: 'ZQHYvrmXEx04ozge8F', logUncaughtExceptions: true, globalLabels: {}, centralConfig: false, + metricsInterval: isDistributable ? '120s' : '30s', + transactionSampleRate: process.env.ELASTIC_APM_TRANSACTION_SAMPLE_RATE + ? parseFloat(process.env.ELASTIC_APM_TRANSACTION_SAMPLE_RATE) + : 1.0, // Can be performance intensive, disabling by default breakdownMetrics: isDistributable ? false : true, @@ -150,8 +155,9 @@ export class ApmConfiguration { globalLabels: { branch: process.env.ghprbSourceBranch || '', targetBranch: process.env.ghprbTargetBranch || '', - ciJobName: process.env.JOB_NAME || '', ciBuildNumber: process.env.BUILD_NUMBER || '', + isPr: process.env.GITHUB_PR_NUMBER ? true : false, + prId: process.env.GITHUB_PR_NUMBER || '', }, }; } diff --git a/vars/kibanaPipeline.groovy b/vars/kibanaPipeline.groovy index 005129370471..7991dd325215 100644 --- a/vars/kibanaPipeline.groovy +++ b/vars/kibanaPipeline.groovy @@ -89,6 +89,7 @@ def withFunctionalTestEnv(List additionalEnvs = [], Closure closure) { def esTransportPort = "61${parallelId}3" def fleetPackageRegistryPort = "61${parallelId}4" def alertingProxyPort = "61${parallelId}5" + def apmActive = githubPr.isPr() ? "false" : "true" withEnv([ "CI_GROUP=${parallelId}", @@ -101,7 +102,9 @@ def withFunctionalTestEnv(List additionalEnvs = [], Closure closure) { "TEST_ES_TRANSPORT_PORT=${esTransportPort}", "KBN_NP_PLUGINS_BUILT=true", "FLEET_PACKAGE_REGISTRY_PORT=${fleetPackageRegistryPort}", - "ALERTING_PROXY_PORT=${alertingProxyPort}" + "ALERTING_PROXY_PORT=${alertingProxyPort}", + "ELASTIC_APM_ACTIVE=${apmActive}", + "ELASTIC_APM_TRANSACTION_SAMPLE_RATE=0.1", ] + additionalEnvs) { closure() } diff --git a/x-pack/plugins/infra/common/http_api/metadata_api.ts b/x-pack/plugins/infra/common/http_api/metadata_api.ts index 5ee96b479be8..41b599c31041 100644 --- a/x-pack/plugins/infra/common/http_api/metadata_api.ts +++ b/x-pack/plugins/infra/common/http_api/metadata_api.ts @@ -29,10 +29,15 @@ export const InfraMetadataOSRT = rt.partial({ name: rt.string, platform: rt.string, version: rt.string, + build: rt.string, }); export const InfraMetadataHostRT = rt.partial({ name: rt.string, + hostname: rt.string, + id: rt.string, + ip: rt.array(rt.string), + mac: rt.array(rt.string), os: InfraMetadataOSRT, architecture: rt.string, containerized: rt.boolean, @@ -43,25 +48,40 @@ export const InfraMetadataInstanceRT = rt.partial({ name: rt.string, }); +export const InfraMetadataAccountRT = rt.partial({ + id: rt.string, + name: rt.string, +}); + export const InfraMetadataProjectRT = rt.partial({ id: rt.string, }); export const InfraMetadataMachineRT = rt.partial({ interface: rt.string, + type: rt.string, }); export const InfraMetadataCloudRT = rt.partial({ instance: InfraMetadataInstanceRT, provider: rt.string, + account: InfraMetadataAccountRT, availability_zone: rt.string, project: InfraMetadataProjectRT, machine: InfraMetadataMachineRT, + region: rt.string, +}); + +export const InfraMetadataAgentRT = rt.partial({ + id: rt.string, + version: rt.string, + policy: rt.string, }); export const InfraMetadataInfoRT = rt.partial({ cloud: InfraMetadataCloudRT, host: InfraMetadataHostRT, + agent: InfraMetadataAgentRT, }); const InfraMetadataRequiredRT = rt.type({ diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/overlay.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/overlay.tsx index 0943ced5e5be..be953ded70d7 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/overlay.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/overlay.tsx @@ -14,8 +14,11 @@ import { InventoryItemType } from '../../../../../../common/inventory_models/typ import { MetricsTab } from './tabs/metrics/metrics'; import { LogsTab } from './tabs/logs'; import { ProcessesTab } from './tabs/processes'; -import { PropertiesTab } from './tabs/properties'; +import { PropertiesTab } from './tabs/properties/index'; import { OVERLAY_Y_START, OVERLAY_BOTTOM_MARGIN, OVERLAY_HEADER_SIZE } from './tabs/shared'; +import { useLinkProps } from '../../../../../hooks/use_link_props'; +import { getNodeDetailUrl } from '../../../../link_to'; +import { findInventoryModel } from '../../../../../../common/inventory_models'; interface Props { isOpen: boolean; @@ -35,6 +38,8 @@ export const NodeContextPopover = ({ }: Props) => { // eslint-disable-next-line react-hooks/exhaustive-deps const tabConfigs = [MetricsTab, LogsTab, ProcessesTab, PropertiesTab]; + const inventoryModel = findInventoryModel(nodeType); + const nodeDetailFrom = currentTime - inventoryModel.metrics.defaultTimeRangeInSeconds * 1000; const tabs = useMemo(() => { return tabConfigs.map((m) => { @@ -50,6 +55,15 @@ export const NodeContextPopover = ({ const [selectedTab, setSelectedTab] = useState(0); + const nodeDetailMenuItemLinkProps = useLinkProps({ + ...getNodeDetailUrl({ + nodeType, + nodeId: node.id, + from: nodeDetailFrom, + to: currentTime, + }), + }); + if (!isOpen) { return null; } @@ -65,9 +79,28 @@ export const NodeContextPopover = ({ - - - + + + + + + + + + + + + diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/metrics/metrics.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/metrics/metrics.tsx index b5628b0a7c9b..789658c06040 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/metrics/metrics.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/metrics/metrics.tsx @@ -17,6 +17,7 @@ import { PointerEvent, } from '@elastic/charts'; import moment from 'moment'; +import { EuiLoadingChart } from '@elastic/eui'; import { TabContent, TabProps } from '../shared'; import { useSnapshot } from '../../../../hooks/use_snaphot'; import { useWaffleOptionsContext } from '../../../../hooks/use_waffle_options'; @@ -82,9 +83,9 @@ const TabComponent = (props: TabProps) => { } const buildCustomMetric = useCallback( - (field: string, id: string) => ({ + (field: string, id: string, aggregation: string = 'avg') => ({ type: 'custom' as SnapshotMetricType, - aggregation: 'avg', + aggregation, field, id, }), @@ -110,6 +111,7 @@ const TabComponent = (props: TabProps) => { buildCustomMetric('system.load.15', 'load15m'), buildCustomMetric('system.memory.actual.used.bytes', 'usedMemory'), buildCustomMetric('system.memory.actual.free', 'freeMemory'), + buildCustomMetric('system.cpu.cores', 'cores', 'max'), ], [], nodeType, @@ -223,6 +225,7 @@ const TabComponent = (props: TabProps) => { const load15mMetricsTs = useMemo(() => getTimeseries('load15m'), [getTimeseries]); const usedMemoryMetricsTs = useMemo(() => getTimeseries('usedMemory'), [getTimeseries]); const freeMemoryMetricsTs = useMemo(() => getTimeseries('freeMemory'), [getTimeseries]); + const coresMetricsTs = useMemo(() => getTimeseries('cores'), [getTimeseries]); useEffect(() => { reload(); @@ -239,7 +242,7 @@ const TabComponent = (props: TabProps) => { !usedMemoryMetricsTs || !freeMemoryMetricsTs ) { - return
; + return ; } const cpuChartMetrics = buildChartMetricLabels([SYSTEM_METRIC_NAME, USER_METRIC_NAME], 'avg'); @@ -253,6 +256,23 @@ const TabComponent = (props: TabProps) => { 'rate' ); + systemMetricsTs.rows = systemMetricsTs.rows.slice().map((r, idx) => { + const metric = r.metric_0 as number | undefined; + const cores = coresMetricsTs!.rows[idx].metric_0 as number | undefined; + if (metric && cores) { + r.metric_0 = metric / cores; + } + return r; + }); + + userMetricsTs.rows = userMetricsTs.rows.slice().map((r, idx) => { + const metric = r.metric_0 as number | undefined; + const cores = coresMetricsTs!.rows[idx].metric_0 as number | undefined; + if (metric && cores) { + r.metric_0 = metric / cores; + } + return r; + }); const cpuTimeseries = mergeTimeseries(systemMetricsTs, userMetricsTs); const networkTimeseries = mergeTimeseries(rxMetricsTs, txMetricsTs); const loadTimeseries = mergeTimeseries(load1mMetricsTs, load5mMetricsTs, load15mMetricsTs); @@ -467,6 +487,23 @@ const ChartContainer: React.FC = ({ children }) => (
); +const LoadingPlaceholder = () => { + return ( +
+ +
+ ); +}; + export const MetricsTab = { id: 'metrics', name: i18n.translate('xpack.infra.nodeDetails.tabs.metrics', { diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/properties.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/properties.tsx deleted file mode 100644 index 8157aca9b141..000000000000 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/properties.tsx +++ /dev/null @@ -1,21 +0,0 @@ -/* - * 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 React from 'react'; -import { i18n } from '@kbn/i18n'; -import { TabContent, TabProps } from './shared'; - -const TabComponent = (props: TabProps) => { - return Properties Placeholder; -}; - -export const PropertiesTab = { - id: 'properties', - name: i18n.translate('xpack.infra.nodeDetails.tabs.properties', { - defaultMessage: 'Properties', - }), - content: TabComponent, -}; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/properties/build_fields.ts b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/properties/build_fields.ts new file mode 100644 index 000000000000..79610ba3eef0 --- /dev/null +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/properties/build_fields.ts @@ -0,0 +1,116 @@ +/* + * 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 { InfraMetadata } from '../../../../../../../../common/http_api'; + +export const getFields = (metadata: InfraMetadata, group: 'cloud' | 'host' | 'agent') => { + switch (group) { + case 'host': + return prune([ + { + name: 'host.architecture', + value: metadata.info?.host?.architecture, + }, + { + name: 'host.hostname', + value: metadata.info?.host?.name, + }, + { + name: 'host.id', + value: metadata.info?.host?.id, + }, + { + name: 'host.ip', + value: metadata.info?.host?.ip, + }, + { + name: 'host.mac', + value: metadata.info?.host?.mac, + }, + { + name: 'host.name', + value: metadata.info?.host?.name, + }, + { + name: 'host.os.build', + value: metadata.info?.host?.os?.build, + }, + { + name: 'host.os.family', + value: metadata.info?.host?.os?.family, + }, + { + name: 'host.os.name', + value: metadata.info?.host?.os?.name, + }, + { + name: 'host.os.kernel', + value: metadata.info?.host?.os?.kernel, + }, + { + name: 'host.os.platform', + value: metadata.info?.host?.os?.platform, + }, + { + name: 'host.os.version', + value: metadata.info?.host?.os?.version, + }, + ]); + case 'cloud': + return prune([ + { + name: 'cloud.account.id', + value: metadata.info?.cloud?.account?.id, + }, + { + name: 'cloud.account.name', + value: metadata.info?.cloud?.account?.name, + }, + { + name: 'cloud.availability_zone', + value: metadata.info?.cloud?.availability_zone, + }, + { + name: 'cloud.instance.id', + value: metadata.info?.cloud?.instance?.id, + }, + { + name: 'cloud.instance.name', + value: metadata.info?.cloud?.instance?.name, + }, + { + name: 'cloud.machine.type', + value: metadata.info?.cloud?.machine?.type, + }, + { + name: 'cloud.provider', + value: metadata.info?.cloud?.provider, + }, + { + name: 'cloud.region', + value: metadata.info?.cloud?.region, + }, + ]); + case 'agent': + return prune([ + { + name: 'agent.id', + value: metadata.info?.agent?.id, + }, + { + name: 'agent.version', + value: metadata.info?.agent?.version, + }, + { + name: 'agent.policy', + value: metadata.info?.agent?.policy, + }, + ]); + } +}; + +const prune = (fields: Array<{ name: string; value: string | string[] | undefined }>) => + fields.filter((f) => !!f.value); diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/properties/index.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/properties/index.tsx new file mode 100644 index 000000000000..b901c3748438 --- /dev/null +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/properties/index.tsx @@ -0,0 +1,131 @@ +/* + * 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 React, { useCallback, useContext, useMemo } from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiLoadingChart } from '@elastic/eui'; +import { TabContent, TabProps } from '../shared'; +import { Source } from '../../../../../../../containers/source'; +import { findInventoryModel } from '../../../../../../../../common/inventory_models'; +import { InventoryItemType } from '../../../../../../../../common/inventory_models/types'; +import { useMetadata } from '../../../../../metric_detail/hooks/use_metadata'; +import { getFields } from './build_fields'; +import { useWaffleTimeContext } from '../../../../hooks/use_waffle_time'; +import { Table } from './table'; +import { euiStyled } from '../../../../../../../../../observability/public'; +import { useWaffleFiltersContext } from '../../../../hooks/use_waffle_filters'; + +const TabComponent = (props: TabProps) => { + const nodeId = props.node.id; + const nodeType = props.nodeType as InventoryItemType; + const inventoryModel = findInventoryModel(nodeType); + const { sourceId } = useContext(Source.Context); + const { currentTimeRange } = useWaffleTimeContext(); + const { applyFilterQuery } = useWaffleFiltersContext(); + const { loading: metadataLoading, metadata } = useMetadata( + nodeId, + nodeType, + inventoryModel.requiredMetrics, + sourceId, + currentTimeRange + ); + + const hostFields = useMemo(() => { + if (!metadata) return null; + return getFields(metadata, 'host'); + }, [metadata]); + + const cloudFields = useMemo(() => { + if (!metadata) return null; + return getFields(metadata, 'cloud'); + }, [metadata]); + + const agentFields = useMemo(() => { + if (!metadata) return null; + return getFields(metadata, 'agent'); + }, [metadata]); + + const onFilter = useCallback( + (item: { name: string; value: string }) => { + applyFilterQuery({ + kind: 'kuery', + expression: `${item.name}: "${item.value}"`, + }); + }, + [applyFilterQuery] + ); + + if (metadataLoading) { + return ; + } + + return ( + + {hostFields && hostFields.length > 0 && ( + + + + )} + {cloudFields && cloudFields.length > 0 && ( + +
+ + )} + {agentFields && agentFields.length > 0 && ( + +
+ + )} + + ); +}; + +const TableWrapper = euiStyled.div` + margin-bottom: 20px +`; + +const LoadingPlaceholder = () => { + return ( +
+ +
+ ); +}; + +export const PropertiesTab = { + id: 'properties', + name: i18n.translate('xpack.infra.nodeDetails.tabs.metadata.title', { + defaultMessage: 'Metadata', + }), + content: TabComponent, +}; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/properties/table.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/properties/table.tsx new file mode 100644 index 000000000000..c3e47b6084eb --- /dev/null +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/properties/table.tsx @@ -0,0 +1,158 @@ +/* + * 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 { EuiText } from '@elastic/eui'; +import { EuiToolTip } from '@elastic/eui'; +import { EuiButtonIcon } from '@elastic/eui'; +import { EuiFlexGroup } from '@elastic/eui'; +import { EuiFlexItem } from '@elastic/eui'; +import { EuiLink } from '@elastic/eui'; +import { EuiBasicTable } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { first } from 'lodash'; +import React, { useCallback, useMemo, useState } from 'react'; +import { FormattedMessage } from 'react-intl'; +import { euiStyled } from '../../../../../../../../../observability/public'; + +interface Row { + name: string; + value: string | string[] | undefined; +} + +interface Props { + rows: Row[]; + title: string; + onClick(item: Row): void; +} + +export const Table = (props: Props) => { + const { rows, title, onClick } = props; + const columns = useMemo( + () => [ + { + field: 'name', + name: '', + width: '35%', + sortable: false, + render: (name: string, item: Row) => ( + + {item.name} + + ), + }, + { + field: 'value', + name: '', + width: '65%', + sortable: false, + render: (_name: string, item: Row) => { + return ( + + + + + onClick(item)} + /> + + + {!Array.isArray(item.value) && item.value} + {Array.isArray(item.value) && } + + + + + ); + }, + }, + ], + [onClick] + ); + + return ( + <> + + +

{title}

+
+
+ + + ); +}; + +const TitleWrapper = euiStyled.div` + margin-bottom: 10px +`; + +class TableWithoutHeader extends EuiBasicTable { + renderTableHead() { + return <>; + } +} + +interface MoreProps { + values: string[]; +} +const ArrayValue = (props: MoreProps) => { + const { values } = props; + const [isExpanded, setIsExpanded] = useState(false); + const expand = useCallback(() => { + setIsExpanded(true); + }, []); + + const collapse = useCallback(() => { + setIsExpanded(false); + }, []); + + return ( + <> + {!isExpanded && ( + + + {first(values)} + {' ... '} + + + + + + + + )} + {isExpanded && ( +
+ {values.map((v) => ( +
{v}
+ ))} + + {i18n.translate('xpack.infra.nodeDetails.tabs.metadata.seeLess', { + defaultMessage: 'See less', + })} + +
+ )} + + ); +}; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/node.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/node.tsx index f2d9da960df8..03fb53898e31 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/node.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/node.tsx @@ -54,6 +54,7 @@ export const Node = class extends React.PureComponent { defaultMessage: '{nodeName}, click to open menu', values: { nodeName: node.name }, }); + return ( <> { } private togglePopover = () => { - this.setState((prevState) => ({ isPopoverOpen: !prevState.isPopoverOpen })); + const { nodeType } = this.props; + if (nodeType === 'host') { + this.toggleNewOverlay(); + } else { + this.setState((prevState) => ({ isPopoverOpen: !prevState.isPopoverOpen })); + } }; private toggleNewOverlay = () => { diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/node_context_menu.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/node_context_menu.tsx index 91c6ad801000..3179d4aa0526 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/node_context_menu.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/node_context_menu.tsx @@ -161,14 +161,6 @@ export const NodeContextMenu: React.FC = withTheme }, }; - const openNewOverlayMenuItem: SectionLinkProps = { - label: i18n.translate('xpack.infra.nodeContextMenu.openNewOverlay', { - defaultMessage: '**** [NEW] Overlay ***', - }), - style: { color: theme?.eui.euiLinkColor || '#006BB4', fontWeight: 500, padding: 0 }, - onClick: openNewOverlay, - }; - return ( <> = withTheme - diff --git a/x-pack/plugins/infra/server/routes/metadata/lib/get_node_info.ts b/x-pack/plugins/infra/server/routes/metadata/lib/get_node_info.ts index f1341c7ec810..b378b42e2ff5 100644 --- a/x-pack/plugins/infra/server/routes/metadata/lib/get_node_info.ts +++ b/x-pack/plugins/infra/server/routes/metadata/lib/get_node_info.ts @@ -58,7 +58,7 @@ export const getNodeInfo = async ( index: sourceConfiguration.metricAlias, body: { size: 1, - _source: ['host.*', 'cloud.*'], + _source: ['host.*', 'cloud.*', 'agent.*'], sort: [{ [timestampField]: 'desc' }], query: { bool: { diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/common_fields/common_processor_fields.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/common_fields/common_processor_fields.tsx index e66534ae1b25..706a2c47a348 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/common_fields/common_processor_fields.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/common_fields/common_processor_fields.tsx @@ -6,6 +6,7 @@ import React, { FunctionComponent } from 'react'; import { i18n } from '@kbn/i18n'; +import { PainlessLang } from '@kbn/monaco'; import { FieldConfig, @@ -56,6 +57,8 @@ const tagConfig: FieldConfig = { }; export const CommonProcessorFields: FunctionComponent = () => { + const suggestionProvider = PainlessLang.getSuggestionProvider('processor_conditional'); + return (
{ component={TextEditor} componentProps={{ editorProps: { - languageId: 'painless', + languageId: PainlessLang.ID, + suggestionProvider, height: EDITOR_PX_HEIGHT.extraSmall, options: { lineNumbers: 'off', diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/script.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/script.tsx index de28f6676660..8685738b3927 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/script.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/script.tsx @@ -4,12 +4,19 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useState } from 'react'; +import React, { useState, useEffect } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; +import { PainlessLang } from '@kbn/monaco'; import { EuiCode, EuiSwitch, EuiFormRow } from '@elastic/eui'; -import { FIELD_TYPES, fieldValidators, UseField, Field } from '../../../../../../shared_imports'; +import { + FIELD_TYPES, + fieldValidators, + UseField, + Field, + useFormData, +} from '../../../../../../shared_imports'; import { XJsonEditor, TextEditor } from '../field_components'; @@ -122,6 +129,17 @@ const fieldsConfig: FieldsConfig = { export const Script: FormFieldsComponent = ({ initialFieldValues }) => { const [showId, setShowId] = useState(() => !!initialFieldValues?.id); + const [scriptLanguage, setScriptLanguage] = useState('plaintext'); + + const [{ fields }] = useFormData({ watch: 'fields.lang' }); + + const suggestionProvider = PainlessLang.getSuggestionProvider('processor_conditional'); + + useEffect(() => { + const isPainlessLang = fields?.lang === 'painless' || fields?.lang === ''; // Scripting language defaults to painless if none specified + setScriptLanguage(isPainlessLang ? PainlessLang.ID : 'plaintext'); + }, [fields]); + return ( <> @@ -147,6 +165,9 @@ export const Script: FormFieldsComponent = ({ initialFieldValues }) => { component={TextEditor} componentProps={{ editorProps: { + languageId: scriptLanguage, + suggestionProvider: + scriptLanguage === PainlessLang.ID ? suggestionProvider : undefined, height: EDITOR_PX_HEIGHT.medium, 'aria-label': i18n.translate( 'xpack.ingestPipelines.pipelineEditor.scriptForm.sourceFieldAriaLabel', diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx index 4231f4b53997..f372c0c25b43 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx @@ -247,8 +247,7 @@ export function LayerPanel( const isFromTheSameGroup = isDraggedOperation(dragging) && dragging.groupId === group.groupId && - dragging.columnId !== accessor && - dragging.groupId !== 'y'; // TODO: remove this line when https://github.com/elastic/elastic-charts/issues/868 is fixed + dragging.columnId !== accessor; const isDroppable = isDraggedOperation(dragging) ? dragType === 'reorder' diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx index dbfffb5c2bd5..450918f1d13f 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx @@ -1229,6 +1229,24 @@ describe('IndexPatternDimensionEditorPanel', () => { ); }); + it('should not update when selecting the current field again', () => { + wrapper = mount(); + + const comboBox = wrapper + .find(EuiComboBox) + .filter('[data-test-subj="indexPattern-dimension-field"]'); + + const option = comboBox + .prop('options')![1] + .options!.find(({ label }) => label === 'timestampLabel')!; + + act(() => { + comboBox.prop('onChange')!([option]); + }); + + expect(setState).not.toHaveBeenCalled(); + }); + it('should show all operations that are not filtered out', () => { wrapper = mount( { return ( diff --git a/x-pack/plugins/lens/public/xy_visualization/color_assignment.ts b/x-pack/plugins/lens/public/xy_visualization/color_assignment.ts index e5764eaf0e8c..210101dc25c7 100644 --- a/x-pack/plugins/lens/public/xy_visualization/color_assignment.ts +++ b/x-pack/plugins/lens/public/xy_visualization/color_assignment.ts @@ -24,7 +24,7 @@ export type ColorAssignments = Record< string, { totalSeriesCount: number; - getRank(layer: LayerColorConfig, seriesKey: string, yAccessor: string): number; + getRank(sortedLayer: LayerColorConfig, seriesKey: string, yAccessor: string): number; } >; @@ -72,8 +72,8 @@ export function getColorAssignments( ); return { totalSeriesCount, - getRank(layer: LayerColorConfig, seriesKey: string, yAccessor: string) { - const layerIndex = paletteLayers.indexOf(layer); + getRank(sortedLayer: LayerColorConfig, seriesKey: string, yAccessor: string) { + const layerIndex = paletteLayers.findIndex((l) => sortedLayer.layerId === l.layerId); const currentSeriesPerLayer = seriesPerLayer[layerIndex]; const splitRank = currentSeriesPerLayer.splits.indexOf(seriesKey); return ( @@ -82,8 +82,10 @@ export function getColorAssignments( : seriesPerLayer .slice(0, layerIndex) .reduce((sum, perLayer) => sum + perLayer.numberOfSeries, 0)) + - (layer.splitAccessor && splitRank !== -1 ? splitRank * layer.accessors.length : 0) + - layer.accessors.indexOf(yAccessor) + (sortedLayer.splitAccessor && splitRank !== -1 + ? splitRank * sortedLayer.accessors.length + : 0) + + sortedLayer.accessors.indexOf(yAccessor) ); }, }; @@ -94,13 +96,12 @@ export function getAccessorColorConfig( colorAssignments: ColorAssignments, frame: FramePublicAPI, layer: LayerConfig, - sortedAccessors: string[], paletteService: PaletteRegistry ): AccessorConfig[] { const layerContainsSplits = Boolean(layer.splitAccessor); const currentPalette: PaletteOutput = layer.palette || { type: 'palette', name: 'default' }; const totalSeriesCount = colorAssignments[currentPalette.name].totalSeriesCount; - return sortedAccessors.map((accessor) => { + return layer.accessors.map((accessor) => { const currentYConfig = layer.yConfig?.find((yConfig) => yConfig.forAccessor === accessor); if (layerContainsSplits) { return { diff --git a/x-pack/plugins/lens/public/xy_visualization/visualization.test.ts b/x-pack/plugins/lens/public/xy_visualization/visualization.test.ts index d780ce85bad6..cab1a0185333 100644 --- a/x-pack/plugins/lens/public/xy_visualization/visualization.test.ts +++ b/x-pack/plugins/lens/public/xy_visualization/visualization.test.ts @@ -558,6 +558,30 @@ describe('xy_visualization', () => { const accessorConfig = breakdownConfig!.accessors[0]; expect(typeof accessorConfig !== 'string' && accessorConfig.palette).toEqual(customColors); }); + + it('should respect the order of accessors coming from datasource', () => { + mockDatasource.publicAPIMock.getTableSpec.mockReturnValue([ + { columnId: 'c' }, + { columnId: 'b' }, + ]); + const paletteGetter = jest.spyOn(paletteServiceMock, 'get'); + // overrite palette with a palette returning first blue, then green as color + paletteGetter.mockReturnValue({ + id: 'default', + title: '', + getColors: jest.fn(), + toExpression: jest.fn(), + getColor: jest.fn().mockReturnValueOnce('blue').mockReturnValueOnce('green'), + }); + + const yConfigs = callConfigForYConfigs({}); + expect(yConfigs?.accessors[0].columnId).toEqual('c'); + expect(yConfigs?.accessors[0].color).toEqual('blue'); + expect(yConfigs?.accessors[1].columnId).toEqual('b'); + expect(yConfigs?.accessors[1].color).toEqual('green'); + + paletteGetter.mockClear(); + }); }); }); diff --git a/x-pack/plugins/lens/public/xy_visualization/visualization.tsx b/x-pack/plugins/lens/public/xy_visualization/visualization.tsx index ebf80c61e0cd..e05871fd35a5 100644 --- a/x-pack/plugins/lens/public/xy_visualization/visualization.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/visualization.tsx @@ -187,8 +187,10 @@ export const getXyVisualization = ({ mappedAccessors = getAccessorColorConfig( colorAssignments, frame, - layer, - sortedAccessors, + { + ...layer, + accessors: sortedAccessors.filter((sorted) => layer.accessors.includes(sorted)), + }, paletteService ); } diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.tsx index cd8a5993d3ec..dc6ce285754f 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.tsx @@ -51,6 +51,7 @@ import { TooltipWrapper } from './tooltip_wrapper'; import { getAxesConfiguration } from './axes_configuration'; import { PalettePicker } from '../shared_components'; import { getAccessorColorConfig, getColorAssignments } from './color_assignment'; +import { getSortedAccessors } from './to_expression'; type UnwrapArray = T extends Array ? P : T; type AxesSettingsConfigKeys = keyof AxesSettingsConfig; @@ -579,6 +580,9 @@ const ColorPicker = ({ const currentColor = useMemo(() => { if (overwriteColor || !frame.activeData) return overwriteColor; + const datasource = frame.datasourceLayers[layer.layerId]; + const sortedAccessors: string[] = getSortedAccessors(datasource, layer); + const colorAssignments = getColorAssignments( state.layers, { tables: frame.activeData }, @@ -587,11 +591,14 @@ const ColorPicker = ({ const mappedAccessors = getAccessorColorConfig( colorAssignments, frame, - layer, - [accessor], + { + ...layer, + accessors: sortedAccessors.filter((sorted) => layer.accessors.includes(sorted)), + }, paletteService ); - return mappedAccessors[0].color; + + return mappedAccessors.find((a) => a.columnId === accessor)?.color || null; }, [overwriteColor, frame, paletteService, state.layers, accessor, formatFactory, layer]); const [color, setColor] = useState(currentColor); diff --git a/x-pack/test/api_integration/apis/metrics_ui/metadata.ts b/x-pack/test/api_integration/apis/metrics_ui/metadata.ts index 349b0dcbd9cf..e319e59045d2 100644 --- a/x-pack/test/api_integration/apis/metrics_ui/metadata.ts +++ b/x-pack/test/api_integration/apis/metrics_ui/metadata.ts @@ -109,6 +109,13 @@ export default function ({ getService }: FtrProviderContext) { machine: { type: 'n1-standard-4' }, project: { id: 'elastic-observability' }, }, + agent: { + hostname: 'gke-observability-8--observability-8--bc1afd95-f0zc', + id: 'c91c0d2b-6483-46bb-9731-f06afd32bb59', + ephemeral_id: '7cb259b1-795c-4c76-beaf-2eb8f18f5b02', + type: 'metricbeat', + version: '8.0.0', + }, host: { hostname: 'gke-observability-8--observability-8--bc1afd95-f0zc', os: { @@ -150,6 +157,13 @@ export default function ({ getService }: FtrProviderContext) { region: 'us-east-2', account: { id: '015351775590' }, }, + agent: { + hostname: 'ip-172-31-47-9.us-east-2.compute.internal', + id: 'd0943b36-d0d3-426d-892b-7d79c071b44b', + ephemeral_id: '64c94244-88b8-4a37-adc0-30428fefaf53', + type: 'metricbeat', + version: '8.0.0', + }, host: { hostname: 'ip-172-31-47-9.us-east-2.compute.internal', os: { @@ -197,6 +211,13 @@ export default function ({ getService }: FtrProviderContext) { id: 'elastic-observability', }, }, + agent: { + hostname: 'gke-observability-8--observability-8--bc1afd95-ngmh', + id: '66dc19e6-da36-49d2-9471-2c9475503178', + ephemeral_id: 'a0c3a9ff-470a-41a0-bf43-d1af6b7a3b5b', + type: 'metricbeat', + version: '8.0.0', + }, host: { hostname: 'gke-observability-8--observability-8--bc1afd95-ngmh', name: 'gke-observability-8--observability-8--bc1afd95-ngmh', @@ -244,6 +265,13 @@ export default function ({ getService }: FtrProviderContext) { id: 'elastic-observability', }, }, + agent: { + hostname: 'gke-observability-8--observability-8--bc1afd95-nhhw', + id: 'c58a514c-e971-4590-8206-385400e184dd', + ephemeral_id: 'e9d46cb0-2e89-469d-bd3b-6f32d7c96cc0', + type: 'metricbeat', + version: '8.0.0', + }, host: { hostname: 'gke-observability-8--observability-8--bc1afd95-nhhw', name: 'gke-observability-8--observability-8--bc1afd95-nhhw', diff --git a/x-pack/test/functional/apps/lens/drag_and_drop.ts b/x-pack/test/functional/apps/lens/drag_and_drop.ts index e0130bc39427..b85f36f9f525 100644 --- a/x-pack/test/functional/apps/lens/drag_and_drop.ts +++ b/x-pack/test/functional/apps/lens/drag_and_drop.ts @@ -60,7 +60,7 @@ export default function ({ getPageObjects }: FtrProviderContext) { ]); }); - it('should move the column to compatible dimension group', async () => { + it.skip('should move the column to compatible dimension group', async () => { await PageObjects.lens.switchToVisualization('bar'); expect(await PageObjects.lens.getDimensionTriggersTexts('lnsXY_xDimensionPanel')).to.eql([ 'Top values of @message.raw',