diff --git a/package.json b/package.json index e4bb9e23dbd49..19247a3a1a2fc 100644 --- a/package.json +++ b/package.json @@ -808,7 +808,7 @@ "antlr4ts": "^0.5.0-alpha.3", "archiver": "^5.3.1", "async": "^3.2.3", - "axios": "^0.27.2", + "axios": "^1.4.0", "base64-js": "^1.3.1", "bitmap-sdf": "^1.0.3", "blurhash": "^2.0.1", diff --git a/packages/kbn-ci-stats-reporter/src/ci_stats_reporter.ts b/packages/kbn-ci-stats-reporter/src/ci_stats_reporter.ts index 0f469de2a4ebe..62da562ad1476 100644 --- a/packages/kbn-ci-stats-reporter/src/ci_stats_reporter.ts +++ b/packages/kbn-ci-stats-reporter/src/ci_stats_reporter.ts @@ -18,9 +18,6 @@ import { REPO_ROOT, kibanaPackageJson } from '@kbn/repo-info'; import { parseConfig, Config, CiStatsMetadata } from '@kbn/ci-stats-core'; import type { SomeDevLog } from '@kbn/some-dev-log'; -// @ts-expect-error not "public", but necessary to prevent Jest shimming from breaking things -import httpAdapter from 'axios/lib/adapters/http'; - import type { CiStatsTestGroupInfo, CiStatsTestRun } from './ci_stats_test_group_types'; const BASE_URL = 'https://ci-stats.kibana.dev'; @@ -375,7 +372,7 @@ export class CiStatsReporter { headers, data: body, params: query, - adapter: httpAdapter, + adapter: 'http', // if it can be serialized into a string, send it maxBodyLength: Infinity, diff --git a/packages/kbn-failed-test-reporter-cli/failed_tests_reporter/github_api.ts b/packages/kbn-failed-test-reporter-cli/failed_tests_reporter/github_api.ts index 5ad0f8417d651..5475dfb9f2da8 100644 --- a/packages/kbn-failed-test-reporter-cli/failed_tests_reporter/github_api.ts +++ b/packages/kbn-failed-test-reporter-cli/failed_tests_reporter/github_api.ts @@ -8,7 +8,7 @@ import Url from 'url'; -import Axios, { AxiosRequestConfig, AxiosInstance } from 'axios'; +import Axios, { AxiosRequestConfig, AxiosInstance, AxiosHeaders, AxiosHeaderValue } from 'axios'; import { isAxiosResponseError, isAxiosRequestError } from '@kbn/dev-utils'; import { ToolingLog } from '@kbn/tooling-log'; @@ -130,7 +130,7 @@ export class GithubApi { ): Promise<{ status: number; statusText: string; - headers: Record; + headers: Record; data: T; }> { const executeRequest = !this.dryRun || options.safeForDryRun; @@ -145,7 +145,7 @@ export class GithubApi { return { status: 200, statusText: 'OK', - headers: {}, + headers: new AxiosHeaders(), data: dryRunResponse, }; } @@ -158,7 +158,7 @@ export class GithubApi { const githubApiFailed = isAxiosResponseError(error) && error.response.status >= 500; const errorResponseLog = isAxiosResponseError(error) && - `[${error.config.method} ${error.config.url}] ${error.response.status} ${error.response.statusText} Error`; + `[${error.config?.method} ${error.config?.url}] ${error.response.status} ${error.response.statusText} Error`; if ((unableToReachGithub || githubApiFailed) && attempt < maxAttempts) { const waitMs = 1000 * attempt; diff --git a/packages/kbn-test/src/jest/resolver.js b/packages/kbn-test/src/jest/resolver.js index 6f964b1478fb7..2723851340ae4 100644 --- a/packages/kbn-test/src/jest/resolver.js +++ b/packages/kbn-test/src/jest/resolver.js @@ -43,6 +43,13 @@ module.exports = (request, options) => { return module.exports(request.replace('@elastic/eui/lib/', '@elastic/eui/test-env/'), options); } + if (request === 'axios') { + return resolve.sync('axios/dist/node/axios.cjs', { + basedir: options.basedir, + extensions: options.extensions, + }); + } + if (request === `elastic-apm-node`) { return APM_AGENT_MOCK; } diff --git a/packages/kbn-test/src/kbn_client/kbn_client_requester.ts b/packages/kbn-test/src/kbn_client/kbn_client_requester.ts index be9ea42d94d66..9481e6481b936 100644 --- a/packages/kbn-test/src/kbn_client/kbn_client_requester.ts +++ b/packages/kbn-test/src/kbn_client/kbn_client_requester.ts @@ -17,7 +17,7 @@ import { KbnClientRequesterError } from './kbn_client_requester_error'; const isConcliftOnGetError = (error: any) => { return ( - isAxiosResponseError(error) && error.config.method === 'GET' && error.response.status === 409 + isAxiosResponseError(error) && error.config?.method === 'GET' && error.response.status === 409 ); }; diff --git a/src/dev/build/lib/download.ts b/src/dev/build/lib/download.ts index ea9f56cb4fe35..e7ee49e72f79f 100644 --- a/src/dev/build/lib/download.ts +++ b/src/dev/build/lib/download.ts @@ -16,10 +16,6 @@ import Axios from 'axios'; import { isAxiosResponseError } from '@kbn/dev-utils'; import { ToolingLog } from '@kbn/tooling-log'; -// https://github.com/axios/axios/tree/ffea03453f77a8176c51554d5f6c3c6829294649/lib/adapters -// @ts-expect-error untyped internal module used to prevent axios from using xhr adapter in tests -import AxiosHttpAdapter from 'axios/lib/adapters/http'; - import { mkdirp } from './fs'; function tryUnlink(path: string) { @@ -78,7 +74,7 @@ export async function downloadToDisk({ const response = await Axios.request({ url, responseType: 'stream', - adapter: AxiosHttpAdapter, + adapter: 'http', }); if (response.status !== 200) { @@ -171,7 +167,7 @@ export async function downloadToString({ const resp = await Axios.request({ url, method: 'GET', - adapter: AxiosHttpAdapter, + adapter: 'http', responseType: 'text', validateStatus: !expectStatus ? undefined : (status) => status === expectStatus, }); diff --git a/x-pack/plugins/actions/server/integration_tests/axios_utils_connection.test.ts b/x-pack/plugins/actions/server/integration_tests/axios_utils_connection.test.ts index 7a5ed95170cc6..21d2f09537727 100644 --- a/x-pack/plugins/actions/server/integration_tests/axios_utils_connection.test.ts +++ b/x-pack/plugins/actions/server/integration_tests/axios_utils_connection.test.ts @@ -53,9 +53,6 @@ const UNAUTHORIZED_CA = fsReadFileSync(UNAUTHORIZED_CA_FILE); const Auth = 'elastic:changeme'; const AuthB64 = Buffer.from(Auth).toString('base64'); -// eslint-disable-next-line @typescript-eslint/no-var-requires -const AxiosDefaultsAadapter = require('axios/lib/adapters/http'); - describe('axios connections', () => { let testServer: http.Server | https.Server | null; // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -65,7 +62,7 @@ describe('axios connections', () => { // needed to prevent the dreaded Error: Cross origin http://localhost forbidden // see: https://github.com/axios/axios/issues/1754#issuecomment-572778305 savedAxiosDefaultsAdapter = axios.defaults.adapter; - axios.defaults.adapter = AxiosDefaultsAadapter; + axios.defaults.adapter = 'http'; }); afterEach(() => { diff --git a/x-pack/plugins/actions/server/integration_tests/axios_utils_proxy.test.ts b/x-pack/plugins/actions/server/integration_tests/axios_utils_proxy.test.ts index fb979968a4636..d5dd8ae91632e 100644 --- a/x-pack/plugins/actions/server/integration_tests/axios_utils_proxy.test.ts +++ b/x-pack/plugins/actions/server/integration_tests/axios_utils_proxy.test.ts @@ -39,9 +39,6 @@ const CA = fsReadFileSync(CA_FILE, 'utf8'); const Auth = 'elastic:changeme'; const AuthB64 = Buffer.from(Auth).toString('base64'); -// eslint-disable-next-line @typescript-eslint/no-var-requires -const AxiosDefaultsAadapter = require('axios/lib/adapters/http'); - const ServerResponse = 'A unique response returned by the server!'; describe('axios connections', () => { @@ -53,7 +50,7 @@ describe('axios connections', () => { // needed to prevent the dreaded Error: Cross origin http://localhost forbidden // see: https://github.com/axios/axios/issues/1754#issuecomment-572778305 savedAxiosDefaultsAdapter = axios.defaults.adapter; - axios.defaults.adapter = AxiosDefaultsAadapter; + axios.defaults.adapter = 'http'; }); afterEach(() => { diff --git a/x-pack/plugins/actions/server/lib/axios_utils.ts b/x-pack/plugins/actions/server/lib/axios_utils.ts index 30232991f3964..181893db15f0c 100644 --- a/x-pack/plugins/actions/server/lib/axios_utils.ts +++ b/x-pack/plugins/actions/server/lib/axios_utils.ts @@ -6,7 +6,14 @@ */ import { isObjectLike, isEmpty } from 'lodash'; -import { AxiosInstance, Method, AxiosResponse, AxiosRequestConfig } from 'axios'; +import { + AxiosInstance, + Method, + AxiosResponse, + AxiosRequestConfig, + AxiosHeaders, + AxiosHeaderValue, +} from 'axios'; import { Logger } from '@kbn/core/server'; import { getCustomAgents } from './get_custom_agents'; import { ActionsConfigurationUtilities } from '../actions_config'; @@ -29,7 +36,7 @@ export const request = async ({ method?: Method; data?: T; configurationUtilities: ActionsConfigurationUtilities; - headers?: Record | null; + headers?: Record; sslOverrides?: SSLSettings; } & AxiosRequestConfig): Promise => { const { httpAgent, httpsAgent } = getCustomAgents( @@ -43,8 +50,7 @@ export const request = async ({ return await axios(url, { ...config, method, - // Axios doesn't support `null` value for `headers` property. - headers: headers ?? undefined, + headers, data: data ?? {}, // use httpAgent and httpsAgent and set axios proxy: false, to be able to handle fail on invalid certs httpAgent, @@ -149,6 +155,10 @@ export const createAxiosResponse = (res: Partial): AxiosResponse status: 200, statusText: 'OK', headers: { ['content-type']: 'application/json' }, - config: { method: 'GET', url: 'https://example.com' }, + config: { + method: 'GET', + url: 'https://example.com', + headers: new AxiosHeaders(), + }, ...res, }); diff --git a/x-pack/plugins/actions/server/sub_action_framework/sub_action_connector.test.ts b/x-pack/plugins/actions/server/sub_action_framework/sub_action_connector.test.ts index 006286cc99a35..5980072217dd5 100644 --- a/x-pack/plugins/actions/server/sub_action_framework/sub_action_connector.test.ts +++ b/x-pack/plugins/actions/server/sub_action_framework/sub_action_connector.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import axios, { AxiosError, AxiosInstance, AxiosResponse } from 'axios'; +import axios, { AxiosError, AxiosHeaders, AxiosInstance, AxiosResponse } from 'axios'; import { loggingSystemMock } from '@kbn/core/server/mocks'; import { MockedLogger } from '@kbn/logging-mocks'; import { actionsConfigMock } from '../actions_config.mock'; @@ -29,7 +29,7 @@ const requestMock = utils.request as jest.Mock; const createAxiosError = (): AxiosError => { const error = new Error() as AxiosError; error.isAxiosError = true; - error.config = { method: 'get', url: 'https://example.com' }; + error.config = { method: 'get', url: 'https://example.com', headers: new AxiosHeaders() }; error.response = { data: { errorMessage: 'An error occurred', errorCode: 500 }, } as AxiosResponse; diff --git a/x-pack/plugins/actions/server/sub_action_framework/sub_action_connector.ts b/x-pack/plugins/actions/server/sub_action_framework/sub_action_connector.ts index 66f29afcd5ef1..f8b395d31ce4a 100644 --- a/x-pack/plugins/actions/server/sub_action_framework/sub_action_connector.ts +++ b/x-pack/plugins/actions/server/sub_action_framework/sub_action_connector.ts @@ -8,7 +8,14 @@ import { isPlainObject, isEmpty } from 'lodash'; import { Type } from '@kbn/config-schema'; import { Logger } from '@kbn/logging'; -import axios, { AxiosInstance, AxiosResponse, AxiosError, AxiosRequestHeaders } from 'axios'; +import axios, { + AxiosInstance, + AxiosResponse, + AxiosError, + AxiosRequestHeaders, + AxiosHeaders, + AxiosHeaderValue, +} from 'axios'; import { SavedObjectsClientContract } from '@kbn/core-saved-objects-api-server'; import { ElasticsearchClient } from '@kbn/core-elasticsearch-server'; import { finished } from 'stream/promises'; @@ -75,7 +82,7 @@ export abstract class SubActionConnector { } } - private getHeaders(headers?: AxiosRequestHeaders) { + private getHeaders(headers?: AxiosRequestHeaders): Record { return { ...headers, 'Content-Type': 'application/json' }; } @@ -130,7 +137,7 @@ export abstract class SubActionConnector { method, data: this.normalizeData(data), configurationUtilities: this.configurationUtilities, - headers: this.getHeaders(headers), + headers: this.getHeaders(headers as AxiosHeaders), }); this.validateResponse(responseSchema, res.data); @@ -139,7 +146,7 @@ export abstract class SubActionConnector { } catch (error) { if (isAxiosError(error)) { this.logger.debug( - `Request to external service failed. Connector Id: ${this.connector.id}. Connector type: ${this.connector.type}. Method: ${error.config.method}. URL: ${error.config.url}` + `Request to external service failed. Connector Id: ${this.connector.id}. Connector type: ${this.connector.type}. Method: ${error.config?.method}. URL: ${error.config?.url}` ); let responseBody = ''; diff --git a/x-pack/plugins/apm/scripts/create_apm_users/create_apm_users_cli.ts b/x-pack/plugins/apm/scripts/create_apm_users/create_apm_users_cli.ts index 24cda99fd669b..752c1dba04e93 100644 --- a/x-pack/plugins/apm/scripts/create_apm_users/create_apm_users_cli.ts +++ b/x-pack/plugins/apm/scripts/create_apm_users/create_apm_users_cli.ts @@ -84,7 +84,7 @@ init().catch((e) => { console.error(e.message); } else if (isAxiosError(e)) { console.error( - `${e.config.method?.toUpperCase() || 'GET'} ${e.config.url} (Code: ${ + `${e.config?.method?.toUpperCase() || 'GET'} ${e.config?.url} (Code: ${ e.response?.status })` ); diff --git a/x-pack/plugins/apm/server/test_helpers/create_apm_users/helpers/get_version.ts b/x-pack/plugins/apm/server/test_helpers/create_apm_users/helpers/get_version.ts index a00747ae78582..070c86dd0be9f 100644 --- a/x-pack/plugins/apm/server/test_helpers/create_apm_users/helpers/get_version.ts +++ b/x-pack/plugins/apm/server/test_helpers/create_apm_users/helpers/get_version.ts @@ -31,17 +31,17 @@ export async function getKibanaVersion({ switch (e.response?.status) { case 401: throw new AbortError( - `Could not access Kibana with the provided credentials. Username: "${e.config.auth?.username}". Password: "${e.config.auth?.password}"` + `Could not access Kibana with the provided credentials. Username: "${e.config?.auth?.username}". Password: "${e.config?.auth?.password}"` ); case 404: throw new AbortError( - `Could not get version on ${e.config.url} (Code: 404)` + `Could not get version on ${e.config?.url} (Code: 404)` ); default: throw new AbortError( - `Cannot access Kibana on ${e.config.baseURL}. Please specify Kibana with: "--kibana-url "` + `Cannot access Kibana on ${e.config?.baseURL}. Please specify Kibana with: "--kibana-url "` ); } } diff --git a/x-pack/plugins/infra/public/components/asset_details/__stories__/context/fixtures/asset_details_props.ts b/x-pack/plugins/infra/public/components/asset_details/__stories__/context/fixtures/asset_details_props.ts new file mode 100644 index 0000000000000..05e17c16eb929 --- /dev/null +++ b/x-pack/plugins/infra/public/components/asset_details/__stories__/context/fixtures/asset_details_props.ts @@ -0,0 +1,72 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; +import { type AssetDetailsProps, FlyoutTabIds, type Tab } from '../../../types'; + +const links: AssetDetailsProps['links'] = ['alertRule', 'nodeDetails', 'apmServices']; +const tabs: Tab[] = [ + { + id: FlyoutTabIds.OVERVIEW, + name: i18n.translate('xpack.infra.nodeDetails.tabs.overview.title', { + defaultMessage: 'Overview', + }), + }, + { + id: FlyoutTabIds.LOGS, + name: i18n.translate('xpack.infra.nodeDetails.tabs.logs', { + defaultMessage: 'Logs', + }), + }, + { + id: FlyoutTabIds.METADATA, + name: i18n.translate('xpack.infra.metrics.nodeDetails.tabs.metadata', { + defaultMessage: 'Metadata', + }), + }, + { + id: FlyoutTabIds.PROCESSES, + name: i18n.translate('xpack.infra.metrics.nodeDetails.tabs.processes', { + defaultMessage: 'Processes', + }), + }, + { + id: FlyoutTabIds.ANOMALIES, + name: i18n.translate('xpack.infra.nodeDetails.tabs.anomalies', { + defaultMessage: 'Anomalies', + }), + }, + { + id: FlyoutTabIds.LINK_TO_APM, + name: i18n.translate('xpack.infra.infra.nodeDetails.apmTabLabel', { + defaultMessage: 'APM', + }), + }, +]; + +export const assetDetailsProps: AssetDetailsProps = { + asset: { + name: 'host1', + id: 'host1', + }, + overrides: { + metadata: { + showActionsColumn: true, + }, + }, + assetType: 'host', + renderMode: { + mode: 'page', + }, + dateRange: { + from: '2023-04-09T11:07:49Z', + to: '2023-04-09T11:23:49Z', + }, + tabs, + links, + metricAlias: 'metrics-*', +}; diff --git a/x-pack/plugins/infra/public/components/asset_details/__stories__/context/fixtures/asset_details_state.ts b/x-pack/plugins/infra/public/components/asset_details/__stories__/context/fixtures/asset_details_state.ts deleted file mode 100644 index 4e88dc368ca0a..0000000000000 --- a/x-pack/plugins/infra/public/components/asset_details/__stories__/context/fixtures/asset_details_state.ts +++ /dev/null @@ -1,37 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type { DataViewField, DataView } from '@kbn/data-views-plugin/common'; -import { UseAssetDetailsStateProps } from '../../../hooks/use_asset_details_state'; - -export const assetDetailsState: UseAssetDetailsStateProps['state'] = { - asset: { - name: 'host1', - id: 'host1-macOS', - ip: '192.168.0.1', - }, - overrides: { - overview: { - metricsDataView: { - id: 'default', - getFieldByName: () => 'hostname' as unknown as DataViewField, - } as unknown as DataView, - logsDataView: { - id: 'default', - getFieldByName: () => 'hostname' as unknown as DataViewField, - } as unknown as DataView, - }, - metadata: { - showActionsColumn: true, - }, - }, - assetType: 'host', - dateRange: { - from: '2023-04-09T11:07:49Z', - to: '2023-04-09T11:23:49Z', - }, -}; diff --git a/x-pack/plugins/infra/public/components/asset_details/__stories__/context/fixtures/index.ts b/x-pack/plugins/infra/public/components/asset_details/__stories__/context/fixtures/index.ts index 00467475b80b4..74ddade7c54ef 100644 --- a/x-pack/plugins/infra/public/components/asset_details/__stories__/context/fixtures/index.ts +++ b/x-pack/plugins/infra/public/components/asset_details/__stories__/context/fixtures/index.ts @@ -15,4 +15,4 @@ export { alertsSummaryHttpResponse, type AlertsSummaryHttpMocks } from './alerts export { anomaliesHttpResponse, type AnomaliesHttpMocks } from './anomalies'; export { snapshotAPItHttpResponse, type SnapshotAPIHttpMocks } from './snapshot_api'; export { getLogEntries } from './log_entries'; -export { assetDetailsState } from './asset_details_state'; +export { assetDetailsProps } from './asset_details_props'; diff --git a/x-pack/plugins/infra/public/components/asset_details/__stories__/decorator.tsx b/x-pack/plugins/infra/public/components/asset_details/__stories__/decorator.tsx index e0a9ca3ec8679..340d314be6ce4 100644 --- a/x-pack/plugins/infra/public/components/asset_details/__stories__/decorator.tsx +++ b/x-pack/plugins/infra/public/components/asset_details/__stories__/decorator.tsx @@ -21,17 +21,25 @@ import type { IKibanaSearchRequest, ISearchOptions } from '@kbn/data-plugin/publ import { AlertSummaryWidget } from '@kbn/triggers-actions-ui-plugin/public/application/sections/alert_summary_widget/alert_summary_widget'; import type { Theme } from '@elastic/charts/dist/utils/themes/theme'; import type { AlertSummaryWidgetProps } from '@kbn/triggers-actions-ui-plugin/public/application/sections/alert_summary_widget'; +import { defaultLogViewAttributes } from '@kbn/logs-shared-plugin/common'; +import { DataView, DataViewField } from '@kbn/data-views-plugin/common'; import type { PluginKibanaContextValue } from '../../../hooks/use_kibana'; import { SourceProvider } from '../../../containers/metrics_source'; import { getHttp } from './context/http'; -import { assetDetailsState, getLogEntries } from './context/fixtures'; -import { AssetDetailsStateProvider } from '../hooks/use_asset_details_state'; +import { assetDetailsProps, getLogEntries } from './context/fixtures'; +import { ContextProviders } from '../context_providers'; +import { DataViewsProvider } from '../hooks/use_data_views'; const settings: Record = { 'dateFormat:scaled': [['', 'HH:mm:ss.SSS']], }; const getSettings = (key: string): any => settings[key]; +const mockDataView = { + id: 'default', + getFieldByName: () => 'hostname' as unknown as DataViewField, +} as unknown as DataView; + export const DecorateWithKibanaContext: DecoratorFn = (story) => { const initialProcesses = useParameter<{ mock: string }>('apiResponse', { mock: 'default', @@ -58,6 +66,9 @@ export const DecorateWithKibanaContext: DecoratorFn = (story) => { }, }, }, + dataViews: { + create: () => Promise.resolve(mockDataView), + }, locators: { nodeLogsLocator: { getRedirectUrl: () => { @@ -111,6 +122,22 @@ export const DecorateWithKibanaContext: DecoratorFn = (story) => { }, }, }, + logsShared: { + logViews: { + client: { + getLogView: () => + Promise.resolve({ + id: 'log', + attributes: defaultLogViewAttributes, + origin: 'internal', + }), + getResolvedLogView: () => + Promise.resolve({ + dataViewReference: mockDataView, + } as any), + }, + }, + }, lens: { navigateToPrefilledEditor: () => {}, stateHelperApi: () => new Promise(() => {}), @@ -130,5 +157,17 @@ export const DecorateWithKibanaContext: DecoratorFn = (story) => { }; export const DecorateWithAssetDetailsStateContext: DecoratorFn = (story) => { - return {story()}; + return ( + + {story()} + + ); }; diff --git a/x-pack/plugins/infra/public/components/asset_details/asset_details.stories.tsx b/x-pack/plugins/infra/public/components/asset_details/asset_details.stories.tsx index 824a4e5f65ef0..0816e25dc800b 100644 --- a/x-pack/plugins/infra/public/components/asset_details/asset_details.stories.tsx +++ b/x-pack/plugins/infra/public/components/asset_details/asset_details.stories.tsx @@ -8,52 +8,11 @@ import React, { useState } from 'react'; import { EuiButton } from '@elastic/eui'; import type { Meta, Story } from '@storybook/react/types-6-0'; -import { i18n } from '@kbn/i18n'; import { AssetDetails } from './asset_details'; import { decorateWithGlobalStorybookThemeProviders } from '../../test_utils/use_global_storybook_theme'; -import { FlyoutTabIds, Tab, type AssetDetailsProps } from './types'; +import { type AssetDetailsProps } from './types'; import { DecorateWithKibanaContext } from './__stories__/decorator'; -import { assetDetailsState } from './__stories__/context/fixtures'; - -const links: AssetDetailsProps['links'] = ['alertRule', 'nodeDetails', 'apmServices']; -const tabs: Tab[] = [ - { - id: FlyoutTabIds.OVERVIEW, - name: i18n.translate('xpack.infra.nodeDetails.tabs.overview.title', { - defaultMessage: 'Overview', - }), - }, - { - id: FlyoutTabIds.LOGS, - name: i18n.translate('xpack.infra.nodeDetails.tabs.logs', { - defaultMessage: 'Logs', - }), - }, - { - id: FlyoutTabIds.METADATA, - name: i18n.translate('xpack.infra.metrics.nodeDetails.tabs.metadata', { - defaultMessage: 'Metadata', - }), - }, - { - id: FlyoutTabIds.PROCESSES, - name: i18n.translate('xpack.infra.metrics.nodeDetails.tabs.processes', { - defaultMessage: 'Processes', - }), - }, - { - id: FlyoutTabIds.ANOMALIES, - name: i18n.translate('xpack.infra.nodeDetails.tabs.anomalies', { - defaultMessage: 'Anomalies', - }), - }, - { - id: FlyoutTabIds.LINK_TO_APM, - name: i18n.translate('xpack.infra.infra.nodeDetails.apmTabLabel', { - defaultMessage: 'APM', - }), - }, -]; +import { assetDetailsProps } from './__stories__/context/fixtures'; const stories: Meta = { title: 'infra/Asset Details View', @@ -61,16 +20,14 @@ const stories: Meta = { component: AssetDetails, argTypes: { links: { - options: links, + options: assetDetailsProps.links, control: { type: 'inline-check', }, }, }, args: { - ...assetDetailsState, - tabs, - links, + ...assetDetailsProps, }, }; @@ -99,11 +56,5 @@ const FlyoutTemplate: Story = (args) => { export const Page = PageTemplate.bind({}); export const Flyout = FlyoutTemplate.bind({}); -Flyout.args = { - renderMode: { - mode: 'flyout', - closeFlyout: () => {}, - }, -}; export default stories; diff --git a/x-pack/plugins/infra/public/components/asset_details/asset_details.tsx b/x-pack/plugins/infra/public/components/asset_details/asset_details.tsx index e55a222b54719..95c5b4836a196 100644 --- a/x-pack/plugins/infra/public/components/asset_details/asset_details.tsx +++ b/x-pack/plugins/infra/public/components/asset_details/asset_details.tsx @@ -6,92 +6,42 @@ */ import React from 'react'; -import { EuiFlyout, EuiFlyoutHeader, EuiFlyoutBody } from '@elastic/eui'; -import useEffectOnce from 'react-use/lib/useEffectOnce'; -import type { AssetDetailsProps, RenderMode } from './types'; -import { Content } from './content/content'; -import { Header } from './header/header'; -import { TabSwitcherProvider, useTabSwitcherContext } from './hooks/use_tab_switcher'; -import { - AssetDetailsStateProvider, - useAssetDetailsStateContext, -} from './hooks/use_asset_details_state'; -import { useKibanaContextForPlugin } from '../../hooks/use_kibana'; -import { ASSET_DETAILS_FLYOUT_COMPONENT_NAME } from './constants'; - -interface ContentTemplateProps { - header: React.ReactElement; - body: React.ReactElement; - renderMode: RenderMode; -} - -const ContentTemplate = ({ header, body, renderMode }: ContentTemplateProps) => { - const { assetType } = useAssetDetailsStateContext(); - const { initialActiveTabId } = useTabSwitcherContext(); - const { - services: { telemetry }, - } = useKibanaContextForPlugin(); - - useEffectOnce(() => { - telemetry.reportAssetDetailsFlyoutViewed({ - componentName: ASSET_DETAILS_FLYOUT_COMPONENT_NAME, - assetType, - tabId: initialActiveTabId, - }); - }); +import type { AssetDetailsProps, ContentTemplateProps, RenderMode } from './types'; +import { Flyout } from './template/flyout'; +import { Page } from './template/page'; +import { ContextProviders } from './context_providers'; +import { TabSwitcherProvider } from './hooks/use_tab_switcher'; +import { DataViewsProvider } from './hooks/use_data_views'; +const ContentTemplate = ({ + header, + renderMode, +}: ContentTemplateProps & { renderMode: RenderMode }) => { return renderMode.mode === 'flyout' ? ( - - {header} - {body} - + ) : ( - <> - {header} - {body} - + ); }; export const AssetDetails = ({ - asset, - dateRange, + tabs, + links, + renderMode, activeTabId, - overrides, - onTabsStateChange, - tabs = [], - links = [], - assetType = 'host', - renderMode = { - mode: 'page', - }, + metricAlias, + ...props }: AssetDetailsProps) => { return ( - + 0 ? activeTabId ?? tabs[0].id : undefined} > - } - body={} - renderMode={renderMode} - /> + + + - + ); }; diff --git a/x-pack/plugins/infra/public/components/asset_details/asset_details_embeddable.tsx b/x-pack/plugins/infra/public/components/asset_details/asset_details_embeddable.tsx index 534ba4c2e4265..3aff0ea4b2548 100644 --- a/x-pack/plugins/infra/public/components/asset_details/asset_details_embeddable.tsx +++ b/x-pack/plugins/infra/public/components/asset_details/asset_details_embeddable.tsx @@ -80,6 +80,7 @@ export class AssetDetailsEmbeddable extends Embeddable diff --git a/x-pack/plugins/infra/public/components/asset_details/constants.ts b/x-pack/plugins/infra/public/components/asset_details/constants.ts index e45a842951a4c..726f47450d0cc 100644 --- a/x-pack/plugins/infra/public/components/asset_details/constants.ts +++ b/x-pack/plugins/infra/public/components/asset_details/constants.ts @@ -7,3 +7,4 @@ export const ASSET_DETAILS_FLYOUT_COMPONENT_NAME = 'infraAssetDetailsFlyout'; export const METRIC_CHART_HEIGHT = 300; +export const APM_HOST_FILTER_FIELD = 'host.hostname'; diff --git a/x-pack/plugins/infra/public/components/asset_details/context_providers.tsx b/x-pack/plugins/infra/public/components/asset_details/context_providers.tsx new file mode 100644 index 0000000000000..179a1bfaf80b3 --- /dev/null +++ b/x-pack/plugins/infra/public/components/asset_details/context_providers.tsx @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { AssetDetailsStateProvider } from './hooks/use_asset_details_state'; +import { DateRangeProvider } from './hooks/use_date_range'; +import { MetadataStateProvider } from './hooks/use_metadata_state'; +import { AssetDetailsProps } from './types'; + +export const ContextProviders = ({ + props, + children, +}: { props: Omit } & { + children: React.ReactNode; +}) => { + const { asset, dateRange, overrides, onTabsStateChange, assetType = 'host', renderMode } = props; + return ( + + + + {children} + + + + ); +}; diff --git a/x-pack/plugins/infra/public/components/asset_details/header/flyout_header.tsx b/x-pack/plugins/infra/public/components/asset_details/header/flyout_header.tsx new file mode 100644 index 0000000000000..05f96a331a07c --- /dev/null +++ b/x-pack/plugins/infra/public/components/asset_details/header/flyout_header.tsx @@ -0,0 +1,87 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { + EuiTitle, + EuiSpacer, + EuiFlexItem, + EuiFlexGroup, + EuiTabs, + EuiTab, + useEuiTheme, + useEuiMinBreakpoint, + type EuiPageHeaderProps, +} from '@elastic/eui'; +import { css } from '@emotion/react'; +type Props = Pick; + +export const FlyoutHeader = ({ title, tabs = [], rightSideItems = [] }: Props) => { + const { euiTheme } = useEuiTheme(); + + return ( + <> + + + +

{title}

+
+
+ + + {rightSideItems?.map((item, index) => ( + + {item} + + ))} + + +
+ + + {tabs.map(({ label, ...tab }, index) => ( + + {label} + + ))} + + + ); +}; diff --git a/x-pack/plugins/infra/public/components/asset_details/header/header.tsx b/x-pack/plugins/infra/public/components/asset_details/header/header.tsx deleted file mode 100644 index 2d9993f0f16d0..0000000000000 --- a/x-pack/plugins/infra/public/components/asset_details/header/header.tsx +++ /dev/null @@ -1,148 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { - EuiTitle, - EuiSpacer, - EuiFlexItem, - EuiFlexGroup, - EuiTabs, - EuiTab, - useEuiTheme, - useEuiMinBreakpoint, -} from '@elastic/eui'; -import { css } from '@emotion/react'; -import { capitalize } from 'lodash'; -import { AssetDetailsProps, FlyoutTabIds, LinkOptions, Tab, TabIds } from '../types'; -import { - LinkToApmServices, - LinkToAlertsRule, - LinkToNodeDetails, - TabToApmTraces, - TabToUptime, -} from '../links'; -import { useTabSwitcherContext } from '../hooks/use_tab_switcher'; -import { useAssetDetailsStateContext } from '../hooks/use_asset_details_state'; -import { toTimestampRange } from '../utils'; - -type Props = Pick & { - compact: boolean; -}; - -const APM_FIELD = 'host.hostname'; - -export const Header = ({ tabs = [], links = [], compact }: Props) => { - const { asset, assetType, overrides, dateRange: timeRange } = useAssetDetailsStateContext(); - const { euiTheme } = useEuiTheme(); - const { showTab, activeTabId } = useTabSwitcherContext(); - - const onTabClick = (tabId: TabIds) => { - showTab(tabId); - }; - - const tabLinkComponents = { - [FlyoutTabIds.LINK_TO_APM]: (tab: Tab) => ( - - ), - [FlyoutTabIds.LINK_TO_UPTIME]: (tab: Tab) => ( - - ), - }; - - const topCornerLinkComponents: Record = { - nodeDetails: ( - - ), - alertRule: , - apmServices: , - }; - - const tabEntries = tabs.map(({ name, ...tab }) => { - if (Object.keys(tabLinkComponents).includes(tab.id)) { - return ( - - {tabLinkComponents[tab.id as keyof typeof tabLinkComponents]({ name, ...tab })} - - ); - } - - return ( - onTabClick(tab.id)} - isSelected={tab.id === activeTabId} - > - {name} - - ); - }); - - return ( - <> - - - - {compact ?

{asset.name}

:

{asset.name}

} -
-
- - - {links?.map((link, index) => ( - - {topCornerLinkComponents[link]} - - ))} - - -
- - - {tabEntries} - - - ); -}; diff --git a/x-pack/plugins/infra/public/components/asset_details/hooks/use_asset_details_state.ts b/x-pack/plugins/infra/public/components/asset_details/hooks/use_asset_details_state.ts index 3ef55cc755b03..c8bd88f126859 100644 --- a/x-pack/plugins/infra/public/components/asset_details/hooks/use_asset_details_state.ts +++ b/x-pack/plugins/infra/public/components/asset_details/hooks/use_asset_details_state.ts @@ -6,66 +6,38 @@ */ import createContainer from 'constate'; -import { useMemo } from 'react'; -import { findInventoryModel } from '../../../../common/inventory_models'; -import { useSourceContext } from '../../../containers/metrics_source'; -import { useMetadata } from './use_metadata'; -import { parseDateRange } from '../../../utils/datemath'; import type { AssetDetailsProps } from '../types'; -import { toTimestampRange } from '../utils'; - -const DEFAULT_DATE_RANGE = { - from: 'now-15m', - to: 'now', -}; +import { useDateRangeProviderContext } from './use_date_range'; +import { useMetadataStateProviderContext } from './use_metadata_state'; export interface UseAssetDetailsStateProps { state: Pick< AssetDetailsProps, - 'asset' | 'assetType' | 'overrides' | 'dateRange' | 'onTabsStateChange' | 'renderMode' + 'asset' | 'assetType' | 'overrides' | 'onTabsStateChange' | 'renderMode' >; } export function useAssetDetailsState({ state }: UseAssetDetailsStateProps) { - const { - asset, - assetType, - dateRange: rawDateRange, - onTabsStateChange, - overrides, - renderMode, - } = state; + const { metadata } = useMetadataStateProviderContext(); + const { dateRange, dateRangeTs } = useDateRangeProviderContext(); + const { asset, assetType, onTabsStateChange, overrides, renderMode } = state; - const dateRange = useMemo(() => { - const { from = DEFAULT_DATE_RANGE.from, to = DEFAULT_DATE_RANGE.to } = - parseDateRange(rawDateRange); - - return { from, to }; - }, [rawDateRange]); - - const dateRangeTs = toTimestampRange(dateRange); - - const inventoryModel = findInventoryModel(assetType); - const { sourceId } = useSourceContext(); - const { - loading: metadataLoading, - error: fetchMetadataError, - metadata, - } = useMetadata(asset.name, assetType, inventoryModel.requiredMetrics, sourceId, dateRangeTs); + // When the asset asset.name is known we can load the page faster + // Otherwise we need to use metadata response. + const loading = !asset.name && !metadata?.name; return { - asset, + asset: { + ...asset, + name: asset.name || metadata?.name || 'asset-name', + }, assetType, dateRange, dateRangeTs, onTabsStateChange, overrides, renderMode, - metadataResponse: { - metadataLoading, - fetchMetadataError, - metadata, - }, + loading, }; } diff --git a/x-pack/plugins/infra/public/components/asset_details/hooks/use_data_views.ts b/x-pack/plugins/infra/public/components/asset_details/hooks/use_data_views.ts new file mode 100644 index 0000000000000..ec299be484f2d --- /dev/null +++ b/x-pack/plugins/infra/public/components/asset_details/hooks/use_data_views.ts @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import useAsync from 'react-use/lib/useAsync'; +import createContainer from 'constate'; +import { useLogViewReference } from '../../../hooks/use_log_view_reference'; +import { useDataMetricsAdHocDataView } from '../../../hooks/use_metrics_adhoc_data_view'; + +const useDataViews = ({ metricAlias }: { metricAlias: string }) => { + const { dataView: metricsDataView, loading: metricsDataViewLoading } = + useDataMetricsAdHocDataView({ metricAlias }); + const { + logViewReference, + getLogsDataView, + loading: logsReferenceLoading, + } = useLogViewReference({ + id: 'asset-details-logs-view', + }); + + const { value: logsDataView, loading: logsDataViewLoading } = useAsync( + () => getLogsDataView(logViewReference), + [logViewReference] + ); + + return { + metrics: { dataView: metricsDataView, loading: metricsDataViewLoading }, + logs: { + dataView: logsDataView, + reference: logViewReference, + loading: logsReferenceLoading || logsDataViewLoading, + }, + }; +}; + +export const [DataViewsProvider, useDataViewsProviderContext] = createContainer(useDataViews); diff --git a/x-pack/plugins/infra/public/components/asset_details/hooks/use_date_range.ts b/x-pack/plugins/infra/public/components/asset_details/hooks/use_date_range.ts new file mode 100644 index 0000000000000..bb20ddab56957 --- /dev/null +++ b/x-pack/plugins/infra/public/components/asset_details/hooks/use_date_range.ts @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import createContainer from 'constate'; +import { useMemo } from 'react'; +import { parseDateRange } from '../../../utils/datemath'; +import type { AssetDetailsProps } from '../types'; +import { toTimestampRange } from '../utils'; + +const DEFAULT_DATE_RANGE = { + from: 'now-15m', + to: 'now', +}; + +export type UseAssetDetailsStateProps = Pick; + +export function useDateRangeProvider({ dateRange: rawDateRange }: UseAssetDetailsStateProps) { + const dateRange = useMemo(() => { + const { from = DEFAULT_DATE_RANGE.from, to = DEFAULT_DATE_RANGE.to } = + parseDateRange(rawDateRange); + + return { from, to }; + }, [rawDateRange]); + + const dateRangeTs = toTimestampRange(dateRange); + + return { + dateRange, + dateRangeTs, + }; +} + +export const [DateRangeProvider, useDateRangeProviderContext] = + createContainer(useDateRangeProvider); diff --git a/x-pack/plugins/infra/public/components/asset_details/hooks/use_metadata_state.ts b/x-pack/plugins/infra/public/components/asset_details/hooks/use_metadata_state.ts new file mode 100644 index 0000000000000..e9a162b2a4c7e --- /dev/null +++ b/x-pack/plugins/infra/public/components/asset_details/hooks/use_metadata_state.ts @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import createContainer from 'constate'; +import { findInventoryModel } from '../../../../common/inventory_models'; +import { useSourceContext } from '../../../containers/metrics_source'; +import { useMetadata } from './use_metadata'; +import { AssetDetailsProps } from '../types'; +import { useDateRangeProviderContext } from './use_date_range'; + +export type UseMetadataProviderProps = Pick; + +export function useMetadataProvider({ asset, assetType }: UseMetadataProviderProps) { + const { dateRangeTs } = useDateRangeProviderContext(); + const inventoryModel = findInventoryModel(assetType); + const { sourceId } = useSourceContext(); + + const { loading, error, metadata } = useMetadata( + asset.id, + assetType, + inventoryModel.requiredMetrics, + sourceId, + dateRangeTs + ); + + return { + loading, + error, + metadata, + }; +} + +export const [MetadataStateProvider, useMetadataStateProviderContext] = + createContainer(useMetadataProvider); diff --git a/x-pack/plugins/infra/public/components/asset_details/hooks/use_page_header.tsx b/x-pack/plugins/infra/public/components/asset_details/hooks/use_page_header.tsx new file mode 100644 index 0000000000000..c7db9b6c4893c --- /dev/null +++ b/x-pack/plugins/infra/public/components/asset_details/hooks/use_page_header.tsx @@ -0,0 +1,169 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useEuiTheme, EuiIcon, type EuiPageHeaderProps } from '@elastic/eui'; +import { css } from '@emotion/react'; +import { useLinkProps } from '@kbn/observability-shared-plugin/public'; +import React, { useCallback, useMemo } from 'react'; +import { uptimeOverviewLocatorID } from '@kbn/observability-plugin/public'; +import { capitalize } from 'lodash'; +import { useKibanaContextForPlugin } from '../../../hooks/use_kibana'; +import { APM_HOST_FILTER_FIELD } from '../constants'; +import { LinkToAlertsRule, LinkToApmServices, LinkToNodeDetails, LinkToUptime } from '../links'; +import { FlyoutTabIds, type LinkOptions, type Tab, type TabIds } from '../types'; +import { toTimestampRange } from '../utils'; +import { useAssetDetailsStateContext } from './use_asset_details_state'; +import { useDateRangeProviderContext } from './use_date_range'; +import { useTabSwitcherContext } from './use_tab_switcher'; +import { useMetadataStateProviderContext } from './use_metadata_state'; + +type TabItem = NonNullable['tabs']>[number]; + +export const usePageHeader = (tabs: Tab[], links?: LinkOptions[]) => { + const { rightSideItems } = useRightSideItems(links); + const { tabEntries } = useTabs(tabs); + + return { rightSideItems, tabEntries }; +}; + +const useRightSideItems = (links?: LinkOptions[]) => { + const { dateRange } = useDateRangeProviderContext(); + const { asset, assetType, overrides } = useAssetDetailsStateContext(); + const { metadata } = useMetadataStateProviderContext(); + + const topCornerLinkComponents: Record = useMemo( + () => ({ + nodeDetails: ( + + ), + alertRule: , + apmServices: , + uptime: ( + + ), + }), + [asset, assetType, dateRange, metadata?.info?.host?.ip, overrides?.alertRule?.onCreateRuleClick] + ); + + const rightSideItems = useMemo( + () => links?.map((link) => topCornerLinkComponents[link]), + [links, topCornerLinkComponents] + ); + + return { rightSideItems }; +}; + +const useTabs = (tabs: Tab[]) => { + const { showTab, activeTabId } = useTabSwitcherContext(); + const { asset, assetType } = useAssetDetailsStateContext(); + const { metadata } = useMetadataStateProviderContext(); + const { share } = useKibanaContextForPlugin().services; + const { euiTheme } = useEuiTheme(); + + const onTabClick = useCallback( + (tabId: TabIds) => { + showTab(tabId); + }, + [showTab] + ); + + const apmTracesMenuItemLinkProps = useLinkProps({ + app: 'apm', + hash: 'traces', + search: { + kuery: `${APM_HOST_FILTER_FIELD}:"${asset.name}"`, + }, + }); + + const getTabToApmTraces = useCallback( + (name: string) => ({ + ...apmTracesMenuItemLinkProps, + 'data-test-subj': 'infraAssetDetailsApmServicesLinkTab', + label: ( + <> + + {name} + + ), + }), + [apmTracesMenuItemLinkProps, euiTheme.size.xs] + ); + + const getTabToUptime = useCallback( + (name: string) => ({ + 'data-test-subj': 'infraAssetDetailsUptimeLinkTab', + onClick: () => + share.url.locators.get(uptimeOverviewLocatorID)!.navigate({ + [assetType]: asset.id, + ip: (Array.isArray(metadata?.info?.host?.ip) + ? metadata?.info?.host?.ip ?? [] + : [metadata?.info?.host?.ip])[0], + }), + label: ( + <> + + {name} + + ), + }), + [asset.id, assetType, euiTheme.size.xs, metadata?.info?.host?.ip, share.url.locators] + ); + + const tabbedLinks = useMemo( + () => ({ + [FlyoutTabIds.LINK_TO_APM]: getTabToApmTraces, + [FlyoutTabIds.LINK_TO_UPTIME]: getTabToUptime, + }), + [getTabToApmTraces, getTabToUptime] + ); + + const tabEntries: TabItem[] = useMemo( + () => + tabs.map(({ name, ...tab }) => { + if (Object.keys(tabbedLinks).includes(tab.id)) { + return tabbedLinks[tab.id as keyof typeof tabbedLinks](name); + } + + if (tab.id === FlyoutTabIds.LINK_TO_APM) { + return getTabToUptime(name); + } + + return { + ...tab, + 'data-test-subj': `infraAssetDetails${capitalize(tab.id)}Tab`, + onClick: () => onTabClick(tab.id), + isSelected: tab.id === activeTabId, + label: name, + }; + }), + [activeTabId, getTabToUptime, onTabClick, tabbedLinks, tabs] + ); + + return { tabEntries }; +}; diff --git a/x-pack/plugins/infra/public/components/asset_details/links/index.ts b/x-pack/plugins/infra/public/components/asset_details/links/index.ts index 074879f4ffd54..fa46176e44e50 100644 --- a/x-pack/plugins/infra/public/components/asset_details/links/index.ts +++ b/x-pack/plugins/infra/public/components/asset_details/links/index.ts @@ -6,7 +6,6 @@ */ export { LinkToApmServices } from './link_to_apm_services'; +export { LinkToUptime } from './link_to_uptime'; export { LinkToAlertsRule } from './link_to_alerts'; export { LinkToNodeDetails } from './link_to_node_details'; -export { TabToApmTraces } from './tab_to_apm_traces'; -export { TabToUptime } from './tab_to_uptime'; diff --git a/x-pack/plugins/infra/public/components/asset_details/links/link_to_apm_services.tsx b/x-pack/plugins/infra/public/components/asset_details/links/link_to_apm_services.tsx index b83f76deea829..59cd4ca72ef1e 100644 --- a/x-pack/plugins/infra/public/components/asset_details/links/link_to_apm_services.tsx +++ b/x-pack/plugins/infra/public/components/asset_details/links/link_to_apm_services.tsx @@ -41,7 +41,7 @@ export const LinkToApmServices = ({ assetName, apmField }: LinkToApmServicesProp > diff --git a/x-pack/plugins/infra/public/components/asset_details/links/link_to_node_details.tsx b/x-pack/plugins/infra/public/components/asset_details/links/link_to_node_details.tsx index 66f212582722c..f53ece1159e45 100644 --- a/x-pack/plugins/infra/public/components/asset_details/links/link_to_node_details.tsx +++ b/x-pack/plugins/infra/public/components/asset_details/links/link_to_node_details.tsx @@ -11,15 +11,16 @@ import { useLinkProps } from '@kbn/observability-shared-plugin/public'; import { getNodeDetailUrl } from '../../../pages/link_to'; import { findInventoryModel } from '../../../../common/inventory_models'; import type { InventoryItemType } from '../../../../common/inventory_models/types'; +import type { Asset } from '../types'; export interface LinkToNodeDetailsProps { currentTimestamp: number; - assetName: string; + asset: Asset; assetType: InventoryItemType; } export const LinkToNodeDetails = ({ - assetName, + asset, assetType, currentTimestamp, }: LinkToNodeDetailsProps) => { @@ -29,9 +30,10 @@ export const LinkToNodeDetails = ({ const nodeDetailMenuItemLinkProps = useLinkProps({ ...getNodeDetailUrl({ nodeType: assetType, - nodeId: assetName, + nodeId: asset.id, from: nodeDetailFrom, to: currentTimestamp, + assetName: asset.name, }), }); diff --git a/x-pack/plugins/infra/public/components/asset_details/links/link_to_uptime.tsx b/x-pack/plugins/infra/public/components/asset_details/links/link_to_uptime.tsx new file mode 100644 index 0000000000000..1800741aaf810 --- /dev/null +++ b/x-pack/plugins/infra/public/components/asset_details/links/link_to_uptime.tsx @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiButtonEmpty } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { uptimeOverviewLocatorID } from '@kbn/observability-plugin/public'; +import { useKibanaContextForPlugin } from '../../../hooks/use_kibana'; +import type { InventoryItemType } from '../../../../common/inventory_models/types'; + +export interface LinkToUptimeProps { + assetType: InventoryItemType; + assetName: string; + ip?: string | null; +} + +export const LinkToUptime = ({ assetType, assetName, ip }: LinkToUptimeProps) => { + const { share } = useKibanaContextForPlugin().services; + + return ( + + share.url.locators.get(uptimeOverviewLocatorID)!.navigate({ [assetType]: assetName, ip }) + } + > + + + ); +}; diff --git a/x-pack/plugins/infra/public/components/asset_details/links/tab_to_apm_traces.tsx b/x-pack/plugins/infra/public/components/asset_details/links/tab_to_apm_traces.tsx deleted file mode 100644 index 538009844cdce..0000000000000 --- a/x-pack/plugins/infra/public/components/asset_details/links/tab_to_apm_traces.tsx +++ /dev/null @@ -1,44 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ -import React from 'react'; -import { css } from '@emotion/react'; -import { EuiIcon, useEuiTheme, EuiTab } from '@elastic/eui'; -import { useLinkProps } from '@kbn/observability-shared-plugin/public'; -import type { Tab } from '../types'; - -export interface LinkToApmServicesProps extends Tab { - assetName: string; - apmField: string; -} - -export const TabToApmTraces = ({ assetName, apmField, name, ...props }: LinkToApmServicesProps) => { - const { euiTheme } = useEuiTheme(); - - const apmTracesMenuItemLinkProps = useLinkProps({ - app: 'apm', - hash: 'traces', - search: { - kuery: `${apmField}:"${assetName}"`, - }, - }); - - return ( - - - {name} - - ); -}; diff --git a/x-pack/plugins/infra/public/components/asset_details/links/tab_to_uptime.tsx b/x-pack/plugins/infra/public/components/asset_details/links/tab_to_uptime.tsx deleted file mode 100644 index af830d499901a..0000000000000 --- a/x-pack/plugins/infra/public/components/asset_details/links/tab_to_uptime.tsx +++ /dev/null @@ -1,51 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { EuiTab, EuiIcon, useEuiTheme } from '@elastic/eui'; -import { css } from '@emotion/react'; -import { uptimeOverviewLocatorID } from '@kbn/observability-plugin/public'; -import { useKibanaContextForPlugin } from '../../../hooks/use_kibana'; -import type { InventoryItemType } from '../../../../common/inventory_models/types'; -import type { Tab } from '../types'; - -export interface LinkToUptimeProps extends Tab { - assetType: InventoryItemType; - assetName: string; - nodeIp?: string | null; -} - -export const TabToUptime = ({ - assetType, - assetName, - nodeIp, - name, - ...props -}: LinkToUptimeProps) => { - const { share } = useKibanaContextForPlugin().services; - const { euiTheme } = useEuiTheme(); - - return ( - - share.url.locators - .get(uptimeOverviewLocatorID)! - .navigate({ [assetType]: assetName, ip: nodeIp }) - } - > - - {name} - - ); -}; diff --git a/x-pack/plugins/infra/public/components/asset_details/tabs/logs/logs.tsx b/x-pack/plugins/infra/public/components/asset_details/tabs/logs/logs.tsx index 35337032805c1..10478af29d86e 100644 --- a/x-pack/plugins/infra/public/components/asset_details/tabs/logs/logs.tsx +++ b/x-pack/plugins/infra/public/components/asset_details/tabs/logs/logs.tsx @@ -18,15 +18,17 @@ import { useKibanaContextForPlugin } from '../../../../hooks/use_kibana'; import { findInventoryFields } from '../../../../../common/inventory_models'; import { InfraLoadingPanel } from '../../../loading'; import { useAssetDetailsStateContext } from '../../hooks/use_asset_details_state'; +import { useDataViewsProviderContext } from '../../hooks/use_data_views'; const TEXT_QUERY_THROTTLE_INTERVAL_MS = 500; export const Logs = () => { const { asset, assetType, overrides, onTabsStateChange, dateRangeTs } = useAssetDetailsStateContext(); + const { logs } = useDataViewsProviderContext(); - const { logView: overrideLogView, query: overrideQuery } = overrides?.logs ?? {}; - const { loading: logViewLoading, reference: logViewReference } = overrideLogView ?? {}; + const { query: overrideQuery } = overrides?.logs ?? {}; + const { loading: logViewLoading, reference: logViewReference } = logs ?? {}; const { services } = useKibanaContextForPlugin(); const { locators } = services; diff --git a/x-pack/plugins/infra/public/components/asset_details/tabs/metadata/metadata.test.tsx b/x-pack/plugins/infra/public/components/asset_details/tabs/metadata/metadata.test.tsx index 9f8a04ef64e6c..291f9911a3f85 100644 --- a/x-pack/plugins/infra/public/components/asset_details/tabs/metadata/metadata.test.tsx +++ b/x-pack/plugins/infra/public/components/asset_details/tabs/metadata/metadata.test.tsx @@ -12,7 +12,7 @@ import { useSourceContext } from '../../../../containers/metrics_source'; import { render } from '@testing-library/react'; import { I18nProvider } from '@kbn/i18n-react'; import { EuiThemeProvider } from '@kbn/kibana-react-plugin/common'; -import { AssetDetailsStateProvider } from '../../hooks/use_asset_details_state'; +import { ContextProviders } from '../../context_providers'; jest.mock('../../../../containers/metrics_source'); jest.mock('../../hooks/use_metadata'); @@ -20,12 +20,8 @@ jest.mock('../../hooks/use_metadata'); const renderHostMetadata = () => render( - showActionsColumn: true, }, }, + dateRange: { + from: '2023-04-09T11:07:49Z', + to: '2023-04-09T11:23:49Z', + }, + renderMode: { + mode: 'page', + }, }} > - + , { wrapper: EuiThemeProvider } ); diff --git a/x-pack/plugins/infra/public/components/asset_details/tabs/metadata/metadata.tsx b/x-pack/plugins/infra/public/components/asset_details/tabs/metadata/metadata.tsx index 70f1bda8c0c68..f3b27b9a8caca 100644 --- a/x-pack/plugins/infra/public/components/asset_details/tabs/metadata/metadata.tsx +++ b/x-pack/plugins/infra/public/components/asset_details/tabs/metadata/metadata.tsx @@ -9,10 +9,9 @@ import React, { useCallback, useMemo } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiCallOut, EuiLink } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; -import { TimeRange } from '@kbn/es-query'; -import type { InventoryItemType } from '../../../../../common/inventory_models/types'; import { Table } from './table'; import { getAllFields } from './utils'; +import { useMetadataStateProviderContext } from '../../hooks/use_metadata_state'; import { useAssetDetailsStateContext } from '../../hooks/use_asset_details_state'; export interface MetadataSearchUrlState { @@ -20,19 +19,14 @@ export interface MetadataSearchUrlState { setMetadataSearchUrlState: (metadataSearch: { metadataSearch?: string }) => void; } -export interface MetadataProps { - assetName: string; - assetType: InventoryItemType; - dateRange: TimeRange; - showActionsColumn?: boolean; - search?: string; - onSearchChange?: (query: string) => void; -} - export const Metadata = () => { - const { overrides, onTabsStateChange, metadataResponse } = useAssetDetailsStateContext(); + const { overrides, onTabsStateChange } = useAssetDetailsStateContext(); + const { + metadata, + loading: metadataLoading, + error: fetchMetadataError, + } = useMetadataStateProviderContext(); const { query, showActionsColumn = false } = overrides?.metadata ?? {}; - const { metadataLoading, fetchMetadataError, metadata } = metadataResponse; const fields = useMemo(() => getAllFields(metadata), [metadata]); diff --git a/x-pack/plugins/infra/public/components/asset_details/tabs/osquery/osquery.tsx b/x-pack/plugins/infra/public/components/asset_details/tabs/osquery/osquery.tsx index f2809c86ae5b9..a71778cb98d19 100644 --- a/x-pack/plugins/infra/public/components/asset_details/tabs/osquery/osquery.tsx +++ b/x-pack/plugins/infra/public/components/asset_details/tabs/osquery/osquery.tsx @@ -8,12 +8,11 @@ import { EuiSkeletonText } from '@elastic/eui'; import React, { useMemo } from 'react'; import { useKibanaContextForPlugin } from '../../../../hooks/use_kibana'; -import { useAssetDetailsStateContext } from '../../hooks/use_asset_details_state'; +import { useMetadataStateProviderContext } from '../../hooks/use_metadata_state'; export const Osquery = () => { - const { metadataResponse } = useAssetDetailsStateContext(); + const { metadata, loading: metadataLoading } = useMetadataStateProviderContext(); - const { metadataLoading, metadata } = metadataResponse; const { services: { osquery }, } = useKibanaContextForPlugin(); diff --git a/x-pack/plugins/infra/public/components/asset_details/tabs/overview/alerts.tsx b/x-pack/plugins/infra/public/components/asset_details/tabs/overview/alerts.tsx index 88c62cfa027c0..cb622fe054009 100644 --- a/x-pack/plugins/infra/public/components/asset_details/tabs/overview/alerts.tsx +++ b/x-pack/plugins/infra/public/components/asset_details/tabs/overview/alerts.tsx @@ -111,12 +111,12 @@ const AlertsSectionTitle = () => { -
+ -
+
diff --git a/x-pack/plugins/infra/public/components/asset_details/tabs/overview/metrics/metrics_grid.tsx b/x-pack/plugins/infra/public/components/asset_details/tabs/overview/metrics/metrics_grid.tsx index f41ec41e51868..609feca5222e2 100644 --- a/x-pack/plugins/infra/public/components/asset_details/tabs/overview/metrics/metrics_grid.tsx +++ b/x-pack/plugins/infra/public/components/asset_details/tabs/overview/metrics/metrics_grid.tsx @@ -94,12 +94,12 @@ const MetricsSectionTitle = () => { -
+ -
+
diff --git a/x-pack/plugins/infra/public/components/asset_details/tabs/overview/overview.tsx b/x-pack/plugins/infra/public/components/asset_details/tabs/overview/overview.tsx index 9036f09ad4257..c8e0e08bba85f 100644 --- a/x-pack/plugins/infra/public/components/asset_details/tabs/overview/overview.tsx +++ b/x-pack/plugins/infra/public/components/asset_details/tabs/overview/overview.tsx @@ -15,18 +15,22 @@ import { AlertsSummaryContent } from './alerts'; import { KPIGrid } from './kpis/kpi_grid'; import { MetricsGrid } from './metrics/metrics_grid'; import { useAssetDetailsStateContext } from '../../hooks/use_asset_details_state'; +import { useMetadataStateProviderContext } from '../../hooks/use_metadata_state'; +import { useDataViewsProviderContext } from '../../hooks/use_data_views'; export const Overview = () => { - const { asset, assetType, overrides, dateRange, renderMode, metadataResponse } = - useAssetDetailsStateContext(); - const { logsDataView, metricsDataView } = overrides?.overview ?? {}; - - const { metadataLoading, fetchMetadataError, metadata } = metadataResponse; + const { asset, assetType, dateRange, renderMode } = useAssetDetailsStateContext(); + const { + metadata, + loading: metadataLoading, + error: fetchMetadataError, + } = useMetadataStateProviderContext(); + const { logs, metrics } = useDataViewsProviderContext(); return ( - + {fetchMetadataError ? ( @@ -71,8 +75,8 @@ export const Overview = () => { diff --git a/x-pack/plugins/infra/public/components/asset_details/template/flyout.tsx b/x-pack/plugins/infra/public/components/asset_details/template/flyout.tsx new file mode 100644 index 0000000000000..9bc9a60777fad --- /dev/null +++ b/x-pack/plugins/infra/public/components/asset_details/template/flyout.tsx @@ -0,0 +1,68 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiFlyout, EuiFlyoutBody, EuiFlyoutHeader } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import React from 'react'; +import useEffectOnce from 'react-use/lib/useEffectOnce'; +import { useKibanaContextForPlugin } from '../../../hooks/use_kibana'; +import { InfraLoadingPanel } from '../../loading'; +import { ASSET_DETAILS_FLYOUT_COMPONENT_NAME } from '../constants'; +import { Content } from '../content/content'; +import { FlyoutHeader } from '../header/flyout_header'; +import { useAssetDetailsStateContext } from '../hooks/use_asset_details_state'; +import { usePageHeader } from '../hooks/use_page_header'; +import { useTabSwitcherContext } from '../hooks/use_tab_switcher'; +import type { ContentTemplateProps } from '../types'; + +export const Flyout = ({ + header: { tabs = [], links = [] }, + closeFlyout, +}: ContentTemplateProps & { closeFlyout: () => void }) => { + const { asset, assetType, loading } = useAssetDetailsStateContext(); + const { rightSideItems, tabEntries } = usePageHeader(tabs, links); + const { initialActiveTabId } = useTabSwitcherContext(); + const { + services: { telemetry }, + } = useKibanaContextForPlugin(); + + useEffectOnce(() => { + telemetry.reportAssetDetailsFlyoutViewed({ + componentName: ASSET_DETAILS_FLYOUT_COMPONENT_NAME, + assetType, + tabId: initialActiveTabId, + }); + }); + + return ( + + {loading ? ( + + ) : ( + <> + + + + + + + + )} + + ); +}; diff --git a/x-pack/plugins/infra/public/components/asset_details/template/page.tsx b/x-pack/plugins/infra/public/components/asset_details/template/page.tsx new file mode 100644 index 0000000000000..a95638bfca55a --- /dev/null +++ b/x-pack/plugins/infra/public/components/asset_details/template/page.tsx @@ -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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiFlexGroup, EuiPageTemplate } from '@elastic/eui'; +import { css } from '@emotion/react'; +import { i18n } from '@kbn/i18n'; +import React from 'react'; +import { useKibanaHeader } from '../../../hooks/use_kibana_header'; +import { InfraLoadingPanel } from '../../loading'; +import { Content } from '../content/content'; +import { useAssetDetailsStateContext } from '../hooks/use_asset_details_state'; +import { usePageHeader } from '../hooks/use_page_header'; +import type { ContentTemplateProps } from '../types'; + +export const Page = ({ header: { tabs = [], links = [] } }: ContentTemplateProps) => { + const { asset, loading } = useAssetDetailsStateContext(); + const { rightSideItems, tabEntries } = usePageHeader(tabs, links); + const { headerHeight } = useKibanaHeader(); + + return loading ? ( + + + + ) : ( + + + + + + + + + ); +}; diff --git a/x-pack/plugins/infra/public/components/asset_details/types.ts b/x-pack/plugins/infra/public/components/asset_details/types.ts index cbf66d66f0a27..44cc7fbb4faf9 100644 --- a/x-pack/plugins/infra/public/components/asset_details/types.ts +++ b/x-pack/plugins/infra/public/components/asset_details/types.ts @@ -5,18 +5,13 @@ * 2.0. */ -import type { DataView } from '@kbn/data-views-plugin/public'; -import type { LogViewReference } from '@kbn/logs-shared-plugin/common'; import { TimeRange } from '@kbn/es-query'; import type { InventoryItemType } from '../../../common/inventory_models/types'; -interface Metadata { - ip?: string | null; -} -export type Asset = Metadata & { +export interface Asset { id: string; - name: string; -}; + name?: string; +} export enum FlyoutTabIds { OVERVIEW = 'overview', @@ -32,10 +27,6 @@ export enum FlyoutTabIds { export type TabIds = `${FlyoutTabIds}`; export interface TabState { - overview?: { - metricsDataView?: DataView; - logsDataView?: DataView; - }; metadata?: { query?: string; showActionsColumn?: boolean; @@ -51,10 +42,6 @@ export interface TabState { }; logs?: { query?: string; - logView?: { - reference?: LogViewReference | null; - loading?: boolean; - }; }; } @@ -74,7 +61,7 @@ export interface Tab { name: string; } -export type LinkOptions = 'alertRule' | 'nodeDetails' | 'apmServices'; +export type LinkOptions = 'alertRule' | 'nodeDetails' | 'apmServices' | 'uptime'; export interface AssetDetailsProps { asset: Asset; @@ -83,9 +70,16 @@ export interface AssetDetailsProps { tabs: Tab[]; activeTabId?: TabIds; overrides?: TabState; - renderMode?: RenderMode; + renderMode: RenderMode; onTabsStateChange?: TabsStateChangeFn; links?: LinkOptions[]; + // This is temporary. Once we start using the asset details in other plugins, + // It will have to retrieve the metricAlias internally rather than receive it via props + metricAlias: string; } export type TabsStateChangeFn = (state: TabState & { activeTabId?: TabIds }) => void; + +export interface ContentTemplateProps { + header: Pick; +} diff --git a/x-pack/plugins/infra/public/components/lens/lens_chart.tsx b/x-pack/plugins/infra/public/components/lens/lens_chart.tsx index 2d4b599d56de2..abcbe555a5d6a 100644 --- a/x-pack/plugins/infra/public/components/lens/lens_chart.tsx +++ b/x-pack/plugins/infra/public/components/lens/lens_chart.tsx @@ -87,7 +87,9 @@ export const LensChart = ({ })} anchorClassName="eui-fullWidth" > - {Lens} + {/* EuiToolTip forwards some event handlers to the child component. + Wrapping Lens inside a div prevents that from causing unnecessary re-renders */} +
{Lens}
); }; diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_log_view_reference.ts b/x-pack/plugins/infra/public/hooks/use_log_view_reference.ts similarity index 94% rename from x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_log_view_reference.ts rename to x-pack/plugins/infra/public/hooks/use_log_view_reference.ts index 5d10bc48dce5d..d8c063767a8d1 100644 --- a/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_log_view_reference.ts +++ b/x-pack/plugins/infra/public/hooks/use_log_view_reference.ts @@ -9,8 +9,8 @@ import useAsync from 'react-use/lib/useAsync'; import { v4 as uuidv4 } from 'uuid'; import { DEFAULT_LOG_VIEW, LogViewReference } from '@kbn/logs-shared-plugin/common'; import { useCallback } from 'react'; -import { useLazyRef } from '../../../../hooks/use_lazy_ref'; -import { useKibanaContextForPlugin } from '../../../../hooks/use_kibana'; +import { useLazyRef } from './use_lazy_ref'; +import { useKibanaContextForPlugin } from './use_kibana'; interface Props { id: string; diff --git a/x-pack/plugins/infra/public/hooks/use_metrics_adhoc_data_view.ts b/x-pack/plugins/infra/public/hooks/use_metrics_adhoc_data_view.ts new file mode 100644 index 0000000000000..1761297e9dcff --- /dev/null +++ b/x-pack/plugins/infra/public/hooks/use_metrics_adhoc_data_view.ts @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { v5 as uuidv5 } from 'uuid'; +import useAsyncRetry from 'react-use/lib/useAsyncRetry'; +import { useKibanaContextForPlugin } from './use_kibana'; + +export const TIMESTAMP_FIELD = '@timestamp'; +export const DATA_VIEW_PREFIX = 'infra_metrics'; + +export const generateDataViewId = (indexPattern: string) => { + // generates a unique but the same uuid as long as the index pattern doesn't change + return `${DATA_VIEW_PREFIX}_${uuidv5(indexPattern, uuidv5.DNS)}`; +}; + +export const useDataMetricsAdHocDataView = ({ metricAlias }: { metricAlias: string }) => { + const { + services: { dataViews }, + } = useKibanaContextForPlugin(); + + const state = useAsyncRetry(() => { + return dataViews.create({ + id: generateDataViewId(metricAlias), + title: metricAlias, + timeFieldName: TIMESTAMP_FIELD, + }); + }, [metricAlias]); + + const { value: dataView, loading, error, retry } = state; + + return { + metricAlias, + dataView, + loading, + loadDataView: retry, + error, + }; +}; diff --git a/x-pack/plugins/infra/public/pages/link_to/query_params.ts b/x-pack/plugins/infra/public/pages/link_to/query_params.ts index 45e1bc9a7991d..e071ec0a82e34 100644 --- a/x-pack/plugins/infra/public/pages/link_to/query_params.ts +++ b/x-pack/plugins/infra/public/pages/link_to/query_params.ts @@ -28,3 +28,8 @@ export const getFromFromLocation = (location: Location) => { const timeParam = getParamFromQueryString(getQueryStringFromLocation(location), 'from'); return timeParam ? parseFloat(timeParam) : NaN; }; + +export const getNodeNameFromLocation = (location: Location) => { + const nameParam = getParamFromQueryString(getQueryStringFromLocation(location), 'assetName'); + return nameParam; +}; diff --git a/x-pack/plugins/infra/public/pages/link_to/redirect_to_node_detail.tsx b/x-pack/plugins/infra/public/pages/link_to/redirect_to_node_detail.tsx index 412585fec9cc7..8045c95fe78d6 100644 --- a/x-pack/plugins/infra/public/pages/link_to/redirect_to_node_detail.tsx +++ b/x-pack/plugins/infra/public/pages/link_to/redirect_to_node_detail.tsx @@ -10,7 +10,7 @@ import { Redirect, RouteComponentProps } from 'react-router-dom'; import { LinkDescriptor } from '@kbn/observability-shared-plugin/public'; import { replaceMetricTimeInQueryString } from '../metrics/metric_detail/hooks/use_metrics_time'; -import { getFromFromLocation, getToFromLocation } from './query_params'; +import { getFromFromLocation, getToFromLocation, getNodeNameFromLocation } from './query_params'; import { InventoryItemType } from '../../../common/inventory_models/types'; type RedirectToNodeDetailProps = RouteComponentProps<{ @@ -29,7 +29,16 @@ export const RedirectToNodeDetail = ({ getToFromLocation(location) )(''); - return ; + const queryParams = new URLSearchParams(searchString); + + if (nodeType === 'host') { + const assetName = getNodeNameFromLocation(location); + if (assetName) { + queryParams.set('assetName', assetName); + } + } + + return ; }; export const getNodeDetailUrl = ({ @@ -37,21 +46,25 @@ export const getNodeDetailUrl = ({ nodeId, to, from, + assetName, }: { nodeType: InventoryItemType; nodeId: string; to?: number; from?: number; + assetName?: string; }): LinkDescriptor => { return { app: 'metrics', pathname: `link-to/${nodeType}-detail/${nodeId}`, - search: - to && from + search: { + ...(assetName ? { assetName } : undefined), + ...(to && from ? { to: `${to}`, from: `${from}`, } - : undefined, + : undefined), + }, }; }; diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/components/host_details_flyout/flyout_wrapper.tsx b/x-pack/plugins/infra/public/pages/metrics/hosts/components/host_details_flyout/flyout_wrapper.tsx index 884e0dd389cd3..6daf6b36e5609 100644 --- a/x-pack/plugins/infra/public/pages/metrics/hosts/components/host_details_flyout/flyout_wrapper.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/components/host_details_flyout/flyout_wrapper.tsx @@ -6,45 +6,31 @@ */ import React from 'react'; -import useAsync from 'react-use/lib/useAsync'; + +import { useSourceContext } from '../../../../../containers/metrics_source'; import { useUnifiedSearchContext } from '../../hooks/use_unified_search'; import type { HostNodeRow } from '../../hooks/use_hosts_table'; import { HostFlyout, useHostFlyoutUrlState } from '../../hooks/use_host_flyout_url_state'; import { AssetDetails } from '../../../../../components/asset_details/asset_details'; import { orderedFlyoutTabs } from './tabs'; -import { useLogViewReference } from '../../hooks/use_log_view_reference'; -import { useMetricsDataViewContext } from '../../hooks/use_data_view'; export interface Props { node: HostNodeRow; closeFlyout: () => void; } -export const FlyoutWrapper = ({ node, closeFlyout }: Props) => { +export const FlyoutWrapper = ({ node: { name }, closeFlyout }: Props) => { + const { source } = useSourceContext(); const { searchCriteria } = useUnifiedSearchContext(); - const { dataView } = useMetricsDataViewContext(); - const { logViewReference, loading, getLogsDataView } = useLogViewReference({ - id: 'hosts-flyout-logs-view', - }); - - const { value: logsDataView } = useAsync( - () => getLogsDataView(logViewReference), - [logViewReference] - ); - const [hostFlyoutState, setHostFlyoutState] = useHostFlyoutUrlState(); - return ( + return source ? ( { }, logs: { query: hostFlyoutState?.logsSearch, - logView: { - reference: logViewReference, - loading, - }, }, }} onTabsStateChange={(state) => @@ -74,6 +56,7 @@ export const FlyoutWrapper = ({ node, closeFlyout }: Props) => { mode: 'flyout', closeFlyout, }} + metricAlias={source.configuration.metricAlias} /> - ); + ) : null; }; diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/components/table/entry_title.tsx b/x-pack/plugins/infra/public/pages/metrics/hosts/components/table/entry_title.tsx index 27739b3ed4a49..b853ca3c8f9b1 100644 --- a/x-pack/plugins/infra/public/pages/metrics/hosts/components/table/entry_title.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/components/table/entry_title.tsx @@ -32,6 +32,7 @@ export const EntryTitle = ({ onClick, time, title }: EntryTitleProps) => { pathname: `/detail/host/${name}`, search: { _a: encode({ time: { ...time, interval: '>=1m' } }), + assetName: name, }, }); diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/components/tabs/logs/logs_tab_content.tsx b/x-pack/plugins/infra/public/pages/metrics/hosts/components/tabs/logs/logs_tab_content.tsx index 15ab7b9f0eca6..331cc35722998 100644 --- a/x-pack/plugins/infra/public/pages/metrics/hosts/components/tabs/logs/logs_tab_content.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/components/tabs/logs/logs_tab_content.tsx @@ -16,7 +16,7 @@ import { useLogsSearchUrlState } from '../../../hooks/use_logs_search_url_state' import { LogsLinkToStream } from './logs_link_to_stream'; import { LogsSearchBar } from './logs_search_bar'; import { buildCombinedHostsFilter } from '../../../../../../utils/filters/build'; -import { useLogViewReference } from '../../../hooks/use_log_view_reference'; +import { useLogViewReference } from '../../../../../../hooks/use_log_view_reference'; export const LogsTabContent = () => { const [filterQuery] = useLogsSearchUrlState(); diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/constants.ts b/x-pack/plugins/infra/public/pages/metrics/hosts/constants.ts index aace07448692e..997ae6fd5ed66 100644 --- a/x-pack/plugins/infra/public/pages/metrics/hosts/constants.ts +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/constants.ts @@ -7,9 +7,6 @@ import { HostLimitOptions } from './types'; -export const TIMESTAMP_FIELD = '@timestamp'; -export const DATA_VIEW_PREFIX = 'infra_metrics'; - export const DEFAULT_HOST_LIMIT: HostLimitOptions = 100; export const DEFAULT_PAGE_SIZE = 10; export const LOCAL_STORAGE_HOST_LIMIT_KEY = 'hostsView:hostLimitSelection'; diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_data_view.ts b/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_data_view.ts index 66bbe02c9932a..b7b3103dacfc4 100644 --- a/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_data_view.ts +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_data_view.ts @@ -5,31 +5,16 @@ * 2.0. */ -import { v5 as uuidv5 } from 'uuid'; import createContainer from 'constate'; -import useAsyncRetry from 'react-use/lib/useAsyncRetry'; -import { useKibanaContextForPlugin } from '../../../../hooks/use_kibana'; -import { DATA_VIEW_PREFIX, TIMESTAMP_FIELD } from '../constants'; - -export const generateDataViewId = (indexPattern: string) => { - // generates a unique but the same uuid as long as the index pattern doesn't change - return `${DATA_VIEW_PREFIX}_${uuidv5(indexPattern, uuidv5.DNS)}`; -}; +import { useDataMetricsAdHocDataView } from '../../../../hooks/use_metrics_adhoc_data_view'; export const useDataView = ({ metricAlias }: { metricAlias: string }) => { const { - services: { dataViews }, - } = useKibanaContextForPlugin(); - - const state = useAsyncRetry(() => { - return dataViews.create({ - id: generateDataViewId(metricAlias), - title: metricAlias, - timeFieldName: TIMESTAMP_FIELD, - }); - }, [metricAlias]); - - const { value: dataView, loading, error, retry } = state; + dataView, + loading, + loadDataView: retry, + error, + } = useDataMetricsAdHocDataView({ metricAlias }); return { metricAlias, 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 3314d06fc4693..74fd472629198 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 @@ -83,6 +83,7 @@ export const NodeContextPopover = ({ nodeId: node.id, from: nodeDetailFrom, to: currentTime, + assetName: node.name, }), }); const apmField = nodeType === 'host' ? 'host.hostname' : inventoryModel.fields.id; diff --git a/x-pack/plugins/infra/public/pages/metrics/metric_detail/asset_detail_page.tsx b/x-pack/plugins/infra/public/pages/metrics/metric_detail/asset_detail_page.tsx new file mode 100644 index 0000000000000..e28650aaf5cde --- /dev/null +++ b/x-pack/plugins/infra/public/pages/metrics/metric_detail/asset_detail_page.tsx @@ -0,0 +1,124 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { TimeRange } from '@kbn/es-query'; +import React, { useMemo } from 'react'; +import { useLocation, useRouteMatch } from 'react-router-dom'; +import { i18n } from '@kbn/i18n'; +import { NoRemoteCluster } from '../../../components/empty_states'; +import { SourceErrorPage } from '../../../components/source_error_page'; +import { SourceLoadingPage } from '../../../components/source_loading_page'; +import { useSourceContext } from '../../../containers/metrics_source'; +import { FlyoutTabIds, type Tab } from '../../../components/asset_details/types'; +import type { InventoryItemType } from '../../../../common/inventory_models/types'; +import { AssetDetails } from '../../../components/asset_details/asset_details'; +import { useMetricsTimeContext } from './hooks/use_metrics_time'; +import { MetricsPageTemplate } from '../page_template'; + +const orderedFlyoutTabs: Tab[] = [ + { + id: FlyoutTabIds.OVERVIEW, + name: i18n.translate('xpack.infra.nodeDetails.tabs.overview.title', { + defaultMessage: 'Overview', + }), + }, + { + id: FlyoutTabIds.METADATA, + name: i18n.translate('xpack.infra.nodeDetails.tabs.metadata.title', { + defaultMessage: 'Metadata', + }), + }, + { + id: FlyoutTabIds.PROCESSES, + name: i18n.translate('xpack.infra.metrics.nodeDetails.tabs.processes', { + defaultMessage: 'Processes', + }), + }, + { + id: FlyoutTabIds.LOGS, + name: i18n.translate('xpack.infra.nodeDetails.tabs.logs.title', { + defaultMessage: 'Logs', + }), + }, + { + id: FlyoutTabIds.ANOMALIES, + name: i18n.translate('xpack.infra.nodeDetails.tabs.anomalies', { + defaultMessage: 'Anomalies', + }), + }, + { + id: FlyoutTabIds.OSQUERY, + name: i18n.translate('xpack.infra.nodeDetails.tabs.osquery', { + defaultMessage: 'Osquery', + }), + }, +]; + +export const AssetDetailPage = () => { + const { isLoading, loadSourceFailureMessage, loadSource, source } = useSourceContext(); + const { + params: { type: nodeType, node: nodeId }, + } = useRouteMatch<{ type: InventoryItemType; node: string }>(); + const { search } = useLocation(); + + const assetName = useMemo(() => { + const queryParams = new URLSearchParams(search); + return queryParams.get('assetName') ?? undefined; + }, [search]); + + const { parsedTimeRange } = useMetricsTimeContext(); + + const dateRange: TimeRange = useMemo( + () => ({ + from: new Date(parsedTimeRange.from).toISOString(), + to: new Date(parsedTimeRange.to).toISOString(), + }), + [parsedTimeRange.from, parsedTimeRange.to] + ); + + const { metricIndicesExist, remoteClustersExist } = source?.status ?? {}; + + if (isLoading || !source) return ; + + if (!remoteClustersExist) { + return ; + } + + if (!metricIndicesExist) { + return ( + + ); + } + + if (loadSourceFailureMessage) + return ; + + return ( + + + + ); +}; diff --git a/x-pack/plugins/infra/public/pages/metrics/metric_detail/index.tsx b/x-pack/plugins/infra/public/pages/metrics/metric_detail/index.tsx index e4819242c011d..36a7d3d853eb7 100644 --- a/x-pack/plugins/infra/public/pages/metrics/metric_detail/index.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/metric_detail/index.tsx @@ -5,122 +5,26 @@ * 2.0. */ -import { i18n } from '@kbn/i18n'; -import React, { useState } from 'react'; -import { EuiTheme, withTheme } from '@kbn/kibana-react-plugin/common'; -import { useLinkProps } from '@kbn/observability-shared-plugin/public'; -import { withMetricPageProviders } from './page_providers'; -import { useMetadata } from '../../../components/asset_details/hooks/use_metadata'; -import { useMetricsBreadcrumbs } from '../../../hooks/use_metrics_breadcrumbs'; -import { useSourceContext } from '../../../containers/metrics_source'; -import { InfraLoadingPanel } from '../../../components/loading'; -import { findInventoryModel } from '../../../../common/inventory_models'; -import { NavItem } from './lib/side_nav_context'; -import { NodeDetailsPage } from './components/node_details_page'; -import { InventoryItemType } from '../../../../common/inventory_models/types'; -import { useMetricsTimeContext } from './hooks/use_metrics_time'; -import { MetricsPageTemplate } from '../page_template'; -import { inventoryTitle } from '../../../translations'; - -interface Props { - theme: EuiTheme | undefined; - match: { - params: { - type: string; - node: string; - }; - }; -} - -export const MetricDetail = withMetricPageProviders( - withTheme(({ match }: Props) => { - const nodeId = match.params.node; - const nodeType = match.params.type as InventoryItemType; - const inventoryModel = findInventoryModel(nodeType); - const { sourceId, metricIndicesExist } = useSourceContext(); - - const { - timeRange, - parsedTimeRange, - setTimeRange, - refreshInterval, - setRefreshInterval, - isAutoReloading, - setAutoReload, - triggerRefresh, - } = useMetricsTimeContext(); - const { - name, - filteredRequiredMetrics, - loading: metadataLoading, - cloudId, - metadata, - } = useMetadata(nodeId, nodeType, inventoryModel.requiredMetrics, sourceId, parsedTimeRange); - - const [sideNav, setSideNav] = useState([]); - - const addNavItem = React.useCallback( - (item: NavItem) => { - if (!sideNav.some((n) => n.id === item.id)) { - setSideNav([item, ...sideNav]); - } - }, - [sideNav] - ); - - const inventoryLinkProps = useLinkProps({ - app: 'metrics', - pathname: '/inventory', - }); - - useMetricsBreadcrumbs([ - { - ...inventoryLinkProps, - text: inventoryTitle, - }, - { - text: name, - }, - ]); - - if (metadataLoading && !filteredRequiredMetrics.length) { - return ( - - - - ); - } - - return ( - <> - {metadata ? ( - - ) : null} - - ); - }) -); +import { EuiErrorBoundary } from '@elastic/eui'; +import React from 'react'; +import { useRouteMatch } from 'react-router-dom'; +import type { InventoryItemType } from '../../../../common/inventory_models/types'; +import { AssetDetailPage } from './asset_detail_page'; +import { MetricsTimeProvider } from './hooks/use_metrics_time'; +import { MetricDetailPage } from './metric_detail_page'; + +export const MetricDetail = () => { + const { + params: { type: nodeType }, + } = useRouteMatch<{ type: InventoryItemType; node: string }>(); + + const PageContent = () => (nodeType === 'host' ? : ); + + return ( + + + + + + ); +}; diff --git a/x-pack/plugins/infra/public/pages/metrics/metric_detail/metric_detail_page.tsx b/x-pack/plugins/infra/public/pages/metrics/metric_detail/metric_detail_page.tsx new file mode 100644 index 0000000000000..6823147c8b6b0 --- /dev/null +++ b/x-pack/plugins/infra/public/pages/metrics/metric_detail/metric_detail_page.tsx @@ -0,0 +1,114 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; +import React, { useState } from 'react'; +import { useLinkProps } from '@kbn/observability-shared-plugin/public'; +import { useRouteMatch } from 'react-router-dom'; +import { useMetadata } from '../../../components/asset_details/hooks/use_metadata'; +import { useMetricsBreadcrumbs } from '../../../hooks/use_metrics_breadcrumbs'; +import { useSourceContext } from '../../../containers/metrics_source'; +import { InfraLoadingPanel } from '../../../components/loading'; +import { findInventoryModel } from '../../../../common/inventory_models'; +import type { NavItem } from './lib/side_nav_context'; +import { NodeDetailsPage } from './components/node_details_page'; +import type { InventoryItemType } from '../../../../common/inventory_models/types'; +import { useMetricsTimeContext } from './hooks/use_metrics_time'; +import { MetricsPageTemplate } from '../page_template'; +import { inventoryTitle } from '../../../translations'; + +export const MetricDetailPage = () => { + const { + params: { type: nodeType, node: nodeId }, + } = useRouteMatch<{ type: InventoryItemType; node: string }>(); + const inventoryModel = findInventoryModel(nodeType); + const { sourceId, metricIndicesExist } = useSourceContext(); + + const { + timeRange, + parsedTimeRange, + setTimeRange, + refreshInterval, + setRefreshInterval, + isAutoReloading, + setAutoReload, + triggerRefresh, + } = useMetricsTimeContext(); + const { + name, + filteredRequiredMetrics, + loading: metadataLoading, + cloudId, + metadata, + } = useMetadata(nodeId, nodeType, inventoryModel.requiredMetrics, sourceId, parsedTimeRange); + + const [sideNav, setSideNav] = useState([]); + + const addNavItem = React.useCallback( + (item: NavItem) => { + if (!sideNav.some((n) => n.id === item.id)) { + setSideNav([item, ...sideNav]); + } + }, + [sideNav] + ); + + const inventoryLinkProps = useLinkProps({ + app: 'metrics', + pathname: '/inventory', + }); + + useMetricsBreadcrumbs([ + { + ...inventoryLinkProps, + text: inventoryTitle, + }, + { + text: name, + }, + ]); + + if (metadataLoading && !filteredRequiredMetrics.length) { + return ( + + + + ); + } + + return ( + <> + {metadata ? ( + + ) : null} + + ); +}; diff --git a/x-pack/plugins/profiling/common/calculate_impact_estimates/calculate_impact_estimates.test.ts b/x-pack/plugins/profiling/common/calculate_impact_estimates/calculate_impact_estimates.test.ts index 626ec8a26dfc2..a1abe24a79faf 100644 --- a/x-pack/plugins/profiling/common/calculate_impact_estimates/calculate_impact_estimates.test.ts +++ b/x-pack/plugins/profiling/common/calculate_impact_estimates/calculate_impact_estimates.test.ts @@ -16,35 +16,36 @@ describe('calculateImpactEstimates', () => { }); expect(totalCPU).toEqual({ - annualizedCo2: 17.909333333333336, - annualizedCoreSeconds: 1752000, - annualizedDollarCost: 20.683333333333334, - co2: 0.0005111111111111112, + percentage: 0.1, coreSeconds: 50, + annualizedCoreSeconds: 1752000, + co2: 0.00006265168194444443, + annualizedCo2: 2.1953149353333328, dollarCost: 0.0005902777777777778, - percentage: 0.1, + annualizedDollarCost: 20.683333333333334, }); expect(selfCPU).toEqual({ - annualizedCo2: 8.954666666666668, - annualizedCoreSeconds: 876000, - annualizedDollarCost: 10.341666666666667, - co2: 0.0002555555555555556, + percentage: 0.05, coreSeconds: 25, + annualizedCoreSeconds: 876000, + co2: 0.000031325840972222215, + annualizedCo2: 1.0976574676666664, dollarCost: 0.0002951388888888889, - percentage: 0.05, + annualizedDollarCost: 10.341666666666667, }); expect(totalSamples).toEqual({ percentage: 1, coreSeconds: 500, annualizedCoreSeconds: 17520000, - co2: 0.005111111111111111, - annualizedCo2: 179.09333333333333, + co2: 0.0006265168194444444, + annualizedCo2: 21.95314935333333, dollarCost: 0.0059027777777777785, annualizedDollarCost: 206.83333333333337, }); }); + it('calculates impact', () => { const { selfCPU, totalCPU, totalSamples } = calculateImpactEstimates({ countExclusive: 1000, @@ -54,31 +55,31 @@ describe('calculateImpactEstimates', () => { }); expect(totalCPU).toEqual({ - annualizedCo2: 17.909333333333336, - annualizedCoreSeconds: 1752000, - annualizedDollarCost: 20.683333333333334, - co2: 0.0005111111111111112, + percentage: 0.1, coreSeconds: 50, + annualizedCoreSeconds: 1752000, + co2: 0.00006265168194444443, + annualizedCo2: 2.1953149353333328, dollarCost: 0.0005902777777777778, - percentage: 0.1, + annualizedDollarCost: 20.683333333333334, }); expect(selfCPU).toEqual({ - annualizedCo2: 17.909333333333336, - annualizedCoreSeconds: 1752000, - annualizedDollarCost: 20.683333333333334, - co2: 0.0005111111111111112, + percentage: 0.1, coreSeconds: 50, + annualizedCoreSeconds: 1752000, + co2: 0.00006265168194444443, + annualizedCo2: 2.1953149353333328, dollarCost: 0.0005902777777777778, - percentage: 0.1, + annualizedDollarCost: 20.683333333333334, }); expect(totalSamples).toEqual({ percentage: 1, coreSeconds: 500, annualizedCoreSeconds: 17520000, - co2: 0.005111111111111111, - annualizedCo2: 179.09333333333333, + co2: 0.0006265168194444444, + annualizedCo2: 21.95314935333333, dollarCost: 0.0059027777777777785, annualizedDollarCost: 206.83333333333337, }); diff --git a/x-pack/plugins/profiling/common/calculate_impact_estimates/index.ts b/x-pack/plugins/profiling/common/calculate_impact_estimates/index.ts index 70cfdc109a107..6ebbe26a9fb43 100644 --- a/x-pack/plugins/profiling/common/calculate_impact_estimates/index.ts +++ b/x-pack/plugins/profiling/common/calculate_impact_estimates/index.ts @@ -7,13 +7,20 @@ const ANNUAL_SECONDS = 60 * 60 * 24 * 365; -// The assumed amortized per-core average power consumption. -const PER_CORE_WATT = 40; +// The assumed amortized per-core average power consumption (based on 100% CPU Utilization). +// Reference: https://www.cloudcarbonfootprint.org/docs/methodology/#appendix-i-energy-coefficients +const PER_CORE_WATT = 7; -// The assumed CO2 emissions per KWH (sourced from www.eia.gov) -const CO2_PER_KWH = 0.92; +// The assumed CO2 emissions in kg per kWh (the reference uses metric tons/kWh). +// This value represents "regional carbon intensity" and it defaults to AWS us-east-1. +// Reference: https://www.cloudcarbonfootprint.org/docs/methodology/#appendix-v-grid-emissions-factors +const CO2_PER_KWH = 0.379069; -// The cost of a CPU core per hour, in dollars +// The assumed PUE of the datacenter (1.7 is likely to be an on-prem value). +const DATACENTER_PUE = 1.7; + +// The cost of an x86 CPU core per hour, in US$. +// (ARM is 60% less based graviton 3 data, see https://aws.amazon.com/ec2/graviton/) const CORE_COST_PER_HOUR = 0.0425; export function calculateImpactEstimates({ @@ -61,7 +68,7 @@ function calculateImpact({ const coreSeconds = totalCoreSeconds * percentage; const annualizedCoreSeconds = coreSeconds * annualizedScaleUp; const coreHours = coreSeconds / (60 * 60); - const co2 = ((PER_CORE_WATT * coreHours) / 1000.0) * CO2_PER_KWH; + const co2 = ((PER_CORE_WATT * coreHours) / 1000.0) * CO2_PER_KWH * DATACENTER_PUE; const annualizedCo2 = co2 * annualizedScaleUp; const dollarCost = coreHours * CORE_COST_PER_HOUR; const annualizedDollarCost = dollarCost * annualizedScaleUp; diff --git a/x-pack/plugins/profiling/common/setup.test.ts b/x-pack/plugins/profiling/common/setup.test.ts index 8ffd43c2857e4..100c0efa5cace 100644 --- a/x-pack/plugins/profiling/common/setup.test.ts +++ b/x-pack/plugins/profiling/common/setup.test.ts @@ -82,6 +82,9 @@ describe('Merging partial state operations', () => { const defaultSetupState = createDefaultSetupState(); it('returns false when permission is not configured', () => { const mergedState = mergePartialSetupStates(defaultSetupState, [ + createCollectorPolicyState(true), + createSymbolizerPolicyState(true), + createProfilingInApmPolicyState(true), createResourceState({ enabled: true, created: true }), createSettingsState(true), createPermissionState(false), @@ -92,6 +95,9 @@ describe('Merging partial state operations', () => { it('returns false when resource management is not enabled', () => { const mergedState = mergePartialSetupStates(defaultSetupState, [ + createCollectorPolicyState(true), + createSymbolizerPolicyState(true), + createProfilingInApmPolicyState(true), createResourceState({ enabled: false, created: true }), createSettingsState(true), createPermissionState(true), @@ -102,6 +108,9 @@ describe('Merging partial state operations', () => { it('returns false when resources are not created', () => { const mergedState = mergePartialSetupStates(defaultSetupState, [ + createCollectorPolicyState(true), + createSymbolizerPolicyState(true), + createProfilingInApmPolicyState(true), createResourceState({ enabled: true, created: false }), createSettingsState(true), createPermissionState(true), @@ -112,6 +121,9 @@ describe('Merging partial state operations', () => { it('returns false when settings are not configured', () => { const mergedState = mergePartialSetupStates(defaultSetupState, [ + createCollectorPolicyState(true), + createSymbolizerPolicyState(true), + createProfilingInApmPolicyState(true), createResourceState({ enabled: true, created: true }), createSettingsState(false), createPermissionState(true), @@ -122,6 +134,9 @@ describe('Merging partial state operations', () => { it('returns true when all checks are valid', () => { const mergedState = mergePartialSetupStates(defaultSetupState, [ + createCollectorPolicyState(true), + createSymbolizerPolicyState(true), + createProfilingInApmPolicyState(false), createResourceState({ enabled: true, created: true }), createSettingsState(true), createPermissionState(true), @@ -129,6 +144,45 @@ describe('Merging partial state operations', () => { expect(areResourcesSetupForAdmin(mergedState)).toBeTruthy(); }); + + it('returns false when collector is not found', () => { + const mergedState = mergePartialSetupStates(defaultSetupState, [ + createCollectorPolicyState(false), + createSymbolizerPolicyState(true), + createProfilingInApmPolicyState(false), + createResourceState({ enabled: true, created: true }), + createSettingsState(true), + createPermissionState(true), + ]); + + expect(areResourcesSetupForAdmin(mergedState)).toBeFalsy(); + }); + + it('returns false when symbolizer is not found', () => { + const mergedState = mergePartialSetupStates(defaultSetupState, [ + createCollectorPolicyState(true), + createSymbolizerPolicyState(false), + createProfilingInApmPolicyState(false), + createResourceState({ enabled: true, created: true }), + createSettingsState(true), + createPermissionState(true), + ]); + + expect(areResourcesSetupForAdmin(mergedState)).toBeFalsy(); + }); + + it('returns false when profiling is in APM server', () => { + const mergedState = mergePartialSetupStates(defaultSetupState, [ + createCollectorPolicyState(true), + createSymbolizerPolicyState(true), + createProfilingInApmPolicyState(true), + createResourceState({ enabled: true, created: true }), + createSettingsState(true), + createPermissionState(true), + ]); + + expect(areResourcesSetupForAdmin(mergedState)).toBeFalsy(); + }); }); describe('For viewer users', () => { diff --git a/x-pack/plugins/profiling/common/setup.ts b/x-pack/plugins/profiling/common/setup.ts index b8b998417cc94..e1ee768d9d3d0 100644 --- a/x-pack/plugins/profiling/common/setup.ts +++ b/x-pack/plugins/profiling/common/setup.ts @@ -88,6 +88,7 @@ export function areResourcesSetupForViewer(state: SetupState): boolean { export function areResourcesSetupForAdmin(state: SetupState): boolean { return ( + areResourcesSetupForViewer(state) && state.resource_management.enabled && state.resources.created && state.permissions.configured && diff --git a/x-pack/plugins/profiling/server/lib/setup/fleet_policies.ts b/x-pack/plugins/profiling/server/lib/setup/fleet_policies.ts index c6ac504042da5..8ffe13695cddb 100644 --- a/x-pack/plugins/profiling/server/lib/setup/fleet_policies.ts +++ b/x-pack/plugins/profiling/server/lib/setup/fleet_policies.ts @@ -15,8 +15,8 @@ import { ELASTIC_CLOUD_APM_POLICY, getApmPolicy } from './get_apm_policy'; import { ProfilingSetupOptions } from './types'; const CLOUD_AGENT_POLICY_ID = 'policy-elastic-agent-on-cloud'; -const COLLECTOR_PACKAGE_POLICY_NAME = 'Universal Profiling Collector'; -const SYMBOLIZER_PACKAGE_POLICY_NAME = 'Universal Profiling Symbolizer'; +const COLLECTOR_PACKAGE_POLICY_NAME = 'elastic-universal-profiling-collector'; +const SYMBOLIZER_PACKAGE_POLICY_NAME = 'elastic-universal-profiling-symbolizer'; async function getPackagePolicy({ soClient, @@ -103,7 +103,7 @@ export async function createCollectorPackagePolicy({ enabled: true, package: { name: packageName, - title: COLLECTOR_PACKAGE_POLICY_NAME, + title: 'Universal Profiling Collector', version, }, name: COLLECTOR_PACKAGE_POLICY_NAME, @@ -161,7 +161,7 @@ export async function createSymbolizerPackagePolicy({ enabled: true, package: { name: packageName, - title: SYMBOLIZER_PACKAGE_POLICY_NAME, + title: 'Universal Profiling Symbolizer', version, }, name: SYMBOLIZER_PACKAGE_POLICY_NAME, diff --git a/x-pack/plugins/profiling/server/routes/setup.ts b/x-pack/plugins/profiling/server/routes/setup.ts index 289a360038fad..4e4e2c15b9f66 100644 --- a/x-pack/plugins/profiling/server/routes/setup.ts +++ b/x-pack/plugins/profiling/server/routes/setup.ts @@ -110,10 +110,7 @@ export function registerSetupRoute({ * because of users with viewer privileges * cannot get the cluster settings */ - if ( - areResourcesSetupForViewer(mergedStateForViewer) && - mergedStateForViewer.data.available - ) { + if (areResourcesSetupForViewer(mergedStateForViewer)) { return response.ok({ body: { has_setup: true, diff --git a/x-pack/plugins/screenshotting/server/browsers/chromium/integration_tests/downloads.test.ts b/x-pack/plugins/screenshotting/server/browsers/chromium/integration_tests/downloads.test.ts index 17031c9dc8c70..a2c331e039389 100644 --- a/x-pack/plugins/screenshotting/server/browsers/chromium/integration_tests/downloads.test.ts +++ b/x-pack/plugins/screenshotting/server/browsers/chromium/integration_tests/downloads.test.ts @@ -44,7 +44,7 @@ describe.each(packageInfos)('Chromium archive: %s/%s', (architecture, platform) const originalAxios = axios.defaults.adapter; beforeAll(async () => { - axios.defaults.adapter = require('axios/lib/adapters/http'); // allow Axios to send actual requests + axios.defaults.adapter = 'http'; }); afterAll(() => { diff --git a/x-pack/plugins/security_solution/scripts/endpoint/common/format_axios_error.ts b/x-pack/plugins/security_solution/scripts/endpoint/common/format_axios_error.ts index ccb3dc125f561..f1b69c8665fc6 100644 --- a/x-pack/plugins/security_solution/scripts/endpoint/common/format_axios_error.ts +++ b/x-pack/plugins/security_solution/scripts/endpoint/common/format_axios_error.ts @@ -25,9 +25,9 @@ export class FormattedAxiosError extends Error { super(axiosError.message); this.request = { - method: axiosError.config.method ?? '?', - url: axiosError.config.url ?? '?', - data: axiosError.config.data ?? '', + method: axiosError.config?.method ?? '?', + url: axiosError.config?.url ?? '?', + data: axiosError.config?.data ?? '', }; this.response = { diff --git a/x-pack/plugins/security_solution/server/lib/telemetry/preview_sender.ts b/x-pack/plugins/security_solution/server/lib/telemetry/preview_sender.ts index 2c6a55900a14d..a26bbef5ee9d7 100644 --- a/x-pack/plugins/security_solution/server/lib/telemetry/preview_sender.ts +++ b/x-pack/plugins/security_solution/server/lib/telemetry/preview_sender.ts @@ -6,7 +6,7 @@ */ import type { AxiosInstance, AxiosResponse } from 'axios'; -import axios from 'axios'; +import axios, { AxiosHeaders } from 'axios'; import type { Logger } from '@kbn/core/server'; import type { TelemetryPluginStart, TelemetryPluginSetup } from '@kbn/telemetry-plugin/server'; import type { UsageCounter } from '@kbn/usage-collection-plugin/server'; @@ -71,7 +71,9 @@ export class PreviewTelemetryEventsSender implements ITelemetryEventsSender { status: 200, statusText: 'ok', headers: {}, - config: {}, + config: { + headers: new AxiosHeaders(), + }, }; return Promise.resolve(okResponse); } diff --git a/x-pack/plugins/stack_connectors/server/connector_types/cases_webhook/utils.test.ts b/x-pack/plugins/stack_connectors/server/connector_types/cases_webhook/utils.test.ts index 29c732c1b7d40..8e73e1d6682fd 100644 --- a/x-pack/plugins/stack_connectors/server/connector_types/cases_webhook/utils.test.ts +++ b/x-pack/plugins/stack_connectors/server/connector_types/cases_webhook/utils.test.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { AxiosHeaders } from 'axios'; import { getObjectValueByKeyAsString, stringifyObjValues, @@ -60,6 +61,7 @@ describe('cases_webhook/utils', () => { config: { method: 'post', url: 'https://poster.com', + headers: new AxiosHeaders({}), }, }; it('Throws error when missing content-type', () => { diff --git a/x-pack/plugins/stack_connectors/server/connector_types/email/send_email_graph_api.test.ts b/x-pack/plugins/stack_connectors/server/connector_types/email/send_email_graph_api.test.ts index 199681a13e968..bbb5fa2ae5f4e 100644 --- a/x-pack/plugins/stack_connectors/server/connector_types/email/send_email_graph_api.test.ts +++ b/x-pack/plugins/stack_connectors/server/connector_types/email/send_email_graph_api.test.ts @@ -6,6 +6,7 @@ */ jest.mock('axios', () => ({ create: jest.fn(), + AxiosHeaders: jest.requireActual('axios').AxiosHeaders, })); import axios from 'axios'; diff --git a/x-pack/plugins/stack_connectors/server/connector_types/gen_ai/lib/utils.ts b/x-pack/plugins/stack_connectors/server/connector_types/gen_ai/lib/utils.ts index b0c953eaa3ae1..d51f85c7aa514 100644 --- a/x-pack/plugins/stack_connectors/server/connector_types/gen_ai/lib/utils.ts +++ b/x-pack/plugins/stack_connectors/server/connector_types/gen_ai/lib/utils.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { AxiosRequestHeaders, AxiosResponse, ResponseType } from 'axios'; +import { AxiosResponse, ResponseType } from 'axios'; import { IncomingMessage } from 'http'; import { OpenAiProviderType } from '../../../../common/gen_ai/constants'; import { @@ -77,7 +77,7 @@ export const getAxiosOptions = ( provider: string, apiKey: string, stream: boolean -): { headers: AxiosRequestHeaders; responseType?: ResponseType } => { +): { headers: Record; responseType?: ResponseType } => { const responseType = stream ? { responseType: 'stream' as ResponseType } : {}; switch (provider) { case OpenAiProviderType.OpenAi: diff --git a/x-pack/plugins/stack_connectors/server/connector_types/lib/http_response_retry_header.ts b/x-pack/plugins/stack_connectors/server/connector_types/lib/http_response_retry_header.ts index 7f13c3bcf022c..d3c36219da641 100644 --- a/x-pack/plugins/stack_connectors/server/connector_types/lib/http_response_retry_header.ts +++ b/x-pack/plugins/stack_connectors/server/connector_types/lib/http_response_retry_header.ts @@ -8,10 +8,10 @@ import { fromNullable, Option, map, filter } from 'fp-ts/lib/Option'; import { pipe } from 'fp-ts/lib/pipeable'; -export function getRetryAfterIntervalFromHeaders(headers: Record): Option { +export function getRetryAfterIntervalFromHeaders(headers: Record): Option { return pipe( fromNullable(headers['retry-after']), - map((retryAfter) => parseInt(retryAfter, 10)), + map((retryAfter) => parseInt(retryAfter as string, 10)), filter((retryAfter) => !isNaN(retryAfter)) ); } diff --git a/x-pack/plugins/stack_connectors/server/connector_types/lib/servicenow/utils.test.ts b/x-pack/plugins/stack_connectors/server/connector_types/lib/servicenow/utils.test.ts index d31dbca880f36..79ca4a662608c 100644 --- a/x-pack/plugins/stack_connectors/server/connector_types/lib/servicenow/utils.test.ts +++ b/x-pack/plugins/stack_connectors/server/connector_types/lib/servicenow/utils.test.ts @@ -27,6 +27,7 @@ jest.mock('@kbn/actions-plugin/server/lib/get_oauth_jwt_access_token', () => ({ jest.mock('axios', () => ({ create: jest.fn(), + AxiosHeaders: jest.requireActual('axios').AxiosHeaders, })); const createAxiosInstanceMock = axios.create as jest.Mock; const axiosInstanceMock = { @@ -226,7 +227,7 @@ describe('utils', () => { const mockRequestCallback = (axiosInstanceMock.interceptors.request.use as jest.Mock).mock .calls[0][0]; expect(await mockRequestCallback({ headers: {} })).toEqual({ - headers: { Authorization: 'Bearer tokentokentoken' }, + headers: new axios.AxiosHeaders({ Authorization: 'Bearer tokentokentoken' }), }); expect(getOAuthJwtAccessToken as jest.Mock).toHaveBeenCalledWith({ diff --git a/x-pack/plugins/stack_connectors/server/connector_types/lib/servicenow/utils.ts b/x-pack/plugins/stack_connectors/server/connector_types/lib/servicenow/utils.ts index f5ae7baa3e256..2e6ba5327ed2b 100644 --- a/x-pack/plugins/stack_connectors/server/connector_types/lib/servicenow/utils.ts +++ b/x-pack/plugins/stack_connectors/server/connector_types/lib/servicenow/utils.ts @@ -5,7 +5,7 @@ * 2.0. */ -import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'; +import axios, { AxiosHeaders, AxiosInstance, AxiosResponse } from 'axios'; import { Logger } from '@kbn/core/server'; import { addTimeZoneToDate, getErrorMessage } from '@kbn/actions-plugin/server/lib/axios_utils'; import { ActionsConfigurationUtilities } from '@kbn/actions-plugin/server/actions_config'; @@ -112,7 +112,7 @@ export const getAxiosInstance = ({ } else { axiosInstance = axios.create(); axiosInstance.interceptors.request.use( - async (axiosConfig: AxiosRequestConfig) => { + async (axiosConfig) => { const accessToken = await getOAuthJwtAccessToken({ connectorId, logger, @@ -137,7 +137,10 @@ export const getAxiosInstance = ({ if (!accessToken) { throw new Error(`Unable to retrieve access token for connectorId: ${connectorId}`); } - axiosConfig.headers = { ...axiosConfig.headers, Authorization: accessToken }; + axiosConfig.headers = new AxiosHeaders({ + ...axiosConfig.headers, + Authorization: accessToken, + }); return axiosConfig; }, (error) => { diff --git a/x-pack/plugins/stack_connectors/server/connector_types/slack_api/service.ts b/x-pack/plugins/stack_connectors/server/connector_types/slack_api/service.ts index 2db25dc52f223..ae85127eb457a 100644 --- a/x-pack/plugins/stack_connectors/server/connector_types/slack_api/service.ts +++ b/x-pack/plugins/stack_connectors/server/connector_types/slack_api/service.ts @@ -5,7 +5,7 @@ * 2.0. */ -import axios, { AxiosResponse } from 'axios'; +import axios, { AxiosHeaders, AxiosResponse } from 'axios'; import { Logger } from '@kbn/core/server'; import { i18n } from '@kbn/i18n'; import { ActionsConfigurationUtilities } from '@kbn/actions-plugin/server/actions_config'; @@ -147,7 +147,9 @@ export const createExternalService = ( status: 0, statusText: '', headers: {}, - config: {}, + config: { + headers: new AxiosHeaders({}), + }, }; while (numberOfFetch < RE_TRY) { diff --git a/x-pack/plugins/stack_connectors/server/connector_types/torq/index.ts b/x-pack/plugins/stack_connectors/server/connector_types/torq/index.ts index 860e87397eb44..9d805291cf92c 100644 --- a/x-pack/plugins/stack_connectors/server/connector_types/torq/index.ts +++ b/x-pack/plugins/stack_connectors/server/connector_types/torq/index.ts @@ -366,7 +366,6 @@ function retryResult(actionId: string, serviceMessage: string): ActionTypeExecut function retryResultSeconds( actionId: string, serviceMessage: string, - retryAfter: number ): ActionTypeExecutorResult { const retryEpoch = Date.now() + retryAfter * 1000; diff --git a/x-pack/test/functional/apps/aiops/change_point_detection.ts b/x-pack/test/functional/apps/aiops/change_point_detection.ts index 9b11431b2e700..0c55e081afc92 100644 --- a/x-pack/test/functional/apps/aiops/change_point_detection.ts +++ b/x-pack/test/functional/apps/aiops/change_point_detection.ts @@ -16,7 +16,8 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { // aiops lives in the ML UI so we need some related services. const ml = getService('ml'); - describe('change point detection', async function () { + // Failing: See https://github.com/elastic/kibana/issues/158851 + describe.skip('change point detection', async function () { before(async () => { await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/ml/ecommerce'); await ml.testResources.createIndexPatternIfNeeded('ft_ecommerce', 'order_date'); diff --git a/x-pack/test/functional/apps/infra/home_page.ts b/x-pack/test/functional/apps/infra/home_page.ts index 1e187587ce901..8ea0ad09a50f0 100644 --- a/x-pack/test/functional/apps/infra/home_page.ts +++ b/x-pack/test/functional/apps/infra/home_page.ts @@ -38,7 +38,8 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await pageObjects.infraHome.getNoMetricsIndicesPrompt(); }); - it('renders the correct error page title', async () => { + // Unskip once asset details error handling has been implemented + it.skip('renders the correct error page title', async () => { await pageObjects.common.navigateToUrlWithBrowserHistory( 'infraOps', '/detail/host/test', diff --git a/x-pack/test/functional/apps/saved_objects_management/feature_controls/saved_objects_management_security.ts b/x-pack/test/functional/apps/saved_objects_management/feature_controls/saved_objects_management_security.ts index a626c613fd50d..8e1dae82ab2aa 100644 --- a/x-pack/test/functional/apps/saved_objects_management/feature_controls/saved_objects_management_security.ts +++ b/x-pack/test/functional/apps/saved_objects_management/feature_controls/saved_objects_management_security.ts @@ -245,9 +245,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { // From https://github.com/elastic/kibana/issues/59588 edit view became read-only json view // test description changed from "edit" to "inspect" // Skipping the test to allow code owners to delete or modify the test. - // - // FLAKY: https://github.com/elastic/kibana/issues/116048 - describe.skip('inspect visualization', () => { + describe('inspect visualization', () => { before(async () => { await PageObjects.settings.navigateTo(); await PageObjects.settings.clickKibanaSavedObjects(); diff --git a/x-pack/test/ui_capabilities/common/services/ui_capabilities.ts b/x-pack/test/ui_capabilities/common/services/ui_capabilities.ts index 9cb3da7e8fd5c..4367b53d1f0f2 100644 --- a/x-pack/test/ui_capabilities/common/services/ui_capabilities.ts +++ b/x-pack/test/ui_capabilities/common/services/ui_capabilities.ts @@ -5,7 +5,7 @@ * 2.0. */ -import axios, { AxiosInstance, AxiosRequestHeaders } from 'axios'; +import axios, { AxiosInstance } from 'axios'; import type { Capabilities as UICapabilities } from '@kbn/core/types'; import { format as formatUrl } from 'url'; import util from 'util'; @@ -61,7 +61,7 @@ export class UICapabilitiesService { this.log.debug( `requesting ${spaceUrlPrefix}/api/core/capabilities to parse the uiCapabilities` ); - const requestHeaders: AxiosRequestHeaders = credentials + const requestHeaders: Record = credentials ? { Authorization: `Basic ${Buffer.from( `${credentials.username}:${credentials.password}` diff --git a/yarn.lock b/yarn.lock index a97726e56888d..61fe8a795521c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11291,14 +11291,6 @@ axios@^0.26.0: dependencies: follow-redirects "^1.14.8" -axios@^0.27.2: - version "0.27.2" - resolved "https://registry.yarnpkg.com/axios/-/axios-0.27.2.tgz#207658cc8621606e586c85db4b41a750e756d972" - integrity sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ== - dependencies: - follow-redirects "^1.14.9" - form-data "^4.0.0" - axios@^1.3.4, axios@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/axios/-/axios-1.4.0.tgz#38a7bf1224cd308de271146038b551d725f0be1f" @@ -16705,7 +16697,7 @@ folktale@2.3.2: resolved "https://registry.yarnpkg.com/folktale/-/folktale-2.3.2.tgz#38231b039e5ef36989920cbf805bf6b227bf4fd4" integrity sha512-+8GbtQBwEqutP0v3uajDDoN64K2ehmHd0cjlghhxh0WpcfPzAIjPA03e1VvHlxL02FVGR0A6lwXsNQKn3H1RNQ== -follow-redirects@^1.0.0, follow-redirects@^1.14.0, follow-redirects@^1.14.8, follow-redirects@^1.14.9, follow-redirects@^1.15.0: +follow-redirects@^1.0.0, follow-redirects@^1.14.0, follow-redirects@^1.14.8, follow-redirects@^1.15.0: version "1.15.2" resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.2.tgz#b460864144ba63f2681096f274c4e57026da2c13" integrity sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==