From de569adea7fa59fa17d1bea1188aa5bd51b11c5f Mon Sep 17 00:00:00 2001 From: Zacqary Xeper Date: Wed, 4 Nov 2020 11:35:38 -0600 Subject: [PATCH 01/12] Add process list APIs --- .../common/http_api/host_details/index.ts | 7 ++ .../http_api/host_details/process_list.ts | 20 ++++ x-pack/plugins/infra/common/http_api/index.ts | 1 + .../inventory_view/hooks/use_process_list.ts | 95 +++++++++++++++++++ x-pack/plugins/infra/server/infra_server.ts | 2 + .../server/lib/host_details/process_list.ts | 59 ++++++++++++ .../infra/server/routes/process_list/index.ts | 50 ++++++++++ .../server/utils/get_all_metrics_data.ts | 34 +++++++ 8 files changed, 268 insertions(+) create mode 100644 x-pack/plugins/infra/common/http_api/host_details/index.ts create mode 100644 x-pack/plugins/infra/common/http_api/host_details/process_list.ts create mode 100644 x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_process_list.ts create mode 100644 x-pack/plugins/infra/server/lib/host_details/process_list.ts create mode 100644 x-pack/plugins/infra/server/routes/process_list/index.ts create mode 100644 x-pack/plugins/infra/server/utils/get_all_metrics_data.ts diff --git a/x-pack/plugins/infra/common/http_api/host_details/index.ts b/x-pack/plugins/infra/common/http_api/host_details/index.ts new file mode 100644 index 0000000000000..b323ed8e9e327 --- /dev/null +++ b/x-pack/plugins/infra/common/http_api/host_details/index.ts @@ -0,0 +1,7 @@ +/* + * 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. + */ + +export * from './process_list'; diff --git a/x-pack/plugins/infra/common/http_api/host_details/process_list.ts b/x-pack/plugins/infra/common/http_api/host_details/process_list.ts new file mode 100644 index 0000000000000..4b4a0a54b9d13 --- /dev/null +++ b/x-pack/plugins/infra/common/http_api/host_details/process_list.ts @@ -0,0 +1,20 @@ +/* + * 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 * as rt from 'io-ts'; +import { MetricsAPITimerangeRT, MetricsAPISeriesRT } from '../metrics_api'; + +export const ProcessListAPIRequestRT = rt.type({ + hostTerm: rt.record(rt.string, rt.string), + timerange: MetricsAPITimerangeRT, + indexPattern: rt.string, +}); + +export const ProcessListAPIResponseRT = rt.array(MetricsAPISeriesRT); + +export type ProcessListAPIRequest = rt.TypeOf; + +export type ProcessListAPIResponse = rt.TypeOf; diff --git a/x-pack/plugins/infra/common/http_api/index.ts b/x-pack/plugins/infra/common/http_api/index.ts index 4c729d11ba8c1..914011454a732 100644 --- a/x-pack/plugins/infra/common/http_api/index.ts +++ b/x-pack/plugins/infra/common/http_api/index.ts @@ -11,3 +11,4 @@ export * from './metrics_explorer'; export * from './metrics_api'; export * from './log_alerts'; export * from './snapshot_api'; +export * from './host_details'; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_process_list.ts b/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_process_list.ts new file mode 100644 index 0000000000000..862337cc8d11a --- /dev/null +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_process_list.ts @@ -0,0 +1,95 @@ +/* + * 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 { fold } from 'fp-ts/lib/Either'; +import { identity } from 'fp-ts/lib/function'; +import { pipe } from 'fp-ts/lib/pipeable'; +import { useEffect } from 'react'; +import { throwErrors, createPlainError } from '../../../../../common/runtime_types'; +import { useHTTPRequest } from '../../../../hooks/use_http_request'; +import { + InventoryMetaResponseRT, + InventoryMetaResponse, +} from '../../../../../common/http_api/inventory_meta_api'; + +export function useProcessList(hostname: string, timefield: string) { + const decodeResponse = (response: any) => { + return pipe( + InventoryMetaResponseRT.decode(response), + fold(throwErrors(createPlainError), identity) + ); + }; + + const { error, loading, response, makeRequest } = useHTTPRequest( + '/api/infra/metrics_api', + 'POST', + generateRequest(hostname, timefield), + decodeResponse + ); + + useEffect(() => { + makeRequest(); + }, [makeRequest]); + + return { + error, + loading, + accounts: response ? response.accounts : [], + regions: response ? response.regions : [], + makeRequest, + }; +} + +const generateRequest = (hostname: string, timefield: string = '@timestamp') => { + const to = Date.now(); + const from = to - 15 * 60 * 1000; // 15 minutes + return JSON.stringify({ + timerange: { + field: timefield, + from, + to, + interval: 'modules', + }, + modules: ['system.cpu', 'system.memory'], + groupBy: ['system.process.cmdline'], + filter: [{ term: { 'host.name': hostname } }], + indexPattern: 'metricbeat-*', + limit: 9, + metrics: [ + { + id: 'cpu', + aggregations: { + cpu: { + avg: { + field: 'system.process.cpu.total.norm.pct', + }, + }, + }, + }, + { + id: 'memory', + aggregations: { + memory: { + avg: { + field: 'system.process.memory.rss.pct', + }, + }, + }, + }, + { + id: 'meta', + aggregations: { + meta: { + top_hits: { + size: 1, + sort: [{ '@timestamp': { order: 'desc' } }], + _source: ['system.process.cpu.start_time', 'system.process.state'], + }, + }, + }, + }, + ], + }); +}; diff --git a/x-pack/plugins/infra/server/infra_server.ts b/x-pack/plugins/infra/server/infra_server.ts index 49fe55e3dee01..2bf5687da7e08 100644 --- a/x-pack/plugins/infra/server/infra_server.ts +++ b/x-pack/plugins/infra/server/infra_server.ts @@ -41,6 +41,7 @@ import { initLogSourceConfigurationRoutes, initLogSourceStatusRoutes } from './r import { initSourceRoute } from './routes/source'; import { initAlertPreviewRoute } from './routes/alerting'; import { initGetLogAlertsChartPreviewDataRoute } from './routes/log_alerts'; +import { initProcessListRoute } from './routes/process_list'; export const initInfraServer = (libs: InfraBackendLibs) => { const schema = makeExecutableSchema({ @@ -82,4 +83,5 @@ export const initInfraServer = (libs: InfraBackendLibs) => { initLogSourceStatusRoutes(libs); initAlertPreviewRoute(libs); initGetLogAlertsChartPreviewDataRoute(libs); + initProcessListRoute(libs); }; diff --git a/x-pack/plugins/infra/server/lib/host_details/process_list.ts b/x-pack/plugins/infra/server/lib/host_details/process_list.ts new file mode 100644 index 0000000000000..aebce5a51b313 --- /dev/null +++ b/x-pack/plugins/infra/server/lib/host_details/process_list.ts @@ -0,0 +1,59 @@ +/* + * 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 { ProcessListAPIRequest, MetricsAPIRequest } from '../../../common/http_api'; +import { getAllMetricsData } from '../../utils/get_all_metrics_data'; +import { query } from '../metrics'; +import { ESSearchClient } from '../metrics/types'; + +export const getProcessList = async ( + client: ESSearchClient, + { hostTerm, timerange, indexPattern }: ProcessListAPIRequest +) => { + const queryBody = { + timerange, + modules: ['system.cpu', 'system.memory'], + groupBy: ['system.process.cmdline'], + filter: [{ term: hostTerm }], + indexPattern, + limit: 9, + metrics: [ + { + id: 'cpu', + aggregations: { + cpu: { + avg: { + field: 'system.process.cpu.total.norm.pct', + }, + }, + }, + }, + { + id: 'memory', + aggregations: { + memory: { + avg: { + field: 'system.process.memory.rss.pct', + }, + }, + }, + }, + { + id: 'meta', + aggregations: { + meta: { + top_hits: { + size: 1, + sort: [{ [timerange.field]: { order: 'desc' } }], + _source: ['system.process.cpu.start_time', 'system.process.state'], + }, + }, + }, + }, + ], + } as MetricsAPIRequest; + return await getAllMetricsData((body: MetricsAPIRequest) => query(client, body), queryBody); +}; diff --git a/x-pack/plugins/infra/server/routes/process_list/index.ts b/x-pack/plugins/infra/server/routes/process_list/index.ts new file mode 100644 index 0000000000000..9851613255d8d --- /dev/null +++ b/x-pack/plugins/infra/server/routes/process_list/index.ts @@ -0,0 +1,50 @@ +/* + * 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 Boom from '@hapi/boom'; +import { pipe } from 'fp-ts/lib/pipeable'; +import { fold } from 'fp-ts/lib/Either'; +import { identity } from 'fp-ts/lib/function'; +import { schema } from '@kbn/config-schema'; +import { InfraBackendLibs } from '../../lib/infra_types'; +import { throwErrors } from '../../../common/runtime_types'; +import { createSearchClient } from '../../lib/create_search_client'; +import { getProcessList } from '../../lib/host_details/process_list'; +import { ProcessListAPIRequestRT, ProcessListAPIResponseRT } from '../../../common/http_api'; + +const escapeHatch = schema.object({}, { unknowns: 'allow' }); + +export const initProcessListRoute = (libs: InfraBackendLibs) => { + const { framework } = libs; + framework.registerRoute( + { + method: 'post', + path: '/api/metrics/process_list', + validate: { + body: escapeHatch, + }, + }, + async (requestContext, request, response) => { + try { + const options = pipe( + ProcessListAPIRequestRT.decode(request.body), + fold(throwErrors(Boom.badRequest), identity) + ); + + const client = createSearchClient(requestContext, framework); + const processListResponse = await getProcessList(client, options); + + return response.ok({ + body: ProcessListAPIResponseRT.encode(processListResponse), + }); + } catch (error) { + return response.internalError({ + body: error.message, + }); + } + } + ); +}; diff --git a/x-pack/plugins/infra/server/utils/get_all_metrics_data.ts b/x-pack/plugins/infra/server/utils/get_all_metrics_data.ts new file mode 100644 index 0000000000000..cec58494d1b98 --- /dev/null +++ b/x-pack/plugins/infra/server/utils/get_all_metrics_data.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { MetricsAPIResponse, MetricsAPISeries } from '../../common/http_api/metrics_api'; + +export const getAllMetricsData = async ( + query: (options: Options) => Promise, + options: Options, + previousBuckets: MetricsAPISeries[] = [] +): Promise => { + const response = await query(options); + + // Nothing available, return the previous buckets. + if (response.series.length === 0) { + return previousBuckets; + } + + const currentBuckets = response.series; + + // if there are no currentBuckets then we are finished paginating through the results + if (!response.info.afterKey) { + return previousBuckets.concat(currentBuckets); + } + + // There is possibly more data, concat previous and current buckets and call ourselves recursively. + const newOptions = { + ...options, + afterKey: response.info.afterKey, + }; + return getAllMetricsData(query, newOptions, previousBuckets.concat(currentBuckets)); +}; From 6218a8a5b6b8ffa983d50d7b9bd7168d2860f4e8 Mon Sep 17 00:00:00 2001 From: Zacqary Xeper Date: Fri, 6 Nov 2020 12:51:59 -0600 Subject: [PATCH 02/12] Fix useProcessList hook --- .../inventory_view/hooks/use_process_list.ts | 86 +++++-------------- 1 file changed, 23 insertions(+), 63 deletions(-) diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_process_list.ts b/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_process_list.ts index 862337cc8d11a..a1437deb1707d 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_process_list.ts +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_process_list.ts @@ -7,25 +7,38 @@ import { fold } from 'fp-ts/lib/Either'; import { identity } from 'fp-ts/lib/function'; import { pipe } from 'fp-ts/lib/pipeable'; import { useEffect } from 'react'; +import { ProcessListAPIResponse, ProcessListAPIResponseRT } from '../../../../../common/http_api'; import { throwErrors, createPlainError } from '../../../../../common/runtime_types'; import { useHTTPRequest } from '../../../../hooks/use_http_request'; -import { - InventoryMetaResponseRT, - InventoryMetaResponse, -} from '../../../../../common/http_api/inventory_meta_api'; -export function useProcessList(hostname: string, timefield: string) { +export function useProcessList( + hostTerm: string, + indexPattern: string, + timefield: string, + to: number +) { const decodeResponse = (response: any) => { return pipe( - InventoryMetaResponseRT.decode(response), + ProcessListAPIResponseRT.decode(response), fold(throwErrors(createPlainError), identity) ); }; - const { error, loading, response, makeRequest } = useHTTPRequest( - '/api/infra/metrics_api', + const timerange = { + timefield, + interval: 'modules', + to, + from: to - 15 * 60 * 1000, // 15 minutes + }; + + const { error, loading, response, makeRequest } = useHTTPRequest( + '/api/metrics/process_list', 'POST', - generateRequest(hostname, timefield), + JSON.stringify({ + hostTerm, + timerange, + indexPattern, + }), decodeResponse ); @@ -36,60 +49,7 @@ export function useProcessList(hostname: string, timefield: string) { return { error, loading, - accounts: response ? response.accounts : [], - regions: response ? response.regions : [], + response, makeRequest, }; } - -const generateRequest = (hostname: string, timefield: string = '@timestamp') => { - const to = Date.now(); - const from = to - 15 * 60 * 1000; // 15 minutes - return JSON.stringify({ - timerange: { - field: timefield, - from, - to, - interval: 'modules', - }, - modules: ['system.cpu', 'system.memory'], - groupBy: ['system.process.cmdline'], - filter: [{ term: { 'host.name': hostname } }], - indexPattern: 'metricbeat-*', - limit: 9, - metrics: [ - { - id: 'cpu', - aggregations: { - cpu: { - avg: { - field: 'system.process.cpu.total.norm.pct', - }, - }, - }, - }, - { - id: 'memory', - aggregations: { - memory: { - avg: { - field: 'system.process.memory.rss.pct', - }, - }, - }, - }, - { - id: 'meta', - aggregations: { - meta: { - top_hits: { - size: 1, - sort: [{ '@timestamp': { order: 'desc' } }], - _source: ['system.process.cpu.start_time', 'system.process.state'], - }, - }, - }, - }, - ], - }); -}; From f773c7848fe1434b87e78e5709f649b348d26bd1 Mon Sep 17 00:00:00 2001 From: Zacqary Xeper Date: Mon, 9 Nov 2020 16:37:36 -0600 Subject: [PATCH 03/12] Initial UI --- .../node_details/tabs/processes.tsx | 225 +++++++++++++++++- .../inventory_view/hooks/use_process_list.ts | 4 +- 2 files changed, 225 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes.tsx index 94ba1150c20dd..2534e917c929e 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes.tsx @@ -4,14 +4,235 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; +import React, { useMemo } from 'react'; import { i18n } from '@kbn/i18n'; +import { + EuiBasicTable, + EuiTable, + EuiTableHeader, + EuiTableBody, + EuiTableFooter, + EuiTableHeaderCell, + EuiTableRow, + EuiTableRowCell, + EuiButtonEmpty, + EuiBadge, + EuiCode, +} from '@elastic/eui'; +import { useProcessList } from '../../../hooks/use_process_list'; import { TabContent, TabProps } from './shared'; -const TabComponent = (props: TabProps) => { +const ONE_MINUTE = 60 * 1000; +const ONE_HOUR = ONE_MINUTE * 60; + +const columns = [ + { + field: 'state', + name: i18n.translate('xpack.infra.metrics.nodeDetails.processes.columnLabelState', { + defaultMessage: 'State', + }), + sortable: true, + render: (state: string) => , + width: 84, + }, + { + field: 'command', + name: i18n.translate('xpack.infra.metrics.nodeDetails.processes.columnLabelCommand', { + defaultMessage: 'Command', + }), + sortable: true, + truncateText: true, + width: '40%', + render: (command: string) => {command.slice(0, 16)}, + }, + { + field: 'runtime', + name: i18n.translate('xpack.infra.metrics.nodeDetails.processes.columnLabelTime', { + defaultMessage: 'Time', + }), + rightAlign: true, + }, + { + field: 'cpu', + name: i18n.translate('xpack.infra.metrics.nodeDetails.processes.columnLabelCPU', { + defaultMessage: 'CPU', + }), + }, + { + field: 'memory', + name: i18n.translate('xpack.infra.metrics.nodeDetails.processes.columnLabelMemory', { + defaultMessage: 'Mem.', + }), + }, +]; + +const TabComponent = ({ currentTime, node, nodeType, options }: TabProps) => { + const hostTerm = useMemo(() => { + const field = options.fields[nodeType]; + return { [field]: node.name }; + }, [options, node, nodeType]); + + const { loading, error, response } = useProcessList( + hostTerm, + 'metricbeat-*', + options.fields?.timestamp, + currentTime + ); + + if (!loading && response) { + return ( + + + + ); + } + return Processes Placeholder; }; +const ProcessesTable = ({ processList, currentTime }) => { + const items = processList.map((process) => { + const command = process.id; + let mostRecentPoint; + for (let i = process.rows.length - 1; i >= 0; i--) { + const point = process.rows[i]; + if (point.meta?.length) { + mostRecentPoint = point; + break; + } + } + + const { cpu, memory } = mostRecentPoint; + const { system } = mostRecentPoint.meta[0]; + const startTime = system.process.cpu.start_time; + const state = system.process.state; + + const runtimeLength = currentTime - Date.parse(startTime); + let remainingRuntimeMS = runtimeLength; + const runtimeHours = Math.floor(remainingRuntimeMS / ONE_HOUR); + remainingRuntimeMS -= runtimeHours * ONE_HOUR; + const runtimeMinutes = Math.floor(remainingRuntimeMS / ONE_MINUTE); + remainingRuntimeMS -= runtimeMinutes * ONE_MINUTE; + const runtimeSeconds = Math.floor(remainingRuntimeMS / 1000); + remainingRuntimeMS -= runtimeSeconds * 1000; + + const runtimeDisplayHours = runtimeHours ? `${runtimeHours}:` : ''; + const runtimeDisplayMinutes = + runtimeHours && runtimeMinutes < 10 ? `0${runtimeMinutes}:` : `${runtimeMinutes}:`; + const runtimeDisplaySeconds = runtimeSeconds < 10 ? `0${runtimeSeconds}` : runtimeSeconds; + const runtimeDisplayMS = !runtimeDisplayHours ? `.${remainingRuntimeMS}` : ''; + + const runtime = `${runtimeDisplayHours}${runtimeDisplayMinutes}${runtimeDisplaySeconds}${runtimeDisplayMS}`; + + return { + state, + command, + runtime, + cpu, + memory, + }; + }); + + return ( + + + + {columns.map((column) => ( + + {column.name} + + ))} + + + {items.map((item, i) => { + const cells = columns.map((column) => ( + + {column.render ? column.render(item[column.field]) : item[column.field]} + + )); + return ( + + + + + {cells} + + ); + })} + + + ); +}; + +const StateBadge = ({ state }) => { + switch (state) { + case 'running': + return ( + + {i18n.translate('xpack.infra.metrics.nodeDetails.processes.stateRunning', { + defaultMessage: 'Running', + })} + + ); + case 'sleeping': + return ( + + {i18n.translate('xpack.infra.metrics.nodeDetails.processes.stateSleeping', { + defaultMessage: 'Sleeping', + })} + + ); + case 'dead': + return ( + + {i18n.translate('xpack.infra.metrics.nodeDetails.processes.stateDead', { + defaultMessage: 'Dead', + })} + + ); + case 'stopped': + return ( + + {i18n.translate('xpack.infra.metrics.nodeDetails.processes.stateStopped', { + defaultMessage: 'Stopped', + })} + + ); + case 'idle': + return ( + + {i18n.translate('xpack.infra.metrics.nodeDetails.processes.stateIdle', { + defaultMessage: 'Idle', + })} + + ); + case 'zombie': + return ( + + {i18n.translate('xpack.infra.metrics.nodeDetails.processes.stateZombie', { + defaultMessage: 'Zombie', + })} + + ); + default: + return ( + + {i18n.translate('xpack.infra.metrics.nodeDetails.processes.stateUnknown', { + defaultMessage: 'Unknown', + })} + + ); + } +}; + export const ProcessesTab = { id: 'processes', name: i18n.translate('xpack.infra.nodeDetails.tabs.processes', { diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_process_list.ts b/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_process_list.ts index a1437deb1707d..8e0843fe8b278 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_process_list.ts +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_process_list.ts @@ -12,7 +12,7 @@ import { throwErrors, createPlainError } from '../../../../../common/runtime_typ import { useHTTPRequest } from '../../../../hooks/use_http_request'; export function useProcessList( - hostTerm: string, + hostTerm: Record, indexPattern: string, timefield: string, to: number @@ -25,7 +25,7 @@ export function useProcessList( }; const timerange = { - timefield, + field: timefield, interval: 'modules', to, from: to - 15 * 60 * 1000, // 15 minutes From 1c2f85f836921a21de3a11ccfc07b327eddeec61 Mon Sep 17 00:00:00 2001 From: Zacqary Xeper Date: Mon, 16 Nov 2020 10:49:59 -0600 Subject: [PATCH 04/12] Format process table --- .../node_details/tabs/processes.tsx | 273 +++++++++++------- 1 file changed, 170 insertions(+), 103 deletions(-) diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes.tsx index 2534e917c929e..01800b5f1ca31 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes.tsx @@ -7,24 +7,59 @@ import React, { useMemo } from 'react'; import { i18n } from '@kbn/i18n'; import { - EuiBasicTable, EuiTable, EuiTableHeader, EuiTableBody, - EuiTableFooter, EuiTableHeaderCell, EuiTableRow, EuiTableRowCell, + EuiSpacer, + EuiTablePagination, EuiButtonEmpty, EuiBadge, EuiCode, + EuiLoadingChart, } from '@elastic/eui'; +import { FORMATTERS } from '../../../../../../../common/formatters'; +import { euiStyled } from '../../../../../../../../observability/public'; import { useProcessList } from '../../../hooks/use_process_list'; import { TabContent, TabProps } from './shared'; const ONE_MINUTE = 60 * 1000; const ONE_HOUR = ONE_MINUTE * 60; +const TabComponent = ({ currentTime, node, nodeType, options }: TabProps) => { + const hostTerm = useMemo(() => { + const field = options.fields[nodeType]; + return { [field]: node.name }; + }, [options, node, nodeType]); + + const { loading, error, response } = useProcessList( + hostTerm, + 'metricbeat-*', + options.fields?.timestamp, + currentTime + ); + + return ( + + + + ); +}; + +export const ProcessesTab = { + id: 'processes', + name: i18n.translate('xpack.infra.nodeDetails.tabs.processes', { + defaultMessage: 'Processes', + }), + content: TabComponent, +}; + const columns = [ { field: 'state', @@ -43,7 +78,7 @@ const columns = [ sortable: true, truncateText: true, width: '40%', - render: (command: string) => {command.slice(0, 16)}, + render: (command: string) => {command}, }, { field: 'runtime', @@ -57,118 +92,136 @@ const columns = [ name: i18n.translate('xpack.infra.metrics.nodeDetails.processes.columnLabelCPU', { defaultMessage: 'CPU', }), + render: (value: number) => FORMATTERS.percent(value), }, { field: 'memory', name: i18n.translate('xpack.infra.metrics.nodeDetails.processes.columnLabelMemory', { defaultMessage: 'Mem.', }), + render: (value: number) => FORMATTERS.percent(value), }, ]; -const TabComponent = ({ currentTime, node, nodeType, options }: TabProps) => { - const hostTerm = useMemo(() => { - const field = options.fields[nodeType]; - return { [field]: node.name }; - }, [options, node, nodeType]); +const ProcessesTable = ({ processList, currentTime, isLoading }) => { + const body = !isLoading ? ( + + ) : ( + + ); + return ( + <> + + + + {columns.map((column) => ( + + {column.name} + + ))} + + {body} + + + + + ); +}; - const { loading, error, response } = useProcessList( - hostTerm, - 'metricbeat-*', - options.fields?.timestamp, - currentTime +const LoadingTableBody = () => { + return ( +
+ +
); +}; - if (!loading && response) { - return ( - - - - ); - } +const ProcessesTableBody = ({ processList, currentTime }) => { + const items = useMemo( + () => + processList.map((process) => { + const command = process.id; + let mostRecentPoint; + for (let i = process.rows.length - 1; i >= 0; i--) { + const point = process.rows[i]; + if (point.meta?.length) { + mostRecentPoint = point; + break; + } + } - return Processes Placeholder; -}; + const { cpu, memory } = mostRecentPoint; + const { system } = mostRecentPoint.meta[0]; + const startTime = system.process.cpu.start_time; + const state = system.process.state; + + const runtimeLength = currentTime - Date.parse(startTime); + let remainingRuntimeMS = runtimeLength; + const runtimeHours = Math.floor(remainingRuntimeMS / ONE_HOUR); + remainingRuntimeMS -= runtimeHours * ONE_HOUR; + const runtimeMinutes = Math.floor(remainingRuntimeMS / ONE_MINUTE); + remainingRuntimeMS -= runtimeMinutes * ONE_MINUTE; + const runtimeSeconds = Math.floor(remainingRuntimeMS / 1000); + remainingRuntimeMS -= runtimeSeconds * 1000; + + const runtimeDisplayHours = runtimeHours ? `${runtimeHours}:` : ''; + const runtimeDisplayMinutes = + runtimeHours && runtimeMinutes < 10 ? `0${runtimeMinutes}:` : `${runtimeMinutes}:`; + const runtimeDisplaySeconds = runtimeSeconds < 10 ? `0${runtimeSeconds}` : runtimeSeconds; + const runtimeDisplayMS = !runtimeDisplayHours ? `.${remainingRuntimeMS}` : ''; -const ProcessesTable = ({ processList, currentTime }) => { - const items = processList.map((process) => { - const command = process.id; - let mostRecentPoint; - for (let i = process.rows.length - 1; i >= 0; i--) { - const point = process.rows[i]; - if (point.meta?.length) { - mostRecentPoint = point; - break; - } - } - - const { cpu, memory } = mostRecentPoint; - const { system } = mostRecentPoint.meta[0]; - const startTime = system.process.cpu.start_time; - const state = system.process.state; - - const runtimeLength = currentTime - Date.parse(startTime); - let remainingRuntimeMS = runtimeLength; - const runtimeHours = Math.floor(remainingRuntimeMS / ONE_HOUR); - remainingRuntimeMS -= runtimeHours * ONE_HOUR; - const runtimeMinutes = Math.floor(remainingRuntimeMS / ONE_MINUTE); - remainingRuntimeMS -= runtimeMinutes * ONE_MINUTE; - const runtimeSeconds = Math.floor(remainingRuntimeMS / 1000); - remainingRuntimeMS -= runtimeSeconds * 1000; - - const runtimeDisplayHours = runtimeHours ? `${runtimeHours}:` : ''; - const runtimeDisplayMinutes = - runtimeHours && runtimeMinutes < 10 ? `0${runtimeMinutes}:` : `${runtimeMinutes}:`; - const runtimeDisplaySeconds = runtimeSeconds < 10 ? `0${runtimeSeconds}` : runtimeSeconds; - const runtimeDisplayMS = !runtimeDisplayHours ? `.${remainingRuntimeMS}` : ''; - - const runtime = `${runtimeDisplayHours}${runtimeDisplayMinutes}${runtimeDisplaySeconds}${runtimeDisplayMS}`; - - return { - state, - command, - runtime, - cpu, - memory, - }; - }); + const runtime = `${runtimeDisplayHours}${runtimeDisplayMinutes}${runtimeDisplaySeconds}${runtimeDisplayMS}`; + + return { + state, + command, + runtime, + cpu, + memory, + }; + }), + [processList, currentTime] + ); return ( - - - - {columns.map((column) => ( - + {items.map((item, i) => { + const cells = columns.map((column) => ( + - {column.name} - - ))} - - - {items.map((item, i) => { - const cells = columns.map((column) => ( - - {column.render ? column.render(item[column.field]) : item[column.field]} + {column.render ? column.render(item[column.field]) : item[column.field]} + + )); + return ( + + + - )); - return ( - - - - - {cells} - - ); - })} - - + {cells} + + ); + })} + ); }; @@ -233,10 +286,24 @@ const StateBadge = ({ state }) => { } }; -export const ProcessesTab = { - id: 'processes', - name: i18n.translate('xpack.infra.nodeDetails.tabs.processes', { - defaultMessage: 'Processes', - }), - content: TabComponent, -}; +const ProcessListTabContent = euiStyled(TabContent)` + padding-left: 0; + padding-right: 0; +`; + +const StyledTableBody = euiStyled(EuiTableBody)` + & .euiTableCellContent { + padding-top: 0; + padding-bottom: 0; + + } +`; + +const CodeLine = euiStyled(EuiCode).attrs({ transparentBackground: true })` + text-overflow: ellipsis; + overflow: hidden; + & code.euiCodeBlock__code { + white-space: nowrap !important; + vertical-align: middle; + } +`; From 4a6553588854ba1b9ff60fabab3ea58a8ab2ca06 Mon Sep 17 00:00:00 2001 From: Zacqary Xeper Date: Wed, 11 Nov 2020 13:17:23 -0600 Subject: [PATCH 05/12] Add sorting --- .../node_details/tabs/processes/index.tsx | 81 +++++ .../tabs/processes/parse_process_list.ts | 27 ++ .../tabs/processes/processes_table.tsx | 298 ++++++++++++++++++ .../tabs/processes/state_badge.tsx | 28 ++ .../node_details/tabs/processes/states.ts | 33 ++ .../tabs/processes/summary_table.tsx | 65 ++++ 6 files changed, 532 insertions(+) create mode 100644 x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/index.tsx create mode 100644 x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/parse_process_list.ts create mode 100644 x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/processes_table.tsx create mode 100644 x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/state_badge.tsx create mode 100644 x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/states.ts create mode 100644 x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/summary_table.tsx diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/index.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/index.tsx new file mode 100644 index 0000000000000..44cbd7c7411d5 --- /dev/null +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/index.tsx @@ -0,0 +1,81 @@ +/* + * 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, { useMemo, useState } from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiSearchBar, EuiSpacer, Query } from '@elastic/eui'; +import { euiStyled } from '../../../../../../../../../observability/public'; +import { useProcessList } from '../../../../hooks/use_process_list'; +import { TabContent, TabProps } from '../shared'; +import { STATE_NAMES } from './states'; +import { SummaryTable } from './summary_table'; +import { ProcessesTable } from './processes_table'; + +const TabComponent = ({ currentTime, node, nodeType, options }: TabProps) => { + const [searchFilter, setSearchFilter] = useState(EuiSearchBar.Query.MATCH_ALL); + + const hostTerm = useMemo(() => { + const field = options.fields[nodeType]; + return { [field]: node.name }; + }, [options, node, nodeType]); + + const { loading, error, response } = useProcessList( + hostTerm, + 'metricbeat-*', + options.fields?.timestamp, + currentTime + ); + + return ( + + + + setSearchFilter(query ?? EuiSearchBar.Query.MATCH_ALL)} + box={{ + incremental: true, + placeholder: i18n.translate('xpack.infra.metrics.nodeDetails.searchForProcesses', { + defaultMessage: 'Search for processes…', + }), + }} + filters={[ + { + type: 'field_value_selection', + field: 'state', + name: 'State', + operator: 'exact', + multiSelect: false, + options: Object.entries(STATE_NAMES).map(([value, view]: [string, string]) => ({ + value, + view, + })), + }, + ]} + /> + + + + ); +}; + +export const ProcessesTab = { + id: 'processes', + name: i18n.translate('xpack.infra.metrics.nodeDetails.tabs.processes', { + defaultMessage: 'Processes', + }), + content: TabComponent, +}; + +const ProcessListTabContent = euiStyled(TabContent)` + padding-left: 0; + padding-right: 0; +`; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/parse_process_list.ts b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/parse_process_list.ts new file mode 100644 index 0000000000000..dd42b0dbc8efb --- /dev/null +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/parse_process_list.ts @@ -0,0 +1,27 @@ +/* + * 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 { ProcessListAPIResponse } from '../../../../../../../../common/http_api'; + +export const parseProcessList = (processList: ProcessListAPIResponse) => + processList.map((process) => { + const command = process.id; + let mostRecentPoint; + for (let i = process.rows.length - 1; i >= 0; i--) { + const point = process.rows[i]; + if (point && Array.isArray(point.meta) && point.meta?.length) { + mostRecentPoint = point; + break; + } + } + if (!mostRecentPoint) return { command, cpu: null, memory: null, startTime: null, state: null }; + + const { cpu, memory } = mostRecentPoint; + const { system } = (mostRecentPoint.meta as any[])[0]; + const startTime = system.process.cpu.start_time; + const state = system.process.state; + return { command, cpu, memory, startTime, state }; + }); diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/processes_table.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/processes_table.tsx new file mode 100644 index 0000000000000..6695c761b7451 --- /dev/null +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/processes_table.tsx @@ -0,0 +1,298 @@ +/* + * 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, { useMemo, useState, useEffect, useCallback } from 'react'; +import { omit } from 'lodash'; +import { i18n } from '@kbn/i18n'; +import { + EuiTable, + EuiTableHeader, + EuiTableBody, + EuiTableHeaderCell, + EuiTableRow, + EuiTableRowCell, + EuiSpacer, + EuiTablePagination, + EuiButtonEmpty, + EuiCode, + EuiLoadingChart, + Query, + SortableProperties, + SortableProperty, + LEFT_ALIGNMENT, + RIGHT_ALIGNMENT, +} from '@elastic/eui'; +import { ProcessListAPIResponse } from '../../../../../../../../common/http_api'; +import { FORMATTERS } from '../../../../../../../../common/formatters'; +import { euiStyled } from '../../../../../../../../../observability/public'; +import { parseProcessList } from './parse_process_list'; +import { StateBadge } from './state_badge'; +import { STATE_ORDER } from './states'; + +const ONE_MINUTE = 60 * 1000; +const ONE_HOUR = ONE_MINUTE * 60; + +interface TableBodyProps { + processList: ProcessListAPIResponse; + currentTime: number; +} + +type TableProps = TableBodyProps & { isLoading: boolean; searchFilter: Query }; + +function useSortableProperties( + sortablePropertyItems: SortableProperty[], + defaultSortProperty: string +) { + const [sortableProperties] = useState>( + new SortableProperties(sortablePropertyItems, defaultSortProperty) + ); + const [sortedColumn, setSortedColumn] = useState( + omit(sortableProperties.getSortedProperty(), 'getValue') + ); + + return { + setSortedColumn: useCallback( + (property) => { + sortableProperties.sortOn(property); + setSortedColumn(omit(sortableProperties.getSortedProperty(), 'getValue')); + }, + [sortableProperties] + ), + sortedColumn, + sortItems: (items: T[]) => sortableProperties.sortItems(items), + }; +} + +export const ProcessesTable = ({ + processList, + currentTime, + isLoading, + searchFilter, +}: TableProps) => { + const [currentPage, setCurrentPage] = useState(0); + const [itemsPerPage, setItemsPerPage] = useState(10); + useEffect(() => setCurrentPage(0), [processList, itemsPerPage]); + + const { sortedColumn, sortItems, setSortedColumn } = useSortableProperties< + ProcessListAPIResponse + >( + [ + { + name: 'state', + getValue: (item: any) => STATE_ORDER.indexOf(item.state), + isAscending: true, + }, + { + name: 'command', + getValue: (item: any) => item.command.toLowerCase(), + isAscending: true, + }, + { + name: 'startTime', + getValue: (item: any) => Date.parse(item.startTime), + isAscending: true, + }, + { + name: 'cpu', + getValue: (item: any) => item.cpu, + isAscending: true, + }, + { + name: 'memory', + getValue: (item: any) => item.memory, + isAscending: true, + }, + ], + 'state' + ); + + const currentItems = useMemo(() => { + const filteredItems = Query.execute(searchFilter, parseProcessList(processList)); + if (!filteredItems.length) return []; + const sortedItems = sortItems(filteredItems); + return sortedItems; + }, [processList, searchFilter, sortItems]); + + const pageCount = useMemo(() => Math.ceil(currentItems.length / itemsPerPage), [ + itemsPerPage, + currentItems, + ]); + + const pageStartIdx = useMemo(() => currentPage * itemsPerPage + (currentPage > 0 ? 1 : 0), [ + currentPage, + itemsPerPage, + ]); + const currentItemsPage = useMemo( + () => currentItems.slice(pageStartIdx, pageStartIdx + itemsPerPage), + [pageStartIdx, currentItems, itemsPerPage] + ); + + const body = !isLoading ? ( + + ) : ( + + ); + return ( + <> + + + + {columns.map((column) => ( + setSortedColumn(column.field) : undefined} + isSorted={sortedColumn.name === column.field} + isSortAscending={sortedColumn.name === column.field && sortedColumn.isAscending} + > + {column.name} + + ))} + + {body} + + + + + ); +}; + +const LoadingTableBody = () => { + return ( +
+ +
+ ); +}; + +const ProcessesTableBody = ({ processList, currentTime }: TableBodyProps) => ( + <> + {processList.map((item, i) => { + const cells = columns.map((column) => ( + + {column.render ? column.render(item[column.field], currentTime) : item[column.field]} + + )); + return ( + + + + + {cells} + + ); + })} + +); + +const RuntimeCell = ({ startTime, currentTime }: { startTime: string; currentTime: number }) => { + const runtimeLength = currentTime - Date.parse(startTime); + let remainingRuntimeMS = runtimeLength; + const runtimeHours = Math.floor(remainingRuntimeMS / ONE_HOUR); + remainingRuntimeMS -= runtimeHours * ONE_HOUR; + const runtimeMinutes = Math.floor(remainingRuntimeMS / ONE_MINUTE); + remainingRuntimeMS -= runtimeMinutes * ONE_MINUTE; + const runtimeSeconds = Math.floor(remainingRuntimeMS / 1000); + remainingRuntimeMS -= runtimeSeconds * 1000; + + const runtimeDisplayHours = runtimeHours ? `${runtimeHours}:` : ''; + const runtimeDisplayMinutes = runtimeMinutes < 10 ? `0${runtimeMinutes}:` : `${runtimeMinutes}:`; + const runtimeDisplaySeconds = runtimeSeconds < 10 ? `0${runtimeSeconds}` : runtimeSeconds; + + return <>{`${runtimeDisplayHours}${runtimeDisplayMinutes}${runtimeDisplaySeconds}`}; +}; + +const StyledTableBody = euiStyled(EuiTableBody)` + & .euiTableCellContent { + padding-top: 0; + padding-bottom: 0; + + } +`; + +const CodeLine = euiStyled(EuiCode).attrs({ transparentBackground: true })` + text-overflow: ellipsis; + overflow: hidden; + & code.euiCodeBlock__code { + white-space: nowrap !important; + vertical-align: middle; + } +`; + +const columns: Array<{ + field: string; + name: string; + sortable: boolean; + render?: Function; + width?: string | number; + align?: typeof RIGHT_ALIGNMENT | typeof LEFT_ALIGNMENT; +}> = [ + { + field: 'state', + name: i18n.translate('xpack.infra.metrics.nodeDetails.processes.columnLabelState', { + defaultMessage: 'State', + }), + sortable: true, + render: (state: string) => , + width: 84, + }, + { + field: 'command', + name: i18n.translate('xpack.infra.metrics.nodeDetails.processes.columnLabelCommand', { + defaultMessage: 'Command', + }), + sortable: true, + width: '40%', + render: (command: string) => {command}, + }, + { + field: 'startTime', + name: i18n.translate('xpack.infra.metrics.nodeDetails.processes.columnLabelTime', { + defaultMessage: 'Time', + }), + align: RIGHT_ALIGNMENT, + sortable: true, + render: (startTime: string, currentTime: number) => ( + + ), + }, + { + field: 'cpu', + name: i18n.translate('xpack.infra.metrics.nodeDetails.processes.columnLabelCPU', { + defaultMessage: 'CPU', + }), + sortable: true, + render: (value: number) => FORMATTERS.percent(value), + }, + { + field: 'memory', + name: i18n.translate('xpack.infra.metrics.nodeDetails.processes.columnLabelMemory', { + defaultMessage: 'Mem.', + }), + sortable: true, + render: (value: number) => FORMATTERS.percent(value), + }, +]; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/state_badge.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/state_badge.tsx new file mode 100644 index 0000000000000..5e468bbadb055 --- /dev/null +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/state_badge.tsx @@ -0,0 +1,28 @@ +/* + * 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 { EuiBadge } from '@elastic/eui'; +import { STATE_NAMES } from './states'; + +export const StateBadge = ({ state }) => { + switch (state) { + case 'running': + return {STATE_NAMES.running}; + case 'sleeping': + return {STATE_NAMES.sleeping}; + case 'dead': + return {STATE_NAMES.dead}; + case 'stopped': + return {STATE_NAMES.stopped}; + case 'idle': + return {STATE_NAMES.idle}; + case 'zombie': + return {STATE_NAMES.zombie}; + default: + return {STATE_NAMES.unknown}; + } +}; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/states.ts b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/states.ts new file mode 100644 index 0000000000000..b5e32420709eb --- /dev/null +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/states.ts @@ -0,0 +1,33 @@ +/* + * 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 { i18n } from '@kbn/i18n'; + +export const STATE_NAMES = { + running: i18n.translate('xpack.infra.metrics.nodeDetails.processes.stateRunning', { + defaultMessage: 'Running', + }), + sleeping: i18n.translate('xpack.infra.metrics.nodeDetails.processes.stateSleeping', { + defaultMessage: 'Sleeping', + }), + dead: i18n.translate('xpack.infra.metrics.nodeDetails.processes.stateDead', { + defaultMessage: 'Dead', + }), + stopped: i18n.translate('xpack.infra.metrics.nodeDetails.processes.stateStopped', { + defaultMessage: 'Stopped', + }), + idle: i18n.translate('xpack.infra.metrics.nodeDetails.processes.stateIdle', { + defaultMessage: 'Idle', + }), + zombie: i18n.translate('xpack.infra.metrics.nodeDetails.processes.stateZombie', { + defaultMessage: 'Zombie', + }), + unknown: i18n.translate('xpack.infra.metrics.nodeDetails.processes.stateUnknown', { + defaultMessage: 'Unknown', + }), +}; + +export const STATE_ORDER = ['running', 'sleeping', 'stopped', 'idle', 'dead', 'zombie', 'unknown']; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/summary_table.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/summary_table.tsx new file mode 100644 index 0000000000000..c712255c1abfd --- /dev/null +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/summary_table.tsx @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useMemo } from 'react'; +import { mapValues, countBy } from 'lodash'; +import { i18n } from '@kbn/i18n'; +import { EuiBasicTable } from '@elastic/eui'; +import { euiStyled } from '../../../../../../../../../observability/public'; +import { ProcessListAPIResponse } from '../../../../../../../../common/http_api'; +import { parseProcessList } from './parse_process_list'; +import { STATE_NAMES } from './states'; + +interface Props { + processList: ProcessListAPIResponse; +} + +export const SummaryTable = ({ processList }: Props) => { + const parsedList = parseProcessList(processList); + const processCount = useMemo( + () => ({ + total: parsedList.length, + ...mapValues(STATE_NAMES, () => 0), + ...countBy(parsedList, 'state'), + }), + [parsedList] + ); + return ( + + + + ); +}; + +const columns = [ + { + field: 'total', + name: i18n.translate('xpack.infra.metrics.nodeDetails.processes.headingTotalProcesses', { + defaultMessage: 'Total processes', + }), + width: 125, + }, + ...Object.entries(STATE_NAMES).map(([field, name]) => ({ field, name })), +]; + +const StyleWrapper = euiStyled.div` + & .euiTableHeaderCell { + border-bottom: none; + & .euiTableCellContent { + padding-bottom: 0; + } + & .euiTableCellContent__text { + font-size: ${(props) => props.theme.eui.euiFontSizeS}; + } + } + + & .euiTableRowCell { + border-top: none; + & .euiTableCellContent { + padding-top: 0; + } + } +`; From bb5dfa5a44bc8a1739d2dc545273e1a8e29fcad7 Mon Sep 17 00:00:00 2001 From: Zacqary Xeper Date: Wed, 11 Nov 2020 13:37:38 -0600 Subject: [PATCH 06/12] Fix loading state, add polish --- .../node_details/tabs/processes/index.tsx | 2 +- .../tabs/processes/processes_table.tsx | 22 +++++++++--------- .../tabs/processes/state_badge.tsx | 2 +- .../tabs/processes/summary_table.tsx | 23 +++++++++++++------ 4 files changed, 29 insertions(+), 20 deletions(-) diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/index.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/index.tsx index 44cbd7c7411d5..8186e694d7b1b 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/index.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/index.tsx @@ -31,7 +31,7 @@ const TabComponent = ({ currentTime, node, nodeType, options }: TabProps) => { return ( - + { const [currentPage, setCurrentPage] = useState(0); const [itemsPerPage, setItemsPerPage] = useState(10); - useEffect(() => setCurrentPage(0), [processList, itemsPerPage]); + useEffect(() => setCurrentPage(0), [processList, searchFilter, itemsPerPage]); const { sortedColumn, sortItems, setSortedColumn } = useSortableProperties< ProcessListAPIResponse @@ -93,17 +93,17 @@ export const ProcessesTable = ({ { name: 'startTime', getValue: (item: any) => Date.parse(item.startTime), - isAscending: true, + isAscending: false, }, { name: 'cpu', getValue: (item: any) => item.cpu, - isAscending: true, + isAscending: false, }, { name: 'memory', getValue: (item: any) => item.memory, - isAscending: true, + isAscending: false, }, ], 'state' @@ -130,11 +130,8 @@ export const ProcessesTable = ({ [pageStartIdx, currentItems, itemsPerPage] ); - const body = !isLoading ? ( - - ) : ( - - ); + if (isLoading) return ; + return ( <> @@ -153,7 +150,9 @@ export const ProcessesTable = ({ ))} - {body} + + + { +const LoadingPlaceholder = () => { return (
{ +export const StateBadge = ({ state }: { state: string }) => { switch (state) { case 'running': return {STATE_NAMES.running}; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/summary_table.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/summary_table.tsx index c712255c1abfd..9d9c19de62a27 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/summary_table.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/summary_table.tsx @@ -7,7 +7,7 @@ import React, { useMemo } from 'react'; import { mapValues, countBy } from 'lodash'; import { i18n } from '@kbn/i18n'; -import { EuiBasicTable } from '@elastic/eui'; +import { EuiBasicTable, EuiLoadingSpinner } from '@elastic/eui'; import { euiStyled } from '../../../../../../../../../observability/public'; import { ProcessListAPIResponse } from '../../../../../../../../common/http_api'; import { parseProcessList } from './parse_process_list'; @@ -15,17 +15,18 @@ import { STATE_NAMES } from './states'; interface Props { processList: ProcessListAPIResponse; + isLoading: boolean; } -export const SummaryTable = ({ processList }: Props) => { +export const SummaryTable = ({ processList, isLoading }: Props) => { const parsedList = parseProcessList(processList); const processCount = useMemo( () => ({ - total: parsedList.length, - ...mapValues(STATE_NAMES, () => 0), - ...countBy(parsedList, 'state'), + total: isLoading ? -1 : parsedList.length, + ...mapValues(STATE_NAMES, () => (isLoading ? -1 : 0)), + ...(isLoading ? [] : countBy(parsedList, 'state')), }), - [parsedList] + [parsedList, isLoading] ); return ( @@ -34,6 +35,8 @@ export const SummaryTable = ({ processList }: Props) => { ); }; +const loadingRenderer = (value: number) => (value === -1 ? : value); + const columns = [ { field: 'total', @@ -41,10 +44,16 @@ const columns = [ defaultMessage: 'Total processes', }), width: 125, + render: loadingRenderer, }, - ...Object.entries(STATE_NAMES).map(([field, name]) => ({ field, name })), + ...Object.entries(STATE_NAMES).map(([field, name]) => ({ field, name, render: loadingRenderer })), ]; +const LoadingSpinner = euiStyled(EuiLoadingSpinner).attrs({ size: 'm' })` + margin-top: 2px; + margin-bottom: 3px; +`; + const StyleWrapper = euiStyled.div` & .euiTableHeaderCell { border-bottom: none; From 40a615455585197729d77ad9205b18e1ca40b4fd Mon Sep 17 00:00:00 2001 From: Zacqary Xeper Date: Wed, 11 Nov 2020 16:40:25 -0600 Subject: [PATCH 07/12] Fix process filtering --- x-pack/plugins/infra/server/lib/host_details/process_list.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/infra/server/lib/host_details/process_list.ts b/x-pack/plugins/infra/server/lib/host_details/process_list.ts index aebce5a51b313..5cf9bfd42aeae 100644 --- a/x-pack/plugins/infra/server/lib/host_details/process_list.ts +++ b/x-pack/plugins/infra/server/lib/host_details/process_list.ts @@ -17,7 +17,7 @@ export const getProcessList = async ( timerange, modules: ['system.cpu', 'system.memory'], groupBy: ['system.process.cmdline'], - filter: [{ term: hostTerm }], + filters: [{ term: hostTerm }], indexPattern, limit: 9, metrics: [ From 9e8ec2a4d95eb2b368c2f977f289fb093d3e3a50 Mon Sep 17 00:00:00 2001 From: Zacqary Xeper Date: Thu, 12 Nov 2020 12:42:38 -0600 Subject: [PATCH 08/12] Fix overlay sizing --- .../components/node_details/overlay.tsx | 66 ++++++++++++------- .../node_details/tabs/processes/index.tsx | 9 +-- .../components/node_details/tabs/shared.tsx | 12 ++++ 3 files changed, 55 insertions(+), 32 deletions(-) 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 dd0060f773b49..af712c0611577 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 @@ -4,11 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiTabbedContent } from '@elastic/eui'; +import { EuiPortal, EuiTabs, EuiTab, EuiPanel, EuiTitle } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiPanel } from '@elastic/eui'; -import React, { CSSProperties, useMemo } from 'react'; -import { EuiText } from '@elastic/eui'; +import React, { CSSProperties, useMemo, useState } from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiButtonEmpty } from '@elastic/eui'; import { euiStyled } from '../../../../../../../observability/public'; import { InfraWaffleMapNode, InfraWaffleMapOptions } from '../../../../../lib/lib'; @@ -17,6 +15,7 @@ import { MetricsTab } from './tabs/metrics'; import { LogsTab } from './tabs/logs'; import { ProcessesTab } from './tabs/processes'; import { PropertiesTab } from './tabs/properties'; +import { OVERLAY_Y_START, OVERLAY_BOTTOM_MARGIN, OVERLAY_HEADER_SIZE } from './tabs/shared'; interface Props { isOpen: boolean; @@ -48,46 +47,63 @@ export const NodeContextPopover = ({ }); }, [tabConfigs, node, nodeType, currentTime, options]); + const [selectedTab, setSelectedTab] = useState(0); + if (!isOpen) { return null; } return ( - - - - - -

{node.name}

-
-
- - - - - -
-
- -
+ + + + + + +

{node.name}

+
+
+ + + + + +
+ + {tabs.map((tab, i) => ( + setSelectedTab(i)}> + {tab.name} + + ))} + +
+ {tabs[selectedTab].content} +
+
); }; const OverlayHeader = euiStyled.div` border-color: ${(props) => props.theme.eui.euiBorderColor}; border-bottom-width: ${(props) => props.theme.eui.euiBorderWidthThick}; - padding: ${(props) => props.theme.eui.euiSizeS}; padding-bottom: 0; overflow: hidden; + background-color: ${(props) => props.theme.eui.euiColorLightestShade}; + height: ${OVERLAY_HEADER_SIZE}px; +`; + +const OverlayHeaderTitleWrapper = euiStyled(EuiFlexGroup).attrs({ alignItems: 'center' })` + padding: ${(props) => props.theme.eui.paddingSizes.s} ${(props) => + props.theme.eui.paddingSizes.m} 0; `; const panelStyle: CSSProperties = { position: 'absolute', right: 10, - top: -100, + top: OVERLAY_Y_START, width: '50%', - maxWidth: 600, + maxWidth: 730, zIndex: 2, - height: '50vh', + height: `calc(100vh - ${OVERLAY_Y_START + OVERLAY_BOTTOM_MARGIN}px)`, overflow: 'hidden', }; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/index.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/index.tsx index 8186e694d7b1b..77ce7945a3694 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/index.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/index.tsx @@ -30,7 +30,7 @@ const TabComponent = ({ currentTime, node, nodeType, options }: TabProps) => { ); return ( - + { processList={response ?? []} searchFilter={searchFilter} /> - + ); }; @@ -74,8 +74,3 @@ export const ProcessesTab = { }), content: TabComponent, }; - -const ProcessListTabContent = euiStyled(TabContent)` - padding-left: 0; - padding-right: 0; -`; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/shared.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/shared.tsx index 241ad7104836e..e80af9d395ddd 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/shared.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/shared.tsx @@ -15,6 +15,18 @@ export interface TabProps { nodeType: InventoryItemType; } +export const OVERLAY_Y_START = 266; +export const OVERLAY_BOTTOM_MARGIN = 16; +export const OVERLAY_HEADER_SIZE = 96; +const contentHeightOffset = OVERLAY_Y_START + OVERLAY_BOTTOM_MARGIN + OVERLAY_HEADER_SIZE; export const TabContent = euiStyled.div` +<<<<<<< HEAD padding: ${(props) => props.theme.eui.paddingSizes.l}; +||||||| parent of 097ff579d2c... Fix overlay sizing +======= + padding: ${(props) => props.theme.eui.paddingSizes.s}; + height: calc(100vh - ${contentHeightOffset}px); + overflow-y: auto; + overflow-x: hidden; +>>>>>>> 097ff579d2c... Fix overlay sizing `; From 20cccc4a20a079dbfc032286a5db7876e6cbf144 Mon Sep 17 00:00:00 2001 From: Zacqary Xeper Date: Thu, 12 Nov 2020 17:32:12 -0600 Subject: [PATCH 09/12] Complete expanded row --- .../components/bottom_drawer.tsx | 6 +- .../node_details/tabs/processes/index.tsx | 1 - .../tabs/processes/parse_process_list.ts | 31 ++- .../tabs/processes/process_row.tsx | 254 ++++++++++++++++++ .../tabs/processes/processes_table.tsx | 53 ++-- .../server/lib/host_details/process_list.ts | 7 +- 6 files changed, 314 insertions(+), 38 deletions(-) create mode 100644 x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/process_row.tsx diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/bottom_drawer.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/bottom_drawer.tsx index 7c6e58125b48b..5c6e124914f39 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/bottom_drawer.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/bottom_drawer.tsx @@ -38,7 +38,11 @@ export const BottomDrawer: React.FC<{ - + {isOpen ? hideHistory : showHistory} diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/index.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/index.tsx index 77ce7945a3694..29b97236f76c8 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/index.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/index.tsx @@ -7,7 +7,6 @@ import React, { useMemo, useState } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiSearchBar, EuiSpacer, Query } from '@elastic/eui'; -import { euiStyled } from '../../../../../../../../../observability/public'; import { useProcessList } from '../../../../hooks/use_process_list'; import { TabContent, TabProps } from '../shared'; import { STATE_NAMES } from './states'; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/parse_process_list.ts b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/parse_process_list.ts index dd42b0dbc8efb..306aeafbecef2 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/parse_process_list.ts +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/parse_process_list.ts @@ -20,8 +20,35 @@ export const parseProcessList = (processList: ProcessListAPIResponse) => if (!mostRecentPoint) return { command, cpu: null, memory: null, startTime: null, state: null }; const { cpu, memory } = mostRecentPoint; - const { system } = (mostRecentPoint.meta as any[])[0]; + const { system, process: processMeta, user } = (mostRecentPoint.meta as any[])[0]; const startTime = system.process.cpu.start_time; const state = system.process.state; - return { command, cpu, memory, startTime, state }; + + const timeseries = { + cpu: pickTimeseries(process.rows, 'cpu'), + memory: pickTimeseries(process.rows, 'memory'), + }; + + return { + command, + cpu, + memory, + startTime, + state, + pid: processMeta.pid, + user: user.name, + timeseries, + }; }); + +const pickTimeseries = (rows: any[], metricID: string) => ({ + rows: rows.map((row) => ({ + timestamp: row.timestamp, + metric_0: row[metricID], + })), + columns: [ + { name: 'timestamp', type: 'date' }, + { name: 'metric_0', type: 'number' }, + ], + id: metricID, +}); diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/process_row.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/process_row.tsx new file mode 100644 index 0000000000000..4869112e9ccd7 --- /dev/null +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/process_row.tsx @@ -0,0 +1,254 @@ +/* + * 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, { useState, useMemo } from 'react'; +import moment from 'moment'; +import { first, last } from 'lodash'; +import { i18n } from '@kbn/i18n'; +import { + EuiTableRow, + EuiTableRowCell, + EuiButtonEmpty, + EuiCode, + EuiDescriptionList, + EuiDescriptionListTitle, + EuiDescriptionListDescription, + EuiFlexGrid, + EuiFlexGroup, + EuiFlexItem, + EuiButton, + EuiSpacer, +} from '@elastic/eui'; +import { Axis, Chart, Settings, Position, TooltipValue, niceTimeFormatter } from '@elastic/charts'; +import { AutoSizer } from '../../../../../../../components/auto_sizer'; +import { createFormatter } from '../../../../../../../../common/formatters'; +import { useUiSetting } from '../../../../../../../../../../../src/plugins/kibana_react/public'; +import { getChartTheme } from '../../../../../metrics_explorer/components/helpers/get_chart_theme'; +import { calculateDomain } from '../../../../../metrics_explorer/components/helpers/calculate_domain'; +import { MetricsExplorerChartType } from '../../../../../metrics_explorer/hooks/use_metrics_explorer_options'; +import { MetricExplorerSeriesChart } from '../../../../../metrics_explorer/components/series_chart'; +import { MetricsExplorerAggregation } from '../../../../../../../../common/http_api'; +import { Color } from '../../../../../../../../common/color_palette'; +import { euiStyled } from '../../../../../../../../../observability/public'; + +export const ProcessRow = ({ cells, item }) => { + const [isExpanded, setIsExpanded] = useState(false); + + return ( + <> + + + setIsExpanded(!isExpanded)} + /> + + {cells} + + + {isExpanded && ( + + {({ measureRef, bounds: { height = 0 } }) => ( + + + + + +
+ + {i18n.translate( + 'xpack.infra.metrics.nodeDetails.processes.expandedRowLabelCommand', + { + defaultMessage: 'Command', + } + )} + + + {item.command} + +
+
+ + + {i18n.translate( + 'xpack.infra.metrics.nodeDetails.processes.viewTraceInAPM', + { + defaultMessage: 'View trace in APM', + } + )} + + +
+ + + + {i18n.translate( + 'xpack.infra.metrics.nodeDetails.processes.expandedRowLabelPID', + { + defaultMessage: 'PID', + } + )} + + + {item.pid} + + + + + {i18n.translate( + 'xpack.infra.metrics.nodeDetails.processes.expandedRowLabelUser', + { + defaultMessage: 'User', + } + )} + + + {item.user} + + + + {cpuMetricLabel} + + + + + + {memoryMetricLabel} + + + + + +
+
+ )} +
+ )} +
+ + ); +}; + +const ProcessChart = ({ timeseries, color, label }) => { + const chartMetric = { + color, + aggregation: 'avg' as MetricsExplorerAggregation, + label, + }; + const isDarkMode = useUiSetting('theme:darkMode'); + + const dateFormatter = useMemo(() => { + if (!timeseries) return () => ''; + const firstTimestamp = first(timeseries.rows)?.timestamp; + const lastTimestamp = last(timeseries.rows)?.timestamp; + + if (firstTimestamp == null || lastTimestamp == null) { + return (value: number) => `${value}`; + } + + return niceTimeFormatter([firstTimestamp, lastTimestamp]); + }, [timeseries]); + + const yAxisFormatter = createFormatter('percent'); + + const tooltipProps = { + headerFormatter: (tooltipValue: TooltipValue) => + moment(tooltipValue.value).format('Y-MM-DD HH:mm:ss.SSS'), + }; + + const dataDomain = calculateDomain(timeseries, [chartMetric], false); + const domain = dataDomain + ? { + max: dataDomain.max * 1.1, // add 10% headroom. + min: dataDomain.min, + } + : { max: 0, min: 0 }; + + return ( + + + + + + + + + ); +}; + +export const CodeLine = euiStyled(EuiCode).attrs({ + transparentBackground: true, +})` + text-overflow: ellipsis; + overflow: hidden; + padding: 0 !important; + & code.euiCodeBlock__code { + white-space: nowrap !important; + vertical-align: middle; + } +`; + +const ExpandedCommandLine = euiStyled(EuiCode).attrs({ + transparentBackground: true, +})` + padding: 0 !important; + margin-bottom: ${(props) => props.theme.eui.euiSizeS}; +`; + +const ExpandedRowCell = euiStyled(EuiTableRowCell).attrs({ + textOnly: false, + colSpan: 6, +})<{ commandHeight: number }>` + height: ${(props) => props.commandHeight + 240}px; + padding: 0 ${(props) => props.theme.eui.paddingSizes.m}; + background-color: ${(props) => props.theme.eui.euiColorLightestShade}; +`; + +const ChartContainer = euiStyled.div` + width: 300px; + height: 140px; +`; + +const cpuMetricLabel = i18n.translate( + 'xpack.infra.metrics.nodeDetails.processes.expandedRowLabelCPU', + { + defaultMessage: 'CPU', + } +); + +const memoryMetricLabel = i18n.translate( + 'xpack.infra.metrics.nodeDetails.processes.expandedRowLabelMemory', + { + defaultMessage: 'Memory', + } +); diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/processes_table.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/processes_table.tsx index 7d73e44880ee0..1b5fd346ec7c4 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/processes_table.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/processes_table.tsx @@ -12,29 +12,23 @@ import { EuiTableHeader, EuiTableBody, EuiTableHeaderCell, - EuiTableRow, EuiTableRowCell, EuiSpacer, EuiTablePagination, - EuiButtonEmpty, - EuiCode, EuiLoadingChart, Query, SortableProperties, - SortableProperty, LEFT_ALIGNMENT, RIGHT_ALIGNMENT, } from '@elastic/eui'; import { ProcessListAPIResponse } from '../../../../../../../../common/http_api'; import { FORMATTERS } from '../../../../../../../../common/formatters'; import { euiStyled } from '../../../../../../../../../observability/public'; +import { ProcessRow, CodeLine } from './process_row'; import { parseProcessList } from './parse_process_list'; import { StateBadge } from './state_badge'; import { STATE_ORDER } from './states'; -const ONE_MINUTE = 60 * 1000; -const ONE_HOUR = ONE_MINUTE * 60; - interface TableBodyProps { processList: ProcessListAPIResponse; currentTime: number; @@ -43,7 +37,11 @@ interface TableBodyProps { type TableProps = TableBodyProps & { isLoading: boolean; searchFilter: Query }; function useSortableProperties( - sortablePropertyItems: SortableProperty[], + sortablePropertyItems: Array<{ + name: string; + getValue: (obj: T) => any; + isAscending: boolean; + }>, defaultSortProperty: string ) { const [sortableProperties] = useState>( @@ -192,22 +190,26 @@ const ProcessesTableBody = ({ processList, currentTime }: TableBodyProps) => ( key={`${column.field}-${i}`} header={column.name} align={column.align ?? LEFT_ALIGNMENT} + textOnly={column.textOnly ?? true} > {column.render ? column.render(item[column.field], currentTime) : item[column.field]} )); - return ( - - - - - {cells} - - ); + return ; })} ); +const StyledTableBody = euiStyled(EuiTableBody)` + & .euiTableCellContent { + padding-top: 0; + padding-bottom: 0; + + } +`; + +const ONE_MINUTE = 60 * 1000; +const ONE_HOUR = ONE_MINUTE * 60; const RuntimeCell = ({ startTime, currentTime }: { startTime: string; currentTime: number }) => { const runtimeLength = currentTime - Date.parse(startTime); let remainingRuntimeMS = runtimeLength; @@ -225,29 +227,13 @@ const RuntimeCell = ({ startTime, currentTime }: { startTime: string; currentTim return <>{`${runtimeDisplayHours}${runtimeDisplayMinutes}${runtimeDisplaySeconds}`}; }; -const StyledTableBody = euiStyled(EuiTableBody)` - & .euiTableCellContent { - padding-top: 0; - padding-bottom: 0; - - } -`; - -const CodeLine = euiStyled(EuiCode).attrs({ transparentBackground: true })` - text-overflow: ellipsis; - overflow: hidden; - & code.euiCodeBlock__code { - white-space: nowrap !important; - vertical-align: middle; - } -`; - const columns: Array<{ field: string; name: string; sortable: boolean; render?: Function; width?: string | number; + textOnly?: boolean; align?: typeof RIGHT_ALIGNMENT | typeof LEFT_ALIGNMENT; }> = [ { @@ -258,6 +244,7 @@ const columns: Array<{ sortable: true, render: (state: string) => , width: 84, + textOnly: false, }, { field: 'command', diff --git a/x-pack/plugins/infra/server/lib/host_details/process_list.ts b/x-pack/plugins/infra/server/lib/host_details/process_list.ts index 5cf9bfd42aeae..99e8b2e8f6ab1 100644 --- a/x-pack/plugins/infra/server/lib/host_details/process_list.ts +++ b/x-pack/plugins/infra/server/lib/host_details/process_list.ts @@ -48,7 +48,12 @@ export const getProcessList = async ( top_hits: { size: 1, sort: [{ [timerange.field]: { order: 'desc' } }], - _source: ['system.process.cpu.start_time', 'system.process.state'], + _source: [ + 'system.process.cpu.start_time', + 'system.process.state', + 'process.pid', + 'user.name', + ], }, }, }, From 3a586849dc87c76b4ebcbd516fdee4ca07175827 Mon Sep 17 00:00:00 2001 From: Zacqary Xeper Date: Mon, 16 Nov 2020 13:56:25 -0600 Subject: [PATCH 10/12] Fix types, gate APM trace button --- .../node_details/tabs/processes.tsx | 309 ------------------ .../tabs/processes/parse_process_list.ts | 3 +- .../tabs/processes/process_row.tsx | 37 ++- .../tabs/processes/processes_table.tsx | 29 +- .../node_details/tabs/processes/types.ts | 22 ++ .../components/node_details/tabs/shared.tsx | 5 - 6 files changed, 65 insertions(+), 340 deletions(-) delete mode 100644 x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes.tsx create mode 100644 x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/types.ts diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes.tsx deleted file mode 100644 index 01800b5f1ca31..0000000000000 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes.tsx +++ /dev/null @@ -1,309 +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, { useMemo } from 'react'; -import { i18n } from '@kbn/i18n'; -import { - EuiTable, - EuiTableHeader, - EuiTableBody, - EuiTableHeaderCell, - EuiTableRow, - EuiTableRowCell, - EuiSpacer, - EuiTablePagination, - EuiButtonEmpty, - EuiBadge, - EuiCode, - EuiLoadingChart, -} from '@elastic/eui'; -import { FORMATTERS } from '../../../../../../../common/formatters'; -import { euiStyled } from '../../../../../../../../observability/public'; -import { useProcessList } from '../../../hooks/use_process_list'; -import { TabContent, TabProps } from './shared'; - -const ONE_MINUTE = 60 * 1000; -const ONE_HOUR = ONE_MINUTE * 60; - -const TabComponent = ({ currentTime, node, nodeType, options }: TabProps) => { - const hostTerm = useMemo(() => { - const field = options.fields[nodeType]; - return { [field]: node.name }; - }, [options, node, nodeType]); - - const { loading, error, response } = useProcessList( - hostTerm, - 'metricbeat-*', - options.fields?.timestamp, - currentTime - ); - - return ( - - - - ); -}; - -export const ProcessesTab = { - id: 'processes', - name: i18n.translate('xpack.infra.nodeDetails.tabs.processes', { - defaultMessage: 'Processes', - }), - content: TabComponent, -}; - -const columns = [ - { - field: 'state', - name: i18n.translate('xpack.infra.metrics.nodeDetails.processes.columnLabelState', { - defaultMessage: 'State', - }), - sortable: true, - render: (state: string) => , - width: 84, - }, - { - field: 'command', - name: i18n.translate('xpack.infra.metrics.nodeDetails.processes.columnLabelCommand', { - defaultMessage: 'Command', - }), - sortable: true, - truncateText: true, - width: '40%', - render: (command: string) => {command}, - }, - { - field: 'runtime', - name: i18n.translate('xpack.infra.metrics.nodeDetails.processes.columnLabelTime', { - defaultMessage: 'Time', - }), - rightAlign: true, - }, - { - field: 'cpu', - name: i18n.translate('xpack.infra.metrics.nodeDetails.processes.columnLabelCPU', { - defaultMessage: 'CPU', - }), - render: (value: number) => FORMATTERS.percent(value), - }, - { - field: 'memory', - name: i18n.translate('xpack.infra.metrics.nodeDetails.processes.columnLabelMemory', { - defaultMessage: 'Mem.', - }), - render: (value: number) => FORMATTERS.percent(value), - }, -]; - -const ProcessesTable = ({ processList, currentTime, isLoading }) => { - const body = !isLoading ? ( - - ) : ( - - ); - return ( - <> - - - - {columns.map((column) => ( - - {column.name} - - ))} - - {body} - - - - - ); -}; - -const LoadingTableBody = () => { - return ( -
- -
- ); -}; - -const ProcessesTableBody = ({ processList, currentTime }) => { - const items = useMemo( - () => - processList.map((process) => { - const command = process.id; - let mostRecentPoint; - for (let i = process.rows.length - 1; i >= 0; i--) { - const point = process.rows[i]; - if (point.meta?.length) { - mostRecentPoint = point; - break; - } - } - - const { cpu, memory } = mostRecentPoint; - const { system } = mostRecentPoint.meta[0]; - const startTime = system.process.cpu.start_time; - const state = system.process.state; - - const runtimeLength = currentTime - Date.parse(startTime); - let remainingRuntimeMS = runtimeLength; - const runtimeHours = Math.floor(remainingRuntimeMS / ONE_HOUR); - remainingRuntimeMS -= runtimeHours * ONE_HOUR; - const runtimeMinutes = Math.floor(remainingRuntimeMS / ONE_MINUTE); - remainingRuntimeMS -= runtimeMinutes * ONE_MINUTE; - const runtimeSeconds = Math.floor(remainingRuntimeMS / 1000); - remainingRuntimeMS -= runtimeSeconds * 1000; - - const runtimeDisplayHours = runtimeHours ? `${runtimeHours}:` : ''; - const runtimeDisplayMinutes = - runtimeHours && runtimeMinutes < 10 ? `0${runtimeMinutes}:` : `${runtimeMinutes}:`; - const runtimeDisplaySeconds = runtimeSeconds < 10 ? `0${runtimeSeconds}` : runtimeSeconds; - const runtimeDisplayMS = !runtimeDisplayHours ? `.${remainingRuntimeMS}` : ''; - - const runtime = `${runtimeDisplayHours}${runtimeDisplayMinutes}${runtimeDisplaySeconds}${runtimeDisplayMS}`; - - return { - state, - command, - runtime, - cpu, - memory, - }; - }), - [processList, currentTime] - ); - - return ( - <> - {items.map((item, i) => { - const cells = columns.map((column) => ( - - {column.render ? column.render(item[column.field]) : item[column.field]} - - )); - return ( - - - - - {cells} - - ); - })} - - ); -}; - -const StateBadge = ({ state }) => { - switch (state) { - case 'running': - return ( - - {i18n.translate('xpack.infra.metrics.nodeDetails.processes.stateRunning', { - defaultMessage: 'Running', - })} - - ); - case 'sleeping': - return ( - - {i18n.translate('xpack.infra.metrics.nodeDetails.processes.stateSleeping', { - defaultMessage: 'Sleeping', - })} - - ); - case 'dead': - return ( - - {i18n.translate('xpack.infra.metrics.nodeDetails.processes.stateDead', { - defaultMessage: 'Dead', - })} - - ); - case 'stopped': - return ( - - {i18n.translate('xpack.infra.metrics.nodeDetails.processes.stateStopped', { - defaultMessage: 'Stopped', - })} - - ); - case 'idle': - return ( - - {i18n.translate('xpack.infra.metrics.nodeDetails.processes.stateIdle', { - defaultMessage: 'Idle', - })} - - ); - case 'zombie': - return ( - - {i18n.translate('xpack.infra.metrics.nodeDetails.processes.stateZombie', { - defaultMessage: 'Zombie', - })} - - ); - default: - return ( - - {i18n.translate('xpack.infra.metrics.nodeDetails.processes.stateUnknown', { - defaultMessage: 'Unknown', - })} - - ); - } -}; - -const ProcessListTabContent = euiStyled(TabContent)` - padding-left: 0; - padding-right: 0; -`; - -const StyledTableBody = euiStyled(EuiTableBody)` - & .euiTableCellContent { - padding-top: 0; - padding-bottom: 0; - - } -`; - -const CodeLine = euiStyled(EuiCode).attrs({ transparentBackground: true })` - text-overflow: ellipsis; - overflow: hidden; - & code.euiCodeBlock__code { - white-space: nowrap !important; - vertical-align: middle; - } -`; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/parse_process_list.ts b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/parse_process_list.ts index 306aeafbecef2..88584ef2987e1 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/parse_process_list.ts +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/parse_process_list.ts @@ -5,6 +5,7 @@ */ import { ProcessListAPIResponse } from '../../../../../../../../common/http_api'; +import { Process } from './types'; export const parseProcessList = (processList: ProcessListAPIResponse) => processList.map((process) => { @@ -38,7 +39,7 @@ export const parseProcessList = (processList: ProcessListAPIResponse) => pid: processMeta.pid, user: user.name, timeseries, - }; + } as Process; }); const pickTimeseries = (rows: any[], metricID: string) => ({ diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/process_row.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/process_row.tsx index 4869112e9ccd7..bbf4a25fc49a7 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/process_row.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/process_row.tsx @@ -33,8 +33,14 @@ import { MetricExplorerSeriesChart } from '../../../../../metrics_explorer/compo import { MetricsExplorerAggregation } from '../../../../../../../../common/http_api'; import { Color } from '../../../../../../../../common/color_palette'; import { euiStyled } from '../../../../../../../../../observability/public'; +import { Process } from './types'; -export const ProcessRow = ({ cells, item }) => { +interface Props { + cells: React.ReactNode[]; + item: Process; +} + +export const ProcessRow = ({ cells, item }: Props) => { const [isExpanded, setIsExpanded] = useState(false); return ( @@ -72,16 +78,18 @@ export const ProcessRow = ({ cells, item }) => {
- - - {i18n.translate( - 'xpack.infra.metrics.nodeDetails.processes.viewTraceInAPM', - { - defaultMessage: 'View trace in APM', - } - )} - - + {item.apmTrace && ( + + + {i18n.translate( + 'xpack.infra.metrics.nodeDetails.processes.viewTraceInAPM', + { + defaultMessage: 'View trace in APM', + } + )} + + + )} @@ -141,7 +149,12 @@ export const ProcessRow = ({ cells, item }) => { ); }; -const ProcessChart = ({ timeseries, color, label }) => { +interface ProcessChartProps { + timeseries: Process['timeseries']['x']; + color: Color; + label: string; +} +const ProcessChart = ({ timeseries, color, label }: ProcessChartProps) => { const chartMetric = { color, aggregation: 'avg' as MetricsExplorerAggregation, diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/processes_table.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/processes_table.tsx index 1b5fd346ec7c4..43f3a333fda83 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/processes_table.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/processes_table.tsx @@ -24,18 +24,19 @@ import { import { ProcessListAPIResponse } from '../../../../../../../../common/http_api'; import { FORMATTERS } from '../../../../../../../../common/formatters'; import { euiStyled } from '../../../../../../../../../observability/public'; +import { Process } from './types'; import { ProcessRow, CodeLine } from './process_row'; import { parseProcessList } from './parse_process_list'; import { StateBadge } from './state_badge'; import { STATE_ORDER } from './states'; -interface TableBodyProps { +interface TableProps { processList: ProcessListAPIResponse; currentTime: number; + isLoading: boolean; + searchFilter: Query; } -type TableProps = TableBodyProps & { isLoading: boolean; searchFilter: Query }; - function useSortableProperties( sortablePropertyItems: Array<{ name: string; @@ -74,9 +75,7 @@ export const ProcessesTable = ({ const [itemsPerPage, setItemsPerPage] = useState(10); useEffect(() => setCurrentPage(0), [processList, searchFilter, itemsPerPage]); - const { sortedColumn, sortItems, setSortedColumn } = useSortableProperties< - ProcessListAPIResponse - >( + const { sortedColumn, sortItems, setSortedColumn } = useSortableProperties( [ { name: 'state', @@ -108,7 +107,7 @@ export const ProcessesTable = ({ ); const currentItems = useMemo(() => { - const filteredItems = Query.execute(searchFilter, parseProcessList(processList)); + const filteredItems = Query.execute(searchFilter, parseProcessList(processList)) as Process[]; if (!filteredItems.length) return []; const sortedItems = sortItems(filteredItems); return sortedItems; @@ -137,7 +136,7 @@ export const ProcessesTable = ({ {columns.map((column) => ( setSortedColumn(column.field) : undefined} @@ -149,7 +148,7 @@ export const ProcessesTable = ({ ))} - + @@ -182,12 +181,16 @@ const LoadingPlaceholder = () => { ); }; -const ProcessesTableBody = ({ processList, currentTime }: TableBodyProps) => ( +interface TableBodyProps { + items: Process[]; + currentTime: number; +} +const ProcessesTableBody = ({ items, currentTime }: TableBodyProps) => ( <> - {processList.map((item, i) => { + {items.map((item, i) => { const cells = columns.map((column) => ( props.theme.eui.paddingSizes.l}; -||||||| parent of 097ff579d2c... Fix overlay sizing -======= padding: ${(props) => props.theme.eui.paddingSizes.s}; height: calc(100vh - ${contentHeightOffset}px); overflow-y: auto; overflow-x: hidden; ->>>>>>> 097ff579d2c... Fix overlay sizing `; From c49e5ea4bfddde725a2126d76d137fb1d747aa3e Mon Sep 17 00:00:00 2001 From: Zacqary Xeper Date: Mon, 16 Nov 2020 15:00:40 -0600 Subject: [PATCH 11/12] Fix typecheck --- .../tabs/processes/summary_table.tsx | 23 ++++++++++++------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/summary_table.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/summary_table.tsx index 9d9c19de62a27..59becb0bf534d 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/summary_table.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/summary_table.tsx @@ -7,7 +7,7 @@ import React, { useMemo } from 'react'; import { mapValues, countBy } from 'lodash'; import { i18n } from '@kbn/i18n'; -import { EuiBasicTable, EuiLoadingSpinner } from '@elastic/eui'; +import { EuiBasicTable, EuiLoadingSpinner, EuiBasicTableColumn } from '@elastic/eui'; import { euiStyled } from '../../../../../../../../../observability/public'; import { ProcessListAPIResponse } from '../../../../../../../../common/http_api'; import { parseProcessList } from './parse_process_list'; @@ -18,19 +18,26 @@ interface Props { isLoading: boolean; } +type SummaryColumn = { + total: number; +} & Record; + export const SummaryTable = ({ processList, isLoading }: Props) => { const parsedList = parseProcessList(processList); const processCount = useMemo( - () => ({ - total: isLoading ? -1 : parsedList.length, - ...mapValues(STATE_NAMES, () => (isLoading ? -1 : 0)), - ...(isLoading ? [] : countBy(parsedList, 'state')), - }), + () => + [ + { + total: isLoading ? -1 : parsedList.length, + ...mapValues(STATE_NAMES, () => (isLoading ? -1 : 0)), + ...(isLoading ? [] : countBy(parsedList, 'state')), + }, + ] as SummaryColumn[], [parsedList, isLoading] ); return ( - + ); }; @@ -47,7 +54,7 @@ const columns = [ render: loadingRenderer, }, ...Object.entries(STATE_NAMES).map(([field, name]) => ({ field, name, render: loadingRenderer })), -]; +] as Array>; const LoadingSpinner = euiStyled(EuiLoadingSpinner).attrs({ size: 'm' })` margin-top: 2px; From 89549d40b54e37f30468ef0004b394785b42ba0f Mon Sep 17 00:00:00 2001 From: Zacqary Xeper Date: Mon, 16 Nov 2020 16:09:53 -0600 Subject: [PATCH 12/12] Fix another type issue --- .../node_details/tabs/processes/index.tsx | 35 ++++++++++++++++--- 1 file changed, 31 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/index.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/index.tsx index 29b97236f76c8..836d491e6210e 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/index.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/index.tsx @@ -6,7 +6,7 @@ import React, { useMemo, useState } from 'react'; import { i18n } from '@kbn/i18n'; -import { EuiSearchBar, EuiSpacer, Query } from '@elastic/eui'; +import { EuiSearchBar, EuiSpacer, EuiEmptyPrompt, EuiButton, Query } from '@elastic/eui'; import { useProcessList } from '../../../../hooks/use_process_list'; import { TabContent, TabProps } from '../shared'; import { STATE_NAMES } from './states'; @@ -17,17 +17,44 @@ const TabComponent = ({ currentTime, node, nodeType, options }: TabProps) => { const [searchFilter, setSearchFilter] = useState(EuiSearchBar.Query.MATCH_ALL); const hostTerm = useMemo(() => { - const field = options.fields[nodeType]; + const field = + options.fields && Reflect.has(options.fields, nodeType) + ? Reflect.get(options.fields, nodeType) + : nodeType; return { [field]: node.name }; }, [options, node, nodeType]); - const { loading, error, response } = useProcessList( + const { loading, error, response, makeRequest: reload } = useProcessList( hostTerm, 'metricbeat-*', - options.fields?.timestamp, + options.fields!.timestamp, currentTime ); + if (error) { + return ( + + + {i18n.translate('xpack.infra.metrics.nodeDetails.processListError', { + defaultMessage: 'Unable to show process data', + })} + + } + actions={ + + {i18n.translate('xpack.infra.metrics.nodeDetails.processListRetry', { + defaultMessage: 'Try again', + })} + + } + /> + + ); + } + return (