From b03237a72d6675bd6deb976a73c2dddcdcb6b445 Mon Sep 17 00:00:00 2001 From: Josh Dover <1813008+joshdover@users.noreply.github.com> Date: Mon, 11 Oct 2021 18:16:26 +0200 Subject: [PATCH 1/9] Enable `bearer` scheme by default to support service token authorization (#112654) Co-authored-by: Aleh Zasypkin --- docs/settings/security-settings.asciidoc | 2 +- .../security/authentication/index.asciidoc | 6 +- .../authentication/authenticator.test.ts | 6 +- x-pack/plugins/security/server/config.test.ts | 9 ++ x-pack/plugins/security/server/config.ts | 2 +- .../security_usage_collector.test.ts | 2 +- x-pack/scripts/functional_tests.js | 1 + .../http_bearer.config.ts | 36 ++++++ .../tests/http_bearer/header.ts | 103 ++++++++++++++++++ .../tests/http_bearer/index.ts | 15 +++ 10 files changed, 174 insertions(+), 8 deletions(-) create mode 100644 x-pack/test/security_api_integration/http_bearer.config.ts create mode 100644 x-pack/test/security_api_integration/tests/http_bearer/header.ts create mode 100644 x-pack/test/security_api_integration/tests/http_bearer/index.ts diff --git a/docs/settings/security-settings.asciidoc b/docs/settings/security-settings.asciidoc index 906af1dfbb28e..11072509da1fc 100644 --- a/docs/settings/security-settings.asciidoc +++ b/docs/settings/security-settings.asciidoc @@ -218,7 +218,7 @@ There is a very limited set of cases when you'd want to change these settings. F | Determines if HTTP authentication schemes used by the enabled authentication providers should be automatically supported during HTTP authentication. By default, this setting is set to `true`. | `xpack.security.authc.http.schemes[]` -| List of HTTP authentication schemes that {kib} HTTP authentication should support. By default, this setting is set to `['apikey']` to support HTTP authentication with <> scheme. +| List of HTTP authentication schemes that {kib} HTTP authentication should support. By default, this setting is set to `['apikey', 'bearer']` to support HTTP authentication with the <> and <> schemes. |=== diff --git a/docs/user/security/authentication/index.asciidoc b/docs/user/security/authentication/index.asciidoc index bc564308c057e..2f2b279389799 100644 --- a/docs/user/security/authentication/index.asciidoc +++ b/docs/user/security/authentication/index.asciidoc @@ -437,14 +437,14 @@ This type of authentication is usually useful for machine-to-machine interaction By default {kib} supports <> authentication scheme _and_ any scheme supported by the currently enabled authentication provider. For example, `Basic` authentication scheme is automatically supported when basic authentication provider is enabled, or `Bearer` scheme when any of the token based authentication providers is enabled (Token, SAML, OpenID Connect, PKI or Kerberos). But it's also possible to add support for any other authentication scheme in the `kibana.yml` configuration file, as follows: -NOTE: Don't forget to explicitly specify default `apikey` scheme when you just want to add a new one to the list. +NOTE: Don't forget to explicitly specify the default `apikey` and `bearer` schemes when you just want to add a new one to the list. [source,yaml] -------------------------------------------------------------------------------- -xpack.security.authc.http.schemes: [apikey, basic, something-custom] +xpack.security.authc.http.schemes: [apikey, bearer, basic, something-custom] -------------------------------------------------------------------------------- -With this configuration, you can send requests to {kib} with the `Authorization` header using `ApiKey`, `Basic` or `Something-Custom` HTTP schemes (case insensitive). Under the hood, {kib} relays this header to {es}, then {es} authenticates the request using the credentials in the header. +With this configuration, you can send requests to {kib} with the `Authorization` header using `ApiKey`, `Bearer`, `Basic` or `Something-Custom` HTTP schemes (case insensitive). Under the hood, {kib} relays this header to {es}, then {es} authenticates the request using the credentials in the header. [float] [[embedded-content-authentication]] diff --git a/x-pack/plugins/security/server/authentication/authenticator.test.ts b/x-pack/plugins/security/server/authentication/authenticator.test.ts index ce97c142f5584..4e35b84a93119 100644 --- a/x-pack/plugins/security/server/authentication/authenticator.test.ts +++ b/x-pack/plugins/security/server/authentication/authenticator.test.ts @@ -210,7 +210,7 @@ describe('Authenticator', () => { expect( jest.requireMock('./providers/http').HTTPAuthenticationProvider ).toHaveBeenCalledWith(expect.anything(), { - supportedSchemes: new Set(['apikey', 'basic']), + supportedSchemes: new Set(['apikey', 'bearer', 'basic']), }); }); @@ -238,7 +238,9 @@ describe('Authenticator', () => { expect( jest.requireMock('./providers/http').HTTPAuthenticationProvider - ).toHaveBeenCalledWith(expect.anything(), { supportedSchemes: new Set(['apikey']) }); + ).toHaveBeenCalledWith(expect.anything(), { + supportedSchemes: new Set(['apikey', 'bearer']), + }); }); it('disabled if explicitly disabled', () => { diff --git a/x-pack/plugins/security/server/config.test.ts b/x-pack/plugins/security/server/config.test.ts index 4a7d8c7961cf5..1baf3fd4aac50 100644 --- a/x-pack/plugins/security/server/config.test.ts +++ b/x-pack/plugins/security/server/config.test.ts @@ -27,6 +27,7 @@ describe('config schema', () => { "enabled": true, "schemes": Array [ "apikey", + "bearer", ], }, "providers": Object { @@ -80,6 +81,7 @@ describe('config schema', () => { "enabled": true, "schemes": Array [ "apikey", + "bearer", ], }, "providers": Object { @@ -133,6 +135,7 @@ describe('config schema', () => { "enabled": true, "schemes": Array [ "apikey", + "bearer", ], }, "providers": Object { @@ -311,6 +314,7 @@ describe('config schema', () => { "enabled": true, "schemes": Array [ "apikey", + "bearer", ], }, "oidc": Object { @@ -342,6 +346,7 @@ describe('config schema', () => { "enabled": true, "schemes": Array [ "apikey", + "bearer", ], }, "oidc": Object { @@ -373,6 +378,7 @@ describe('config schema', () => { "enabled": true, "schemes": Array [ "apikey", + "bearer", ], }, "providers": Array [ @@ -391,6 +397,7 @@ describe('config schema', () => { "enabled": true, "schemes": Array [ "apikey", + "bearer", ], }, "providers": Array [ @@ -412,6 +419,7 @@ describe('config schema', () => { "enabled": true, "schemes": Array [ "apikey", + "bearer", ], }, "providers": Array [ @@ -1485,6 +1493,7 @@ describe('createConfig()', () => { "enabled": true, "schemes": Array [ "apikey", + "bearer", ], }, "providers": Object { diff --git a/x-pack/plugins/security/server/config.ts b/x-pack/plugins/security/server/config.ts index 07ff81e092f5f..89918e73369d3 100644 --- a/x-pack/plugins/security/server/config.ts +++ b/x-pack/plugins/security/server/config.ts @@ -269,7 +269,7 @@ export const ConfigSchema = schema.object({ http: schema.object({ enabled: schema.boolean({ defaultValue: true }), autoSchemesEnabled: schema.boolean({ defaultValue: true }), - schemes: schema.arrayOf(schema.string(), { defaultValue: ['apikey'] }), + schemes: schema.arrayOf(schema.string(), { defaultValue: ['apikey', 'bearer'] }), }), }), audit: schema.object( diff --git a/x-pack/plugins/security/server/usage_collector/security_usage_collector.test.ts b/x-pack/plugins/security/server/usage_collector/security_usage_collector.test.ts index 0515a1e1969bf..83f09ef017b01 100644 --- a/x-pack/plugins/security/server/usage_collector/security_usage_collector.test.ts +++ b/x-pack/plugins/security/server/usage_collector/security_usage_collector.test.ts @@ -46,7 +46,7 @@ describe('Security UsageCollector', () => { authProviderCount: 1, enabledAuthProviders: ['basic'], loginSelectorEnabled: false, - httpAuthSchemes: ['apikey'], + httpAuthSchemes: ['apikey', 'bearer'], sessionIdleTimeoutInMinutes: 60, sessionLifespanInMinutes: 43200, sessionCleanupInMinutes: 60, diff --git a/x-pack/scripts/functional_tests.js b/x-pack/scripts/functional_tests.js index 3c1cdd5790f3c..f7b978c2b58bd 100644 --- a/x-pack/scripts/functional_tests.js +++ b/x-pack/scripts/functional_tests.js @@ -54,6 +54,7 @@ const onlyNotInCoverageTests = [ require.resolve('../test/security_api_integration/session_lifespan.config.ts'), require.resolve('../test/security_api_integration/login_selector.config.ts'), require.resolve('../test/security_api_integration/audit.config.ts'), + require.resolve('../test/security_api_integration/http_bearer.config.ts'), require.resolve('../test/security_api_integration/kerberos.config.ts'), require.resolve('../test/security_api_integration/kerberos_anonymous_access.config.ts'), require.resolve('../test/security_api_integration/pki.config.ts'), diff --git a/x-pack/test/security_api_integration/http_bearer.config.ts b/x-pack/test/security_api_integration/http_bearer.config.ts new file mode 100644 index 0000000000000..b0a9f4a920347 --- /dev/null +++ b/x-pack/test/security_api_integration/http_bearer.config.ts @@ -0,0 +1,36 @@ +/* + * 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 { FtrConfigProviderContext } from '@kbn/test'; +import { services } from './services'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const xPackAPITestsConfig = await readConfigFile(require.resolve('../api_integration/config.ts')); + + return { + testFiles: [require.resolve('./tests/http_bearer')], + servers: xPackAPITestsConfig.get('servers'), + security: { disableTestUser: true }, + services, + junit: { + reportName: 'X-Pack Security API Integration Tests (HTTP Bearer)', + }, + + esTestCluster: { + ...xPackAPITestsConfig.get('esTestCluster'), + serverArgs: [ + ...xPackAPITestsConfig.get('esTestCluster.serverArgs'), + 'xpack.security.authc.token.enabled=true', + 'xpack.security.authc.token.timeout=15s', + ], + }, + + kbnTestServer: { + ...xPackAPITestsConfig.get('kbnTestServer'), + }, + }; +} diff --git a/x-pack/test/security_api_integration/tests/http_bearer/header.ts b/x-pack/test/security_api_integration/tests/http_bearer/header.ts new file mode 100644 index 0000000000000..f7ebef4f16d09 --- /dev/null +++ b/x-pack/test/security_api_integration/tests/http_bearer/header.ts @@ -0,0 +1,103 @@ +/* + * 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 expect from '@kbn/expect'; +import { adminTestUser } from '@kbn/test'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function ({ getService }: FtrProviderContext) { + const supertest = getService('supertestWithoutAuth'); + const es = getService('es'); + + async function createToken() { + const { + body: { access_token: accessToken, authentication }, + } = await es.security.getToken({ + body: { + grant_type: 'password', + ...adminTestUser, + }, + }); + + return { + accessToken, + expectedUser: { + ...authentication, + authentication_provider: { name: '__http__', type: 'http' }, + authentication_type: 'token', + }, + }; + } + + describe('header', () => { + it('accepts valid access token via authorization Bearer header', async () => { + const { accessToken, expectedUser } = await createToken(); + + const response = await supertest + .get('/internal/security/me') + .set('kbn-xsrf', 'true') + .set('authorization', `Bearer ${accessToken}`) + .expect(200, expectedUser); + + // Make sure we don't automatically create a session + expect(response.headers['set-cookie']).to.be(undefined); + }); + + it('accepts multiple requests for a single valid access token', async () => { + const { accessToken, expectedUser } = await createToken(); + + // try it once + await supertest + .get('/internal/security/me') + .set('kbn-xsrf', 'true') + .set('authorization', `Bearer ${accessToken}`) + .expect(200, expectedUser); + + // try it again to verity it isn't invalidated after a single request + await supertest + .get('/internal/security/me') + .set('kbn-xsrf', 'true') + .set('authorization', `Bearer ${accessToken}`) + .expect(200, expectedUser); + }); + + it('rejects invalid access token via authorization Bearer header', async () => { + await supertest + .get('/internal/security/me') + .set('kbn-xsrf', 'true') + .set('authorization', 'Bearer notreal') + .expect(401); + }); + + it('rejects invalidated access token via authorization Bearer header', async () => { + const { accessToken } = await createToken(); + await es.security.invalidateToken({ body: { token: accessToken } }); + + await supertest + .get('/internal/security/me') + .set('kbn-xsrf', 'true') + .set('authorization', `Bearer ${accessToken}`) + .expect(401); + }); + + it('rejects expired access token via authorization Bearer header', async function () { + this.timeout(40000); + + const { accessToken } = await createToken(); + + // Access token expiration is set to 15s for API integration tests. + // Let's wait for 20s to make sure token expires. + await new Promise((resolve) => setTimeout(resolve, 20000)); + + await supertest + .get('/internal/security/me') + .set('kbn-xsrf', 'true') + .set('authorization', `Bearer ${accessToken}`) + .expect(401); + }); + }); +} diff --git a/x-pack/test/security_api_integration/tests/http_bearer/index.ts b/x-pack/test/security_api_integration/tests/http_bearer/index.ts new file mode 100644 index 0000000000000..4dbad2660ebaa --- /dev/null +++ b/x-pack/test/security_api_integration/tests/http_bearer/index.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function ({ loadTestFile }: FtrProviderContext) { + describe('security APIs - HTTP Bearer', function () { + this.tags('ciGroup6'); + loadTestFile(require.resolve('./header')); + }); +} From c8a01082696a94453060ded28ee67a32a2487e0d Mon Sep 17 00:00:00 2001 From: Chris Cowan Date: Mon, 11 Oct 2021 10:53:51 -0600 Subject: [PATCH 2/9] [Stack Monitoring] Adding alerts to react app (#114029) * [Stack Monitoring] Adding alerts to react app * Fixing global state context path * adding alerts to pages; adding alerts model to cluster_overview; removing loadAlerts from page template * Fixing request for enable alerts * remove loadAlerts from page template * Adding request error handlers * removing redundent error handling * Changing useRequestErrorHandler function to be async due to error.response.json call * removing old comment * Fixing contexts paths * Converting ajaxRequestErrorHandler to useRequestErrorHandler * Refactoring error handler for page template and setup mode * Removing unnecessary async/await * Removing unnecessary async/await in useClusters * adding alertTypeIds to each page * fixing instance count * Adding alertTypeIds to index page * Adding alert filters for specific pages * Adding alerts to Logstash nodes Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../plugins/monitoring/common/types/alerts.ts | 1 + .../public/alerts/alerts_dropdown.tsx | 5 +- .../external_config_context.tsx | 0 .../{ => contexts}/global_state_context.tsx | 8 +- .../contexts/header_action_menu_context.tsx | 15 + .../application/hooks/use_alerts_modal.ts | 10 +- .../public/application/hooks/use_clusters.ts | 6 +- .../application/hooks/use_monitoring_time.ts | 2 +- .../hooks/use_request_error_handler.tsx | 79 +++ .../monitoring/public/application/index.tsx | 467 +++++++++--------- .../public/application/pages/apm/instance.tsx | 2 +- .../application/pages/apm/instances.tsx | 2 +- .../public/application/pages/apm/overview.tsx | 2 +- .../application/pages/beats/instance.tsx | 2 +- .../application/pages/beats/instances.tsx | 2 +- .../application/pages/beats/overview.tsx | 2 +- .../pages/cluster/overview_page.tsx | 49 +- .../pages/elasticsearch/ccr_page.tsx | 41 +- .../pages/elasticsearch/ccr_shard_page.tsx | 47 +- .../elasticsearch/index_advanced_page.tsx | 46 +- .../pages/elasticsearch/index_page.tsx | 58 ++- .../pages/elasticsearch/indices_page.tsx | 51 +- .../pages/elasticsearch/ml_jobs_page.tsx | 2 +- .../elasticsearch/node_advanced_page.tsx | 63 ++- .../pages/elasticsearch/node_page.tsx | 70 ++- .../pages/elasticsearch/nodes_page.tsx | 66 ++- .../pages/elasticsearch/overview.tsx | 2 +- .../pages/home/cluster_listing.tsx | 32 +- .../application/pages/kibana/instance.tsx | 41 +- .../application/pages/kibana/instances.tsx | 43 +- .../application/pages/kibana/overview.tsx | 2 +- .../public/application/pages/license_page.tsx | 2 +- .../application/pages/logstash/advanced.tsx | 41 +- .../application/pages/logstash/node.tsx | 41 +- .../pages/logstash/node_pipelines.tsx | 2 +- .../application/pages/logstash/nodes.tsx | 40 +- .../application/pages/logstash/overview.tsx | 2 +- .../application/pages/logstash/pipeline.tsx | 4 +- .../application/pages/logstash/pipelines.tsx | 2 +- .../pages/no_data/no_data_page.tsx | 6 +- .../application/pages/page_template.tsx | 44 +- .../public/application/route_init.tsx | 2 +- .../application/setup_mode/setup_mode.tsx | 15 +- .../setup_mode/setup_mode_renderer.js | 16 +- .../public/components/action_menu/index.tsx | 34 ++ .../public/components/shared/toolbar.tsx | 2 +- .../monitoring/public/lib/fetch_alerts.ts | 36 ++ 47 files changed, 990 insertions(+), 517 deletions(-) rename x-pack/plugins/monitoring/public/application/{ => contexts}/external_config_context.tsx (100%) rename x-pack/plugins/monitoring/public/application/{ => contexts}/global_state_context.tsx (90%) create mode 100644 x-pack/plugins/monitoring/public/application/contexts/header_action_menu_context.tsx create mode 100644 x-pack/plugins/monitoring/public/application/hooks/use_request_error_handler.tsx create mode 100644 x-pack/plugins/monitoring/public/components/action_menu/index.tsx create mode 100644 x-pack/plugins/monitoring/public/lib/fetch_alerts.ts diff --git a/x-pack/plugins/monitoring/common/types/alerts.ts b/x-pack/plugins/monitoring/common/types/alerts.ts index 1f68b0c55a046..bbd217169469d 100644 --- a/x-pack/plugins/monitoring/common/types/alerts.ts +++ b/x-pack/plugins/monitoring/common/types/alerts.ts @@ -32,6 +32,7 @@ export interface CommonAlertState { export interface CommonAlertFilter { nodeUuid?: string; shardId?: string; + shardIndex?: string; } export interface CommonAlertParamDetail { diff --git a/x-pack/plugins/monitoring/public/alerts/alerts_dropdown.tsx b/x-pack/plugins/monitoring/public/alerts/alerts_dropdown.tsx index 261685a532882..976569f39de4c 100644 --- a/x-pack/plugins/monitoring/public/alerts/alerts_dropdown.tsx +++ b/x-pack/plugins/monitoring/public/alerts/alerts_dropdown.tsx @@ -14,13 +14,12 @@ import { import { i18n } from '@kbn/i18n'; import React, { useState } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; -import { Legacy } from '../legacy_shims'; import { useKibana } from '../../../../../src/plugins/kibana_react/public'; import { MonitoringStartPluginDependencies } from '../types'; +import { useAlertsModal } from '../application/hooks/use_alerts_modal'; export const AlertsDropdown: React.FC<{}> = () => { - const $injector = Legacy.shims.getAngularInjector(); - const alertsEnableModalProvider: any = $injector.get('enableAlertsModal'); + const alertsEnableModalProvider = useAlertsModal(); const { navigateToApp } = useKibana().services.application; diff --git a/x-pack/plugins/monitoring/public/application/external_config_context.tsx b/x-pack/plugins/monitoring/public/application/contexts/external_config_context.tsx similarity index 100% rename from x-pack/plugins/monitoring/public/application/external_config_context.tsx rename to x-pack/plugins/monitoring/public/application/contexts/external_config_context.tsx diff --git a/x-pack/plugins/monitoring/public/application/global_state_context.tsx b/x-pack/plugins/monitoring/public/application/contexts/global_state_context.tsx similarity index 90% rename from x-pack/plugins/monitoring/public/application/global_state_context.tsx rename to x-pack/plugins/monitoring/public/application/contexts/global_state_context.tsx index 6c952f80eff57..e6638b4c4fede 100644 --- a/x-pack/plugins/monitoring/public/application/global_state_context.tsx +++ b/x-pack/plugins/monitoring/public/application/contexts/global_state_context.tsx @@ -5,10 +5,10 @@ * 2.0. */ import React, { createContext } from 'react'; -import { GlobalState } from '../url_state'; -import { MonitoringStartPluginDependencies } from '../types'; -import { TimeRange, RefreshInterval } from '../../../../../src/plugins/data/public'; -import { Legacy } from '../legacy_shims'; +import { GlobalState } from '../../url_state'; +import { MonitoringStartPluginDependencies } from '../../types'; +import { TimeRange, RefreshInterval } from '../../../../../../src/plugins/data/public'; +import { Legacy } from '../../legacy_shims'; interface GlobalStateProviderProps { query: MonitoringStartPluginDependencies['data']['query']; diff --git a/x-pack/plugins/monitoring/public/application/contexts/header_action_menu_context.tsx b/x-pack/plugins/monitoring/public/application/contexts/header_action_menu_context.tsx new file mode 100644 index 0000000000000..88862d9e6a807 --- /dev/null +++ b/x-pack/plugins/monitoring/public/application/contexts/header_action_menu_context.tsx @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { AppMountParameters } from 'kibana/public'; + +interface ContextProps { + setHeaderActionMenu?: AppMountParameters['setHeaderActionMenu']; +} + +export const HeaderActionMenuContext = React.createContext({}); diff --git a/x-pack/plugins/monitoring/public/application/hooks/use_alerts_modal.ts b/x-pack/plugins/monitoring/public/application/hooks/use_alerts_modal.ts index 9a2a2b80cc40f..123dd39f7b54d 100644 --- a/x-pack/plugins/monitoring/public/application/hooks/use_alerts_modal.ts +++ b/x-pack/plugins/monitoring/public/application/hooks/use_alerts_modal.ts @@ -6,10 +6,11 @@ */ import { useKibana } from '../../../../../../src/plugins/kibana_react/public'; import { showAlertsToast } from '../../alerts/lib/alerts_toast'; -import { ajaxErrorHandlersProvider } from '../../lib/ajax_error_handler'; +import { useRequestErrorHandler } from './use_request_error_handler'; export const useAlertsModal = () => { const { services } = useKibana(); + const handleRequestError = useRequestErrorHandler(); function shouldShowAlertsModal(alerts: {}) { const modalHasBeenShown = @@ -28,12 +29,11 @@ export const useAlertsModal = () => { async function enableAlerts() { try { - const { data } = await services.http?.post('../api/monitoring/v1/alerts/enable', {}); + const response = await services.http?.post('../api/monitoring/v1/alerts/enable', {}); window.localStorage.setItem('ALERTS_MODAL_DECISION_MADE', 'true'); - showAlertsToast(data); + showAlertsToast(response); } catch (err) { - const ajaxErrorHandlers = ajaxErrorHandlersProvider(); - return ajaxErrorHandlers(err); + await handleRequestError(err); } } diff --git a/x-pack/plugins/monitoring/public/application/hooks/use_clusters.ts b/x-pack/plugins/monitoring/public/application/hooks/use_clusters.ts index b4b8c21ca4d40..1961bd53b909f 100644 --- a/x-pack/plugins/monitoring/public/application/hooks/use_clusters.ts +++ b/x-pack/plugins/monitoring/public/application/hooks/use_clusters.ts @@ -7,6 +7,7 @@ import { useState, useEffect } from 'react'; import { useKibana } from '../../../../../../src/plugins/kibana_react/public'; import { fetchClusters } from '../../lib/fetch_clusters'; +import { useRequestErrorHandler } from './use_request_error_handler'; export function useClusters(clusterUuid?: string | null, ccs?: any, codePaths?: string[]) { const { services } = useKibana<{ data: any }>(); @@ -17,6 +18,7 @@ export function useClusters(clusterUuid?: string | null, ccs?: any, codePaths?: const [clusters, setClusters] = useState([] as any); const [loaded, setLoaded] = useState(false); + const handleRequestError = useRequestErrorHandler(); useEffect(() => { async function makeRequest() { @@ -34,13 +36,13 @@ export function useClusters(clusterUuid?: string | null, ccs?: any, codePaths?: setClusters(response); } } catch (e) { - // TODO: Handle errors + handleRequestError(e); } finally { setLoaded(true); } } makeRequest(); - }, [clusterUuid, ccs, services.http, codePaths, min, max]); + }, [handleRequestError, clusterUuid, ccs, services.http, codePaths, min, max]); return { clusters, loaded }; } diff --git a/x-pack/plugins/monitoring/public/application/hooks/use_monitoring_time.ts b/x-pack/plugins/monitoring/public/application/hooks/use_monitoring_time.ts index 3054714ec3aa6..e8973ce18232c 100644 --- a/x-pack/plugins/monitoring/public/application/hooks/use_monitoring_time.ts +++ b/x-pack/plugins/monitoring/public/application/hooks/use_monitoring_time.ts @@ -8,7 +8,7 @@ import { useCallback, useState, useContext, useEffect } from 'react'; import createContainer from 'constate'; import { useKibana } from '../../../../../../src/plugins/kibana_react/public'; import { Legacy } from '../../legacy_shims'; -import { GlobalStateContext } from '../../application/global_state_context'; +import { GlobalStateContext } from '../contexts/global_state_context'; interface TimeOptions { from: string; diff --git a/x-pack/plugins/monitoring/public/application/hooks/use_request_error_handler.tsx b/x-pack/plugins/monitoring/public/application/hooks/use_request_error_handler.tsx new file mode 100644 index 0000000000000..3a64531844451 --- /dev/null +++ b/x-pack/plugins/monitoring/public/application/hooks/use_request_error_handler.tsx @@ -0,0 +1,79 @@ +/* + * 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, { useCallback } from 'react'; +import { includes } from 'lodash'; +import { IHttpFetchError } from 'kibana/public'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiButton, EuiSpacer, EuiText } from '@elastic/eui'; +import { formatMsg } from '../../../../../../src/plugins/kibana_legacy/public'; +import { toMountPoint, useKibana } from '../../../../../../src/plugins/kibana_react/public'; +import { MonitoringStartPluginDependencies } from '../../types'; + +export function formatMonitoringError(err: IHttpFetchError) { + if (err.response?.status && err.response?.status !== -1) { + return ( + +

{err.body?.message}

+ + + +
+ ); + } + + return formatMsg(err); +} + +export const useRequestErrorHandler = () => { + const { services } = useKibana(); + return useCallback( + (err: IHttpFetchError) => { + if (err.response?.status === 403) { + // redirect to error message view + history.replaceState(null, '', '#/access-denied'); + } else if (err.response?.status === 404 && !includes(window.location.hash, 'no-data')) { + // pass through if this is a 404 and we're already on the no-data page + const formattedError = formatMonitoringError(err); + services.notifications?.toasts.addDanger({ + title: toMountPoint( + + ), + text: toMountPoint( +
+ {formattedError} + + window.location.reload()}> + + +
+ ), + }); + } else { + services.notifications?.toasts.addDanger({ + title: toMountPoint( + + ), + text: toMountPoint(formatMonitoringError(err)), + }); + } + }, + [services.notifications] + ); +}; diff --git a/x-pack/plugins/monitoring/public/application/index.tsx b/x-pack/plugins/monitoring/public/application/index.tsx index bc81dd826f849..7b4c73475338f 100644 --- a/x-pack/plugins/monitoring/public/application/index.tsx +++ b/x-pack/plugins/monitoring/public/application/index.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { CoreStart, AppMountParameters } from 'kibana/public'; +import { CoreStart, AppMountParameters, MountPoint } from 'kibana/public'; import React from 'react'; import ReactDOM from 'react-dom'; import { Route, Switch, Redirect, Router } from 'react-router-dom'; @@ -15,8 +15,8 @@ import { LicensePage } from './pages/license_page'; import { ClusterOverview } from './pages/cluster/overview_page'; import { ClusterListing } from './pages/home/cluster_listing'; import { MonitoringStartPluginDependencies } from '../types'; -import { GlobalStateProvider } from './global_state_context'; -import { ExternalConfigContext, ExternalConfig } from './external_config_context'; +import { GlobalStateProvider } from './contexts/global_state_context'; +import { ExternalConfigContext, ExternalConfig } from './contexts/external_config_context'; import { createPreserveQueryHistory } from './preserve_query_history'; import { RouteInit } from './route_init'; import { NoDataPage } from './pages/no_data'; @@ -45,6 +45,7 @@ import { ElasticsearchCcrPage } from './pages/elasticsearch/ccr_page'; import { ElasticsearchCcrShardPage } from './pages/elasticsearch/ccr_shard_page'; import { MonitoringTimeContainer } from './hooks/use_monitoring_time'; import { BreadcrumbContainer } from './hooks/use_breadcrumbs'; +import { HeaderActionMenuContext } from './contexts/header_action_menu_context'; import { LogStashOverviewPage } from './pages/logstash/overview'; import { LogStashNodesPage } from './pages/logstash/nodes'; import { LogStashPipelinesPage } from './pages/logstash/pipelines'; @@ -58,11 +59,16 @@ import { LogStashNodePipelinesPage } from './pages/logstash/node_pipelines'; export const renderApp = ( core: CoreStart, plugins: MonitoringStartPluginDependencies, - { element }: AppMountParameters, + { element, setHeaderActionMenu }: AppMountParameters, externalConfig: ExternalConfig ) => { ReactDOM.render( - , + , element ); @@ -75,236 +81,239 @@ const MonitoringApp: React.FC<{ core: CoreStart; plugins: MonitoringStartPluginDependencies; externalConfig: ExternalConfig; -}> = ({ core, plugins, externalConfig }) => { + setHeaderActionMenu: (element: MountPoint | undefined) => void; +}> = ({ core, plugins, externalConfig, setHeaderActionMenu }) => { const history = createPreserveQueryHistory(); return ( - - - - - - - - - - - {/* ElasticSearch Views */} - - - - - - - - - - - - - - - - - - - - - {/* Kibana Views */} - - - - - - - {/* Beats Views */} - - - - - - - {/* Logstash Routes */} - - - - - - - - - - - - - - - {/* APM Views */} - - - - - - - - - - - + + + + + + + + + + + + {/* ElasticSearch Views */} + + + + + + + + + + + + + + + + + + + + + {/* Kibana Views */} + + + + + + + {/* Beats Views */} + + + + + + + {/* Logstash Routes */} + + + + + + + + + + + + + + + {/* APM Views */} + + + + + + + + + + + + diff --git a/x-pack/plugins/monitoring/public/application/pages/apm/instance.tsx b/x-pack/plugins/monitoring/public/application/pages/apm/instance.tsx index dc55ecb22b61a..3fa7819c5e417 100644 --- a/x-pack/plugins/monitoring/public/application/pages/apm/instance.tsx +++ b/x-pack/plugins/monitoring/public/application/pages/apm/instance.tsx @@ -10,7 +10,7 @@ import { useParams } from 'react-router-dom'; import { i18n } from '@kbn/i18n'; import { find } from 'lodash'; import { ComponentProps } from '../../route_init'; -import { GlobalStateContext } from '../../global_state_context'; +import { GlobalStateContext } from '../../contexts/global_state_context'; import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; import { useCharts } from '../../hooks/use_charts'; import { BreadcrumbContainer } from '../../hooks/use_breadcrumbs'; diff --git a/x-pack/plugins/monitoring/public/application/pages/apm/instances.tsx b/x-pack/plugins/monitoring/public/application/pages/apm/instances.tsx index bc60f26cdbfad..fedb07fa65a40 100644 --- a/x-pack/plugins/monitoring/public/application/pages/apm/instances.tsx +++ b/x-pack/plugins/monitoring/public/application/pages/apm/instances.tsx @@ -9,7 +9,7 @@ import React, { useContext, useState, useCallback, useEffect } from 'react'; import { i18n } from '@kbn/i18n'; import { find } from 'lodash'; import { ComponentProps } from '../../route_init'; -import { GlobalStateContext } from '../../global_state_context'; +import { GlobalStateContext } from '../../contexts/global_state_context'; import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; import { useTable } from '../../hooks/use_table'; import { ApmTemplate } from './apm_template'; diff --git a/x-pack/plugins/monitoring/public/application/pages/apm/overview.tsx b/x-pack/plugins/monitoring/public/application/pages/apm/overview.tsx index cca31c0a7e65d..516c293c53546 100644 --- a/x-pack/plugins/monitoring/public/application/pages/apm/overview.tsx +++ b/x-pack/plugins/monitoring/public/application/pages/apm/overview.tsx @@ -10,7 +10,7 @@ import { i18n } from '@kbn/i18n'; import { find } from 'lodash'; import { ComponentProps } from '../../route_init'; import { ApmTemplate } from './apm_template'; -import { GlobalStateContext } from '../../global_state_context'; +import { GlobalStateContext } from '../../contexts/global_state_context'; import { useCharts } from '../../hooks/use_charts'; import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; import { BreadcrumbContainer } from '../../hooks/use_breadcrumbs'; diff --git a/x-pack/plugins/monitoring/public/application/pages/beats/instance.tsx b/x-pack/plugins/monitoring/public/application/pages/beats/instance.tsx index f7ff03898fda6..4c66bbba631fb 100644 --- a/x-pack/plugins/monitoring/public/application/pages/beats/instance.tsx +++ b/x-pack/plugins/monitoring/public/application/pages/beats/instance.tsx @@ -10,7 +10,7 @@ import { useParams } from 'react-router-dom'; import { i18n } from '@kbn/i18n'; import { find } from 'lodash'; import { ComponentProps } from '../../route_init'; -import { GlobalStateContext } from '../../global_state_context'; +import { GlobalStateContext } from '../../contexts/global_state_context'; import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; import { useCharts } from '../../hooks/use_charts'; // @ts-ignore diff --git a/x-pack/plugins/monitoring/public/application/pages/beats/instances.tsx b/x-pack/plugins/monitoring/public/application/pages/beats/instances.tsx index 18f941c398af0..489ad110c40fd 100644 --- a/x-pack/plugins/monitoring/public/application/pages/beats/instances.tsx +++ b/x-pack/plugins/monitoring/public/application/pages/beats/instances.tsx @@ -9,7 +9,7 @@ import React, { useContext, useState, useCallback, useEffect } from 'react'; import { i18n } from '@kbn/i18n'; import { find } from 'lodash'; import { ComponentProps } from '../../route_init'; -import { GlobalStateContext } from '../../global_state_context'; +import { GlobalStateContext } from '../../contexts/global_state_context'; import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; import { useTable } from '../../hooks/use_table'; import { BeatsTemplate } from './beats_template'; diff --git a/x-pack/plugins/monitoring/public/application/pages/beats/overview.tsx b/x-pack/plugins/monitoring/public/application/pages/beats/overview.tsx index 8d28119c4ec1b..1fa37a2c7b3e6 100644 --- a/x-pack/plugins/monitoring/public/application/pages/beats/overview.tsx +++ b/x-pack/plugins/monitoring/public/application/pages/beats/overview.tsx @@ -10,7 +10,7 @@ import { i18n } from '@kbn/i18n'; import { find } from 'lodash'; import { ComponentProps } from '../../route_init'; import { BeatsTemplate } from './beats_template'; -import { GlobalStateContext } from '../../global_state_context'; +import { GlobalStateContext } from '../../contexts/global_state_context'; import { useCharts } from '../../hooks/use_charts'; import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; // @ts-ignore diff --git a/x-pack/plugins/monitoring/public/application/pages/cluster/overview_page.tsx b/x-pack/plugins/monitoring/public/application/pages/cluster/overview_page.tsx index 3a717036396e9..b78df27cd12c4 100644 --- a/x-pack/plugins/monitoring/public/application/pages/cluster/overview_page.tsx +++ b/x-pack/plugins/monitoring/public/application/pages/cluster/overview_page.tsx @@ -10,14 +10,17 @@ import { i18n } from '@kbn/i18n'; import { CODE_PATH_ALL } from '../../../../common/constants'; import { PageTemplate } from '../page_template'; import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; -import { GlobalStateContext } from '../../global_state_context'; +import { GlobalStateContext } from '../../contexts/global_state_context'; import { TabMenuItem } from '../page_template'; import { Overview } from '../../../components/cluster/overview'; -import { ExternalConfigContext } from '../../external_config_context'; +import { ExternalConfigContext } from '../../contexts/external_config_context'; import { SetupModeRenderer, SetupModeProps } from '../../setup_mode/setup_mode_renderer'; import { SetupModeContext } from '../../../components/setup_mode/setup_mode_context'; import { BreadcrumbContainer } from '../../hooks/use_breadcrumbs'; import { fetchClusters } from '../../../lib/fetch_clusters'; +import { AlertsByName } from '../../../alerts/types'; +import { fetchAlerts } from '../../../lib/fetch_alerts'; +import { EnableAlertsModal } from '../../../alerts/enable_alerts_modal'; const CODE_PATHS = [CODE_PATH_ALL]; @@ -28,6 +31,7 @@ export const ClusterOverview: React.FC<{}> = () => { const clusterUuid = state.cluster_uuid; const ccs = state.ccs; const [clusters, setClusters] = useState([] as any); + const [alerts, setAlerts] = useState({}); const [loaded, setLoaded] = useState(false); const { generate: generateBreadcrumbs } = useContext(BreadcrumbContainer.Context); @@ -54,23 +58,27 @@ export const ClusterOverview: React.FC<{}> = () => { const getPageData = useCallback(async () => { const bounds = services.data?.query.timefilter.timefilter.getBounds(); - try { - if (services.http?.fetch) { - const response = await fetchClusters({ - fetch: services.http.fetch, - timeRange: { - min: bounds.min.toISOString(), - max: bounds.max.toISOString(), - }, - ccs, - clusterUuid, - codePaths: CODE_PATHS, - }); - setClusters(response); - } - } catch (err) { - // TODO: handle errors - } finally { + if (services.http?.fetch && clusterUuid) { + const response = await fetchClusters({ + fetch: services.http.fetch, + timeRange: { + min: bounds.min.toISOString(), + max: bounds.max.toISOString(), + }, + ccs, + clusterUuid, + codePaths: CODE_PATHS, + }); + setClusters(response); + const alertsResponse = await fetchAlerts({ + fetch: services.http.fetch, + clusterUuid, + timeRange: { + min: bounds.min.valueOf(), + max: bounds.max.valueOf(), + }, + }); + setAlerts(alertsResponse); setLoaded(true); } }, [ccs, clusterUuid, services.data?.query.timefilter.timefilter, services.http]); @@ -89,7 +97,7 @@ export const ClusterOverview: React.FC<{}> = () => { {flyoutComponent} @@ -98,6 +106,7 @@ export const ClusterOverview: React.FC<{}> = () => { )} /> + ); }; diff --git a/x-pack/plugins/monitoring/public/application/pages/elasticsearch/ccr_page.tsx b/x-pack/plugins/monitoring/public/application/pages/elasticsearch/ccr_page.tsx index 294aeade5e38b..8a9a736286c3f 100644 --- a/x-pack/plugins/monitoring/public/application/pages/elasticsearch/ccr_page.tsx +++ b/x-pack/plugins/monitoring/public/application/pages/elasticsearch/ccr_page.tsx @@ -9,13 +9,15 @@ import { i18n } from '@kbn/i18n'; import { find } from 'lodash'; import { ElasticsearchTemplate } from './elasticsearch_template'; import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; -import { GlobalStateContext } from '../../global_state_context'; +import { GlobalStateContext } from '../../contexts/global_state_context'; // @ts-ignore import { Ccr } from '../../../components/elasticsearch/ccr'; import { ComponentProps } from '../../route_init'; import { SetupModeRenderer } from '../../setup_mode/setup_mode_renderer'; import { SetupModeContext } from '../../../components/setup_mode/setup_mode_context'; -import { ELASTICSEARCH_SYSTEM_ID } from '../../../../common/constants'; +import { AlertsByName } from '../../../alerts/types'; +import { fetchAlerts } from '../../../lib/fetch_alerts'; +import { ELASTICSEARCH_SYSTEM_ID, RULE_CCR_READ_EXCEPTIONS } from '../../../../common/constants'; interface SetupModeProps { setupMode: any; @@ -33,6 +35,7 @@ export const ElasticsearchCcrPage: React.FC = ({ clusters }) => }) as any; const ccs = globalState.ccs; const [data, setData] = useState({} as any); + const [alerts, setAlerts] = useState({}); const title = i18n.translate('xpack.monitoring.elasticsearch.ccr.title', { defaultMessage: 'Elasticsearch - Ccr', @@ -46,18 +49,30 @@ export const ElasticsearchCcrPage: React.FC = ({ clusters }) => const bounds = services.data?.query.timefilter.timefilter.getBounds(); const url = `../api/monitoring/v1/clusters/${clusterUuid}/elasticsearch/ccr`; - const response = await services.http?.fetch(url, { - method: 'POST', - body: JSON.stringify({ - ccs, + if (services.http?.fetch && clusterUuid) { + const response = await services.http?.fetch(url, { + method: 'POST', + body: JSON.stringify({ + ccs, + timeRange: { + min: bounds.min.toISOString(), + max: bounds.max.toISOString(), + }, + }), + }); + + setData(response); + const alertsResponse = await fetchAlerts({ + fetch: services.http.fetch, + alertTypeIds: [RULE_CCR_READ_EXCEPTIONS], + clusterUuid, timeRange: { - min: bounds.min.toISOString(), - max: bounds.max.toISOString(), + min: bounds.min.valueOf(), + max: bounds.max.valueOf(), }, - }), - }); - - setData(response); + }); + setAlerts(alertsResponse); + } }, [ccs, clusterUuid, services.data?.query.timefilter.timefilter, services.http]); return ( @@ -73,7 +88,7 @@ export const ElasticsearchCcrPage: React.FC = ({ clusters }) => render={({ flyoutComponent, bottomBarComponent }: SetupModeProps) => ( {flyoutComponent} - + {bottomBarComponent} )} diff --git a/x-pack/plugins/monitoring/public/application/pages/elasticsearch/ccr_shard_page.tsx b/x-pack/plugins/monitoring/public/application/pages/elasticsearch/ccr_shard_page.tsx index bec2f278f1774..21f9fd10f0806 100644 --- a/x-pack/plugins/monitoring/public/application/pages/elasticsearch/ccr_shard_page.tsx +++ b/x-pack/plugins/monitoring/public/application/pages/elasticsearch/ccr_shard_page.tsx @@ -10,13 +10,15 @@ import { get } from 'lodash'; import { i18n } from '@kbn/i18n'; import { PageTemplate } from '../page_template'; import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; -import { GlobalStateContext } from '../../global_state_context'; +import { GlobalStateContext } from '../../contexts/global_state_context'; // @ts-ignore import { CcrShardReact } from '../../../components/elasticsearch/ccr_shard'; import { ComponentProps } from '../../route_init'; import { SetupModeRenderer } from '../../setup_mode/setup_mode_renderer'; import { SetupModeContext } from '../../../components/setup_mode/setup_mode_context'; -import { ELASTICSEARCH_SYSTEM_ID } from '../../../../common/constants'; +import { AlertsByName } from '../../../alerts/types'; +import { fetchAlerts } from '../../../lib/fetch_alerts'; +import { ELASTICSEARCH_SYSTEM_ID, RULE_CCR_READ_EXCEPTIONS } from '../../../../common/constants'; interface SetupModeProps { setupMode: any; @@ -24,7 +26,7 @@ interface SetupModeProps { bottomBarComponent: any; } -export const ElasticsearchCcrShardPage: React.FC = ({ clusters }) => { +export const ElasticsearchCcrShardPage: React.FC = () => { const globalState = useContext(GlobalStateContext); const { services } = useKibana<{ data: any }>(); const { index, shardId }: { index: string; shardId: string } = useParams(); @@ -32,6 +34,7 @@ export const ElasticsearchCcrShardPage: React.FC = ({ clusters } const clusterUuid = globalState.cluster_uuid; const ccs = globalState.ccs; const [data, setData] = useState({} as any); + const [alerts, setAlerts] = useState({}); const title = i18n.translate('xpack.monitoring.elasticsearch.ccr.shard.title', { defaultMessage: 'Elasticsearch - Ccr - Shard', @@ -57,18 +60,34 @@ export const ElasticsearchCcrShardPage: React.FC = ({ clusters } const bounds = services.data?.query.timefilter.timefilter.getBounds(); const url = `../api/monitoring/v1/clusters/${clusterUuid}/elasticsearch/ccr/${index}/shard/${shardId}`; - const response = await services.http?.fetch(url, { - method: 'POST', - body: JSON.stringify({ - ccs, + if (services.http?.fetch && clusterUuid) { + const response = await services.http?.fetch(url, { + method: 'POST', + body: JSON.stringify({ + ccs, + timeRange: { + min: bounds.min.toISOString(), + max: bounds.max.toISOString(), + }, + }), + }); + setData(response); + const alertsResponse = await fetchAlerts({ + fetch: services.http.fetch, + alertTypeIds: [RULE_CCR_READ_EXCEPTIONS], + clusterUuid, + filters: [ + { + shardId, + }, + ], timeRange: { - min: bounds.min.toISOString(), - max: bounds.max.toISOString(), + min: bounds.min.valueOf(), + max: bounds.max.valueOf(), }, - }), - }); - - setData(response); + }); + setAlerts(alertsResponse); + } }, [ccs, clusterUuid, services.data?.query.timefilter.timefilter, services.http, index, shardId]); return ( @@ -84,7 +103,7 @@ export const ElasticsearchCcrShardPage: React.FC = ({ clusters } render={({ flyoutComponent, bottomBarComponent }: SetupModeProps) => ( {flyoutComponent} - + {bottomBarComponent} )} diff --git a/x-pack/plugins/monitoring/public/application/pages/elasticsearch/index_advanced_page.tsx b/x-pack/plugins/monitoring/public/application/pages/elasticsearch/index_advanced_page.tsx index a635d98fcbbb0..86dba4e2f921c 100644 --- a/x-pack/plugins/monitoring/public/application/pages/elasticsearch/index_advanced_page.tsx +++ b/x-pack/plugins/monitoring/public/application/pages/elasticsearch/index_advanced_page.tsx @@ -8,7 +8,7 @@ import React, { useContext, useState, useCallback } from 'react'; import { i18n } from '@kbn/i18n'; import { useParams } from 'react-router-dom'; import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; -import { GlobalStateContext } from '../../global_state_context'; +import { GlobalStateContext } from '../../contexts/global_state_context'; import { ComponentProps } from '../../route_init'; import { SetupModeRenderer, SetupModeProps } from '../../setup_mode/setup_mode_renderer'; import { SetupModeContext } from '../../../components/setup_mode/setup_mode_context'; @@ -16,15 +16,18 @@ import { useCharts } from '../../hooks/use_charts'; import { ItemTemplate } from './item_template'; // @ts-ignore import { AdvancedIndex } from '../../../components/elasticsearch/index/advanced'; -import { ELASTICSEARCH_SYSTEM_ID } from '../../../../common/constants'; +import { AlertsByName } from '../../../alerts/types'; +import { fetchAlerts } from '../../../lib/fetch_alerts'; +import { ELASTICSEARCH_SYSTEM_ID, RULE_LARGE_SHARD_SIZE } from '../../../../common/constants'; -export const ElasticsearchIndexAdvancedPage: React.FC = ({ clusters }) => { +export const ElasticsearchIndexAdvancedPage: React.FC = () => { const globalState = useContext(GlobalStateContext); const { services } = useKibana<{ data: any }>(); const { index }: { index: string } = useParams(); const { zoomInfo, onBrush } = useCharts(); const clusterUuid = globalState.cluster_uuid; const [data, setData] = useState({} as any); + const [alerts, setAlerts] = useState({}); const title = i18n.translate('xpack.monitoring.elasticsearch.index.advanced.title', { defaultMessage: 'Elasticsearch - Indices - {indexName} - Advanced', @@ -36,17 +39,34 @@ export const ElasticsearchIndexAdvancedPage: React.FC = ({ clust const getPageData = useCallback(async () => { const bounds = services.data?.query.timefilter.timefilter.getBounds(); const url = `../api/monitoring/v1/clusters/${clusterUuid}/elasticsearch/indices/${index}`; - const response = await services.http?.fetch(url, { - method: 'POST', - body: JSON.stringify({ + if (services.http?.fetch && clusterUuid) { + const response = await services.http?.fetch(url, { + method: 'POST', + body: JSON.stringify({ + timeRange: { + min: bounds.min.toISOString(), + max: bounds.max.toISOString(), + }, + is_advanced: true, + }), + }); + setData(response); + const alertsResponse = await fetchAlerts({ + fetch: services.http.fetch, + alertTypeIds: [RULE_LARGE_SHARD_SIZE], + filters: [ + { + shardIndex: index, + }, + ], + clusterUuid, timeRange: { - min: bounds.min.toISOString(), - max: bounds.max.toISOString(), + min: bounds.min.valueOf(), + max: bounds.max.valueOf(), }, - is_advanced: true, - }), - }); - setData(response); + }); + setAlerts(alertsResponse); + } }, [clusterUuid, services.data?.query.timefilter.timefilter, services.http, index]); return ( @@ -58,7 +78,7 @@ export const ElasticsearchIndexAdvancedPage: React.FC = ({ clust {flyoutComponent} = ({ clusters }) => { +export const ElasticsearchIndexPage: React.FC = () => { const globalState = useContext(GlobalStateContext); const { services } = useKibana<{ data: any }>(); const { index }: { index: string } = useParams(); @@ -31,6 +33,7 @@ export const ElasticsearchIndexPage: React.FC = ({ clusters }) = const [data, setData] = useState({} as any); const [indexLabel, setIndexLabel] = useState(labels.index as any); const [nodesByIndicesData, setNodesByIndicesData] = useState([]); + const [alerts, setAlerts] = useState({}); const title = i18n.translate('xpack.monitoring.elasticsearch.index.overview.title', { defaultMessage: 'Elasticsearch - Indices - {indexName} - Overview', @@ -49,23 +52,40 @@ export const ElasticsearchIndexPage: React.FC = ({ clusters }) = const getPageData = useCallback(async () => { const bounds = services.data?.query.timefilter.timefilter.getBounds(); const url = `../api/monitoring/v1/clusters/${clusterUuid}/elasticsearch/indices/${index}`; - const response = await services.http?.fetch(url, { - method: 'POST', - body: JSON.stringify({ + if (services.http?.fetch && clusterUuid) { + const response = await services.http?.fetch(url, { + method: 'POST', + body: JSON.stringify({ + timeRange: { + min: bounds.min.toISOString(), + max: bounds.max.toISOString(), + }, + is_advanced: false, + }), + }); + setData(response); + const transformer = indicesByNodes(); + setNodesByIndicesData(transformer(response.shards, response.nodes)); + + const shards = response.shards; + if (shards.some((shard: any) => shard.state === 'UNASSIGNED')) { + setIndexLabel(labels.indexWithUnassigned); + } + const alertsResponse = await fetchAlerts({ + fetch: services.http.fetch, + alertTypeIds: [RULE_LARGE_SHARD_SIZE], + filters: [ + { + shardIndex: index, + }, + ], + clusterUuid, timeRange: { - min: bounds.min.toISOString(), - max: bounds.max.toISOString(), + min: bounds.min.valueOf(), + max: bounds.max.valueOf(), }, - is_advanced: false, - }), - }); - setData(response); - const transformer = indicesByNodes(); - setNodesByIndicesData(transformer(response.shards, response.nodes)); - - const shards = response.shards; - if (shards.some((shard: any) => shard.state === 'UNASSIGNED')) { - setIndexLabel(labels.indexWithUnassigned); + }); + setAlerts(alertsResponse); } }, [clusterUuid, services.data?.query.timefilter.timefilter, services.http, index]); @@ -85,7 +105,7 @@ export const ElasticsearchIndexPage: React.FC = ({ clusters }) = = ({ clusters }) => { const globalState = useContext(GlobalStateContext); @@ -32,6 +34,7 @@ export const ElasticsearchIndicesPage: React.FC = ({ clusters }) 'showSystemIndices', false ); + const [alerts, setAlerts] = useState({}); const title = i18n.translate('xpack.monitoring.elasticsearch.indices.routeTitle', { defaultMessage: 'Elasticsearch - Indices', @@ -49,26 +52,38 @@ export const ElasticsearchIndicesPage: React.FC = ({ clusters }) const getPageData = useCallback(async () => { const bounds = services.data?.query.timefilter.timefilter.getBounds(); const url = `../api/monitoring/v1/clusters/${clusterUuid}/elasticsearch/indices`; - const response = await services.http?.fetch(url, { - method: 'POST', - query: { - show_system_indices: showSystemIndices, - }, - body: JSON.stringify({ - ccs, + if (services.http?.fetch && clusterUuid) { + const response = await services.http?.fetch(url, { + method: 'POST', + query: { + show_system_indices: showSystemIndices, + }, + body: JSON.stringify({ + ccs, + timeRange: { + min: bounds.min.toISOString(), + max: bounds.max.toISOString(), + }, + }), + }); + setData(response); + const alertsResponse = await fetchAlerts({ + fetch: services.http.fetch, + clusterUuid, + alertTypeIds: [RULE_LARGE_SHARD_SIZE], timeRange: { - min: bounds.min.toISOString(), - max: bounds.max.toISOString(), + min: bounds.min.valueOf(), + max: bounds.max.valueOf(), }, - }), - }); - setData(response); + }); + setAlerts(alertsResponse); + } }, [ - ccs, - showSystemIndices, - clusterUuid, services.data?.query.timefilter.timefilter, services.http, + clusterUuid, + showSystemIndices, + ccs, ]); return ( @@ -88,7 +103,7 @@ export const ElasticsearchIndicesPage: React.FC = ({ clusters }) = ({ clusters }) => { +export const ElasticsearchNodeAdvancedPage: React.FC = () => { const globalState = useContext(GlobalStateContext); const { zoomInfo, onBrush } = useCharts(); @@ -25,6 +35,7 @@ export const ElasticsearchNodeAdvancedPage: React.FC = ({ cluste const clusterUuid = globalState.cluster_uuid; const ccs = globalState.ccs; const [data, setData] = useState({} as any); + const [alerts, setAlerts] = useState({}); const title = i18n.translate('xpack.monitoring.elasticsearch.node.advanced.title', { defaultMessage: 'Elasticsearch - Nodes - {nodeName} - Advanced', @@ -43,20 +54,42 @@ export const ElasticsearchNodeAdvancedPage: React.FC = ({ cluste const getPageData = useCallback(async () => { const bounds = services.data?.query.timefilter.timefilter.getBounds(); const url = `../api/monitoring/v1/clusters/${clusterUuid}/elasticsearch/nodes/${node}`; - - const response = await services.http?.fetch(url, { - method: 'POST', - body: JSON.stringify({ - ccs, + if (services.http?.fetch && clusterUuid) { + const response = await services.http?.fetch(url, { + method: 'POST', + body: JSON.stringify({ + ccs, + timeRange: { + min: bounds.min.toISOString(), + max: bounds.max.toISOString(), + }, + is_advanced: true, + }), + }); + setData(response); + const alertsResponse = await fetchAlerts({ + fetch: services.http.fetch, + clusterUuid, + alertTypeIds: [ + RULE_CPU_USAGE, + RULE_THREAD_POOL_SEARCH_REJECTIONS, + RULE_THREAD_POOL_WRITE_REJECTIONS, + RULE_MISSING_MONITORING_DATA, + RULE_DISK_USAGE, + RULE_MEMORY_USAGE, + ], + filters: [ + { + nodeUuid: node, + }, + ], timeRange: { - min: bounds.min.toISOString(), - max: bounds.max.toISOString(), + min: bounds.min.valueOf(), + max: bounds.max.valueOf(), }, - is_advanced: true, - }), - }); - - setData(response); + }); + setAlerts(alertsResponse); + } }, [ccs, clusterUuid, services.data?.query.timefilter.timefilter, services.http, node]); return ( @@ -69,7 +102,7 @@ export const ElasticsearchNodeAdvancedPage: React.FC = ({ cluste > = ({ clusters }) => { +export const ElasticsearchNodePage: React.FC = () => { const globalState = useContext(GlobalStateContext); const { zoomInfo, onBrush } = useCharts(); const [showSystemIndices, setShowSystemIndices] = useLocalStorage( 'showSystemIndices', false ); + const [alerts, setAlerts] = useState({}); const { node }: { node: string } = useParams(); const { services } = useKibana<{ data: any }>(); @@ -54,30 +65,49 @@ export const ElasticsearchNodePage: React.FC = ({ clusters }) => const getPageData = useCallback(async () => { const bounds = services.data?.query.timefilter.timefilter.getBounds(); const url = `../api/monitoring/v1/clusters/${clusterUuid}/elasticsearch/nodes/${node}`; + if (services.http?.fetch && clusterUuid) { + const response = await services.http?.fetch(url, { + method: 'POST', + body: JSON.stringify({ + showSystemIndices, + ccs, + timeRange: { + min: bounds.min.toISOString(), + max: bounds.max.toISOString(), + }, + is_advanced: false, + }), + }); - const response = await services.http?.fetch(url, { - method: 'POST', - body: JSON.stringify({ - showSystemIndices, - ccs, + setData(response); + const transformer = nodesByIndices(); + setNodesByIndicesData(transformer(response.shards, response.nodes)); + const alertsResponse = await fetchAlerts({ + fetch: services.http.fetch, + alertTypeIds: [ + RULE_CPU_USAGE, + RULE_THREAD_POOL_SEARCH_REJECTIONS, + RULE_THREAD_POOL_WRITE_REJECTIONS, + RULE_MISSING_MONITORING_DATA, + RULE_DISK_USAGE, + RULE_MEMORY_USAGE, + ], + filters: [{ nodeUuid: node }], + clusterUuid, timeRange: { - min: bounds.min.toISOString(), - max: bounds.max.toISOString(), + min: bounds.min.valueOf(), + max: bounds.max.valueOf(), }, - is_advanced: false, - }), - }); - - setData(response); - const transformer = nodesByIndices(); - setNodesByIndicesData(transformer(response.shards, response.nodes)); + }); + setAlerts(alertsResponse); + } }, [ - ccs, - clusterUuid, services.data?.query.timefilter.timefilter, services.http, + clusterUuid, node, showSystemIndices, + ccs, ]); const toggleShowSystemIndices = useCallback(() => { @@ -98,7 +128,7 @@ export const ElasticsearchNodePage: React.FC = ({ clusters }) => {flyoutComponent} = ({ clusters }) => { const globalState = useContext(GlobalStateContext); @@ -32,6 +42,7 @@ export const ElasticsearchNodesPage: React.FC = ({ clusters }) = cluster_uuid: clusterUuid, }) as any; const [data, setData] = useState({} as any); + const [alerts, setAlerts] = useState({}); const title = i18n.translate('xpack.monitoring.elasticsearch.nodes.routeTitle', { defaultMessage: 'Elasticsearch - Nodes', @@ -52,25 +63,44 @@ export const ElasticsearchNodesPage: React.FC = ({ clusters }) = const getPageData = useCallback(async () => { const bounds = services.data?.query.timefilter.timefilter.getBounds(); const url = `../api/monitoring/v1/clusters/${clusterUuid}/elasticsearch/nodes`; - const response = await services.http?.fetch(url, { - method: 'POST', - body: JSON.stringify({ - ccs, + if (services.http?.fetch && clusterUuid) { + const response = await services.http?.fetch(url, { + method: 'POST', + body: JSON.stringify({ + ccs, + timeRange: { + min: bounds.min.toISOString(), + max: bounds.max.toISOString(), + }, + ...getPaginationRouteOptions(), + }), + }); + + setData(response); + updateTotalItemCount(response.totalNodeCount); + const alertsResponse = await fetchAlerts({ + fetch: services.http.fetch, + clusterUuid, + alertTypeIds: [ + RULE_CPU_USAGE, + RULE_DISK_USAGE, + RULE_THREAD_POOL_SEARCH_REJECTIONS, + RULE_THREAD_POOL_WRITE_REJECTIONS, + RULE_MEMORY_USAGE, + RULE_MISSING_MONITORING_DATA, + ], timeRange: { - min: bounds.min.toISOString(), - max: bounds.max.toISOString(), + min: bounds.min.valueOf(), + max: bounds.max.valueOf(), }, - ...getPaginationRouteOptions(), - }), - }); - - setData(response); - updateTotalItemCount(response.totalNodeCount); + }); + setAlerts(alertsResponse); + } }, [ - ccs, - clusterUuid, services.data?.query.timefilter.timefilter, services.http, + clusterUuid, + ccs, getPaginationRouteOptions, updateTotalItemCount, ]); @@ -94,7 +124,7 @@ export const ElasticsearchNodesPage: React.FC = ({ clusters }) = clusterUuid={globalState.cluster_uuid} setupMode={setupMode} nodes={data.nodes} - alerts={{}} + alerts={alerts} showCgroupMetricsElasticsearch={showCgroupMetricsElasticsearch} {...getPaginationTableProps()} /> diff --git a/x-pack/plugins/monitoring/public/application/pages/elasticsearch/overview.tsx b/x-pack/plugins/monitoring/public/application/pages/elasticsearch/overview.tsx index 3334c7e7b880a..c58aaa5dffb04 100644 --- a/x-pack/plugins/monitoring/public/application/pages/elasticsearch/overview.tsx +++ b/x-pack/plugins/monitoring/public/application/pages/elasticsearch/overview.tsx @@ -9,7 +9,7 @@ import { i18n } from '@kbn/i18n'; import { find } from 'lodash'; import { ElasticsearchTemplate } from './elasticsearch_template'; import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; -import { GlobalStateContext } from '../../global_state_context'; +import { GlobalStateContext } from '../../contexts/global_state_context'; import { ElasticsearchOverview } from '../../../components/elasticsearch'; import { ComponentProps } from '../../route_init'; import { useCharts } from '../../hooks/use_charts'; diff --git a/x-pack/plugins/monitoring/public/application/pages/home/cluster_listing.tsx b/x-pack/plugins/monitoring/public/application/pages/home/cluster_listing.tsx index 906db1b57f0f5..a31f2bc317fa6 100644 --- a/x-pack/plugins/monitoring/public/application/pages/home/cluster_listing.tsx +++ b/x-pack/plugins/monitoring/public/application/pages/home/cluster_listing.tsx @@ -12,8 +12,8 @@ import { useKibana } from '../../../../../../../src/plugins/kibana_react/public' // @ts-ignore import { Listing } from '../../../components/cluster/listing'; import { EnableAlertsModal } from '../../../alerts/enable_alerts_modal'; -import { GlobalStateContext } from '../../global_state_context'; -import { ExternalConfigContext } from '../../external_config_context'; +import { GlobalStateContext } from '../../contexts/global_state_context'; +import { ExternalConfigContext } from '../../contexts/external_config_context'; import { ComponentProps } from '../../route_init'; import { useTable } from '../../hooks/use_table'; import { PageTemplate, TabMenuItem } from '../page_template'; @@ -69,23 +69,19 @@ export const ClusterListing: React.FC = () => { const getPageData = useCallback(async () => { const bounds = services.data?.query.timefilter.timefilter.getBounds(); - try { - if (services.http?.fetch) { - const response = await fetchClusters({ - fetch: services.http.fetch, - timeRange: { - min: bounds.min.toISOString(), - max: bounds.max.toISOString(), - }, - ccs: globalState.ccs, - codePaths: ['all'], - }); - setClusters(response); - } - } catch (err) { - // TODO: handle errors + if (services.http?.fetch) { + const response = await fetchClusters({ + fetch: services.http.fetch, + timeRange: { + min: bounds.min.toISOString(), + max: bounds.max.toISOString(), + }, + ccs: globalState.ccs, + codePaths: ['all'], + }); + setClusters(response); } - }, [globalState, services.data?.query.timefilter.timefilter, services.http]); + }, [globalState.ccs, services.data?.query.timefilter.timefilter, services.http]); if (globalState.save && clusters.length === 1) { globalState.cluster_uuid = clusters[0].cluster_uuid; diff --git a/x-pack/plugins/monitoring/public/application/pages/kibana/instance.tsx b/x-pack/plugins/monitoring/public/application/pages/kibana/instance.tsx index 8b88fc47a9007..444794d118b0f 100644 --- a/x-pack/plugins/monitoring/public/application/pages/kibana/instance.tsx +++ b/x-pack/plugins/monitoring/public/application/pages/kibana/instance.tsx @@ -19,7 +19,7 @@ import { EuiPanel, } from '@elastic/eui'; import { ComponentProps } from '../../route_init'; -import { GlobalStateContext } from '../../global_state_context'; +import { GlobalStateContext } from '../../contexts/global_state_context'; import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; import { useCharts } from '../../hooks/use_charts'; // @ts-ignore @@ -30,6 +30,9 @@ import { MonitoringTimeseriesContainer } from '../../../components/chart'; import { DetailStatus } from '../../../components/kibana/detail_status'; import { PageTemplate } from '../page_template'; import { AlertsCallout } from '../../../alerts/callout'; +import { AlertsByName } from '../../../alerts/types'; +import { fetchAlerts } from '../../../lib/fetch_alerts'; +import { RULE_KIBANA_VERSION_MISMATCH } from '../../../../common/constants'; const KibanaInstance = ({ data, alerts }: { data: any; alerts: any }) => { const { zoomInfo, onBrush } = useCharts(); @@ -112,6 +115,7 @@ export const KibanaInstancePage: React.FC = ({ clusters }) => { }) as any; const [data, setData] = useState({} as any); const [instanceName, setInstanceName] = useState(''); + const [alerts, setAlerts] = useState({}); const title = `Kibana - ${instanceName}`; const pageTitle = i18n.translate('xpack.monitoring.kibana.instance.pageTitle', { @@ -133,19 +137,30 @@ export const KibanaInstancePage: React.FC = ({ clusters }) => { const getPageData = useCallback(async () => { const bounds = services.data?.query.timefilter.timefilter.getBounds(); const url = `../api/monitoring/v1/clusters/${clusterUuid}/kibana/${instance}`; - const response = await services.http?.fetch(url, { - method: 'POST', - body: JSON.stringify({ - ccs, + if (services.http?.fetch && clusterUuid) { + const response = await services.http?.fetch(url, { + method: 'POST', + body: JSON.stringify({ + ccs, + timeRange: { + min: bounds.min.toISOString(), + max: bounds.max.toISOString(), + }, + }), + }); + setData(response); + setInstanceName(response.kibanaSummary.name); + const alertsResponse = await fetchAlerts({ + fetch: services.http.fetch, + alertTypeIds: [RULE_KIBANA_VERSION_MISMATCH], + clusterUuid, timeRange: { - min: bounds.min.toISOString(), - max: bounds.max.toISOString(), + min: bounds.min.valueOf(), + max: bounds.max.valueOf(), }, - }), - }); - - setData(response); - setInstanceName(response.kibanaSummary.name); + }); + setAlerts(alertsResponse); + } }, [ccs, clusterUuid, instance, services.data?.query.timefilter.timefilter, services.http]); return ( @@ -156,7 +171,7 @@ export const KibanaInstancePage: React.FC = ({ clusters }) => { data-test-subj="kibanaInstancePage" >
- +
); diff --git a/x-pack/plugins/monitoring/public/application/pages/kibana/instances.tsx b/x-pack/plugins/monitoring/public/application/pages/kibana/instances.tsx index 436a1a72b2fdb..ae0237ea40472 100644 --- a/x-pack/plugins/monitoring/public/application/pages/kibana/instances.tsx +++ b/x-pack/plugins/monitoring/public/application/pages/kibana/instances.tsx @@ -9,7 +9,7 @@ import React, { useContext, useState, useCallback, useEffect } from 'react'; import { i18n } from '@kbn/i18n'; import { find } from 'lodash'; import { ComponentProps } from '../../route_init'; -import { GlobalStateContext } from '../../global_state_context'; +import { GlobalStateContext } from '../../contexts/global_state_context'; import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; import { useTable } from '../../hooks/use_table'; import { KibanaTemplate } from './kibana_template'; @@ -19,7 +19,9 @@ import { KibanaInstances } from '../../../components/kibana/instances'; import { SetupModeRenderer, SetupModeProps } from '../../setup_mode/setup_mode_renderer'; import { SetupModeContext } from '../../../components/setup_mode/setup_mode_context'; import { BreadcrumbContainer } from '../../hooks/use_breadcrumbs'; -import { KIBANA_SYSTEM_ID } from '../../../../common/constants'; +import { AlertsByName } from '../../../alerts/types'; +import { fetchAlerts } from '../../../lib/fetch_alerts'; +import { KIBANA_SYSTEM_ID, RULE_KIBANA_VERSION_MISMATCH } from '../../../../common/constants'; export const KibanaInstancesPage: React.FC = ({ clusters }) => { const { cluster_uuid: clusterUuid, ccs } = useContext(GlobalStateContext); @@ -30,6 +32,7 @@ export const KibanaInstancesPage: React.FC = ({ clusters }) => { cluster_uuid: clusterUuid, }) as any; const [data, setData] = useState({} as any); + const [alerts, setAlerts] = useState({}); const title = i18n.translate('xpack.monitoring.kibana.instances.routeTitle', { defaultMessage: 'Kibana - Instances', @@ -50,19 +53,31 @@ export const KibanaInstancesPage: React.FC = ({ clusters }) => { const getPageData = useCallback(async () => { const bounds = services.data?.query.timefilter.timefilter.getBounds(); const url = `../api/monitoring/v1/clusters/${clusterUuid}/kibana/instances`; - const response = await services.http?.fetch(url, { - method: 'POST', - body: JSON.stringify({ - ccs, + if (services.http?.fetch && clusterUuid) { + const response = await services.http?.fetch(url, { + method: 'POST', + body: JSON.stringify({ + ccs, + timeRange: { + min: bounds.min.toISOString(), + max: bounds.max.toISOString(), + }, + }), + }); + + setData(response); + updateTotalItemCount(response.kibanas.length); + const alertsResponse = await fetchAlerts({ + fetch: services.http.fetch, + alertTypeIds: [RULE_KIBANA_VERSION_MISMATCH], + clusterUuid, timeRange: { - min: bounds.min.toISOString(), - max: bounds.max.toISOString(), + min: bounds.min.valueOf(), + max: bounds.max.valueOf(), }, - }), - }); - - setData(response); - updateTotalItemCount(response.stats.total); + }); + setAlerts(alertsResponse); + } }, [ ccs, clusterUuid, @@ -85,7 +100,7 @@ export const KibanaInstancesPage: React.FC = ({ clusters }) => { {flyoutComponent} = ({ clusters }) => { const globalState = useContext(GlobalStateContext); @@ -42,6 +45,7 @@ export const LogStashNodeAdvancedPage: React.FC = ({ clusters }) }); const [data, setData] = useState({} as any); + const [alerts, setAlerts] = useState({}); const title = i18n.translate('xpack.monitoring.logstash.node.advanced.routeTitle', { defaultMessage: 'Logstash - {nodeName} - Advanced', @@ -60,19 +64,30 @@ export const LogStashNodeAdvancedPage: React.FC = ({ clusters }) const getPageData = useCallback(async () => { const bounds = services.data?.query.timefilter.timefilter.getBounds(); const url = `../api/monitoring/v1/clusters/${clusterUuid}/logstash/node/${match.params.uuid}`; - const response = await services.http?.fetch(url, { - method: 'POST', - body: JSON.stringify({ - ccs, + if (services.http?.fetch && clusterUuid) { + const response = await services.http?.fetch(url, { + method: 'POST', + body: JSON.stringify({ + ccs, + timeRange: { + min: bounds.min.toISOString(), + max: bounds.max.toISOString(), + }, + is_advanced: true, + }), + }); + setData(response); + const alertsResponse = await fetchAlerts({ + fetch: services.http.fetch, + alertTypeIds: [RULE_LOGSTASH_VERSION_MISMATCH], + clusterUuid, timeRange: { - min: bounds.min.toISOString(), - max: bounds.max.toISOString(), + min: bounds.min.valueOf(), + max: bounds.max.valueOf(), }, - is_advanced: true, - }), - }); - - setData(response); + }); + setAlerts(alertsResponse); + } }, [ ccs, clusterUuid, @@ -105,7 +120,7 @@ export const LogStashNodeAdvancedPage: React.FC = ({ clusters }) {data.nodeSummary && } - + {metricsToShow.map((metric, index) => ( diff --git a/x-pack/plugins/monitoring/public/application/pages/logstash/node.tsx b/x-pack/plugins/monitoring/public/application/pages/logstash/node.tsx index 301d3c45dedb5..1163a619dd84b 100644 --- a/x-pack/plugins/monitoring/public/application/pages/logstash/node.tsx +++ b/x-pack/plugins/monitoring/public/application/pages/logstash/node.tsx @@ -18,7 +18,7 @@ import { EuiFlexItem, } from '@elastic/eui'; import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; -import { GlobalStateContext } from '../../global_state_context'; +import { GlobalStateContext } from '../../contexts/global_state_context'; import { ComponentProps } from '../../route_init'; // @ts-ignore import { List } from '../../../components/logstash/pipeline_viewer/models/list'; @@ -30,6 +30,9 @@ import { DetailStatus } from '../../../components/logstash/detail_status'; import { MonitoringTimeseriesContainer } from '../../../components/chart'; import { AlertsCallout } from '../../../alerts/callout'; import { useCharts } from '../../hooks/use_charts'; +import { AlertsByName } from '../../../alerts/types'; +import { fetchAlerts } from '../../../lib/fetch_alerts'; +import { RULE_LOGSTASH_VERSION_MISMATCH } from '../../../../common/constants'; export const LogStashNodePage: React.FC = ({ clusters }) => { const match = useRouteMatch<{ uuid: string | undefined }>(); @@ -41,6 +44,7 @@ export const LogStashNodePage: React.FC = ({ clusters }) => { cluster_uuid: clusterUuid, }); const [data, setData] = useState({} as any); + const [alerts, setAlerts] = useState({}); const { zoomInfo, onBrush } = useCharts(); const title = i18n.translate('xpack.monitoring.logstash.node.routeTitle', { defaultMessage: 'Logstash - {nodeName}', @@ -59,19 +63,30 @@ export const LogStashNodePage: React.FC = ({ clusters }) => { const getPageData = useCallback(async () => { const url = `../api/monitoring/v1/clusters/${clusterUuid}/logstash/node/${match.params.uuid}`; const bounds = services.data?.query.timefilter.timefilter.getBounds(); - const response = await services.http?.fetch(url, { - method: 'POST', - body: JSON.stringify({ - ccs, + if (services.http?.fetch && clusterUuid) { + const response = await services.http?.fetch(url, { + method: 'POST', + body: JSON.stringify({ + ccs, + timeRange: { + min: bounds.min.toISOString(), + max: bounds.max.toISOString(), + }, + is_advanced: false, + }), + }); + setData(response); + const alertsResponse = await fetchAlerts({ + fetch: services.http.fetch, + alertTypeIds: [RULE_LOGSTASH_VERSION_MISMATCH], + clusterUuid, timeRange: { - min: bounds.min.toISOString(), - max: bounds.max.toISOString(), + min: bounds.min.valueOf(), + max: bounds.max.valueOf(), }, - is_advanced: false, - }), - }); - - setData(response); + }); + setAlerts(alertsResponse); + } }, [ccs, clusterUuid, services.data?.query.timefilter.timefilter, services.http, match.params]); const metricsToShow = useMemo(() => { @@ -99,7 +114,7 @@ export const LogStashNodePage: React.FC = ({ clusters }) => { {data.nodeSummary && } - + {metricsToShow.map((metric, index) => ( diff --git a/x-pack/plugins/monitoring/public/application/pages/logstash/node_pipelines.tsx b/x-pack/plugins/monitoring/public/application/pages/logstash/node_pipelines.tsx index 1c956603f99bd..e09850eaad5c9 100644 --- a/x-pack/plugins/monitoring/public/application/pages/logstash/node_pipelines.tsx +++ b/x-pack/plugins/monitoring/public/application/pages/logstash/node_pipelines.tsx @@ -12,7 +12,7 @@ import { useRouteMatch } from 'react-router-dom'; // @ts-ignore import { isPipelineMonitoringSupportedInVersion } from '../../../lib/logstash/pipelines'; import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; -import { GlobalStateContext } from '../../global_state_context'; +import { GlobalStateContext } from '../../contexts/global_state_context'; import { ComponentProps } from '../../route_init'; // @ts-ignore import { Listing } from '../../../components/logstash/listing'; diff --git a/x-pack/plugins/monitoring/public/application/pages/logstash/nodes.tsx b/x-pack/plugins/monitoring/public/application/pages/logstash/nodes.tsx index 09a97925c56f5..0fd10a93bcd83 100644 --- a/x-pack/plugins/monitoring/public/application/pages/logstash/nodes.tsx +++ b/x-pack/plugins/monitoring/public/application/pages/logstash/nodes.tsx @@ -8,7 +8,7 @@ import React, { useContext, useState, useCallback } from 'react'; import { i18n } from '@kbn/i18n'; import { find } from 'lodash'; import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; -import { GlobalStateContext } from '../../global_state_context'; +import { GlobalStateContext } from '../../contexts/global_state_context'; import { ComponentProps } from '../../route_init'; // @ts-ignore import { Listing } from '../../../components/logstash/listing'; @@ -16,7 +16,9 @@ import { LogstashTemplate } from './logstash_template'; import { SetupModeRenderer } from '../../setup_mode/setup_mode_renderer'; import { SetupModeContext } from '../../../components/setup_mode/setup_mode_context'; import { useTable } from '../../hooks/use_table'; -import { LOGSTASH_SYSTEM_ID } from '../../../../common/constants'; +import { LOGSTASH_SYSTEM_ID, RULE_LOGSTASH_VERSION_MISMATCH } from '../../../../common/constants'; +import { AlertsByName } from '../../../alerts/types'; +import { fetchAlerts } from '../../../lib/fetch_alerts'; interface SetupModeProps { setupMode: any; @@ -33,6 +35,7 @@ export const LogStashNodesPage: React.FC = ({ clusters }) => { cluster_uuid: clusterUuid, }); const [data, setData] = useState({} as any); + const [alerts, setAlerts] = useState({}); const { getPaginationTableProps } = useTable('logstash.nodes'); const title = i18n.translate('xpack.monitoring.logstash.nodes.routeTitle', { @@ -46,18 +49,30 @@ export const LogStashNodesPage: React.FC = ({ clusters }) => { const getPageData = useCallback(async () => { const bounds = services.data?.query.timefilter.timefilter.getBounds(); const url = `../api/monitoring/v1/clusters/${clusterUuid}/logstash/nodes`; - const response = await services.http?.fetch(url, { - method: 'POST', - body: JSON.stringify({ - ccs, + if (services.http?.fetch && clusterUuid) { + const response = await services.http?.fetch(url, { + method: 'POST', + body: JSON.stringify({ + ccs, + timeRange: { + min: bounds.min.toISOString(), + max: bounds.max.toISOString(), + }, + }), + }); + + setData(response); + const alertsResponse = await fetchAlerts({ + fetch: services.http.fetch, + alertTypeIds: [RULE_LOGSTASH_VERSION_MISMATCH], + clusterUuid, timeRange: { - min: bounds.min.toISOString(), - max: bounds.max.toISOString(), + min: bounds.min.valueOf(), + max: bounds.max.valueOf(), }, - }), - }); - - setData(response); + }); + setAlerts(alertsResponse); + } }, [ccs, clusterUuid, services.data?.query.timefilter.timefilter, services.http]); return ( @@ -78,6 +93,7 @@ export const LogStashNodesPage: React.FC = ({ clusters }) => { metrics={data.metrics} data={data.nodes} setupMode={setupMode} + alerts={alerts} {...getPaginationTableProps()} /> {bottomBarComponent} diff --git a/x-pack/plugins/monitoring/public/application/pages/logstash/overview.tsx b/x-pack/plugins/monitoring/public/application/pages/logstash/overview.tsx index 1edbe5cf71e7d..339b9e9395569 100644 --- a/x-pack/plugins/monitoring/public/application/pages/logstash/overview.tsx +++ b/x-pack/plugins/monitoring/public/application/pages/logstash/overview.tsx @@ -8,7 +8,7 @@ import React, { useContext, useState, useCallback } from 'react'; import { i18n } from '@kbn/i18n'; import { find } from 'lodash'; import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; -import { GlobalStateContext } from '../../global_state_context'; +import { GlobalStateContext } from '../../contexts/global_state_context'; import { ComponentProps } from '../../route_init'; import { useCharts } from '../../hooks/use_charts'; // @ts-ignore diff --git a/x-pack/plugins/monitoring/public/application/pages/logstash/pipeline.tsx b/x-pack/plugins/monitoring/public/application/pages/logstash/pipeline.tsx index abff0ab17b992..20f1caee2b1d8 100644 --- a/x-pack/plugins/monitoring/public/application/pages/logstash/pipeline.tsx +++ b/x-pack/plugins/monitoring/public/application/pages/logstash/pipeline.tsx @@ -10,7 +10,7 @@ import { find } from 'lodash'; import moment from 'moment'; import { useRouteMatch } from 'react-router-dom'; import { useKibana, useUiSetting } from '../../../../../../../src/plugins/kibana_react/public'; -import { GlobalStateContext } from '../../global_state_context'; +import { GlobalStateContext } from '../../contexts/global_state_context'; import { ComponentProps } from '../../route_init'; // @ts-ignore import { List } from '../../../components/logstash/pipeline_viewer/models/list'; @@ -24,7 +24,7 @@ import { PipelineState } from '../../../components/logstash/pipeline_viewer/mode import { vertexFactory } from '../../../components/logstash/pipeline_viewer/models/graph/vertex_factory'; import { LogstashTemplate } from './logstash_template'; import { useTable } from '../../hooks/use_table'; -import { ExternalConfigContext } from '../../external_config_context'; +import { ExternalConfigContext } from '../../contexts/external_config_context'; import { formatTimestampToDuration } from '../../../../common'; import { CALCULATE_DURATION_SINCE } from '../../../../common/constants'; import { getSafeForExternalLink } from '../../../lib/get_safe_for_external_link'; diff --git a/x-pack/plugins/monitoring/public/application/pages/logstash/pipelines.tsx b/x-pack/plugins/monitoring/public/application/pages/logstash/pipelines.tsx index 5f4fe634177de..ac750ff81ddaa 100644 --- a/x-pack/plugins/monitoring/public/application/pages/logstash/pipelines.tsx +++ b/x-pack/plugins/monitoring/public/application/pages/logstash/pipelines.tsx @@ -8,7 +8,7 @@ import React, { useContext, useState, useCallback } from 'react'; import { i18n } from '@kbn/i18n'; import { find } from 'lodash'; import { useKibana, useUiSetting } from '../../../../../../../src/plugins/kibana_react/public'; -import { GlobalStateContext } from '../../global_state_context'; +import { GlobalStateContext } from '../../contexts/global_state_context'; import { ComponentProps } from '../../route_init'; import { useCharts } from '../../hooks/use_charts'; // @ts-ignore diff --git a/x-pack/plugins/monitoring/public/application/pages/no_data/no_data_page.tsx b/x-pack/plugins/monitoring/public/application/pages/no_data/no_data_page.tsx index b05bd783b2ff2..26072f53f4752 100644 --- a/x-pack/plugins/monitoring/public/application/pages/no_data/no_data_page.tsx +++ b/x-pack/plugins/monitoring/public/application/pages/no_data/no_data_page.tsx @@ -18,7 +18,8 @@ import { Legacy } from '../../../legacy_shims'; import { Enabler } from './enabler'; import { BreadcrumbContainer } from '../../hooks/use_breadcrumbs'; import { initSetupModeState } from '../../setup_mode/setup_mode'; -import { GlobalStateContext } from '../../global_state_context'; +import { GlobalStateContext } from '../../contexts/global_state_context'; +import { useRequestErrorHandler } from '../../hooks/use_request_error_handler'; const CODE_PATHS = [CODE_PATH_LICENSE]; @@ -77,7 +78,8 @@ export const NoDataPage = () => { ]); const globalState = useContext(GlobalStateContext); - initSetupModeState(globalState, services.http); + const handleRequestError = useRequestErrorHandler(); + initSetupModeState(globalState, services.http, handleRequestError); // From x-pack/plugins/monitoring/public/views/no_data/model_updater.js const updateModel = useCallback( diff --git a/x-pack/plugins/monitoring/public/application/pages/page_template.tsx b/x-pack/plugins/monitoring/public/application/pages/page_template.tsx index 927c464552087..5c030814d9cdf 100644 --- a/x-pack/plugins/monitoring/public/application/pages/page_template.tsx +++ b/x-pack/plugins/monitoring/public/application/pages/page_template.tsx @@ -6,8 +6,9 @@ */ import { EuiTab, EuiTabs } from '@elastic/eui'; -import React, { useContext, useState, useEffect } from 'react'; +import React, { useContext, useState, useEffect, useCallback } from 'react'; import { useHistory } from 'react-router-dom'; +import { IHttpFetchError } from 'kibana/public'; import { useTitle } from '../hooks/use_title'; import { MonitoringToolbar } from '../../components/shared/toolbar'; import { MonitoringTimeContainer } from '../hooks/use_monitoring_time'; @@ -18,6 +19,9 @@ import { updateSetupModeData, } from '../setup_mode/setup_mode'; import { SetupModeFeature } from '../../../common/enums'; +import { AlertsDropdown } from '../../alerts/alerts_dropdown'; +import { ActionMenu } from '../../components/action_menu'; +import { useRequestErrorHandler } from '../hooks/use_request_error_handler'; export interface TabMenuItem { id: string; @@ -46,34 +50,52 @@ export const PageTemplate: React.FC = ({ const { currentTimerange } = useContext(MonitoringTimeContainer.Context); const [loaded, setLoaded] = useState(false); const history = useHistory(); + const [hasError, setHasError] = useState(false); + const handleRequestError = useRequestErrorHandler(); + + const getPageDataResponseHandler = useCallback( + (result: any) => { + setHasError(false); + return result; + }, + [setHasError] + ); useEffect(() => { getPageData?.() - .catch((err) => { - // TODO: handle errors + .then(getPageDataResponseHandler) + .catch((err: IHttpFetchError) => { + handleRequestError(err); + setHasError(true); }) .finally(() => { setLoaded(true); }); - }, [getPageData, currentTimerange]); + }, [getPageData, currentTimerange, getPageDataResponseHandler, handleRequestError]); const onRefresh = () => { - const requests = [getPageData?.()]; + getPageData?.().then(getPageDataResponseHandler).catch(handleRequestError); + if (isSetupModeFeatureEnabled(SetupModeFeature.MetricbeatMigration)) { - requests.push(updateSetupModeData()); + updateSetupModeData(); } - - Promise.allSettled(requests).then((results) => { - // TODO: handle errors - }); }; const createHref = (route: string) => history.createHref({ pathname: route }); const isTabSelected = (route: string) => history.location.pathname === route; + const renderContent = () => { + if (hasError) return null; + if (getPageData && !loaded) return ; + return children; + }; + return (
+ + + {tabs && ( @@ -93,7 +115,7 @@ export const PageTemplate: React.FC = ({ })} )} -
{!getPageData ? children : loaded ? children : }
+
{renderContent()}
); }; diff --git a/x-pack/plugins/monitoring/public/application/route_init.tsx b/x-pack/plugins/monitoring/public/application/route_init.tsx index 8a9a906dbd563..8a11df3de50ae 100644 --- a/x-pack/plugins/monitoring/public/application/route_init.tsx +++ b/x-pack/plugins/monitoring/public/application/route_init.tsx @@ -7,7 +7,7 @@ import React, { useContext } from 'react'; import { Route, Redirect, useLocation } from 'react-router-dom'; import { useClusters } from './hooks/use_clusters'; -import { GlobalStateContext } from './global_state_context'; +import { GlobalStateContext } from './contexts/global_state_context'; import { getClusterFromClusters } from '../lib/get_cluster_from_clusters'; export interface ComponentProps { diff --git a/x-pack/plugins/monitoring/public/application/setup_mode/setup_mode.tsx b/x-pack/plugins/monitoring/public/application/setup_mode/setup_mode.tsx index 70932e5177337..bfdf96ef5b2c1 100644 --- a/x-pack/plugins/monitoring/public/application/setup_mode/setup_mode.tsx +++ b/x-pack/plugins/monitoring/public/application/setup_mode/setup_mode.tsx @@ -9,13 +9,13 @@ import React from 'react'; import { render } from 'react-dom'; import { get, includes } from 'lodash'; import { i18n } from '@kbn/i18n'; -import { HttpStart } from 'kibana/public'; +import { HttpStart, IHttpFetchError } from 'kibana/public'; import { KibanaContextProvider } from '../../../../../../src/plugins/kibana_react/public'; import { Legacy } from '../../legacy_shims'; import { SetupModeEnterButton } from '../../components/setup_mode/enter_button'; import { SetupModeFeature } from '../../../common/enums'; import { ISetupModeContext } from '../../components/setup_mode/setup_mode_context'; -import { State as GlobalState } from '../../application/global_state_context'; +import { State as GlobalState } from '../contexts/global_state_context'; function isOnPage(hash: string) { return includes(window.location.hash, hash); @@ -23,6 +23,7 @@ function isOnPage(hash: string) { let globalState: GlobalState; let httpService: HttpStart; +let errorHandler: (error: IHttpFetchError) => void; interface ISetupModeState { enabled: boolean; @@ -65,8 +66,8 @@ export const fetchCollectionData = async (uuid?: string, fetchWithoutClusterUuid }); return response; } catch (err) { - // TODO: handle errors - throw new Error(err); + errorHandler(err); + throw err; } }; @@ -122,8 +123,8 @@ export const disableElasticsearchInternalCollection = async () => { const response = await httpService.post(url); return response; } catch (err) { - // TODO: handle errors - throw new Error(err); + errorHandler(err); + throw err; } }; @@ -161,10 +162,12 @@ export const setSetupModeMenuItem = () => { export const initSetupModeState = async ( state: GlobalState, http: HttpStart, + handleErrors: (error: IHttpFetchError) => void, callback?: () => void ) => { globalState = state; httpService = http; + errorHandler = handleErrors; if (callback) { setupModeState.callback = callback; } diff --git a/x-pack/plugins/monitoring/public/application/setup_mode/setup_mode_renderer.js b/x-pack/plugins/monitoring/public/application/setup_mode/setup_mode_renderer.js index 337dacd4ecae9..a9ee2464cd423 100644 --- a/x-pack/plugins/monitoring/public/application/setup_mode/setup_mode_renderer.js +++ b/x-pack/plugins/monitoring/public/application/setup_mode/setup_mode_renderer.js @@ -27,8 +27,9 @@ import { import { findNewUuid } from '../../components/renderers/lib/find_new_uuid'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { GlobalStateContext } from '../../application/global_state_context'; +import { GlobalStateContext } from '../../application/contexts/global_state_context'; import { withKibana } from '../../../../../../src/plugins/kibana_react/public'; +import { useRequestErrorHandler } from '../hooks/use_request_error_handler'; class WrappedSetupModeRenderer extends React.Component { globalState; @@ -42,8 +43,8 @@ class WrappedSetupModeRenderer extends React.Component { UNSAFE_componentWillMount() { this.globalState = this.context; - const { kibana } = this.props; - initSetupModeState(this.globalState, kibana.services.http, (_oldData) => { + const { kibana, onHttpError } = this.props; + initSetupModeState(this.globalState, kibana.services.http, onHttpError, (_oldData) => { const newState = { renderState: true }; const { productName } = this.props; @@ -213,5 +214,12 @@ class WrappedSetupModeRenderer extends React.Component { } } +function withErrorHandler(Component) { + return function WrappedComponent(props) { + const handleRequestError = useRequestErrorHandler(); + return ; + }; +} + WrappedSetupModeRenderer.contextType = GlobalStateContext; -export const SetupModeRenderer = withKibana(WrappedSetupModeRenderer); +export const SetupModeRenderer = withKibana(withErrorHandler(WrappedSetupModeRenderer)); diff --git a/x-pack/plugins/monitoring/public/components/action_menu/index.tsx b/x-pack/plugins/monitoring/public/components/action_menu/index.tsx new file mode 100644 index 0000000000000..1348ac170395e --- /dev/null +++ b/x-pack/plugins/monitoring/public/components/action_menu/index.tsx @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useContext, useEffect } from 'react'; +import { + KibanaContextProvider, + toMountPoint, + useKibana, +} from '../../../../../../src/plugins/kibana_react/public'; +import { HeaderActionMenuContext } from '../../application/contexts/header_action_menu_context'; + +export const ActionMenu: React.FC<{}> = ({ children }) => { + const { services } = useKibana(); + const { setHeaderActionMenu } = useContext(HeaderActionMenuContext); + useEffect(() => { + if (setHeaderActionMenu) { + setHeaderActionMenu((element) => { + const mount = toMountPoint( + {children} + ); + return mount(element); + }); + return () => { + setHeaderActionMenu(undefined); + }; + } + }, [children, setHeaderActionMenu, services]); + + return null; +}; diff --git a/x-pack/plugins/monitoring/public/components/shared/toolbar.tsx b/x-pack/plugins/monitoring/public/components/shared/toolbar.tsx index 32bbdd6ecbeda..6a1ed1dd16f48 100644 --- a/x-pack/plugins/monitoring/public/components/shared/toolbar.tsx +++ b/x-pack/plugins/monitoring/public/components/shared/toolbar.tsx @@ -14,7 +14,7 @@ import { } from '@elastic/eui'; import React, { useContext, useCallback } from 'react'; import { MonitoringTimeContainer } from '../../application/hooks/use_monitoring_time'; -import { GlobalStateContext } from '../../application/global_state_context'; +import { GlobalStateContext } from '../../application/contexts/global_state_context'; import { Legacy } from '../../legacy_shims'; interface MonitoringToolbarProps { diff --git a/x-pack/plugins/monitoring/public/lib/fetch_alerts.ts b/x-pack/plugins/monitoring/public/lib/fetch_alerts.ts new file mode 100644 index 0000000000000..c0ce7ed260889 --- /dev/null +++ b/x-pack/plugins/monitoring/public/lib/fetch_alerts.ts @@ -0,0 +1,36 @@ +/* + * 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 { HttpHandler } from 'kibana/public'; +import { CommonAlertFilter } from '../../common/types/alerts'; +import { AlertsByName } from '../alerts/types'; + +interface FetchAlertsParams { + alertTypeIds?: string[]; + filters?: CommonAlertFilter[]; + timeRange: { min: number; max: number }; + clusterUuid: string; + fetch: HttpHandler; +} + +export const fetchAlerts = async ({ + alertTypeIds, + filters, + timeRange, + clusterUuid, + fetch, +}: FetchAlertsParams): Promise => { + const url = `../api/monitoring/v1/alert/${clusterUuid}/status`; + const response = await fetch(url, { + method: 'POST', + body: JSON.stringify({ + alertTypeIds, + filters, + timeRange, + }), + }); + return response as unknown as AlertsByName; +}; From badc77828ec21960708531e4470952ae2d051040 Mon Sep 17 00:00:00 2001 From: Jonathan Buttner <56361221+jonathan-buttner@users.noreply.github.com> Date: Mon, 11 Oct 2021 12:55:06 -0400 Subject: [PATCH 3/9] [Cases][Observability] Do not sync alerts status with case status (#114318) * set sync status according to disable alerts * Adding test Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../components/create/form_context.test.tsx | 23 +++++++++++++++++++ .../public/components/create/form_context.tsx | 10 +++++++- .../cases/public/components/create/index.tsx | 2 ++ 3 files changed, 34 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/cases/public/components/create/form_context.test.tsx b/x-pack/plugins/cases/public/components/create/form_context.test.tsx index b988f13ee34ce..b55542499fbe4 100644 --- a/x-pack/plugins/cases/public/components/create/form_context.test.tsx +++ b/x-pack/plugins/cases/public/components/create/form_context.test.tsx @@ -239,6 +239,29 @@ describe('Create case', () => { ); }); + it('should set sync alerts to false when the sync setting is passed in as false and alerts are disabled', async () => { + useConnectorsMock.mockReturnValue({ + ...sampleConnectorData, + connectors: connectorsMock, + }); + + const wrapper = mount( + + + + + + + ); + + fillForm(wrapper); + wrapper.find(`[data-test-subj="create-case-submit"]`).first().simulate('click'); + + await waitFor(() => + expect(postCase).toBeCalledWith({ ...sampleData, settings: { syncAlerts: false } }) + ); + }); + it('it should select the default connector set in the configuration', async () => { useCaseConfigureMock.mockImplementation(() => ({ ...useCaseConfigureResponse, diff --git a/x-pack/plugins/cases/public/components/create/form_context.tsx b/x-pack/plugins/cases/public/components/create/form_context.tsx index f59e1822c70be..03d8ec56fb0ae 100644 --- a/x-pack/plugins/cases/public/components/create/form_context.tsx +++ b/x-pack/plugins/cases/public/components/create/form_context.tsx @@ -34,6 +34,7 @@ interface Props { children?: JSX.Element | JSX.Element[]; hideConnectorServiceNowSir?: boolean; onSuccess?: (theCase: Case) => Promise; + syncAlertsDefaultValue?: boolean; } export const FormContext: React.FC = ({ @@ -42,6 +43,7 @@ export const FormContext: React.FC = ({ children, hideConnectorServiceNowSir, onSuccess, + syncAlertsDefaultValue = true, }) => { const { connectors, loading: isLoadingConnectors } = useConnectors(); const owner = useOwnerContext(); @@ -51,7 +53,12 @@ export const FormContext: React.FC = ({ const submitCase = useCallback( async ( - { connectorId: dataConnectorId, fields, syncAlerts = true, ...dataWithoutConnectorId }, + { + connectorId: dataConnectorId, + fields, + syncAlerts = syncAlertsDefaultValue, + ...dataWithoutConnectorId + }, isValid ) => { if (isValid) { @@ -94,6 +101,7 @@ export const FormContext: React.FC = ({ onSuccess, postComment, pushCaseToExternalService, + syncAlertsDefaultValue, ] ); diff --git a/x-pack/plugins/cases/public/components/create/index.tsx b/x-pack/plugins/cases/public/components/create/index.tsx index 7f8b8f664529e..d3eaba1ea0bc4 100644 --- a/x-pack/plugins/cases/public/components/create/index.tsx +++ b/x-pack/plugins/cases/public/components/create/index.tsx @@ -58,6 +58,8 @@ const CreateCaseComponent = ({ caseType={caseType} hideConnectorServiceNowSir={hideConnectorServiceNowSir} onSuccess={onSuccess} + // if we are disabling alerts, then we should not sync alerts + syncAlertsDefaultValue={!disableAlerts} > Date: Mon, 11 Oct 2021 12:30:14 -0500 Subject: [PATCH 4/9] [monitoring] fixup types (#114342) Co-authored-by: spalger Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../public/application/hooks/use_table.ts | 8 +--- .../setup_mode/setup_mode_renderer.d.ts | 3 +- .../public/components/apm/status_icon.js | 6 +-- .../components/cluster/overview/index.d.ts | 11 ++++- .../elasticsearch/cluster_status/index.d.ts | 9 +++- .../components/elasticsearch/index.d.ts | 12 ------ .../elasticsearch/{index.js => index.ts} | 0 .../indices/{index.js => index.ts} | 0 .../elasticsearch/indices/indices.d.ts | 20 +++++++++ .../ml_job_listing/status_icon.tsx | 10 ++--- .../elasticsearch/ml_jobs/ml_jobs.tsx | 2 +- .../elasticsearch/node/{index.js => index.ts} | 0 .../components/elasticsearch/node/node.d.ts | 20 +++++++++ .../elasticsearch/node/node_react.d.ts | 19 +++++++++ .../node/status_icon.d.ts} | 8 +++- .../elasticsearch/node/status_icon.js | 4 +- .../nodes/{index.js => index.ts} | 0 .../components/elasticsearch/nodes/nodes.d.ts | 15 +++++++ .../overview/{index.js => index.ts} | 0 .../elasticsearch/overview/overview.d.ts | 18 ++++++++ .../transformers/nodes_by_indices.d.ts | 2 +- .../components/elasticsearch/status_icon.js | 4 +- .../public/components/{index.js => index.ts} | 0 .../components/kibana/instances/instances.tsx | 5 +-- .../public/components/kibana/status_icon.js | 6 +-- .../license/{index.js => index.tsx} | 35 ++++++++++++---- .../components/no_data/{index.js => index.ts} | 0 .../{index.d.ts => no_data/no_data.d.ts} | 5 ++- .../page_loading/{index.js => index.tsx} | 12 ++++-- .../{formatting.js => formatting.ts} | 4 +- .../public/components/status_icon/index.js | 28 ------------- .../public/components/status_icon/index.tsx | 42 +++++++++++++++++++ .../summary_status/summary_status.js | 2 +- .../table/{eui_table.js => eui_table.tsx} | 16 +++---- .../table/eui_table_ssp.d.ts} | 8 ++-- .../public/components/table/index.d.ts | 10 ----- .../components/table/{index.js => index.ts} | 0 .../table/{storage.js => storage.ts} | 29 +++++++++---- ...usters.js => get_cluster_from_clusters.ts} | 13 ++++-- 39 files changed, 267 insertions(+), 119 deletions(-) delete mode 100644 x-pack/plugins/monitoring/public/components/elasticsearch/index.d.ts rename x-pack/plugins/monitoring/public/components/elasticsearch/{index.js => index.ts} (100%) rename x-pack/plugins/monitoring/public/components/elasticsearch/indices/{index.js => index.ts} (100%) create mode 100644 x-pack/plugins/monitoring/public/components/elasticsearch/indices/indices.d.ts rename x-pack/plugins/monitoring/public/components/elasticsearch/node/{index.js => index.ts} (100%) create mode 100644 x-pack/plugins/monitoring/public/components/elasticsearch/node/node.d.ts create mode 100644 x-pack/plugins/monitoring/public/components/elasticsearch/node/node_react.d.ts rename x-pack/plugins/monitoring/public/components/{status_icon/index.d.ts => elasticsearch/node/status_icon.d.ts} (56%) rename x-pack/plugins/monitoring/public/components/elasticsearch/nodes/{index.js => index.ts} (100%) create mode 100644 x-pack/plugins/monitoring/public/components/elasticsearch/nodes/nodes.d.ts rename x-pack/plugins/monitoring/public/components/elasticsearch/overview/{index.js => index.ts} (100%) create mode 100644 x-pack/plugins/monitoring/public/components/elasticsearch/overview/overview.d.ts rename x-pack/plugins/monitoring/public/components/{index.js => index.ts} (100%) rename x-pack/plugins/monitoring/public/components/license/{index.js => index.tsx} (86%) rename x-pack/plugins/monitoring/public/components/no_data/{index.js => index.ts} (100%) rename x-pack/plugins/monitoring/public/components/{index.d.ts => no_data/no_data.d.ts} (71%) rename x-pack/plugins/monitoring/public/components/page_loading/{index.js => index.tsx} (89%) rename x-pack/plugins/monitoring/public/components/setup_mode/{formatting.js => formatting.ts} (93%) delete mode 100644 x-pack/plugins/monitoring/public/components/status_icon/index.js create mode 100644 x-pack/plugins/monitoring/public/components/status_icon/index.tsx rename x-pack/plugins/monitoring/public/components/table/{eui_table.js => eui_table.tsx} (88%) rename x-pack/plugins/monitoring/public/{lib/get_cluster_from_clusters.d.ts => components/table/eui_table_ssp.d.ts} (68%) delete mode 100644 x-pack/plugins/monitoring/public/components/table/index.d.ts rename x-pack/plugins/monitoring/public/components/table/{index.js => index.ts} (100%) rename x-pack/plugins/monitoring/public/components/table/{storage.js => storage.ts} (71%) rename x-pack/plugins/monitoring/public/lib/{get_cluster_from_clusters.js => get_cluster_from_clusters.ts} (74%) diff --git a/x-pack/plugins/monitoring/public/application/hooks/use_table.ts b/x-pack/plugins/monitoring/public/application/hooks/use_table.ts index 2e6018ec89809..45d1f717f5d49 100644 --- a/x-pack/plugins/monitoring/public/application/hooks/use_table.ts +++ b/x-pack/plugins/monitoring/public/application/hooks/use_table.ts @@ -6,6 +6,7 @@ */ import { useState, useCallback } from 'react'; +import { EuiTableSortingType } from '@elastic/eui'; import { euiTableStorageGetter, euiTableStorageSetter } from '../../components/table'; import { Storage } from '../../../../../../src/plugins/kibana_utils/public'; @@ -23,12 +24,7 @@ interface Page { index: number; } -interface Sorting { - sort: { - field: string; - direction: string; - }; -} +type Sorting = EuiTableSortingType; const PAGE_SIZE_OPTIONS = [5, 10, 20, 50]; diff --git a/x-pack/plugins/monitoring/public/application/setup_mode/setup_mode_renderer.d.ts b/x-pack/plugins/monitoring/public/application/setup_mode/setup_mode_renderer.d.ts index 48e8ee13059c0..c0eda496a09b2 100644 --- a/x-pack/plugins/monitoring/public/application/setup_mode/setup_mode_renderer.d.ts +++ b/x-pack/plugins/monitoring/public/application/setup_mode/setup_mode_renderer.d.ts @@ -5,8 +5,9 @@ * 2.0. */ -export const SetupModeRenderer: FunctionComponent; +import { FunctionComponent } from 'react'; +export const SetupModeRenderer: FunctionComponent>; export interface SetupModeProps { setupMode: any; flyoutComponent: any; diff --git a/x-pack/plugins/monitoring/public/components/apm/status_icon.js b/x-pack/plugins/monitoring/public/components/apm/status_icon.js index f27bcefc20bcb..14a51313e4aa7 100644 --- a/x-pack/plugins/monitoring/public/components/apm/status_icon.js +++ b/x-pack/plugins/monitoring/public/components/apm/status_icon.js @@ -6,17 +6,17 @@ */ import React from 'react'; -import { StatusIcon } from '../../components/status_icon'; +import { StatusIcon, STATUS_ICON_TYPES } from '../../components/status_icon'; import { i18n } from '@kbn/i18n'; export function ApmStatusIcon({ status, availability = true }) { const type = (() => { if (!availability) { - return StatusIcon.TYPES.GRAY; + return STATUS_ICON_TYPES.GRAY; } const statusKey = status.toUpperCase(); - return StatusIcon.TYPES[statusKey] || StatusIcon.TYPES.YELLOW; + return STATUS_ICON_TYPES[statusKey] || STATUS_ICON_TYPES.YELLOW; })(); return ( diff --git a/x-pack/plugins/monitoring/public/components/cluster/overview/index.d.ts b/x-pack/plugins/monitoring/public/components/cluster/overview/index.d.ts index 2cfd37e8e27eb..3dc7121446a7a 100644 --- a/x-pack/plugins/monitoring/public/components/cluster/overview/index.d.ts +++ b/x-pack/plugins/monitoring/public/components/cluster/overview/index.d.ts @@ -5,4 +5,13 @@ * 2.0. */ -export const Overview: FunctionComponent; +import { FunctionComponent } from 'react'; + +export const Overview: FunctionComponent; + +export interface OverviewProps { + cluster: unknown; + setupMode: unknown; + showLicenseExpiration: boolean; + alerts: unknown; +} diff --git a/x-pack/plugins/monitoring/public/components/elasticsearch/cluster_status/index.d.ts b/x-pack/plugins/monitoring/public/components/elasticsearch/cluster_status/index.d.ts index b7196d25d1791..4f314101ed299 100644 --- a/x-pack/plugins/monitoring/public/components/elasticsearch/cluster_status/index.d.ts +++ b/x-pack/plugins/monitoring/public/components/elasticsearch/cluster_status/index.d.ts @@ -5,4 +5,11 @@ * 2.0. */ -export const ClusterStatus: FunctionComponent; +import { FunctionComponent } from 'react'; + +export const ClusterStatus: FunctionComponent; + +export interface ClusterStatusProps { + stats: unknown; + alerts?: unknown; +} diff --git a/x-pack/plugins/monitoring/public/components/elasticsearch/index.d.ts b/x-pack/plugins/monitoring/public/components/elasticsearch/index.d.ts deleted file mode 100644 index 09f6c1085cfa3..0000000000000 --- a/x-pack/plugins/monitoring/public/components/elasticsearch/index.d.ts +++ /dev/null @@ -1,12 +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. - */ - -export const ElasticsearchOverview: FunctionComponent; -export const ElasticsearchNodes: FunctionComponent; -export const ElasticsearchIndices: FunctionComponent; -export const ElasticsearchMLJobs: FunctionComponent; -export const NodeReact: FunctionComponent; diff --git a/x-pack/plugins/monitoring/public/components/elasticsearch/index.js b/x-pack/plugins/monitoring/public/components/elasticsearch/index.ts similarity index 100% rename from x-pack/plugins/monitoring/public/components/elasticsearch/index.js rename to x-pack/plugins/monitoring/public/components/elasticsearch/index.ts diff --git a/x-pack/plugins/monitoring/public/components/elasticsearch/indices/index.js b/x-pack/plugins/monitoring/public/components/elasticsearch/indices/index.ts similarity index 100% rename from x-pack/plugins/monitoring/public/components/elasticsearch/indices/index.js rename to x-pack/plugins/monitoring/public/components/elasticsearch/indices/index.ts diff --git a/x-pack/plugins/monitoring/public/components/elasticsearch/indices/indices.d.ts b/x-pack/plugins/monitoring/public/components/elasticsearch/indices/indices.d.ts new file mode 100644 index 0000000000000..2b8ea60b651a6 --- /dev/null +++ b/x-pack/plugins/monitoring/public/components/elasticsearch/indices/indices.d.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FunctionComponent } from 'react'; + +export const ElasticsearchIndices: FunctionComponent; +export interface ElasticsearchIndicesProps { + clusterStatus: unknown; + indices: unknown; + sorting: unknown; + pagination: unknown; + onTableChange: unknown; + toggleShowSystemIndices: unknown; + showSystemIndices: unknown; + alerts: unknown; +} diff --git a/x-pack/plugins/monitoring/public/components/elasticsearch/ml_job_listing/status_icon.tsx b/x-pack/plugins/monitoring/public/components/elasticsearch/ml_job_listing/status_icon.tsx index d5c65aecdec21..a45c8316d1aa3 100644 --- a/x-pack/plugins/monitoring/public/components/elasticsearch/ml_job_listing/status_icon.tsx +++ b/x-pack/plugins/monitoring/public/components/elasticsearch/ml_job_listing/status_icon.tsx @@ -7,22 +7,22 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; -import { StatusIcon } from '../../status_icon'; +import { StatusIcon, STATUS_ICON_TYPES } from '../../status_icon'; export function MachineLearningJobStatusIcon({ status }: { status: string }) { const type = (() => { const statusKey = status.toUpperCase(); if (statusKey === 'OPENED') { - return StatusIcon.TYPES.GREEN; + return STATUS_ICON_TYPES.GREEN; } else if (statusKey === 'CLOSED') { - return StatusIcon.TYPES.GRAY; + return STATUS_ICON_TYPES.GRAY; } else if (statusKey === 'FAILED') { - return StatusIcon.TYPES.RED; + return STATUS_ICON_TYPES.RED; } // basically a "changing" state like OPENING or CLOSING - return StatusIcon.TYPES.YELLOW; + return STATUS_ICON_TYPES.YELLOW; })(); return ( diff --git a/x-pack/plugins/monitoring/public/components/elasticsearch/ml_jobs/ml_jobs.tsx b/x-pack/plugins/monitoring/public/components/elasticsearch/ml_jobs/ml_jobs.tsx index 635f9ecd1e10a..dba9c40fabb2b 100644 --- a/x-pack/plugins/monitoring/public/components/elasticsearch/ml_jobs/ml_jobs.tsx +++ b/x-pack/plugins/monitoring/public/components/elasticsearch/ml_jobs/ml_jobs.tsx @@ -31,7 +31,7 @@ import { ClusterStatus } from '../cluster_status'; interface Props { clusterStatus: boolean; jobs: MLJobs; - onTableChange: () => void; + onTableChange: (props: any) => void; sorting: EuiTableSortingType; pagination: Pagination; } diff --git a/x-pack/plugins/monitoring/public/components/elasticsearch/node/index.js b/x-pack/plugins/monitoring/public/components/elasticsearch/node/index.ts similarity index 100% rename from x-pack/plugins/monitoring/public/components/elasticsearch/node/index.js rename to x-pack/plugins/monitoring/public/components/elasticsearch/node/index.ts diff --git a/x-pack/plugins/monitoring/public/components/elasticsearch/node/node.d.ts b/x-pack/plugins/monitoring/public/components/elasticsearch/node/node.d.ts new file mode 100644 index 0000000000000..9d7a062e942bb --- /dev/null +++ b/x-pack/plugins/monitoring/public/components/elasticsearch/node/node.d.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FunctionComponent } from 'react'; + +export const Node: FunctionComponent; +export interface NodeProps { + nodeSummary: unknown; + metrics: unknown; + logs: unknown; + alerts: unknown; + nodeId: unknown; + clusterUuid: unknown; + scope: unknown; + [key: string]: any; +} diff --git a/x-pack/plugins/monitoring/public/components/elasticsearch/node/node_react.d.ts b/x-pack/plugins/monitoring/public/components/elasticsearch/node/node_react.d.ts new file mode 100644 index 0000000000000..e0c4f6b301fdb --- /dev/null +++ b/x-pack/plugins/monitoring/public/components/elasticsearch/node/node_react.d.ts @@ -0,0 +1,19 @@ +/* + * 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 { FunctionComponent } from 'react'; + +export const NodeReact: FunctionComponent; +export interface NodeReactProps { + nodeSummary: unknown; + metrics: unknown; + logs: unknown; + alerts: unknown; + nodeId: unknown; + clusterUuid: unknown; + [key: string]: any; +} diff --git a/x-pack/plugins/monitoring/public/components/status_icon/index.d.ts b/x-pack/plugins/monitoring/public/components/elasticsearch/node/status_icon.d.ts similarity index 56% rename from x-pack/plugins/monitoring/public/components/status_icon/index.d.ts rename to x-pack/plugins/monitoring/public/components/elasticsearch/node/status_icon.d.ts index 147c2821e3a2a..dfa07524619c9 100644 --- a/x-pack/plugins/monitoring/public/components/status_icon/index.d.ts +++ b/x-pack/plugins/monitoring/public/components/elasticsearch/node/status_icon.d.ts @@ -5,4 +5,10 @@ * 2.0. */ -export const StatusIcon: FunctionComponent; +import { FunctionComponent } from 'react'; + +export const NodeStatusIcon: FunctionComponent; +export interface NodeStatusIconProps { + isOnline: boolean; + status: string; +} diff --git a/x-pack/plugins/monitoring/public/components/elasticsearch/node/status_icon.js b/x-pack/plugins/monitoring/public/components/elasticsearch/node/status_icon.js index 7bfffc7b73954..9905a6c3573f7 100644 --- a/x-pack/plugins/monitoring/public/components/elasticsearch/node/status_icon.js +++ b/x-pack/plugins/monitoring/public/components/elasticsearch/node/status_icon.js @@ -6,11 +6,11 @@ */ import React from 'react'; -import { StatusIcon } from '../../status_icon'; +import { StatusIcon, STATUS_ICON_TYPES } from '../../status_icon'; import { i18n } from '@kbn/i18n'; export function NodeStatusIcon({ isOnline, status }) { - const type = isOnline ? StatusIcon.TYPES.GREEN : StatusIcon.TYPES.GRAY; + const type = isOnline ? STATUS_ICON_TYPES.GREEN : STATUS_ICON_TYPES.GRAY; return ( ; +export interface ElasticsearchNodesProps { + clusterStatus: unknown; + showCgroupMetricsElasticsearch: unknown; + [key: string]: any; +} diff --git a/x-pack/plugins/monitoring/public/components/elasticsearch/overview/index.js b/x-pack/plugins/monitoring/public/components/elasticsearch/overview/index.ts similarity index 100% rename from x-pack/plugins/monitoring/public/components/elasticsearch/overview/index.js rename to x-pack/plugins/monitoring/public/components/elasticsearch/overview/index.ts diff --git a/x-pack/plugins/monitoring/public/components/elasticsearch/overview/overview.d.ts b/x-pack/plugins/monitoring/public/components/elasticsearch/overview/overview.d.ts new file mode 100644 index 0000000000000..d4c893f87cbd2 --- /dev/null +++ b/x-pack/plugins/monitoring/public/components/elasticsearch/overview/overview.d.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FunctionComponent } from 'react'; + +export const ElasticsearchOverview: FunctionComponent; +export interface ElasticsearchOverviewProps { + clusterStatus: unknown; + metrics: unknown; + logs: unknown; + cluster: unknown; + shardActivity: unknown; + [key: string]: any; +} diff --git a/x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/transformers/nodes_by_indices.d.ts b/x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/transformers/nodes_by_indices.d.ts index d0ec9b85edae7..c430c0ee7b48a 100644 --- a/x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/transformers/nodes_by_indices.d.ts +++ b/x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/transformers/nodes_by_indices.d.ts @@ -5,4 +5,4 @@ * 2.0. */ -export const nodesByIndices: () => (shards, nodes) => any; +export const nodesByIndices: () => (shards: any, nodes: any) => any; diff --git a/x-pack/plugins/monitoring/public/components/elasticsearch/status_icon.js b/x-pack/plugins/monitoring/public/components/elasticsearch/status_icon.js index 7c51a1e89d91e..ec027d71a192d 100644 --- a/x-pack/plugins/monitoring/public/components/elasticsearch/status_icon.js +++ b/x-pack/plugins/monitoring/public/components/elasticsearch/status_icon.js @@ -6,13 +6,13 @@ */ import React from 'react'; -import { StatusIcon } from '../status_icon'; +import { StatusIcon, STATUS_ICON_TYPES } from '../status_icon'; import { i18n } from '@kbn/i18n'; export function ElasticsearchStatusIcon({ status }) { const type = (() => { const statusKey = status.toUpperCase(); - return StatusIcon.TYPES[statusKey] || StatusIcon.TYPES.GRAY; + return STATUS_ICON_TYPES[statusKey] || STATUS_ICON_TYPES.GRAY; })(); return ( diff --git a/x-pack/plugins/monitoring/public/components/index.js b/x-pack/plugins/monitoring/public/components/index.ts similarity index 100% rename from x-pack/plugins/monitoring/public/components/index.js rename to x-pack/plugins/monitoring/public/components/index.ts diff --git a/x-pack/plugins/monitoring/public/components/kibana/instances/instances.tsx b/x-pack/plugins/monitoring/public/components/kibana/instances/instances.tsx index 4e939682b1dba..3766a09f91b80 100644 --- a/x-pack/plugins/monitoring/public/components/kibana/instances/instances.tsx +++ b/x-pack/plugins/monitoring/public/components/kibana/instances/instances.tsx @@ -25,8 +25,7 @@ import { capitalize, get } from 'lodash'; import { ClusterStatus } from '../cluster_status'; // @ts-ignore import { EuiMonitoringTable } from '../../table'; -// @ts-ignore -import { StatusIcon } from '../../status_icon'; +import { STATUS_ICON_TYPES } from '../../status_icon'; // @ts-ignore import { formatMetric, formatNumber } from '../../../lib/format_number'; import { getSafeForExternalLink } from '../../../lib/get_safe_for_external_link'; @@ -205,7 +204,7 @@ export const KibanaInstances: React.FC = (props: Props) => { _instances.push({ kibana: { ...(instance as any).instance.kibana, - status: StatusIcon.TYPES.GRAY, + status: STATUS_ICON_TYPES.GRAY, }, }); } diff --git a/x-pack/plugins/monitoring/public/components/kibana/status_icon.js b/x-pack/plugins/monitoring/public/components/kibana/status_icon.js index e5b501b1e15e7..976b3ff992e3b 100644 --- a/x-pack/plugins/monitoring/public/components/kibana/status_icon.js +++ b/x-pack/plugins/monitoring/public/components/kibana/status_icon.js @@ -6,17 +6,17 @@ */ import React from 'react'; -import { StatusIcon } from '../status_icon'; +import { StatusIcon, STATUS_ICON_TYPES } from '../status_icon'; import { i18n } from '@kbn/i18n'; export function KibanaStatusIcon({ status, availability = true }) { const type = (() => { if (!availability) { - return StatusIcon.TYPES.GRAY; + return STATUS_ICON_TYPES.GRAY; } const statusKey = status.toUpperCase(); - return StatusIcon.TYPES[statusKey] || StatusIcon.TYPES.YELLOW; + return STATUS_ICON_TYPES[statusKey] || STATUS_ICON_TYPES.YELLOW; })(); return ( diff --git a/x-pack/plugins/monitoring/public/components/license/index.js b/x-pack/plugins/monitoring/public/components/license/index.tsx similarity index 86% rename from x-pack/plugins/monitoring/public/components/license/index.js rename to x-pack/plugins/monitoring/public/components/license/index.tsx index ad16663c88ea7..766f0af3bccc8 100644 --- a/x-pack/plugins/monitoring/public/components/license/index.js +++ b/x-pack/plugins/monitoring/public/components/license/index.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { Fragment } from 'react'; +import React, { Fragment, FunctionComponent } from 'react'; import { EuiPage, EuiPageBody, @@ -27,7 +27,10 @@ import { import { FormattedMessage } from '@kbn/i18n/react'; import { Legacy } from '../../legacy_shims'; -export const AddLicense = ({ uploadPath }) => { +interface AddLicenseProps { + uploadPath?: string; +} +const AddLicense: FunctionComponent = ({ uploadPath }) => { return ( { ); }; -export class LicenseStatus extends React.PureComponent { +export interface LicenseStatusProps { + isExpired: boolean; + status: string; + type: string; + expiryDate: string | Date; +} + +class LicenseStatus extends React.PureComponent { render() { const { isExpired, status, type, expiryDate } = this.props; const typeTitleCase = type.charAt(0).toUpperCase() + type.substr(1).toLowerCase(); @@ -133,7 +143,15 @@ export class LicenseStatus extends React.PureComponent { } } -const LicenseUpdateInfoForPrimary = ({ isPrimaryCluster, uploadLicensePath }) => { +export interface LicenseUpdateInfoProps { + isPrimaryCluster: boolean; + uploadLicensePath?: string; +} + +const LicenseUpdateInfoForPrimary: FunctionComponent = ({ + isPrimaryCluster, + uploadLicensePath, +}) => { if (!isPrimaryCluster) { return null; } @@ -142,7 +160,9 @@ const LicenseUpdateInfoForPrimary = ({ isPrimaryCluster, uploadLicensePath }) => return ; }; -const LicenseUpdateInfoForRemote = ({ isPrimaryCluster }) => { +const LicenseUpdateInfoForRemote: FunctionComponent = ({ + isPrimaryCluster, +}) => { if (isPrimaryCluster) { return null; } @@ -168,7 +188,8 @@ const LicenseUpdateInfoForRemote = ({ isPrimaryCluster }) => { ); }; -export function License(props) { +export interface LicenseProps extends LicenseStatusProps, LicenseUpdateInfoProps {} +export const License: FunctionComponent = (props) => { const { status, type, isExpired, expiryDate } = props; const licenseManagement = `${Legacy.shims.getBasePath()}/app/management/stack/license_management`; return ( @@ -199,4 +220,4 @@ export function License(props) {
); -} +}; diff --git a/x-pack/plugins/monitoring/public/components/no_data/index.js b/x-pack/plugins/monitoring/public/components/no_data/index.ts similarity index 100% rename from x-pack/plugins/monitoring/public/components/no_data/index.js rename to x-pack/plugins/monitoring/public/components/no_data/index.ts diff --git a/x-pack/plugins/monitoring/public/components/index.d.ts b/x-pack/plugins/monitoring/public/components/no_data/no_data.d.ts similarity index 71% rename from x-pack/plugins/monitoring/public/components/index.d.ts rename to x-pack/plugins/monitoring/public/components/no_data/no_data.d.ts index fc1a81cc4dba2..b87d326e834af 100644 --- a/x-pack/plugins/monitoring/public/components/index.d.ts +++ b/x-pack/plugins/monitoring/public/components/no_data/no_data.d.ts @@ -5,5 +5,6 @@ * 2.0. */ -export const PageLoading: FunctionComponent; -export const License: FunctionComponent; +import { FunctionComponent } from 'react'; + +export const NoData: FunctionComponent>; diff --git a/x-pack/plugins/monitoring/public/components/page_loading/index.js b/x-pack/plugins/monitoring/public/components/page_loading/index.tsx similarity index 89% rename from x-pack/plugins/monitoring/public/components/page_loading/index.js rename to x-pack/plugins/monitoring/public/components/page_loading/index.tsx index fd4aa9d848150..e7535fc3dc859 100644 --- a/x-pack/plugins/monitoring/public/components/page_loading/index.js +++ b/x-pack/plugins/monitoring/public/components/page_loading/index.tsx @@ -48,17 +48,21 @@ function PageLoadingUI() { ); } -function PageLoadingTracking({ pageViewTitle }) { +const PageLoadingTracking: React.FunctionComponent<{ pageViewTitle: string }> = ({ + pageViewTitle, +}) => { const path = pageViewTitle.toLowerCase().replace(/-/g, '').replace(/\s+/g, '_'); useTrackPageview({ app: 'stack_monitoring', path }); useTrackPageview({ app: 'stack_monitoring', path, delay: 15000 }); return ; -} +}; -export function PageLoading({ pageViewTitle }) { +export const PageLoading: React.FunctionComponent<{ pageViewTitle?: string }> = ({ + pageViewTitle, +}) => { if (pageViewTitle) { return ; } return ; -} +}; diff --git a/x-pack/plugins/monitoring/public/components/setup_mode/formatting.js b/x-pack/plugins/monitoring/public/components/setup_mode/formatting.ts similarity index 93% rename from x-pack/plugins/monitoring/public/components/setup_mode/formatting.js rename to x-pack/plugins/monitoring/public/components/setup_mode/formatting.ts index 11e8ca48719fd..06eb029fcc4a0 100644 --- a/x-pack/plugins/monitoring/public/components/setup_mode/formatting.js +++ b/x-pack/plugins/monitoring/public/components/setup_mode/formatting.ts @@ -34,7 +34,7 @@ const SERVER_IDENTIFIER_PLURAL = i18n.translate('xpack.monitoring.setupMode.serv defaultMessage: `servers`, }); -export function formatProductName(productName) { +export function formatProductName(productName: string) { if (productName === APM_SYSTEM_ID) { return productName.toUpperCase(); } @@ -43,7 +43,7 @@ export function formatProductName(productName) { const PRODUCTS_THAT_USE_NODES = [LOGSTASH_SYSTEM_ID, ELASTICSEARCH_SYSTEM_ID]; const PRODUCTS_THAT_USE_INSTANCES = [KIBANA_SYSTEM_ID, BEATS_SYSTEM_ID]; -export function getIdentifier(productName, usePlural = false) { +export function getIdentifier(productName: string, usePlural = false) { if (PRODUCTS_THAT_USE_INSTANCES.includes(productName)) { return usePlural ? INSTANCE_IDENTIFIER_PLURAL : INSTANCE_IDENTIFIER_SINGULAR; } diff --git a/x-pack/plugins/monitoring/public/components/status_icon/index.js b/x-pack/plugins/monitoring/public/components/status_icon/index.js deleted file mode 100644 index bcd4b58d6912f..0000000000000 --- a/x-pack/plugins/monitoring/public/components/status_icon/index.js +++ /dev/null @@ -1,28 +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 { EuiIcon } from '@elastic/eui'; - -export function StatusIcon({ type, label }) { - const typeToIconMap = { - [StatusIcon.TYPES.RED]: 'danger', - [StatusIcon.TYPES.YELLOW]: 'warning', - [StatusIcon.TYPES.GREEN]: 'success', - [StatusIcon.TYPES.GRAY]: 'subdued', - }; - const icon = typeToIconMap[type]; - - return ; -} - -StatusIcon.TYPES = { - RED: 'RED', - YELLOW: 'YELLOW', - GREEN: 'GREEN', - GRAY: 'GRAY', -}; diff --git a/x-pack/plugins/monitoring/public/components/status_icon/index.tsx b/x-pack/plugins/monitoring/public/components/status_icon/index.tsx new file mode 100644 index 0000000000000..59c87866d57d3 --- /dev/null +++ b/x-pack/plugins/monitoring/public/components/status_icon/index.tsx @@ -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 React from 'react'; +import { EuiIcon } from '@elastic/eui'; + +export const STATUS_ICON_TYPES = { + RED: 'RED' as const, + YELLOW: 'YELLOW' as const, + GREEN: 'GREEN' as const, + GRAY: 'GRAY' as const, +}; + +const typeToIconMap = { + [STATUS_ICON_TYPES.RED]: 'danger', + [STATUS_ICON_TYPES.YELLOW]: 'warning', + [STATUS_ICON_TYPES.GREEN]: 'success', + [STATUS_ICON_TYPES.GRAY]: 'subdued', +}; + +export interface StatusIconProps { + type: keyof typeof STATUS_ICON_TYPES; + label: string; +} +export const StatusIcon: React.FunctionComponent = ({ type, label }) => { + const icon = typeToIconMap[type]; + + return ( + + ); +}; diff --git a/x-pack/plugins/monitoring/public/components/summary_status/summary_status.js b/x-pack/plugins/monitoring/public/components/summary_status/summary_status.js index 71fcf4e193f20..db4ac9098532b 100644 --- a/x-pack/plugins/monitoring/public/components/summary_status/summary_status.js +++ b/x-pack/plugins/monitoring/public/components/summary_status/summary_status.js @@ -9,7 +9,7 @@ import React, { Fragment } from 'react'; import PropTypes from 'prop-types'; import { isEmpty, capitalize } from 'lodash'; import { EuiFlexGroup, EuiFlexItem, EuiStat } from '@elastic/eui'; -import { StatusIcon } from '../status_icon/index.js'; +import { StatusIcon } from '../status_icon'; import { AlertsStatus } from '../../alerts/status'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; diff --git a/x-pack/plugins/monitoring/public/components/table/eui_table.js b/x-pack/plugins/monitoring/public/components/table/eui_table.tsx similarity index 88% rename from x-pack/plugins/monitoring/public/components/table/eui_table.js rename to x-pack/plugins/monitoring/public/components/table/eui_table.tsx index a702fdc033572..a383fcf1cd666 100644 --- a/x-pack/plugins/monitoring/public/components/table/eui_table.js +++ b/x-pack/plugins/monitoring/public/components/table/eui_table.tsx @@ -5,21 +5,21 @@ * 2.0. */ -import React, { Fragment } from 'react'; +import React, { Fragment, FunctionComponent } from 'react'; import { EuiInMemoryTable, EuiButton, EuiSpacer, EuiSearchBar } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { getIdentifier } from '../setup_mode/formatting'; import { isSetupModeFeatureEnabled } from '../../lib/setup_mode'; import { SetupModeFeature } from '../../../common/enums'; -export function EuiMonitoringTable({ +export const EuiMonitoringTable: FunctionComponent> = ({ rows: items, search = {}, columns: _columns, setupMode, productName, ...props -}) { +}) => { const [hasItems, setHasItem] = React.useState(items.length > 0); if (search.box && !search.box['data-test-subj']) { @@ -32,15 +32,17 @@ export function EuiMonitoringTable({ if (search) { const oldOnChange = search.onChange; - search.onChange = (arg) => { + search.onChange = (arg: any) => { const filteredItems = EuiSearchBar.Query.execute(arg.query, items, props.executeQueryOptions); setHasItem(filteredItems.length > 0); - oldOnChange && oldOnChange(arg); + if (oldOnChange) { + oldOnChange(arg); + } return true; }; } - const columns = _columns.map((column) => { + const columns = _columns.map((column: any) => { if (!('sortable' in column)) { column.sortable = true; } @@ -78,4 +80,4 @@ export function EuiMonitoringTable({ {footerContent} ); -} +}; diff --git a/x-pack/plugins/monitoring/public/lib/get_cluster_from_clusters.d.ts b/x-pack/plugins/monitoring/public/components/table/eui_table_ssp.d.ts similarity index 68% rename from x-pack/plugins/monitoring/public/lib/get_cluster_from_clusters.d.ts rename to x-pack/plugins/monitoring/public/components/table/eui_table_ssp.d.ts index 5a310c977efae..bdc8199b3c57c 100644 --- a/x-pack/plugins/monitoring/public/lib/get_cluster_from_clusters.d.ts +++ b/x-pack/plugins/monitoring/public/components/table/eui_table_ssp.d.ts @@ -5,8 +5,6 @@ * 2.0. */ -export const getClusterFromClusters: ( - clusters: any, - globalState: State, - unsetGlobalState: boolean -) => any; +import { FunctionComponent } from 'react'; + +export const EuiMonitoringSSPTable: FunctionComponent>; diff --git a/x-pack/plugins/monitoring/public/components/table/index.d.ts b/x-pack/plugins/monitoring/public/components/table/index.d.ts deleted file mode 100644 index 23406ba9e3a5e..0000000000000 --- a/x-pack/plugins/monitoring/public/components/table/index.d.ts +++ /dev/null @@ -1,10 +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. - */ - -export const euiTableStorageGetter: (string) => any; -export const euiTableStorageSetter: (string) => any; -export const EuiMonitoringTable: FunctionComponent; diff --git a/x-pack/plugins/monitoring/public/components/table/index.js b/x-pack/plugins/monitoring/public/components/table/index.ts similarity index 100% rename from x-pack/plugins/monitoring/public/components/table/index.js rename to x-pack/plugins/monitoring/public/components/table/index.ts diff --git a/x-pack/plugins/monitoring/public/components/table/storage.js b/x-pack/plugins/monitoring/public/components/table/storage.ts similarity index 71% rename from x-pack/plugins/monitoring/public/components/table/storage.js rename to x-pack/plugins/monitoring/public/components/table/storage.ts index b9694dc5db420..411bd09872858 100644 --- a/x-pack/plugins/monitoring/public/components/table/storage.js +++ b/x-pack/plugins/monitoring/public/components/table/storage.ts @@ -8,9 +8,22 @@ import { set } from '@elastic/safer-lodash-set'; import { get } from 'lodash'; import { STORAGE_KEY } from '../../../common/constants'; +import { Storage } from '../../../../../../src/plugins/kibana_utils/public'; -export const tableStorageGetter = (keyPrefix) => { - return (storage) => { +interface TableValues { + filterText: any; + pageIndex: any; + sortKey: any; + sortOrder: any; +} + +interface EuiTableValues { + sort: any; + page: any; +} + +export const tableStorageGetter = (keyPrefix: string) => { + return (storage: Storage): TableValues => { const localStorageData = storage.get(STORAGE_KEY) || {}; const filterText = get(localStorageData, [keyPrefix, 'filterText']); const pageIndex = get(localStorageData, [keyPrefix, 'pageIndex']); @@ -21,8 +34,8 @@ export const tableStorageGetter = (keyPrefix) => { }; }; -export const tableStorageSetter = (keyPrefix) => { - return (storage, { filterText, pageIndex, sortKey, sortOrder }) => { +export const tableStorageSetter = (keyPrefix: string) => { + return (storage: Storage, { filterText, pageIndex, sortKey, sortOrder }: TableValues) => { const localStorageData = storage.get(STORAGE_KEY) || {}; set(localStorageData, [keyPrefix, 'filterText'], filterText || undefined); // don`t store empty data @@ -36,8 +49,8 @@ export const tableStorageSetter = (keyPrefix) => { }; }; -export const euiTableStorageGetter = (keyPrefix) => { - return (storage) => { +export const euiTableStorageGetter = (keyPrefix: string) => { + return (storage: Storage): EuiTableValues => { const localStorageData = storage.get(STORAGE_KEY) || {}; const sort = get(localStorageData, [keyPrefix, 'sort']); const page = get(localStorageData, [keyPrefix, 'page']); @@ -46,8 +59,8 @@ export const euiTableStorageGetter = (keyPrefix) => { }; }; -export const euiTableStorageSetter = (keyPrefix) => { - return (storage, { sort, page }) => { +export const euiTableStorageSetter = (keyPrefix: string) => { + return (storage: Storage, { sort, page }: EuiTableValues) => { const localStorageData = storage.get(STORAGE_KEY) || {}; set(localStorageData, [keyPrefix, 'sort'], sort || undefined); // don`t store empty data diff --git a/x-pack/plugins/monitoring/public/lib/get_cluster_from_clusters.js b/x-pack/plugins/monitoring/public/lib/get_cluster_from_clusters.ts similarity index 74% rename from x-pack/plugins/monitoring/public/lib/get_cluster_from_clusters.js rename to x-pack/plugins/monitoring/public/lib/get_cluster_from_clusters.ts index 94bd39aa769fd..837f59aaf7c20 100644 --- a/x-pack/plugins/monitoring/public/lib/get_cluster_from_clusters.js +++ b/x-pack/plugins/monitoring/public/lib/get_cluster_from_clusters.ts @@ -6,15 +6,20 @@ */ import { find, first } from 'lodash'; +import { State } from '../application/global_state_context'; -export function getClusterFromClusters(clusters, globalState, unsetGlobalState = false) { +export function getClusterFromClusters( + clusters: any, + globalState: State, + unsetGlobalState = false +) { const cluster = (() => { const existingCurrent = find(clusters, { cluster_uuid: globalState.cluster_uuid }); if (existingCurrent) { return existingCurrent; } - const firstCluster = first(clusters); + const firstCluster: any = first(clusters); if (firstCluster && firstCluster.cluster_uuid) { return firstCluster; } @@ -25,7 +30,9 @@ export function getClusterFromClusters(clusters, globalState, unsetGlobalState = if (cluster && cluster.license) { globalState.cluster_uuid = unsetGlobalState ? undefined : cluster.cluster_uuid; globalState.ccs = unsetGlobalState ? undefined : cluster.ccs; - globalState.save(); + if (globalState.save) { + globalState.save(); + } return cluster; } From 50b3602d7f5eb046fe57e68c5718ab9c2169def5 Mon Sep 17 00:00:00 2001 From: spalger Date: Mon, 11 Oct 2021 17:41:49 +0000 Subject: [PATCH 5/9] fix import --- .../plugins/monitoring/public/lib/get_cluster_from_clusters.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/monitoring/public/lib/get_cluster_from_clusters.ts b/x-pack/plugins/monitoring/public/lib/get_cluster_from_clusters.ts index 837f59aaf7c20..93d0c5a6f790e 100644 --- a/x-pack/plugins/monitoring/public/lib/get_cluster_from_clusters.ts +++ b/x-pack/plugins/monitoring/public/lib/get_cluster_from_clusters.ts @@ -6,7 +6,7 @@ */ import { find, first } from 'lodash'; -import { State } from '../application/global_state_context'; +import { State } from '../application/contexts/global_state_context'; export function getClusterFromClusters( clusters: any, From 9d498b962c88680c306722d172ad1841c7ddea27 Mon Sep 17 00:00:00 2001 From: Nathan L Smith Date: Mon, 11 Oct 2021 12:55:06 -0500 Subject: [PATCH 6/9] [APM] Disabling apm e2e test (#114544) [skip-ci] APM Cypress tests are again failing on PRs. Disable temporarily. Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- vars/tasks.groovy | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/vars/tasks.groovy b/vars/tasks.groovy index 1842e278282b1..5a015bddc8fbc 100644 --- a/vars/tasks.groovy +++ b/vars/tasks.groovy @@ -146,13 +146,14 @@ def functionalXpack(Map params = [:]) { } } - whenChanged([ - 'x-pack/plugins/apm/', - ]) { - if (githubPr.isPr()) { - task(kibanaPipeline.functionalTestProcess('xpack-APMCypress', './test/scripts/jenkins_apm_cypress.sh')) - } - } + //temporarily disable apm e2e test since it's breaking. + // whenChanged([ + // 'x-pack/plugins/apm/', + // ]) { + // if (githubPr.isPr()) { + // task(kibanaPipeline.functionalTestProcess('xpack-APMCypress', './test/scripts/jenkins_apm_cypress.sh')) + // } + // } whenChanged([ 'x-pack/plugins/uptime/', From e32dd1c493b747345b4d1103d14f61898ad5ccfe Mon Sep 17 00:00:00 2001 From: Chris Roberson Date: Mon, 11 Oct 2021 14:19:28 -0400 Subject: [PATCH 7/9] [Alerting] Allow rule types to specify a default and minimum interval (#113650) * WIP * Remove test defaults * Fix types * Add tests * Add missing files * Fix issue with using default interval after user manually changed it * PR feedback * Fix types * Remove debug --- x-pack/plugins/alerting/README.md | 2 + x-pack/plugins/alerting/common/alert_type.ts | 2 + .../alerting/server/routes/rule_types.test.ts | 6 + .../alerting/server/routes/rule_types.ts | 4 + .../server/rule_type_registry.test.ts | 57 +++- .../alerting/server/rule_type_registry.ts | 44 +++ .../server/rules_client/rules_client.ts | 22 ++ .../server/rules_client/tests/create.test.ts | 26 ++ .../server/rules_client/tests/update.test.ts | 46 +++ x-pack/plugins/alerting/server/types.ts | 2 + .../public/application/constants/index.ts | 2 + .../components/alert_details.tsx | 1 + .../sections/alert_form/alert_add.test.tsx | 24 +- .../sections/alert_form/alert_add.tsx | 57 +++- .../sections/alert_form/alert_edit.test.tsx | 34 ++- .../sections/alert_form/alert_edit.tsx | 33 +- .../sections/alert_form/alert_errors.test.tsx | 284 ++++++++++++++++++ .../sections/alert_form/alert_errors.ts | 132 ++++++++ .../sections/alert_form/alert_form.tsx | 149 +++------ .../alerts_list/components/alerts_list.tsx | 25 +- .../triggers_actions_ui/public/types.ts | 6 +- 21 files changed, 822 insertions(+), 136 deletions(-) create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_errors.test.tsx create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_errors.ts diff --git a/x-pack/plugins/alerting/README.md b/x-pack/plugins/alerting/README.md index 58d2ca35dea7e..343960aee9dfb 100644 --- a/x-pack/plugins/alerting/README.md +++ b/x-pack/plugins/alerting/README.md @@ -122,6 +122,8 @@ The following table describes the properties of the `options` object. |useSavedObjectReferences.extractReferences|(Optional) When developing a rule type, you can choose to implement hooks for extracting saved object references from rule parameters. This hook will be invoked when a rule is created or updated. Implementing this hook is optional, but if an extract hook is implemented, an inject hook must also be implemented.|Function |useSavedObjectReferences.injectReferences|(Optional) When developing a rule type, you can choose to implement hooks for injecting saved object references into rule parameters. This hook will be invoked when a rule is retrieved (get or find). Implementing this hook is optional, but if an inject hook is implemented, an extract hook must also be implemented.|Function |isExportable|Whether the rule type is exportable from the Saved Objects Management UI.|boolean| +|defaultScheduleInterval|The default interval that will show up in the UI when creating a rule of this rule type.|boolean| +|minimumScheduleInterval|The minimum interval that will be allowed for all rules of this rule type.|boolean| ### Executor diff --git a/x-pack/plugins/alerting/common/alert_type.ts b/x-pack/plugins/alerting/common/alert_type.ts index e56034a4c41f8..d71540b4418e8 100644 --- a/x-pack/plugins/alerting/common/alert_type.ts +++ b/x-pack/plugins/alerting/common/alert_type.ts @@ -21,6 +21,8 @@ export interface AlertType< producer: string; minimumLicenseRequired: LicenseType; isExportable: boolean; + defaultScheduleInterval?: string; + minimumScheduleInterval?: string; } export interface ActionGroup { diff --git a/x-pack/plugins/alerting/server/routes/rule_types.test.ts b/x-pack/plugins/alerting/server/routes/rule_types.test.ts index 2e8f43508a969..e4247c9de6cad 100644 --- a/x-pack/plugins/alerting/server/routes/rule_types.test.ts +++ b/x-pack/plugins/alerting/server/routes/rule_types.test.ts @@ -57,6 +57,8 @@ describe('ruleTypesRoute', () => { }, producer: 'test', enabledInLicense: true, + minimumScheduleInterval: '1m', + defaultScheduleInterval: '10m', } as RegistryAlertTypeWithAuth, ]; const expectedResult: Array> = [ @@ -70,7 +72,9 @@ describe('ruleTypesRoute', () => { }, ], default_action_group_id: 'default', + default_schedule_interval: '10m', minimum_license_required: 'basic', + minimum_schedule_interval: '1m', is_exportable: true, recovery_action_group: RecoveredActionGroup, authorized_consumers: {}, @@ -102,10 +106,12 @@ describe('ruleTypesRoute', () => { }, "authorized_consumers": Object {}, "default_action_group_id": "default", + "default_schedule_interval": "10m", "enabled_in_license": true, "id": "1", "is_exportable": true, "minimum_license_required": "basic", + "minimum_schedule_interval": "1m", "name": "name", "producer": "test", "recovery_action_group": Object { diff --git a/x-pack/plugins/alerting/server/routes/rule_types.ts b/x-pack/plugins/alerting/server/routes/rule_types.ts index 153ae96ff68ea..72502b25e9aff 100644 --- a/x-pack/plugins/alerting/server/routes/rule_types.ts +++ b/x-pack/plugins/alerting/server/routes/rule_types.ts @@ -22,6 +22,8 @@ const rewriteBodyRes: RewriteResponseCase = (result isExportable, actionVariables, authorizedConsumers, + minimumScheduleInterval, + defaultScheduleInterval, ...rest }) => ({ ...rest, @@ -33,6 +35,8 @@ const rewriteBodyRes: RewriteResponseCase = (result is_exportable: isExportable, action_variables: actionVariables, authorized_consumers: authorizedConsumers, + minimum_schedule_interval: minimumScheduleInterval, + default_schedule_interval: defaultScheduleInterval, }) ); }; diff --git a/x-pack/plugins/alerting/server/rule_type_registry.test.ts b/x-pack/plugins/alerting/server/rule_type_registry.test.ts index 1c44e862c261c..beb5f264eb725 100644 --- a/x-pack/plugins/alerting/server/rule_type_registry.test.ts +++ b/x-pack/plugins/alerting/server/rule_type_registry.test.ts @@ -114,7 +114,7 @@ describe('register()', () => { test('throws if AlertType ruleTaskTimeout is not a valid duration', () => { const alertType: AlertType = { - id: 123 as unknown as string, + id: '123', name: 'Test', actionGroups: [ { @@ -138,6 +138,59 @@ describe('register()', () => { ); }); + test('throws if defaultScheduleInterval isnt valid', () => { + const alertType: AlertType = { + id: '123', + name: 'Test', + actionGroups: [ + { + id: 'default', + name: 'Default', + }, + ], + + defaultActionGroupId: 'default', + minimumLicenseRequired: 'basic', + isExportable: true, + executor: jest.fn(), + producer: 'alerts', + defaultScheduleInterval: 'foobar', + }; + const registry = new RuleTypeRegistry(ruleTypeRegistryParams); + + expect(() => registry.register(alertType)).toThrowError( + new Error( + `Rule type \"123\" has invalid default interval: string is not a valid duration: foobar.` + ) + ); + }); + + test('throws if minimumScheduleInterval isnt valid', () => { + const alertType: AlertType = { + id: '123', + name: 'Test', + actionGroups: [ + { + id: 'default', + name: 'Default', + }, + ], + defaultActionGroupId: 'default', + minimumLicenseRequired: 'basic', + isExportable: true, + executor: jest.fn(), + producer: 'alerts', + minimumScheduleInterval: 'foobar', + }; + const registry = new RuleTypeRegistry(ruleTypeRegistryParams); + + expect(() => registry.register(alertType)).toThrowError( + new Error( + `Rule type \"123\" has invalid minimum interval: string is not a valid duration: foobar.` + ) + ); + }); + test('throws if RuleType action groups contains reserved group id', () => { const alertType: AlertType = { id: 'test', @@ -465,10 +518,12 @@ describe('list()', () => { "state": Array [], }, "defaultActionGroupId": "testActionGroup", + "defaultScheduleInterval": undefined, "enabledInLicense": false, "id": "test", "isExportable": true, "minimumLicenseRequired": "basic", + "minimumScheduleInterval": undefined, "name": "Test", "producer": "alerts", "recoveryActionGroup": Object { diff --git a/x-pack/plugins/alerting/server/rule_type_registry.ts b/x-pack/plugins/alerting/server/rule_type_registry.ts index dc72b644b2c7b..db02edf4d19dd 100644 --- a/x-pack/plugins/alerting/server/rule_type_registry.ts +++ b/x-pack/plugins/alerting/server/rule_type_registry.ts @@ -48,6 +48,8 @@ export interface RegistryRuleType | 'producer' | 'minimumLicenseRequired' | 'isExportable' + | 'minimumScheduleInterval' + | 'defaultScheduleInterval' > { id: string; enabledInLicense: boolean; @@ -188,6 +190,44 @@ export class RuleTypeRegistry { } alertType.actionVariables = normalizedActionVariables(alertType.actionVariables); + // validate defaultScheduleInterval here + if (alertType.defaultScheduleInterval) { + const invalidDefaultTimeout = validateDurationSchema(alertType.defaultScheduleInterval); + if (invalidDefaultTimeout) { + throw new Error( + i18n.translate( + 'xpack.alerting.ruleTypeRegistry.register.invalidDefaultTimeoutAlertTypeError', + { + defaultMessage: 'Rule type "{id}" has invalid default interval: {errorMessage}.', + values: { + id: alertType.id, + errorMessage: invalidDefaultTimeout, + }, + } + ) + ); + } + } + + // validate minimumScheduleInterval here + if (alertType.minimumScheduleInterval) { + const invalidMinimumTimeout = validateDurationSchema(alertType.minimumScheduleInterval); + if (invalidMinimumTimeout) { + throw new Error( + i18n.translate( + 'xpack.alerting.ruleTypeRegistry.register.invalidMinimumTimeoutAlertTypeError', + { + defaultMessage: 'Rule type "{id}" has invalid minimum interval: {errorMessage}.', + values: { + id: alertType.id, + errorMessage: invalidMinimumTimeout, + }, + } + ) + ); + } + } + const normalizedAlertType = augmentActionGroupsWithReserved< Params, ExtractedParams, @@ -287,6 +327,8 @@ export class RuleTypeRegistry { producer, minimumLicenseRequired, isExportable, + minimumScheduleInterval, + defaultScheduleInterval, }, ]: [string, UntypedNormalizedAlertType]) => ({ id, @@ -298,6 +340,8 @@ export class RuleTypeRegistry { producer, minimumLicenseRequired, isExportable, + minimumScheduleInterval, + defaultScheduleInterval, enabledInLicense: !!this.licenseState.getLicenseCheckForAlertType( id, name, diff --git a/x-pack/plugins/alerting/server/rules_client/rules_client.ts b/x-pack/plugins/alerting/server/rules_client/rules_client.ts index 231d19ce9a6f8..2228b5d27910f 100644 --- a/x-pack/plugins/alerting/server/rules_client/rules_client.ts +++ b/x-pack/plugins/alerting/server/rules_client/rules_client.ts @@ -296,6 +296,17 @@ export class RulesClient { await this.validateActions(ruleType, data.actions); + // Validate intervals, if configured + if (ruleType.minimumScheduleInterval) { + const intervalInMs = parseDuration(data.schedule.interval); + const minimumScheduleIntervalInMs = parseDuration(ruleType.minimumScheduleInterval); + if (intervalInMs < minimumScheduleIntervalInMs) { + throw Boom.badRequest( + `Error updating rule: the interval is less than the minimum interval of ${ruleType.minimumScheduleInterval}` + ); + } + } + // Extract saved object references for this rule const { references, @@ -847,6 +858,17 @@ export class RulesClient { ); await this.validateActions(ruleType, data.actions); + // Validate intervals, if configured + if (ruleType.minimumScheduleInterval) { + const intervalInMs = parseDuration(data.schedule.interval); + const minimumScheduleIntervalInMs = parseDuration(ruleType.minimumScheduleInterval); + if (intervalInMs < minimumScheduleIntervalInMs) { + throw Boom.badRequest( + `Error updating rule: the interval is less than the minimum interval of ${ruleType.minimumScheduleInterval}` + ); + } + } + // Extract saved object references for this rule const { references, diff --git a/x-pack/plugins/alerting/server/rules_client/tests/create.test.ts b/x-pack/plugins/alerting/server/rules_client/tests/create.test.ts index 2bb92046db68f..fc8f272702e0d 100644 --- a/x-pack/plugins/alerting/server/rules_client/tests/create.test.ts +++ b/x-pack/plugins/alerting/server/rules_client/tests/create.test.ts @@ -2268,4 +2268,30 @@ describe('create()', () => { expect(unsecuredSavedObjectsClient.create).not.toHaveBeenCalled(); expect(taskManager.schedule).not.toHaveBeenCalled(); }); + + test('throws error when updating with an interval less than the minimum configured one', async () => { + ruleTypeRegistry.get.mockImplementation(() => ({ + id: '123', + name: 'Test', + actionGroups: [{ id: 'default', name: 'Default' }], + recoveryActionGroup: RecoveredActionGroup, + defaultActionGroupId: 'default', + minimumLicenseRequired: 'basic', + isExportable: true, + async executor() {}, + producer: 'alerts', + minimumScheduleInterval: '5m', + useSavedObjectReferences: { + extractReferences: jest.fn(), + injectReferences: jest.fn(), + }, + })); + + const data = getMockData({ schedule: { interval: '1m' } }); + await expect(rulesClient.create({ data })).rejects.toThrowErrorMatchingInlineSnapshot( + `"Error updating rule: the interval is less than the minimum interval of 5m"` + ); + expect(unsecuredSavedObjectsClient.create).not.toHaveBeenCalled(); + expect(taskManager.schedule).not.toHaveBeenCalled(); + }); }); diff --git a/x-pack/plugins/alerting/server/rules_client/tests/update.test.ts b/x-pack/plugins/alerting/server/rules_client/tests/update.test.ts index 1328b666f96e7..55ffc49fd3394 100644 --- a/x-pack/plugins/alerting/server/rules_client/tests/update.test.ts +++ b/x-pack/plugins/alerting/server/rules_client/tests/update.test.ts @@ -140,6 +140,7 @@ describe('update()', () => { recoveryActionGroup: RecoveredActionGroup, async executor() {}, producer: 'alerts', + minimumScheduleInterval: '5s', }); }); @@ -1966,4 +1967,49 @@ describe('update()', () => { ); }); }); + + test('throws error when updating with an interval less than the minimum configured one', async () => { + await expect( + rulesClient.update({ + id: '1', + data: { + schedule: { interval: '1s' }, + name: 'abc', + tags: ['foo'], + params: { + bar: true, + }, + throttle: null, + notifyWhen: 'onActiveAlert', + actions: [ + { + group: 'default', + id: '1', + params: { + foo: true, + }, + }, + { + group: 'default', + id: '1', + params: { + foo: true, + }, + }, + { + group: 'default', + id: '2', + params: { + foo: true, + }, + }, + ], + }, + }) + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"Error updating rule: the interval is less than the minimum interval of 5s"` + ); + expect(unsecuredSavedObjectsClient.create).not.toHaveBeenCalled(); + expect(taskManager.schedule).not.toHaveBeenCalled(); + }); }); diff --git a/x-pack/plugins/alerting/server/types.ts b/x-pack/plugins/alerting/server/types.ts index c73ce86acf785..1dc8291d28756 100644 --- a/x-pack/plugins/alerting/server/types.ts +++ b/x-pack/plugins/alerting/server/types.ts @@ -157,6 +157,8 @@ export interface AlertType< injectReferences: (params: ExtractedParams, references: SavedObjectReference[]) => Params; }; isExportable: boolean; + defaultScheduleInterval?: string; + minimumScheduleInterval?: string; ruleTaskTimeout?: string; } export type UntypedAlertType = AlertType< diff --git a/x-pack/plugins/triggers_actions_ui/public/application/constants/index.ts b/x-pack/plugins/triggers_actions_ui/public/application/constants/index.ts index bed7b09110d87..c69cbcfe8ac04 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/constants/index.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/constants/index.ts @@ -36,3 +36,5 @@ export enum SORT_ORDERS { } export const DEFAULT_SEARCH_PAGE_SIZE: number = 10; + +export const DEFAULT_ALERT_INTERVAL = '1m'; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.tsx index 2b13bdf613d96..3b15295cf7a3a 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.tsx @@ -171,6 +171,7 @@ export const AlertDetails: React.FunctionComponent = ({ }} actionTypeRegistry={actionTypeRegistry} ruleTypeRegistry={ruleTypeRegistry} + ruleType={alertType} onSave={setAlert} /> )} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.test.tsx index 514594ffb855b..4ae570a62f7d9 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.test.tsx @@ -69,7 +69,8 @@ describe('alert_add', () => { async function setup( initialValues?: Partial, - onClose: AlertAddProps['onClose'] = jest.fn() + onClose: AlertAddProps['onClose'] = jest.fn(), + defaultScheduleInterval?: string ) { const mocks = coreMock.createSetup(); const { loadAlertTypes } = jest.requireMock('../../lib/alert_api'); @@ -84,6 +85,7 @@ describe('alert_add', () => { }, ], defaultActionGroupId: 'testActionGroup', + defaultScheduleInterval, minimumLicenseRequired: 'basic', recoveryActionGroup: { id: 'recovered', name: 'Recovered' }, producer: ALERTS_FEATURE_ID, @@ -243,6 +245,26 @@ describe('alert_add', () => { expect(onClose).toHaveBeenCalledWith(AlertFlyoutCloseReason.SAVED); }); + + it('should enforce any default inteval', async () => { + await setup({ alertTypeId: 'my-alert-type' }, jest.fn(), '3h'); + await delay(1000); + + // Wait for handlers to fire + await act(async () => { + await nextTick(); + wrapper.update(); + }); + + const intervalInputUnit = wrapper + .find('[data-test-subj="intervalInputUnit"]') + .first() + .getElement().props.value; + const intervalInput = wrapper.find('[data-test-subj="intervalInput"]').first().getElement() + .props.value; + expect(intervalInputUnit).toBe('h'); + expect(intervalInput).toBe(3); + }); }); function mockAlert(overloads: Partial = {}): Alert { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.tsx index 5e4ca42523b39..2b376ea0d0b30 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.tsx @@ -17,10 +17,12 @@ import { AlertFlyoutCloseReason, IErrorObject, AlertAddProps, + RuleTypeIndex, } from '../../../types'; -import { AlertForm, getAlertActionErrors, getAlertErrors, isValidAlert } from './alert_form'; +import { AlertForm } from './alert_form'; +import { getAlertActionErrors, getAlertErrors, isValidAlert } from './alert_errors'; import { alertReducer, InitialAlert, InitialAlertReducer } from './alert_reducer'; -import { createAlert } from '../../lib/alert_api'; +import { createAlert, loadAlertTypes } from '../../lib/alert_api'; import { HealthCheck } from '../../components/health_check'; import { ConfirmAlertSave } from './confirm_alert_save'; import { ConfirmAlertClose } from './confirm_alert_close'; @@ -30,6 +32,7 @@ import { HealthContextProvider } from '../../context/health_context'; import { useKibana } from '../../../common/lib/kibana'; import { hasAlertChanged, haveAlertParamsChanged } from './has_alert_changed'; import { getAlertWithInvalidatedFields } from '../../lib/value_validators'; +import { DEFAULT_ALERT_INTERVAL } from '../../constants'; const AlertAdd = ({ consumer, @@ -39,26 +42,28 @@ const AlertAdd = ({ canChangeTrigger, alertTypeId, initialValues, + reloadAlerts, onSave, metadata, + ...props }: AlertAddProps) => { const onSaveHandler = onSave ?? reloadAlerts; - const initialAlert: InitialAlert = useMemo( - () => ({ + + const initialAlert: InitialAlert = useMemo(() => { + return { params: {}, consumer, alertTypeId, schedule: { - interval: '1m', + interval: DEFAULT_ALERT_INTERVAL, }, actions: [], tags: [], notifyWhen: 'onActionGroupChange', ...(initialValues ? initialValues : {}), - }), - [alertTypeId, consumer, initialValues] - ); + }; + }, [alertTypeId, consumer, initialValues]); const [{ alert }, dispatch] = useReducer(alertReducer as InitialAlertReducer, { alert: initialAlert, @@ -67,6 +72,10 @@ const AlertAdd = ({ const [isSaving, setIsSaving] = useState(false); const [isConfirmAlertSaveModalOpen, setIsConfirmAlertSaveModalOpen] = useState(false); const [isConfirmAlertCloseModalOpen, setIsConfirmAlertCloseModalOpen] = useState(false); + const [ruleTypeIndex, setRuleTypeIndex] = useState( + props.ruleTypeIndex + ); + const [changedFromDefaultInterval, setChangedFromDefaultInterval] = useState(false); const setAlert = (value: InitialAlert) => { dispatch({ command: { type: 'setAlert' }, payload: { key: 'alert', value } }); @@ -90,6 +99,19 @@ const AlertAdd = ({ } }, [alertTypeId]); + useEffect(() => { + if (!props.ruleTypeIndex) { + (async () => { + const alertTypes = await loadAlertTypes({ http }); + const index: RuleTypeIndex = new Map(); + for (const alertType of alertTypes) { + index.set(alertType.id, alertType); + } + setRuleTypeIndex(index); + })(); + } + }, [props.ruleTypeIndex, http]); + useEffect(() => { if (isEmpty(alert.params) && !isEmpty(initialAlertParams)) { // alert params are explicitly cleared when the alert type is cleared. @@ -115,6 +137,21 @@ const AlertAdd = ({ })(); }, [alert, actionTypeRegistry]); + useEffect(() => { + if (alert.alertTypeId && ruleTypeIndex) { + const type = ruleTypeIndex.get(alert.alertTypeId); + if (type?.defaultScheduleInterval && !changedFromDefaultInterval) { + setAlertProperty('schedule', { interval: type.defaultScheduleInterval }); + } + } + }, [alert.alertTypeId, ruleTypeIndex, alert.schedule.interval, changedFromDefaultInterval]); + + useEffect(() => { + if (alert.schedule.interval !== DEFAULT_ALERT_INTERVAL && !changedFromDefaultInterval) { + setChangedFromDefaultInterval(true); + } + }, [alert.schedule.interval, changedFromDefaultInterval]); + const checkForChangesAndCloseFlyout = () => { if ( hasAlertChanged(alert, initialAlert, false) || @@ -138,9 +175,11 @@ const AlertAdd = ({ }; const alertType = alert.alertTypeId ? ruleTypeRegistry.get(alert.alertTypeId) : null; + const { alertBaseErrors, alertErrors, alertParamsErrors } = getAlertErrors( alert as Alert, - alertType + alertType, + alert.alertTypeId ? ruleTypeIndex?.get(alert.alertTypeId) : undefined ); // Confirm before saving if user is able to add actions but hasn't added any to this alert diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_edit.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_edit.test.tsx index 1aea6c68acbf8..467f2af6ed704 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_edit.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_edit.test.tsx @@ -35,6 +35,23 @@ jest.mock('../../lib/alert_api', () => ({ })), })); +jest.mock('./alert_errors', () => ({ + getAlertActionErrors: jest.fn().mockImplementation(() => { + return []; + }), + getAlertErrors: jest.fn().mockImplementation(() => ({ + alertParamsErrors: {}, + alertBaseErrors: {}, + alertErrors: { + name: new Array(), + interval: new Array(), + alertTypeId: new Array(), + actionConnectors: new Array(), + }, + })), + isValidAlert: jest.fn(), +})); + jest.mock('../../../common/lib/health_api', () => ({ triggersActionsUiHealth: jest.fn(() => ({ isAlertsAvailable: true })), })); @@ -47,7 +64,7 @@ describe('alert_edit', () => { mockedCoreSetup = coreMock.createSetup(); }); - async function setup() { + async function setup(initialAlertFields = {}) { const [ { application: { capabilities }, @@ -154,6 +171,7 @@ describe('alert_edit', () => { status: 'unknown', lastExecutionDate: new Date('2020-08-20T19:23:38Z'), }, + ...initialAlertFields, }; actionTypeRegistry.get.mockReturnValueOnce(actionTypeModel); actionTypeRegistry.has.mockReturnValue(true); @@ -188,7 +206,11 @@ describe('alert_edit', () => { }); it('displays a toast message on save for server errors', async () => { - await setup(); + const { isValidAlert } = jest.requireMock('./alert_errors'); + (isValidAlert as jest.Mock).mockImplementation(() => { + return true; + }); + await setup({ name: undefined }); await act(async () => { wrapper.find('[data-test-subj="saveEditedAlertButton"]').first().simulate('click'); @@ -197,4 +219,12 @@ describe('alert_edit', () => { 'Fail message' ); }); + + it('should pass in the server alert type into `getAlertErrors`', async () => { + const { getAlertErrors } = jest.requireMock('./alert_errors'); + await setup(); + const lastCall = getAlertErrors.mock.calls[getAlertErrors.mock.calls.length - 1]; + expect(lastCall[2]).toBeDefined(); + expect(lastCall[2].id).toBe('my-alert-type'); + }); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_edit.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_edit.tsx index 8b79950d03ac9..f8c506fa1e8ba 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_edit.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_edit.tsx @@ -24,10 +24,17 @@ import { } from '@elastic/eui'; import { cloneDeep } from 'lodash'; import { i18n } from '@kbn/i18n'; -import { Alert, AlertFlyoutCloseReason, AlertEditProps, IErrorObject } from '../../../types'; -import { AlertForm, getAlertActionErrors, getAlertErrors, isValidAlert } from './alert_form'; +import { + Alert, + AlertFlyoutCloseReason, + AlertEditProps, + IErrorObject, + AlertType, +} from '../../../types'; +import { AlertForm } from './alert_form'; +import { getAlertActionErrors, getAlertErrors, isValidAlert } from './alert_errors'; import { alertReducer, ConcreteAlertReducer } from './alert_reducer'; -import { updateAlert } from '../../lib/alert_api'; +import { updateAlert, loadAlertTypes } from '../../lib/alert_api'; import { HealthCheck } from '../../components/health_check'; import { HealthContextProvider } from '../../context/health_context'; import { useKibana } from '../../../common/lib/kibana'; @@ -43,6 +50,7 @@ export const AlertEdit = ({ ruleTypeRegistry, actionTypeRegistry, metadata, + ...props }: AlertEditProps) => { const onSaveHandler = onSave ?? reloadAlerts; const [{ alert }, dispatch] = useReducer(alertReducer as ConcreteAlertReducer, { @@ -55,6 +63,9 @@ export const AlertEdit = ({ const [isConfirmAlertCloseModalOpen, setIsConfirmAlertCloseModalOpen] = useState(false); const [alertActionsErrors, setAlertActionsErrors] = useState([]); const [isLoading, setIsLoading] = useState(false); + const [serverRuleType, setServerRuleType] = useState | undefined>( + props.ruleType + ); const { http, @@ -75,9 +86,23 @@ export const AlertEdit = ({ })(); }, [alert, actionTypeRegistry]); + useEffect(() => { + if (!props.ruleType && !serverRuleType) { + (async () => { + const serverRuleTypes = await loadAlertTypes({ http }); + for (const _serverRuleType of serverRuleTypes) { + if (alertType.id === _serverRuleType.id) { + setServerRuleType(_serverRuleType); + } + } + })(); + } + }, [props.ruleType, alertType.id, serverRuleType, http]); + const { alertBaseErrors, alertErrors, alertParamsErrors } = getAlertErrors( alert as Alert, - alertType + alertType, + serverRuleType ); const checkForChangesAndCloseFlyout = () => { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_errors.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_errors.test.tsx new file mode 100644 index 0000000000000..5ca0fd9dafc18 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_errors.test.tsx @@ -0,0 +1,284 @@ +/* + * 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 uuid from 'uuid'; +import React, { Fragment } from 'react'; +import { + validateBaseProperties, + getAlertErrors, + getAlertActionErrors, + hasObjectErrors, + isValidAlert, +} from './alert_errors'; +import { Alert, AlertType, AlertTypeModel } from '../../../types'; +import { actionTypeRegistryMock } from '../../action_type_registry.mock'; + +describe('alert_errors', () => { + describe('validateBaseProperties()', () => { + it('should validate the name', () => { + const alert = mockAlert(); + alert.name = ''; + const result = validateBaseProperties(alert); + expect(result.errors).toStrictEqual({ + name: ['Name is required.'], + interval: [], + alertTypeId: [], + actionConnectors: [], + }); + }); + + it('should validate the interval', () => { + const alert = mockAlert(); + alert.schedule.interval = ''; + const result = validateBaseProperties(alert); + expect(result.errors).toStrictEqual({ + name: [], + interval: ['Check interval is required.'], + alertTypeId: [], + actionConnectors: [], + }); + }); + + it('should validate the minimumScheduleInterval', () => { + const alert = mockAlert(); + alert.schedule.interval = '2m'; + const result = validateBaseProperties( + alert, + mockserverRuleType({ minimumScheduleInterval: '5m' }) + ); + expect(result.errors).toStrictEqual({ + name: [], + interval: ['Interval is below minimum (5m) for this rule type'], + alertTypeId: [], + actionConnectors: [], + }); + }); + + it('should validate the alertTypeId', () => { + const alert = mockAlert(); + alert.alertTypeId = ''; + const result = validateBaseProperties(alert); + expect(result.errors).toStrictEqual({ + name: [], + interval: [], + alertTypeId: ['Rule type is required.'], + actionConnectors: [], + }); + }); + + it('should validate the connectors', () => { + const alert = mockAlert(); + alert.actions = [ + { + id: '1234', + actionTypeId: 'myActionType', + group: '', + params: { + name: 'yes', + }, + }, + ]; + const result = validateBaseProperties(alert); + expect(result.errors).toStrictEqual({ + name: [], + interval: [], + alertTypeId: [], + actionConnectors: ['Action for myActionType connector is required.'], + }); + }); + }); + + describe('getAlertErrors()', () => { + it('should return all errors', () => { + const result = getAlertErrors( + mockAlert({ + name: '', + }), + mockAlertTypeModel({ + validate: () => ({ + errors: { + field: ['This is wrong'], + }, + }), + }), + mockserverRuleType() + ); + expect(result).toStrictEqual({ + alertParamsErrors: { field: ['This is wrong'] }, + alertBaseErrors: { + name: ['Name is required.'], + interval: [], + alertTypeId: [], + actionConnectors: [], + }, + alertErrors: { + name: ['Name is required.'], + field: ['This is wrong'], + interval: [], + alertTypeId: [], + actionConnectors: [], + }, + }); + }); + }); + + describe('getAlertActionErrors()', () => { + it('should return an array of errors', async () => { + const actionTypeRegistry = actionTypeRegistryMock.create(); + actionTypeRegistry.get.mockImplementation((actionTypeId: string) => ({ + ...actionTypeRegistryMock.createMockActionTypeModel(), + validateParams: jest.fn().mockImplementation(() => ({ + errors: { + [actionTypeId]: ['Yes, this failed'], + }, + })), + })); + const result = await getAlertActionErrors( + mockAlert({ + actions: [ + { + id: '1234', + actionTypeId: 'myActionType', + group: '', + params: { + name: 'yes', + }, + }, + { + id: '5678', + actionTypeId: 'myActionType2', + group: '', + params: { + name: 'yes', + }, + }, + ], + }), + actionTypeRegistry + ); + expect(result).toStrictEqual([ + { + myActionType: ['Yes, this failed'], + }, + { + myActionType2: ['Yes, this failed'], + }, + ]); + }); + }); + + describe('hasObjectErrors()', () => { + it('should return true for any errors', () => { + expect( + hasObjectErrors({ + foo: ['1'], + }) + ).toBe(true); + expect( + hasObjectErrors({ + foo: { + foo: ['1'], + }, + }) + ).toBe(true); + }); + it('should return false for no errors', () => { + expect(hasObjectErrors({})).toBe(false); + }); + }); + + describe('isValidAlert()', () => { + it('should return true for a valid alert', () => { + const result = isValidAlert(mockAlert(), {}, []); + expect(result).toBe(true); + }); + it('should return false for an invalid alert', () => { + expect( + isValidAlert( + mockAlert(), + { + name: ['This is wrong'], + }, + [] + ) + ).toBe(false); + expect( + isValidAlert(mockAlert(), {}, [ + { + name: ['This is wrong'], + }, + ]) + ).toBe(false); + }); + }); +}); + +function mockserverRuleType( + overloads: Partial> = {} +): AlertType { + return { + actionGroups: [], + defaultActionGroupId: 'default', + minimumLicenseRequired: 'basic', + recoveryActionGroup: { + id: 'recovery', + name: 'doRecovery', + }, + id: 'myAppAlertType', + name: 'myAppAlertType', + producer: 'myApp', + authorizedConsumers: {}, + enabledInLicense: true, + actionVariables: { + context: [], + state: [], + params: [], + }, + ...overloads, + }; +} + +function mockAlertTypeModel(overloads: Partial = {}): AlertTypeModel { + return { + id: 'alertTypeModel', + description: 'some alert', + iconClass: 'something', + documentationUrl: null, + validate: () => ({ errors: {} }), + alertParamsExpression: () => , + requiresAppContext: false, + ...overloads, + }; +} + +function mockAlert(overloads: Partial = {}): Alert { + return { + id: uuid.v4(), + enabled: true, + name: `alert-${uuid.v4()}`, + tags: [], + alertTypeId: '.noop', + consumer: 'consumer', + schedule: { interval: '1m' }, + actions: [], + params: {}, + createdBy: null, + updatedBy: null, + createdAt: new Date(), + updatedAt: new Date(), + apiKeyOwner: null, + throttle: null, + notifyWhen: null, + muteAll: false, + mutedInstanceIds: [], + executionStatus: { + status: 'unknown', + lastExecutionDate: new Date('2020-08-20T19:23:38Z'), + }, + ...overloads, + }; +} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_errors.ts b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_errors.ts new file mode 100644 index 0000000000000..3ca6a822b2d2d --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_errors.ts @@ -0,0 +1,132 @@ +/* + * 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 { isObject } from 'lodash'; +import { i18n } from '@kbn/i18n'; +import { parseDuration } from '../../../../../alerting/common/parse_duration'; +import { + AlertTypeModel, + Alert, + IErrorObject, + AlertAction, + AlertType, + ValidationResult, + ActionTypeRegistryContract, +} from '../../../types'; +import { InitialAlert } from './alert_reducer'; + +export function validateBaseProperties( + alertObject: InitialAlert, + serverRuleType?: AlertType +): ValidationResult { + const validationResult = { errors: {} }; + const errors = { + name: new Array(), + interval: new Array(), + alertTypeId: new Array(), + actionConnectors: new Array(), + }; + validationResult.errors = errors; + if (!alertObject.name) { + errors.name.push( + i18n.translate('xpack.triggersActionsUI.sections.alertForm.error.requiredNameText', { + defaultMessage: 'Name is required.', + }) + ); + } + if (alertObject.schedule.interval.length < 2) { + errors.interval.push( + i18n.translate('xpack.triggersActionsUI.sections.alertForm.error.requiredIntervalText', { + defaultMessage: 'Check interval is required.', + }) + ); + } else if (serverRuleType?.minimumScheduleInterval) { + const duration = parseDuration(alertObject.schedule.interval); + const minimumDuration = parseDuration(serverRuleType.minimumScheduleInterval); + if (duration < minimumDuration) { + errors.interval.push( + i18n.translate('xpack.triggersActionsUI.sections.alertForm.error.belowMinimumText', { + defaultMessage: 'Interval is below minimum ({minimum}) for this rule type', + values: { + minimum: serverRuleType.minimumScheduleInterval, + }, + }) + ); + } + } + + if (!alertObject.alertTypeId) { + errors.alertTypeId.push( + i18n.translate('xpack.triggersActionsUI.sections.alertForm.error.requiredRuleTypeIdText', { + defaultMessage: 'Rule type is required.', + }) + ); + } + const emptyConnectorActions = alertObject.actions.find( + (actionItem) => /^\d+$/.test(actionItem.id) && Object.keys(actionItem.params).length > 0 + ); + if (emptyConnectorActions !== undefined) { + errors.actionConnectors.push( + i18n.translate('xpack.triggersActionsUI.sections.alertForm.error.requiredActionConnector', { + defaultMessage: 'Action for {actionTypeId} connector is required.', + values: { actionTypeId: emptyConnectorActions.actionTypeId }, + }) + ); + } + return validationResult; +} + +export function getAlertErrors( + alert: Alert, + alertTypeModel: AlertTypeModel | null, + serverRuleType?: AlertType +) { + const alertParamsErrors: IErrorObject = alertTypeModel + ? alertTypeModel.validate(alert.params).errors + : []; + const alertBaseErrors = validateBaseProperties(alert, serverRuleType).errors as IErrorObject; + const alertErrors = { + ...alertParamsErrors, + ...alertBaseErrors, + } as IErrorObject; + + return { + alertParamsErrors, + alertBaseErrors, + alertErrors, + }; +} + +export async function getAlertActionErrors( + alert: Alert, + actionTypeRegistry: ActionTypeRegistryContract +): Promise { + return await Promise.all( + alert.actions.map( + async (alertAction: AlertAction) => + ( + await actionTypeRegistry.get(alertAction.actionTypeId)?.validateParams(alertAction.params) + ).errors + ) + ); +} + +export const hasObjectErrors: (errors: IErrorObject) => boolean = (errors) => + !!Object.values(errors).find((errorList) => { + if (isObject(errorList)) return hasObjectErrors(errorList as IErrorObject); + return errorList.length >= 1; + }); + +export function isValidAlert( + alertObject: InitialAlert | Alert, + validationResult: IErrorObject, + actionsErrors: IErrorObject[] +): alertObject is Alert { + return ( + !hasObjectErrors(validationResult) && + actionsErrors.every((error: IErrorObject) => !hasObjectErrors(error)) + ); +} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.tsx index 5facda85e6c89..3f6bf3c955e8b 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.tsx @@ -35,7 +35,7 @@ import { EuiToolTip, EuiCallOut, } from '@elastic/eui'; -import { capitalize, isObject } from 'lodash'; +import { capitalize } from 'lodash'; import { KibanaFeature } from '../../../../../features/public'; import { getDurationNumberInItsUnit, @@ -48,9 +48,8 @@ import { Alert, IErrorObject, AlertAction, - AlertTypeIndex, + RuleTypeIndex, AlertType, - ValidationResult, RuleTypeRegistryContract, ActionTypeRegistryContract, } from '../../../types'; @@ -74,101 +73,10 @@ import { checkAlertTypeEnabled } from '../../lib/check_alert_type_enabled'; import { alertTypeCompare, alertTypeGroupCompare } from '../../lib/alert_type_compare'; import { VIEW_LICENSE_OPTIONS_LINK } from '../../../common/constants'; import { SectionLoading } from '../../components/section_loading'; +import { DEFAULT_ALERT_INTERVAL } from '../../constants'; const ENTER_KEY = 13; -export function validateBaseProperties(alertObject: InitialAlert): ValidationResult { - const validationResult = { errors: {} }; - const errors = { - name: new Array(), - interval: new Array(), - alertTypeId: new Array(), - actionConnectors: new Array(), - }; - validationResult.errors = errors; - if (!alertObject.name) { - errors.name.push( - i18n.translate('xpack.triggersActionsUI.sections.alertForm.error.requiredNameText', { - defaultMessage: 'Name is required.', - }) - ); - } - if (alertObject.schedule.interval.length < 2) { - errors.interval.push( - i18n.translate('xpack.triggersActionsUI.sections.alertForm.error.requiredIntervalText', { - defaultMessage: 'Check interval is required.', - }) - ); - } - if (!alertObject.alertTypeId) { - errors.alertTypeId.push( - i18n.translate('xpack.triggersActionsUI.sections.alertForm.error.requiredRuleTypeIdText', { - defaultMessage: 'Rule type is required.', - }) - ); - } - const emptyConnectorActions = alertObject.actions.find( - (actionItem) => /^\d+$/.test(actionItem.id) && Object.keys(actionItem.params).length > 0 - ); - if (emptyConnectorActions !== undefined) { - errors.actionConnectors.push( - i18n.translate('xpack.triggersActionsUI.sections.alertForm.error.requiredActionConnector', { - defaultMessage: 'Action for {actionTypeId} connector is required.', - values: { actionTypeId: emptyConnectorActions.actionTypeId }, - }) - ); - } - return validationResult; -} - -export function getAlertErrors(alert: Alert, alertTypeModel: AlertTypeModel | null) { - const alertParamsErrors: IErrorObject = alertTypeModel - ? alertTypeModel.validate(alert.params).errors - : []; - const alertBaseErrors = validateBaseProperties(alert).errors as IErrorObject; - const alertErrors = { - ...alertParamsErrors, - ...alertBaseErrors, - } as IErrorObject; - - return { - alertParamsErrors, - alertBaseErrors, - alertErrors, - }; -} - -export async function getAlertActionErrors( - alert: Alert, - actionTypeRegistry: ActionTypeRegistryContract -): Promise { - return await Promise.all( - alert.actions.map( - async (alertAction: AlertAction) => - ( - await actionTypeRegistry.get(alertAction.actionTypeId)?.validateParams(alertAction.params) - ).errors - ) - ); -} - -export const hasObjectErrors: (errors: IErrorObject) => boolean = (errors) => - !!Object.values(errors).find((errorList) => { - if (isObject(errorList)) return hasObjectErrors(errorList as IErrorObject); - return errorList.length >= 1; - }); - -export function isValidAlert( - alertObject: InitialAlert | Alert, - validationResult: IErrorObject, - actionsErrors: IErrorObject[] -): alertObject is Alert { - return ( - !hasObjectErrors(validationResult) && - actionsErrors.every((error: IErrorObject) => !hasObjectErrors(error)) - ); -} - function getProducerFeatureName(producer: string, kibanaFeatures: KibanaFeature[]) { return kibanaFeatures.find((featureItem) => featureItem.id === producer)?.name; } @@ -186,6 +94,9 @@ interface AlertFormProps> { metadata?: MetaData; } +const defaultScheduleInterval = getDurationNumberInItsUnit(DEFAULT_ALERT_INTERVAL); +const defaultScheduleIntervalUnit = getDurationUnitValue(DEFAULT_ALERT_INTERVAL); + export const AlertForm = ({ alert, canChangeTrigger = true, @@ -212,10 +123,14 @@ export const AlertForm = ({ const [alertTypeModel, setAlertTypeModel] = useState(null); const [alertInterval, setAlertInterval] = useState( - alert.schedule.interval ? getDurationNumberInItsUnit(alert.schedule.interval) : undefined + alert.schedule.interval + ? getDurationNumberInItsUnit(alert.schedule.interval) + : defaultScheduleInterval ); const [alertIntervalUnit, setAlertIntervalUnit] = useState( - alert.schedule.interval ? getDurationUnitValue(alert.schedule.interval) : 'm' + alert.schedule.interval + ? getDurationUnitValue(alert.schedule.interval) + : defaultScheduleIntervalUnit ); const [alertThrottle, setAlertThrottle] = useState( alert.throttle ? getDurationNumberInItsUnit(alert.throttle) : null @@ -224,7 +139,7 @@ export const AlertForm = ({ alert.throttle ? getDurationUnitValue(alert.throttle) : 'h' ); const [defaultActionGroupId, setDefaultActionGroupId] = useState(undefined); - const [alertTypesIndex, setAlertTypesIndex] = useState(null); + const [ruleTypeIndex, setRuleTypeIndex] = useState(null); const [availableAlertTypes, setAvailableAlertTypes] = useState< Array<{ alertTypeModel: AlertTypeModel; alertType: AlertType }> @@ -243,14 +158,14 @@ export const AlertForm = ({ (async () => { try { const alertTypesResult = await loadAlertTypes({ http }); - const index: AlertTypeIndex = new Map(); + const index: RuleTypeIndex = new Map(); for (const alertTypeItem of alertTypesResult) { index.set(alertTypeItem.id, alertTypeItem); } if (alert.alertTypeId && index.has(alert.alertTypeId)) { setDefaultActionGroupId(index.get(alert.alertTypeId)!.defaultActionGroupId); } - setAlertTypesIndex(index); + setRuleTypeIndex(index); const availableAlertTypesResult = getAvailableAlertTypes(alertTypesResult); setAvailableAlertTypes(availableAlertTypesResult); @@ -287,10 +202,24 @@ export const AlertForm = ({ useEffect(() => { setAlertTypeModel(alert.alertTypeId ? ruleTypeRegistry.get(alert.alertTypeId) : null); - if (alert.alertTypeId && alertTypesIndex && alertTypesIndex.has(alert.alertTypeId)) { - setDefaultActionGroupId(alertTypesIndex.get(alert.alertTypeId)!.defaultActionGroupId); + if (alert.alertTypeId && ruleTypeIndex && ruleTypeIndex.has(alert.alertTypeId)) { + setDefaultActionGroupId(ruleTypeIndex.get(alert.alertTypeId)!.defaultActionGroupId); + } + }, [alert, alert.alertTypeId, ruleTypeIndex, ruleTypeRegistry]); + + useEffect(() => { + if (alert.schedule.interval) { + const interval = getDurationNumberInItsUnit(alert.schedule.interval); + const intervalUnit = getDurationUnitValue(alert.schedule.interval); + + if (interval !== defaultScheduleInterval) { + setAlertInterval(interval); + } + if (intervalUnit !== defaultScheduleIntervalUnit) { + setAlertIntervalUnit(intervalUnit); + } } - }, [alert, alert.alertTypeId, alertTypesIndex, ruleTypeRegistry]); + }, [alert.schedule.interval]); const setAlertProperty = useCallback( (key: Key, value: Alert[Key] | null) => { @@ -372,9 +301,7 @@ export const AlertForm = ({ ? !item.alertTypeModel.requiresAppContext : item.alertType!.producer === alert.consumer ); - const selectedAlertType = alert?.alertTypeId - ? alertTypesIndex?.get(alert?.alertTypeId) - : undefined; + const selectedAlertType = alert?.alertTypeId ? ruleTypeIndex?.get(alert?.alertTypeId) : undefined; const recoveryActionGroup = selectedAlertType?.recoveryActionGroup?.id; const getDefaultActionParams = useCallback( (actionTypeId: string, actionGroupId: string): Record | undefined => @@ -499,8 +426,8 @@ export const AlertForm = ({ setActions([]); setAlertTypeModel(item.alertTypeItem); setAlertProperty('params', {}); - if (alertTypesIndex && alertTypesIndex.has(item.id)) { - setDefaultActionGroupId(alertTypesIndex.get(item.id)!.defaultActionGroupId); + if (ruleTypeIndex && ruleTypeIndex.has(item.id)) { + setDefaultActionGroupId(ruleTypeIndex.get(item.id)!.defaultActionGroupId); } }} /> @@ -518,8 +445,8 @@ export const AlertForm = ({
- {alert.alertTypeId && alertTypesIndex && alertTypesIndex.has(alert.alertTypeId) - ? alertTypesIndex.get(alert.alertTypeId)!.name + {alert.alertTypeId && ruleTypeIndex && ruleTypeIndex.has(alert.alertTypeId) + ? ruleTypeIndex.get(alert.alertTypeId)!.name : ''}
@@ -870,7 +797,7 @@ export const AlertForm = ({ ) : null} {alertTypeNodes} - ) : alertTypesIndex ? ( + ) : ruleTypeIndex ? ( ) : ( diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx index 941d400104082..1daaf3b996126 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx @@ -33,7 +33,14 @@ import { import { useHistory } from 'react-router-dom'; import { isEmpty } from 'lodash'; -import { ActionType, Alert, AlertTableItem, AlertTypeIndex, Pagination } from '../../../../types'; +import { + ActionType, + Alert, + AlertTableItem, + AlertType, + RuleTypeIndex, + Pagination, +} from '../../../../types'; import { AlertAdd, AlertEdit } from '../../alert_form'; import { BulkOperationPopover } from '../../common/components/bulk_operation_popover'; import { AlertQuickEditButtonsWithApi as AlertQuickEditButtons } from '../../common/components/alert_quick_edit_buttons'; @@ -74,7 +81,7 @@ const ENTER_KEY = 13; interface AlertTypeState { isLoading: boolean; isInitialized: boolean; - data: AlertTypeIndex; + data: RuleTypeIndex; } interface AlertState { isLoading: boolean; @@ -161,7 +168,7 @@ export const AlertsList: React.FunctionComponent = () => { try { setAlertTypesState({ ...alertTypesState, isLoading: true }); const alertTypes = await loadAlertTypes({ http }); - const index: AlertTypeIndex = new Map(); + const index: RuleTypeIndex = new Map(); for (const alertType of alertTypes) { index.set(alertType.id, alertType); } @@ -895,6 +902,7 @@ export const AlertsList: React.FunctionComponent = () => { }} actionTypeRegistry={actionTypeRegistry} ruleTypeRegistry={ruleTypeRegistry} + ruleTypeIndex={alertTypesState.data} onSave={loadAlertsData} /> )} @@ -906,6 +914,9 @@ export const AlertsList: React.FunctionComponent = () => { }} actionTypeRegistry={actionTypeRegistry} ruleTypeRegistry={ruleTypeRegistry} + ruleType={ + alertTypesState.data.get(currentRuleToEdit.alertTypeId) as AlertType + } onSave={loadAlertsData} /> )} @@ -944,17 +955,17 @@ function filterAlertsById(alerts: Alert[], ids: string[]): Alert[] { function convertAlertsToTableItems( alerts: Alert[], - alertTypesIndex: AlertTypeIndex, + ruleTypeIndex: RuleTypeIndex, canExecuteActions: boolean ) { return alerts.map((alert) => ({ ...alert, actionsCount: alert.actions.length, tagsText: alert.tags.join(', '), - alertType: alertTypesIndex.get(alert.alertTypeId)?.name ?? alert.alertTypeId, + alertType: ruleTypeIndex.get(alert.alertTypeId)?.name ?? alert.alertTypeId, isEditable: - hasAllPrivilege(alert, alertTypesIndex.get(alert.alertTypeId)) && + hasAllPrivilege(alert, ruleTypeIndex.get(alert.alertTypeId)) && (canExecuteActions || (!canExecuteActions && !alert.actions.length)), - enabledInLicense: !!alertTypesIndex.get(alert.alertTypeId)?.enabledInLicense, + enabledInLicense: !!ruleTypeIndex.get(alert.alertTypeId)?.enabledInLicense, })); } diff --git a/x-pack/plugins/triggers_actions_ui/public/types.ts b/x-pack/plugins/triggers_actions_ui/public/types.ts index ae4fd5152794f..2ef20f36b7ca9 100644 --- a/x-pack/plugins/triggers_actions_ui/public/types.ts +++ b/x-pack/plugins/triggers_actions_ui/public/types.ts @@ -66,7 +66,7 @@ export { }; export type ActionTypeIndex = Record; -export type AlertTypeIndex = Map; +export type RuleTypeIndex = Map; export type ActionTypeRegistryContract< ActionConnector = unknown, ActionParams = unknown @@ -197,6 +197,8 @@ export interface AlertType< | 'minimumLicenseRequired' | 'recoveryActionGroup' | 'defaultActionGroupId' + | 'defaultScheduleInterval' + | 'minimumScheduleInterval' > { actionVariables: ActionVariables; authorizedConsumers: Record; @@ -285,6 +287,7 @@ export interface AlertEditProps> { reloadAlerts?: () => Promise; onSave?: () => Promise; metadata?: MetaData; + ruleType?: AlertType; } export interface AlertAddProps> { @@ -299,4 +302,5 @@ export interface AlertAddProps> { reloadAlerts?: () => Promise; onSave?: () => Promise; metadata?: MetaData; + ruleTypeIndex?: RuleTypeIndex; } From a3895208498e835a692fee918191b4b20802bb59 Mon Sep 17 00:00:00 2001 From: Shahzad Date: Mon, 11 Oct 2021 20:27:46 +0200 Subject: [PATCH 8/9] [Utpime] Remove unnecessary usememo in url params hooks (#114252) --- x-pack/plugins/uptime/public/hooks/use_url_params.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/uptime/public/hooks/use_url_params.ts b/x-pack/plugins/uptime/public/hooks/use_url_params.ts index 329e0ccef4d96..1318b635693c7 100644 --- a/x-pack/plugins/uptime/public/hooks/use_url_params.ts +++ b/x-pack/plugins/uptime/public/hooks/use_url_params.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { useCallback, useEffect, useMemo } from 'react'; +import { useCallback, useEffect } from 'react'; import { parse, stringify } from 'query-string'; import { useLocation, useHistory } from 'react-router-dom'; import { useDispatch, useSelector } from 'react-redux'; @@ -28,7 +28,7 @@ const getParsedParams = (search: string) => { export const useGetUrlParams: GetUrlParams = () => { const { search } = useLocation(); - return useMemo(() => getSupportedUrlParams(getParsedParams(search)), [search]); + return getSupportedUrlParams(getParsedParams(search)); }; const getMapFromFilters = (value: any): Map | undefined => { From e8d16cddca6003383c972a01229f9a284799a2ed Mon Sep 17 00:00:00 2001 From: Jonathan Budzenski Date: Mon, 11 Oct 2021 13:30:33 -0500 Subject: [PATCH 9/9] skip flaky suite. #114541, #114542 --- x-pack/test/accessibility/apps/index_lifecycle_management.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/x-pack/test/accessibility/apps/index_lifecycle_management.ts b/x-pack/test/accessibility/apps/index_lifecycle_management.ts index da56cfd702abf..65faa77fc497b 100644 --- a/x-pack/test/accessibility/apps/index_lifecycle_management.ts +++ b/x-pack/test/accessibility/apps/index_lifecycle_management.ts @@ -57,7 +57,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { throw new Error(`Could not find ${policyName} in policy table`); }; - describe('Index Lifecycle Management', async () => { + // FLAKY + // https://github.com/elastic/kibana/issues/114541 + // https://github.com/elastic/kibana/issues/114542 + describe.skip('Index Lifecycle Management', async () => { before(async () => { await esClient.ilm.putLifecycle({ policy: POLICY_NAME, body: POLICY_ALL_PHASES }); await esClient.indices.putIndexTemplate({