diff --git a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/application.ts b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/application.ts index 257ba8a4711b0a..9ca84735cac168 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/application.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/application.ts @@ -59,6 +59,10 @@ export interface RenderDeps { savedDashboards: SavedObjectLoader; dashboardConfig: KibanaLegacyStart['dashboardConfig']; dashboardCapabilities: any; + embeddableCapabilities: { + visualizeCapabilities: any; + mapsCapabilities: any; + }; uiSettings: IUiSettingsClient; chrome: ChromeStart; addBasePath: (path: string) => string; diff --git a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_app_controller.tsx b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_app_controller.tsx index 84dd73882d1343..af3347afa9c5f2 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_app_controller.tsx +++ b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_app_controller.tsx @@ -108,6 +108,7 @@ export class DashboardAppController { embeddable, share, dashboardCapabilities, + embeddableCapabilities: { visualizeCapabilities, mapsCapabilities }, data: { query: queryService }, core: { notifications, @@ -134,7 +135,6 @@ export class DashboardAppController { } = syncQueryStateWithUrl(queryService, kbnUrlStateStorage); let lastReloadRequestTime = 0; - const dash = ($scope.dash = $route.current.locals.dash); if (dash.id) { chrome.docTitle.change(dash.title); @@ -180,11 +180,18 @@ export class DashboardAppController { dashboardStateManager.getIsViewMode() && !dashboardConfig.getHideWriteControls(); - const getIsEmptyInReadonlyMode = () => - !dashboardStateManager.getPanels().length && - !getShouldShowEditHelp() && - !getShouldShowViewHelp() && - dashboardConfig.getHideWriteControls(); + const shouldShowUnauthorizedEmptyState = () => { + const readonlyMode = + !dashboardStateManager.getPanels().length && + !getShouldShowEditHelp() && + !getShouldShowViewHelp() && + dashboardConfig.getHideWriteControls(); + const userHasNoPermissions = + !dashboardStateManager.getPanels().length && + !visualizeCapabilities.save && + !mapsCapabilities.save; + return readonlyMode || userHasNoPermissions; + }; const addVisualization = () => { navActions[TopNavIds.VISUALIZE](); @@ -250,7 +257,7 @@ export class DashboardAppController { } const shouldShowEditHelp = getShouldShowEditHelp(); const shouldShowViewHelp = getShouldShowViewHelp(); - const isEmptyInReadonlyMode = getIsEmptyInReadonlyMode(); + const isEmptyInReadonlyMode = shouldShowUnauthorizedEmptyState(); return { id: dashboardStateManager.savedDashboard.id || '', filters: queryFilter.getFilters(), @@ -307,7 +314,7 @@ export class DashboardAppController { dashboardContainer.renderEmpty = () => { const shouldShowEditHelp = getShouldShowEditHelp(); const shouldShowViewHelp = getShouldShowViewHelp(); - const isEmptyInReadOnlyMode = getIsEmptyInReadonlyMode(); + const isEmptyInReadOnlyMode = shouldShowUnauthorizedEmptyState(); const isEmptyState = shouldShowEditHelp || shouldShowViewHelp || isEmptyInReadOnlyMode; return isEmptyState ? ( {}); -jest.mock('ui/agg_types', () => { - class MockSchemas {} - return { - Schemas: MockSchemas, - }; -}); jest.mock('ui/timefilter', () => {}); jest.mock('../vector_layer', () => {}); diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/es_source.js b/x-pack/legacy/plugins/maps/public/layers/sources/es_source.js index 782f2845ceeffc..5074b218dd615a 100644 --- a/x-pack/legacy/plugins/maps/public/layers/sources/es_source.js +++ b/x-pack/legacy/plugins/maps/public/layers/sources/es_source.js @@ -14,7 +14,6 @@ import { import { createExtentFilter } from '../../elasticsearch_geo_utils'; import { timefilter } from 'ui/timefilter'; import _ from 'lodash'; -import { AggConfigs } from 'ui/agg_types'; import { i18n } from '@kbn/i18n'; import uuid from 'uuid/v4'; import { copyPersistentState } from '../../reducers/util'; @@ -151,27 +150,18 @@ export class AbstractESSource extends AbstractVectorSource { { sourceQuery, query, timeFilters, filters, applyGlobalQuery }, 0 ); - const geoField = await this._getGeoField(); - const indexPattern = await this.getIndexPattern(); - - const geoBoundsAgg = [ - { - type: 'geo_bounds', - enabled: true, - params: { - field: geoField, + searchSource.setField('aggs', { + fitToBounds: { + geo_bounds: { + field: this._descriptor.geoField, }, - schema: 'metric', }, - ]; - - const aggConfigs = new AggConfigs(indexPattern, geoBoundsAgg); - searchSource.setField('aggs', aggConfigs.toDsl()); + }); let esBounds; try { const esResp = await searchSource.fetch(); - esBounds = _.get(esResp, 'aggregations.1.bounds'); + esBounds = _.get(esResp, 'aggregations.fitToBounds.bounds'); } catch (error) { esBounds = { top_left: { diff --git a/x-pack/plugins/infra/kibana.json b/x-pack/plugins/infra/kibana.json index 7ba81127c1e676..f12bbd6cf77235 100644 --- a/x-pack/plugins/infra/kibana.json +++ b/x-pack/plugins/infra/kibana.json @@ -2,7 +2,17 @@ "id": "infra", "version": "8.0.0", "kibanaVersion": "kibana", - "requiredPlugins": ["features", "apm", "usageCollection", "spaces", "home", "data", "data_enhanced", "metrics"], + "requiredPlugins": [ + "features", + "apm", + "usageCollection", + "spaces", + "home", + "data", + "data_enhanced", + "metrics", + "alerting" + ], "server": true, "ui": true, "configPath": ["xpack", "infra"] diff --git a/x-pack/plugins/infra/server/lib/adapters/framework/adapter_types.ts b/x-pack/plugins/infra/server/lib/adapters/framework/adapter_types.ts index f8177365c9bdd8..c7ac577dd8a81f 100644 --- a/x-pack/plugins/infra/server/lib/adapters/framework/adapter_types.ts +++ b/x-pack/plugins/infra/server/lib/adapters/framework/adapter_types.ts @@ -13,6 +13,7 @@ import { SpacesPluginSetup } from '../../../../../../plugins/spaces/server'; import { VisTypeTimeseriesSetup } from '../../../../../../../src/plugins/vis_type_timeseries/server'; import { APMPluginContract } from '../../../../../../plugins/apm/server'; import { HomeServerPluginSetup } from '../../../../../../../src/plugins/home/server'; +import { PluginSetupContract as AlertingPluginContract } from '../../../../../../plugins/alerting/server'; // NP_TODO: Compose real types from plugins we depend on, no "any" export interface InfraServerPluginDeps { @@ -25,6 +26,7 @@ export interface InfraServerPluginDeps { }; features: FeaturesPluginSetup; apm: APMPluginContract; + alerting: AlertingPluginContract; } export interface CallWithRequestParams extends GenericParams { diff --git a/x-pack/plugins/infra/server/lib/alerting/index.ts b/x-pack/plugins/infra/server/lib/alerting/index.ts new file mode 100644 index 00000000000000..90287d8f219f95 --- /dev/null +++ b/x-pack/plugins/infra/server/lib/alerting/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { registerAlertTypes } from './register_alert_types'; diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/register_metric_threshold_alert_type.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/register_metric_threshold_alert_type.ts new file mode 100644 index 00000000000000..9bc54c1a49c8f7 --- /dev/null +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/register_metric_threshold_alert_type.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; + * you may not use this file except in compliance with the Elastic License. + */ +import uuid from 'uuid'; +import { i18n } from '@kbn/i18n'; +import { schema } from '@kbn/config-schema'; +import { + MetricThresholdAlertTypeParams, + Comparator, + AlertStates, + METRIC_THRESHOLD_ALERT_TYPE_ID, +} from './types'; +import { AlertServices, PluginSetupContract } from '../../../../../alerting/server'; + +const FIRED_ACTIONS = { + id: 'metrics.threshold.fired', + name: i18n.translate('xpack.infra.metrics.alerting.threshold.fired', { + defaultMessage: 'Fired', + }), +}; + +async function getMetric( + { callCluster }: AlertServices, + { metric, aggType, timeUnit, timeSize, indexPattern }: MetricThresholdAlertTypeParams +) { + const interval = `${timeSize}${timeUnit}`; + const searchBody = { + query: { + bool: { + filter: [ + { + range: { + '@timestamp': { + gte: `now-${interval}`, + }, + }, + exists: { + field: metric, + }, + }, + ], + }, + }, + size: 0, + aggs: { + aggregatedIntervals: { + date_histogram: { + field: '@timestamp', + fixed_interval: interval, + }, + aggregations: { + aggregatedValue: { + [aggType]: { + field: metric, + }, + }, + }, + }, + }, + }; + + const result = await callCluster('search', { + body: searchBody, + index: indexPattern, + }); + + const { buckets } = result.aggregations.aggregatedIntervals; + const { value } = buckets[buckets.length - 1].aggregatedValue; + return value; +} + +const comparatorMap = { + [Comparator.BETWEEN]: (value: number, [a, b]: number[]) => + value >= Math.min(a, b) && value <= Math.max(a, b), + // `threshold` is always an array of numbers in case the BETWEEN comparator is + // used; all other compartors will just destructure the first value in the array + [Comparator.GT]: (a: number, [b]: number[]) => a > b, + [Comparator.LT]: (a: number, [b]: number[]) => a < b, + [Comparator.GT_OR_EQ]: (a: number, [b]: number[]) => a >= b, + [Comparator.LT_OR_EQ]: (a: number, [b]: number[]) => a <= b, +}; + +export async function registerMetricThresholdAlertType(alertingPlugin: PluginSetupContract) { + if (!alertingPlugin) { + throw new Error( + 'Cannot register metric threshold alert type. Both the actions and alerting plugins need to be enabled.' + ); + } + const alertUUID = uuid.v4(); + + alertingPlugin.registerType({ + id: METRIC_THRESHOLD_ALERT_TYPE_ID, + name: 'Metric Alert - Threshold', + validate: { + params: schema.object({ + threshold: schema.arrayOf(schema.number()), + comparator: schema.string(), + aggType: schema.string(), + metric: schema.string(), + timeUnit: schema.string(), + timeSize: schema.number(), + indexPattern: schema.string(), + }), + }, + defaultActionGroupId: FIRED_ACTIONS.id, + actionGroups: [FIRED_ACTIONS], + async executor({ services, params }) { + const { threshold, comparator } = params as MetricThresholdAlertTypeParams; + const alertInstance = services.alertInstanceFactory(alertUUID); + const currentValue = await getMetric(services, params as MetricThresholdAlertTypeParams); + if (typeof currentValue === 'undefined') + throw new Error('Could not get current value of metric'); + + const comparisonFunction = comparatorMap[comparator]; + + const isValueInAlertState = comparisonFunction(currentValue, threshold); + + if (isValueInAlertState) { + alertInstance.scheduleActions(FIRED_ACTIONS.id, { + value: currentValue, + }); + } + + // Future use: ability to fetch display current alert state + alertInstance.replaceState({ + alertState: isValueInAlertState ? AlertStates.ALERT : AlertStates.OK, + }); + }, + }); +} diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/types.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/types.ts new file mode 100644 index 00000000000000..2618469ff693c7 --- /dev/null +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/types.ts @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { MetricsExplorerAggregation } from '../../../../common/http_api/metrics_explorer'; + +export const METRIC_THRESHOLD_ALERT_TYPE_ID = 'metrics.alert.threshold'; + +export enum Comparator { + GT = '>', + LT = '<', + GT_OR_EQ = '>=', + LT_OR_EQ = '<=', + BETWEEN = 'between', +} + +export enum AlertStates { + OK, + ALERT, +} + +export type TimeUnit = 's' | 'm' | 'h' | 'd'; + +export interface MetricThresholdAlertTypeParams { + aggType: MetricsExplorerAggregation; + metric: string; + timeSize: number; + timeUnit: TimeUnit; + indexPattern: string; + threshold: number[]; + comparator: Comparator; +} diff --git a/x-pack/plugins/infra/server/lib/alerting/register_alert_types.ts b/x-pack/plugins/infra/server/lib/alerting/register_alert_types.ts new file mode 100644 index 00000000000000..6ec6f31256b787 --- /dev/null +++ b/x-pack/plugins/infra/server/lib/alerting/register_alert_types.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { PluginSetupContract } from '../../../../alerting/server'; +import { registerMetricThresholdAlertType } from './metric_threshold/register_metric_threshold_alert_type'; + +const registerAlertTypes = (alertingPlugin: PluginSetupContract) => { + if (alertingPlugin) { + const registerFns = [registerMetricThresholdAlertType]; + + registerFns.forEach(fn => { + fn(alertingPlugin); + }); + } +}; + +export { registerAlertTypes }; diff --git a/x-pack/plugins/infra/server/plugin.ts b/x-pack/plugins/infra/server/plugin.ts index bcdbccf6f22945..64fc496f3597e1 100644 --- a/x-pack/plugins/infra/server/plugin.ts +++ b/x-pack/plugins/infra/server/plugin.ts @@ -27,6 +27,7 @@ import { InfraServerPluginDeps } from './lib/adapters/framework'; import { METRICS_FEATURE, LOGS_FEATURE } from './features'; import { UsageCollector } from './usage/usage_collector'; import { InfraStaticSourceConfiguration } from './lib/sources/types'; +import { registerAlertTypes } from './lib/alerting'; export const config = { schema: schema.object({ @@ -146,6 +147,7 @@ export class InfraServerPlugin { ]); initInfraServer(this.libs); + registerAlertTypes(plugins.alerting); // Telemetry UsageCollector.registerUsageCollector(plugins.usageCollection);