From 239b9571917a113a00270e1f2572f011e11441da Mon Sep 17 00:00:00 2001 From: Thom Heymann <190132+thomheymann@users.noreply.github.com> Date: Fri, 9 Feb 2024 07:06:06 +0000 Subject: [PATCH] [Logs Explorer] Add ability to create alerts within the Explorer app (#175777) ## Summary Users in Logs Explorer should be able to define rules that will create alerts when the number of logs passes a certain threshold. As part of this issue, support for the new custom threshold rule will be added in the header. ## Screenshot Screenshot 2024-01-29 at 12 48 28 ## Notes for reviewers Apologies for the noise across multiple plugins but I tried to update the `RuleAddProps` type since it offered no type safety making it incredibly difficult and error prone to understand what properties are required for the selected rule type. The lack of type safety in the exposed alerting/rules related public APIs caused various bugs during integration, each dependant on specific conditions and requiring a lot of manual testing. I have tried to fix as many of these bugs as possible but had to base it mainly on trial and error due to the lack of correct typing with too many optional (but in reality required) properties. An example of this are filter badges in the universal search component. These were not displayed correctly due to missing props on the `FilterMeta` interface which are all marked as optional but somehow assumed to be there by the UI components that render them. Another issue was caused by implicit service dependencies with no validation in place by consuming components. An example of this is the `triggersActionsUi.getAddRuleFlyout` method which requires `unifiedSearch`, `dataViews`, `dataViewEditor` and `lens` services in React context but for the majority silently fails causing bugs only under specific conditions or when trying to carry out specific actions. Integration is made even more difficult since these can differ between different rule types. It would be great if these are made either explicit or if validation is put in place to warn developers of integration issues. There is also an existing bug in that filters displayed by the universal search component provide the ability to edit the filter but when attempting to do so and clicking "Update filter" to confirm nothing happens despite the `SearchBar.onFiltersUpdated` being defined. I have tested this behaviour in other integrations which all have the same bugs so am treating these as existing issues. ## Acceptance criteria - Add an `Alerts` item to the header that will include two options: - `Create rule` that will open the custom threshold rule flyout - `Manage rules` that will link to the observability rules management page (`app/observability/alerts/rules`) - Set default configuration that will be used in the flyout - The Ad Hoc data view that is created as part of the selector - The query from the KQL bar - Role visibility should be hidden --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Maryam Saeidi --- .../top_nav/open_alerts_popover.tsx | 7 +- .../public/filter_badge/filter_badge.tsx | 2 +- x-pack/plugins/alerting/common/rule.ts | 1 + ...s_upgrade_and_rollback_checks.test.ts.snap | 95 ++++++++++++ .../ui_components/alerting_flyout/index.tsx | 10 +- .../components/alerting/utils/helper.ts | 4 +- .../alert_details_app_section.test.tsx.snap | 8 +- .../alert_details_app_section.tsx | 10 +- .../rule_condition_chart.test.tsx | 3 +- .../rule_condition_chart.tsx | 20 ++- .../custom_threshold_rule_expression.tsx | 14 +- .../lib/check_missing_group.ts | 35 +++-- .../custom_threshold/lib/evaluate_rule.ts | 4 +- .../rules/custom_threshold/lib/get_data.ts | 7 +- .../custom_threshold/lib/metric_query.test.ts | 87 ++++++++++- .../custom_threshold/lib/metric_query.ts | 46 +++--- .../register_custom_threshold_rule_type.ts | 8 + .../server/utils/get_parsed_filtered_query.ts | 30 +++- .../utils/convert_discover_app_state.ts | 91 ++++++----- .../observability_logs_explorer/kibana.jsonc | 7 +- .../observability_logs_explorer.tsx | 20 ++- .../public/components/alerts_popover.tsx | 142 ++++++++++++++++++ .../public/components/discover_link.tsx | 18 ++- .../components/logs_explorer_top_nav_menu.tsx | 5 + .../public/types.ts | 10 ++ .../observability_logs_explorer/tsconfig.json | 7 + .../application/sections/rule_form/index.tsx | 11 +- .../sections/rule_form/rule_add.tsx | 10 +- .../sections/rule_form/rule_edit.tsx | 11 +- .../public/common/get_add_rule_flyout.tsx | 9 +- .../public/common/get_edit_rule_flyout.tsx | 9 +- .../triggers_actions_ui/public/mocks.ts | 6 +- .../triggers_actions_ui/public/plugin.ts | 26 ++-- .../triggers_actions_ui/public/types.ts | 30 +++- 34 files changed, 650 insertions(+), 153 deletions(-) create mode 100644 x-pack/plugins/observability_solution/observability_logs_explorer/public/components/alerts_popover.tsx diff --git a/src/plugins/discover/public/application/main/components/top_nav/open_alerts_popover.tsx b/src/plugins/discover/public/application/main/components/top_nav/open_alerts_popover.tsx index 985b0c303cd21..ee80e467e7fb4 100644 --- a/src/plugins/discover/public/application/main/components/top_nav/open_alerts_popover.tsx +++ b/src/plugins/discover/public/application/main/components/top_nav/open_alerts_popover.tsx @@ -19,6 +19,7 @@ import { RuleCreationValidConsumer, STACK_ALERTS_FEATURE_ID, } from '@kbn/rule-data-utils'; +import { RuleTypeMetaData } from '@kbn/alerting-plugin/common'; import { DiscoverStateContainer } from '../../services/discover_state'; import { DiscoverServices } from '../../../../build_services'; @@ -42,7 +43,7 @@ interface AlertsPopoverProps { isPlainRecord?: boolean; } -interface EsQueryAlertMetaData { +interface EsQueryAlertMetaData extends RuleTypeMetaData { isManagementPage?: boolean; adHocDataViewList: DataView[]; } @@ -110,11 +111,11 @@ export function AlertsPopover({ metadata: discoverMetadata, consumer: 'alerts', onClose: (_, metadata) => { - onFinishFlyoutInteraction(metadata as EsQueryAlertMetaData); + onFinishFlyoutInteraction(metadata!); onClose(); }, onSave: async (metadata) => { - onFinishFlyoutInteraction(metadata as EsQueryAlertMetaData); + onFinishFlyoutInteraction(metadata!); }, canChangeTrigger: false, ruleTypeId: ES_QUERY_ID, diff --git a/src/plugins/unified_search/public/filter_badge/filter_badge.tsx b/src/plugins/unified_search/public/filter_badge/filter_badge.tsx index 7b20eab971e9c..e20429d5e9f36 100644 --- a/src/plugins/unified_search/public/filter_badge/filter_badge.tsx +++ b/src/plugins/unified_search/public/filter_badge/filter_badge.tsx @@ -67,7 +67,7 @@ function FilterBadge({ `} > - {!hideAlias && filter.meta.alias !== null ? ( + {filter.meta.alias && !hideAlias ? ( <> {prefix} diff --git a/x-pack/plugins/alerting/common/rule.ts b/x-pack/plugins/alerting/common/rule.ts index 7cec5bdbdd7a6..6a66b39720402 100644 --- a/x-pack/plugins/alerting/common/rule.ts +++ b/x-pack/plugins/alerting/common/rule.ts @@ -19,6 +19,7 @@ export type { ActionVariable } from '@kbn/alerting-types'; export type RuleTypeState = Record; export type RuleTypeParams = Record; +export type RuleTypeMetaData = Record; // rule type defined alert fields to persist in alerts index export type RuleAlertData = Record; diff --git a/x-pack/plugins/alerting/server/integration_tests/__snapshots__/serverless_upgrade_and_rollback_checks.test.ts.snap b/x-pack/plugins/alerting/server/integration_tests/__snapshots__/serverless_upgrade_and_rollback_checks.test.ts.snap index 70ffc475d01d6..6360d65c0e66c 100644 --- a/x-pack/plugins/alerting/server/integration_tests/__snapshots__/serverless_upgrade_and_rollback_checks.test.ts.snap +++ b/x-pack/plugins/alerting/server/integration_tests/__snapshots__/serverless_upgrade_and_rollback_checks.test.ts.snap @@ -3064,6 +3064,101 @@ Object { "presence": "optional", }, "keys": Object { + "filter": Object { + "flags": Object { + "default": [Function], + "error": [Function], + "presence": "optional", + }, + "items": Array [ + Object { + "flags": Object { + "default": Object { + "special": "deep", + }, + "error": [Function], + "presence": "optional", + }, + "keys": Object { + "meta": Object { + "flags": Object { + "error": [Function], + }, + "rules": Array [ + Object { + "args": Object { + "key": Object { + "flags": Object { + "error": [Function], + }, + "rules": Array [ + Object { + "args": Object { + "method": [Function], + }, + "name": "custom", + }, + ], + "type": "string", + }, + "value": Object { + "flags": Object { + "error": [Function], + }, + "type": "any", + }, + }, + "name": "entries", + }, + ], + "type": "record", + }, + "query": Object { + "flags": Object { + "default": [Function], + "error": [Function], + "presence": "optional", + }, + "rules": Array [ + Object { + "args": Object { + "key": Object { + "flags": Object { + "error": [Function], + }, + "rules": Array [ + Object { + "args": Object { + "method": [Function], + }, + "name": "custom", + }, + ], + "type": "string", + }, + "value": Object { + "flags": Object { + "error": [Function], + }, + "type": "any", + }, + }, + "name": "entries", + }, + ], + "type": "record", + }, + }, + "preferences": Object { + "stripUnknown": Object { + "objects": false, + }, + }, + "type": "object", + }, + ], + "type": "array", + }, "index": Object { "flags": Object { "error": [Function], diff --git a/x-pack/plugins/apm/public/components/alerting/ui_components/alerting_flyout/index.tsx b/x-pack/plugins/apm/public/components/alerting/ui_components/alerting_flyout/index.tsx index 95820bf8f84d4..c671bc2dda540 100644 --- a/x-pack/plugins/apm/public/components/alerting/ui_components/alerting_flyout/index.tsx +++ b/x-pack/plugins/apm/public/components/alerting/ui_components/alerting_flyout/index.tsx @@ -8,6 +8,7 @@ import React, { useCallback, useMemo } from 'react'; import { useKibana } from '@kbn/kibana-react-plugin/public'; import { ApmRuleType } from '@kbn/rule-data-utils'; +import type { RuleTypeParams } from '@kbn/alerting-plugin/common'; import { APM_SERVER_FEATURE_ID } from '../../../../../common/rules/apm_rule_types'; import { getInitialAlertValues } from '../../utils/get_initial_alert_values'; import { ApmPluginStartDeps } from '../../../../plugin'; @@ -35,7 +36,7 @@ export function AlertingFlyout(props: Props) { const { start, end } = useTimeRange({ rangeFrom, rangeTo, optional: true }); const environment = - 'environment' in query ? query.environment : ENVIRONMENT_ALL.value; + 'environment' in query ? query.environment! : ENVIRONMENT_ALL.value; const transactionType = 'transactionType' in query ? query.transactionType : undefined; const transactionName = @@ -53,7 +54,10 @@ export function AlertingFlyout(props: Props) { const addAlertFlyout = useMemo( () => ruleType && - services.triggersActionsUi.getAddRuleFlyout({ + services.triggersActionsUi.getAddRuleFlyout< + RuleTypeParams, + AlertMetadata + >({ consumer: APM_SERVER_FEATURE_ID, onClose: onCloseAddFlyout, ruleTypeId: ruleType, @@ -67,7 +71,7 @@ export function AlertingFlyout(props: Props) { errorGroupingKey, start, end, - } as AlertMetadata, + }, useRuleProducer: true, }), /* eslint-disable-next-line react-hooks/exhaustive-deps */ diff --git a/x-pack/plugins/apm/public/components/alerting/utils/helper.ts b/x-pack/plugins/apm/public/components/alerting/utils/helper.ts index 7cc0d958aaf9e..66cfe522388f7 100644 --- a/x-pack/plugins/apm/public/components/alerting/utils/helper.ts +++ b/x-pack/plugins/apm/public/components/alerting/utils/helper.ts @@ -6,9 +6,11 @@ */ import { TIME_UNITS } from '@kbn/triggers-actions-ui-plugin/public'; +import type { RuleTypeMetaData } from '@kbn/alerting-plugin/common'; + import moment from 'moment'; -export interface AlertMetadata { +export interface AlertMetadata extends RuleTypeMetaData { environment: string; serviceName?: string; transactionType?: string; diff --git a/x-pack/plugins/observability/public/components/custom_threshold/components/alert_details_app_section/__snapshots__/alert_details_app_section.test.tsx.snap b/x-pack/plugins/observability/public/components/custom_threshold/components/alert_details_app_section/__snapshots__/alert_details_app_section.test.tsx.snap index b963137281b70..a9a77b477f1a2 100644 --- a/x-pack/plugins/observability/public/components/custom_threshold/components/alert_details_app_section/__snapshots__/alert_details_app_section.test.tsx.snap +++ b/x-pack/plugins/observability/public/components/custom_threshold/components/alert_details_app_section/__snapshots__/alert_details_app_section.test.tsx.snap @@ -28,7 +28,6 @@ Array [ }, ], "dataView": undefined, - "filterQuery": "", "groupBy": Array [ "host.hostname", ], @@ -46,6 +45,13 @@ Array [ "timeSize": 15, "timeUnit": "m", }, + "searchConfiguration": Object { + "index": "mockedIndex", + "query": Object { + "language": "kuery", + "query": "host.hostname: Users-System.local and service.type: system", + }, + }, "seriesType": "bar_stacked", "timeRange": Object { "from": "2023-03-28T10:43:13.802Z", diff --git a/x-pack/plugins/observability/public/components/custom_threshold/components/alert_details_app_section/alert_details_app_section.tsx b/x-pack/plugins/observability/public/components/custom_threshold/components/alert_details_app_section/alert_details_app_section.tsx index 2506516efd81a..f07a6ddac4501 100644 --- a/x-pack/plugins/observability/public/components/custom_threshold/components/alert_details_app_section/alert_details_app_section.tsx +++ b/x-pack/plugins/observability/public/components/custom_threshold/components/alert_details_app_section/alert_details_app_section.tsx @@ -5,7 +5,6 @@ * 2.0. */ -import { Query } from '@kbn/es-query'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; import React, { useEffect, useState } from 'react'; @@ -54,7 +53,6 @@ import { LogRateAnalysis } from './log_rate_analysis'; import { Groups } from './groups'; import { Tags } from './tags'; import { RuleConditionChart } from '../rule_condition_chart/rule_condition_chart'; -import { getFilterQuery } from './helpers/get_filter_query'; // TODO Use a generic props for app sections https://github.com/elastic/kibana/issues/152690 export type CustomThresholdRule = Rule; @@ -118,7 +116,6 @@ export default function AlertDetailsAppSection({ const { euiTheme } = useEuiTheme(); const hasLogRateAnalysisLicense = hasAtLeast('platinum'); const [dataView, setDataView] = useState(); - const [filterQuery, setFilterQuery] = useState(''); const [, setDataViewError] = useState(); const ruleParams = rule.params as RuleTypeParams & AlertParams; const chartProps = { @@ -204,11 +201,6 @@ export default function AlertDetailsAppSection({ setAlertSummaryFields(alertSummaryFields); }, [groups, tags, rule, ruleLink, setAlertSummaryFields]); - useEffect(() => { - const query = `${(ruleParams.searchConfiguration?.query as Query)?.query as string}`; - setFilterQuery(getFilterQuery(query, groups)); - }, [groups, ruleParams.searchConfiguration]); - useEffect(() => { const initDataView = async () => { const ruleSearchConfiguration = ruleParams.searchConfiguration; @@ -271,7 +263,7 @@ export default function AlertDetailsAppSection({ { ); diff --git a/x-pack/plugins/observability/public/components/custom_threshold/custom_threshold_rule_expression.tsx b/x-pack/plugins/observability/public/components/custom_threshold/custom_threshold_rule_expression.tsx index cd93a5e134c2b..19a1c0fcd164e 100644 --- a/x-pack/plugins/observability/public/components/custom_threshold/custom_threshold_rule_expression.tsx +++ b/x-pack/plugins/observability/public/components/custom_threshold/custom_threshold_rule_expression.tsx @@ -405,7 +405,7 @@ export default function Expressions(props: Props) { indexPatterns={dataView ? [dataView] : undefined} showQueryInput={true} showQueryMenu={false} - showFilterBar={false} + showFilterBar={!!ruleParams.searchConfiguration?.filter} showDatePicker={false} showSubmitButton={false} displayStyle="inPage" @@ -413,6 +413,16 @@ export default function Expressions(props: Props) { onQuerySubmit={onFilterChange} dataTestSubj="thresholdRuleUnifiedSearchBar" query={ruleParams.searchConfiguration?.query as Query} + filters={ruleParams.searchConfiguration?.filter} + onFiltersUpdated={(filter) => { + // Since rule params will be sent to the API as is, and we only need meta and query parameters to be + // saved in the rule's saved object, we filter extra fields here (such as $state). + const filters = filter.map(({ meta, query }) => ({ meta, query })); + setRuleParams('searchConfiguration', { + ...ruleParams.searchConfiguration, + filter: filters, + }); + }} /> {errors.filterQuery && ( @@ -454,7 +464,7 @@ export default function Expressions(props: Props) { { - const groupByFilters = Object.values(group.bucketKey).map((key, index) => { - return { - match: { - [groupByFields[index]]: key, - }, - }; - }); + const groupByQueries: QueryDslQueryContainer[] = Object.values(group.bucketKey).map( + (key, index) => { + return { + match: { + [groupByFields[index]]: key, + }, + }; + } + ); + const query = createBoolQuery( + currentTimeFrame, + timeFieldName, + searchConfiguration, + groupByQueries + ); return [ { index: indexPattern }, { size: 0, terminate_after: 1, track_total_hits: true, - query: { - bool: { - filter: [...baseFilters, ...groupByFilters], - }, - }, + query, }, ]; }); diff --git a/x-pack/plugins/observability/server/lib/rules/custom_threshold/lib/evaluate_rule.ts b/x-pack/plugins/observability/server/lib/rules/custom_threshold/lib/evaluate_rule.ts index 59f5801613dd0..87b7d9983465b 100644 --- a/x-pack/plugins/observability/server/lib/rules/custom_threshold/lib/evaluate_rule.ts +++ b/x-pack/plugins/observability/server/lib/rules/custom_threshold/lib/evaluate_rule.ts @@ -69,7 +69,7 @@ export const evaluateRule = async { @@ -27,6 +28,18 @@ describe("The Metric Threshold Alert's getElasticsearchMetricQuery", () => { threshold: [1], comparator: Comparator.GT, }; + const searchConfiguration: SearchConfigurationType = { + index: { + id: 'dataset-logs-*-*', + name: 'All logs', + timeFieldName: '@timestamp', + title: 'logs-*-*', + }, + query: { + language: 'kuery', + query: '', + }, + }; const groupBy = 'host.doggoname'; const timeFieldName = 'mockedTimeFieldName'; @@ -35,13 +48,14 @@ describe("The Metric Threshold Alert's getElasticsearchMetricQuery", () => { end: moment().valueOf(), }; - describe('when passed no filterQuery', () => { + describe('when passed no KQL query', () => { const searchBody = getElasticsearchMetricQuery( expressionParams, timeframe, timeFieldName, 100, true, + searchConfiguration, void 0, groupBy ); @@ -78,11 +92,18 @@ describe("The Metric Threshold Alert's getElasticsearchMetricQuery", () => { }); }); - describe('when passed a filterQuery', () => { + describe('when passed a KQL query', () => { // This is adapted from a real-world query that previously broke alerts // We want to make sure it doesn't override any existing filters // https://github.com/elastic/kibana/issues/68492 - const filterQuery = 'NOT host.name:dv* and NOT host.name:ts*'; + const query = 'NOT host.name:dv* and NOT host.name:ts*'; + const currentSearchConfiguration = { + ...searchConfiguration, + query: { + language: 'kuery', + query, + }, + }; const searchBody = getElasticsearchMetricQuery( expressionParams, @@ -90,9 +111,9 @@ describe("The Metric Threshold Alert's getElasticsearchMetricQuery", () => { timeFieldName, 100, true, + currentSearchConfiguration, void 0, - groupBy, - filterQuery + groupBy ); test('includes a range filter', () => { expect( @@ -164,4 +185,60 @@ describe("The Metric Threshold Alert's getElasticsearchMetricQuery", () => { ); }); }); + + describe('when passed a filter', () => { + const currentSearchConfiguration = { + ...searchConfiguration, + query: { + language: 'kuery', + query: '', + }, + filter: [ + { + meta: { + alias: null, + disabled: false, + field: 'service.name', + key: 'service.name', + negate: false, + params: { + query: 'synth-node-2', + }, + type: 'phrase', + index: 'dataset-logs-*-*', + }, + query: { + match_phrase: { + 'service.name': 'synth-node-2', + }, + }, + }, + ], + }; + + const searchBody = getElasticsearchMetricQuery( + expressionParams, + timeframe, + timeFieldName, + 100, + true, + currentSearchConfiguration, + void 0, + groupBy + ); + test('includes a range filter', () => { + expect( + searchBody.query.bool.filter.find((filter) => filter.hasOwnProperty('range')) + ).toBeTruthy(); + }); + + test('includes a metric field filter', () => { + expect(searchBody.query.bool.filter).toMatchObject( + expect.arrayContaining([ + { range: { mockedTimeFieldName: expect.any(Object) } }, + { match_phrase: { 'service.name': 'synth-node-2' } }, + ]) + ); + }); + }); }); diff --git a/x-pack/plugins/observability/server/lib/rules/custom_threshold/lib/metric_query.ts b/x-pack/plugins/observability/server/lib/rules/custom_threshold/lib/metric_query.ts index 3cc1eee92fec9..14c18e4af1334 100644 --- a/x-pack/plugins/observability/server/lib/rules/custom_threshold/lib/metric_query.ts +++ b/x-pack/plugins/observability/server/lib/rules/custom_threshold/lib/metric_query.ts @@ -6,10 +6,14 @@ */ import moment from 'moment'; +import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types'; +import { Filter } from '@kbn/es-query'; import { Aggregators, CustomMetricExpressionParams, } from '../../../../../common/custom_threshold_rule/types'; +import { getSearchConfigurationBoolQuery } from '../../../../utils/get_parsed_filtered_query'; +import { SearchConfigurationType } from '../types'; import { createCustomMetricsAggregations } from './create_custom_metrics_aggregations'; import { CONTAINER_ID, @@ -20,7 +24,6 @@ import { } from '../utils'; import { createBucketSelector } from './create_bucket_selector'; import { wrapInCurrentPeriod } from './wrap_in_period'; -import { getParsedFilterQuery } from '../../../../utils/get_parsed_filtered_query'; export const calculateCurrentTimeFrame = ( metricParams: CustomMetricExpressionParams, @@ -38,25 +41,30 @@ export const calculateCurrentTimeFrame = ( }; }; -export const createBaseFilters = ( +const QueryDslQueryContainerToFilter = (queries: QueryDslQueryContainer[]): Filter[] => { + return queries.map((query) => ({ + meta: {}, + query, + })); +}; + +export const createBoolQuery = ( timeframe: { start: number; end: number }, timeFieldName: string, - filterQuery?: string + searchConfiguration: SearchConfigurationType, + additionalQueries: QueryDslQueryContainer[] = [] ) => { - const rangeFilters = [ - { - range: { - [timeFieldName]: { - gte: moment(timeframe.start).toISOString(), - lte: moment(timeframe.end).toISOString(), - }, + const rangeQuery: QueryDslQueryContainer = { + range: { + [timeFieldName]: { + gte: moment(timeframe.start).toISOString(), + lte: moment(timeframe.end).toISOString(), }, }, - ]; - - const parsedFilterQuery = getParsedFilterQuery(filterQuery); + }; + const filters = QueryDslQueryContainerToFilter([rangeQuery, ...additionalQueries]); - return [...rangeFilters, ...parsedFilterQuery]; + return getSearchConfigurationBoolQuery(searchConfiguration, filters); }; export const getElasticsearchMetricQuery = ( @@ -65,9 +73,9 @@ export const getElasticsearchMetricQuery = ( timeFieldName: string, compositeSize: number, alertOnGroupDisappear: boolean, + searchConfiguration: SearchConfigurationType, lastPeriodEnd?: number, groupBy?: string | string[], - filterQuery?: string, afterKey?: Record, fieldsExisted?: Record | null ) => { @@ -196,15 +204,11 @@ export const getElasticsearchMetricQuery = ( aggs.groupings.composite.after = afterKey; } - const baseFilters = createBaseFilters(timeframe, timeFieldName, filterQuery); + const query = createBoolQuery(timeframe, timeFieldName, searchConfiguration); return { track_total_hits: true, - query: { - bool: { - filter: baseFilters, - }, - }, + query, size: 0, aggs, }; diff --git a/x-pack/plugins/observability/server/lib/rules/custom_threshold/register_custom_threshold_rule_type.ts b/x-pack/plugins/observability/server/lib/rules/custom_threshold/register_custom_threshold_rule_type.ts index 5e9c2e0cea019..df64a67ca8e4a 100644 --- a/x-pack/plugins/observability/server/lib/rules/custom_threshold/register_custom_threshold_rule_type.ts +++ b/x-pack/plugins/observability/server/lib/rules/custom_threshold/register_custom_threshold_rule_type.ts @@ -58,6 +58,14 @@ export const searchConfigurationSchema = schema.object({ }), query: schema.string(), }), + filter: schema.maybe( + schema.arrayOf( + schema.object({ + query: schema.maybe(schema.recordOf(schema.string(), schema.any())), + meta: schema.recordOf(schema.string(), schema.any()), + }) + ) + ), }); type CreateLifecycleExecutor = ReturnType; diff --git a/x-pack/plugins/observability/server/utils/get_parsed_filtered_query.ts b/x-pack/plugins/observability/server/utils/get_parsed_filtered_query.ts index fabefa63f0695..033a6cadc282e 100644 --- a/x-pack/plugins/observability/server/utils/get_parsed_filtered_query.ts +++ b/x-pack/plugins/observability/server/utils/get_parsed_filtered_query.ts @@ -5,7 +5,14 @@ * 2.0. */ -import { fromKueryExpression, toElasticsearchQuery } from '@kbn/es-query'; +import { + BoolQuery, + buildEsQuery, + Filter, + fromKueryExpression, + toElasticsearchQuery, +} from '@kbn/es-query'; +import { SearchConfigurationType } from '../lib/rules/custom_threshold/types'; export const getParsedFilterQuery: (filter: string | undefined) => Array> = ( filter @@ -19,3 +26,24 @@ export const getParsedFilterQuery: (filter: string | undefined) => Array { bool: BoolQuery } = (searchConfiguration, additionalFilters) => { + try { + const searchConfigurationFilters = (searchConfiguration.filter as Filter[]) || []; + const filters = [...additionalFilters, ...searchConfigurationFilters]; + + return buildEsQuery(undefined, searchConfiguration.query, filters, {}); + } catch (error) { + return { + bool: { + must: [], + must_not: [], + filter: [], + should: [], + }, + }; + } +}; diff --git a/x-pack/plugins/observability_solution/logs_explorer/public/utils/convert_discover_app_state.ts b/x-pack/plugins/observability_solution/logs_explorer/public/utils/convert_discover_app_state.ts index 90d51f75e8c7c..639d4bdb2b0d1 100644 --- a/x-pack/plugins/observability_solution/logs_explorer/public/utils/convert_discover_app_state.ts +++ b/x-pack/plugins/observability_solution/logs_explorer/public/utils/convert_discover_app_state.ts @@ -16,7 +16,7 @@ import { GridColumnDisplayOptions, GridRowsDisplayOptions, } from '../../common'; -import { ControlOptions, OptionsListControlOption } from '../controller'; +import type { ControlOptions, OptionsListControl } from '../controller'; export const getGridColumnDisplayOptionsFromDiscoverAppState = ( discoverAppState: DiscoverAppState @@ -79,55 +79,78 @@ const createDiscoverPhrasesFilter = ({ key, values, negate, + index, }: { - values: PhraseFilterValue[]; + index: string; key: string; + values: PhraseFilterValue[]; negate?: boolean; -}): PhrasesFilter => - ({ - meta: { - key, - negate, - type: FILTERS.PHRASES, - params: values, - }, - query: { - bool: { - should: values.map((value) => ({ match_phrase: { [key]: value.toString() } })), - minimum_should_match: 1, - }, +}): PhrasesFilter => ({ + meta: { + index, + type: FILTERS.PHRASES, + key, + params: values.map((value) => value.toString()), + negate, + }, + query: { + bool: { + should: values.map((value) => ({ match_phrase: { [key]: value.toString() } })), + minimum_should_match: 1, }, - } as PhrasesFilter); + }, +}); const createDiscoverExistsFilter = ({ + index, key, negate, }: { key: string; + index: string; negate?: boolean; }): ExistsFilter => ({ meta: { + index, + type: FILTERS.EXISTS, + value: FILTERS.EXISTS, // Required for the filter to be displayed correctly in FilterBadge key, negate, - type: FILTERS.EXISTS, }, query: { exists: { field: key } }, }); -export const getDiscoverFiltersFromState = (filters: Filter[] = [], controls?: ControlOptions) => [ - ...filters, - ...(controls - ? (Object.keys(controls) as Array).map((key) => - controls[key as keyof ControlOptions]?.selection.type === 'exists' - ? createDiscoverExistsFilter({ - key, - negate: controls[key]?.mode === 'exclude', - }) - : createDiscoverPhrasesFilter({ - key, - values: (controls[key]?.selection as OptionsListControlOption).selectedOptions, - negate: controls[key]?.mode === 'exclude', - }) - ) - : []), -]; +export const getDiscoverFiltersFromState = ( + index: string, + filters: Filter[] = [], + controls?: ControlOptions +) => { + return [ + ...filters, + ...(controls + ? (Object.entries(controls) as Array<[keyof ControlOptions, OptionsListControl]>).reduce< + Filter[] + >((acc, [key, control]) => { + if (control.selection.type === 'exists') { + acc.push( + createDiscoverExistsFilter({ + index, + key, + negate: control.mode === 'exclude', + }) + ); + } else if (control.selection.selectedOptions.length > 0) { + acc.push( + createDiscoverPhrasesFilter({ + index, + key, + values: control.selection.selectedOptions, + negate: control.mode === 'exclude', + }) + ); + } + return acc; + }, []) + : []), + ]; +}; diff --git a/x-pack/plugins/observability_solution/observability_logs_explorer/kibana.jsonc b/x-pack/plugins/observability_solution/observability_logs_explorer/kibana.jsonc index 42d762820aaad..8f6e248557efa 100644 --- a/x-pack/plugins/observability_solution/observability_logs_explorer/kibana.jsonc +++ b/x-pack/plugins/observability_solution/observability_logs_explorer/kibana.jsonc @@ -23,7 +23,12 @@ "datasetQuality" ], "optionalPlugins": [ - "serverless" + "serverless", + "triggersActionsUi", + "unifiedSearch", + "dataViews", + "dataViewEditor", + "lens" ], "requiredBundles": [ "kibanaReact" diff --git a/x-pack/plugins/observability_solution/observability_logs_explorer/public/applications/observability_logs_explorer.tsx b/x-pack/plugins/observability_solution/observability_logs_explorer/public/applications/observability_logs_explorer.tsx index 5f6739a5dfe3d..cab3742c06f05 100644 --- a/x-pack/plugins/observability_solution/observability_logs_explorer/public/applications/observability_logs_explorer.tsx +++ b/x-pack/plugins/observability_solution/observability_logs_explorer/public/applications/observability_logs_explorer.tsx @@ -6,6 +6,7 @@ */ import { CoreStart } from '@kbn/core/public'; +import { EuiThemeProvider } from '@kbn/kibana-react-plugin/common'; import { KibanaRenderContextProvider } from '@kbn/react-kibana-context-render'; import { Route, Router, Routes } from '@kbn/shared-ux-router'; import React from 'react'; @@ -57,6 +58,7 @@ export const ObservabilityLogsExplorerApp = ({ plugins, pluginStart, }: ObservabilityLogsExplorerAppProps) => { + const isDarkMode = core.theme.getTheme().darkMode; const KibanaContextProviderForPlugin = useKibanaContextForPluginProvider( core, plugins, @@ -69,10 +71,20 @@ export const ObservabilityLogsExplorerApp = ({ - - } /> - } /> - + + + } + /> + } + /> + + diff --git a/x-pack/plugins/observability_solution/observability_logs_explorer/public/components/alerts_popover.tsx b/x-pack/plugins/observability_solution/observability_logs_explorer/public/components/alerts_popover.tsx new file mode 100644 index 0000000000000..22f689010a2df --- /dev/null +++ b/x-pack/plugins/observability_solution/observability_logs_explorer/public/components/alerts_popover.tsx @@ -0,0 +1,142 @@ +/* + * 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 { EuiPopover, EuiButtonEmpty, EuiContextMenuPanel, EuiContextMenuItem } from '@elastic/eui'; +import React, { useMemo, useReducer } from 'react'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { OBSERVABILITY_THRESHOLD_RULE_TYPE_ID } from '@kbn/rule-data-utils'; +import { useActor } from '@xstate/react'; +import { hydrateDatasetSelection } from '@kbn/logs-explorer-plugin/common'; +import { getDiscoverFiltersFromState } from '@kbn/logs-explorer-plugin/public'; +import type { AlertParams } from '@kbn/observability-plugin/public/components/custom_threshold/types'; +import { useLinkProps } from '@kbn/observability-shared-plugin/public'; +import { useKibanaContextForPlugin } from '../utils/use_kibana'; +import { useObservabilityLogsExplorerPageStateContext } from '../state_machines/observability_logs_explorer/src'; + +type ThresholdRuleTypeParams = Pick; + +interface AlertsPopoverState { + isPopoverOpen: boolean; + isAddRuleFlyoutOpen: boolean; +} + +type AlertsPopoverAction = + | { + type: 'togglePopover'; + isOpen?: boolean; + } + | { + type: 'toggleAddRuleFlyout'; + isOpen?: boolean; + }; + +function alertsPopoverReducer(state: AlertsPopoverState, action: AlertsPopoverAction) { + switch (action.type) { + case 'togglePopover': + return { + isPopoverOpen: action.isOpen ?? !state.isPopoverOpen, + isAddRuleFlyoutOpen: state.isAddRuleFlyoutOpen, + }; + + case 'toggleAddRuleFlyout': + return { + isPopoverOpen: false, + isAddRuleFlyoutOpen: action.isOpen ?? !state.isAddRuleFlyoutOpen, + }; + + default: + return state; + } +} + +export const AlertsPopover = () => { + const { + services: { triggersActionsUi }, + } = useKibanaContextForPlugin(); + + const manageRulesLinkProps = useLinkProps({ app: 'observability', pathname: '/alerts/rules' }); + + const [pageState] = useActor(useObservabilityLogsExplorerPageStateContext()); + + const [state, dispatch] = useReducer(alertsPopoverReducer, { + isPopoverOpen: false, + isAddRuleFlyoutOpen: false, + }); + + const togglePopover = () => dispatch({ type: 'togglePopover' }); + const closePopover = () => dispatch({ type: 'togglePopover', isOpen: false }); + const openAddRuleFlyout = () => dispatch({ type: 'toggleAddRuleFlyout', isOpen: true }); + const closeAddRuleFlyout = () => dispatch({ type: 'toggleAddRuleFlyout', isOpen: false }); + + const addRuleFlyout = useMemo(() => { + if ( + state.isAddRuleFlyoutOpen && + triggersActionsUi && + pageState.matches({ initialized: 'validLogsExplorerState' }) + ) { + const { logsExplorerState } = pageState.context; + const index = hydrateDatasetSelection(logsExplorerState.datasetSelection).toDataviewSpec(); + + return triggersActionsUi.getAddRuleFlyout({ + consumer: 'logs', + ruleTypeId: OBSERVABILITY_THRESHOLD_RULE_TYPE_ID, + canChangeTrigger: false, + initialValues: { + params: { + searchConfiguration: { + index, + query: logsExplorerState.query, + filter: getDiscoverFiltersFromState( + index.id, + logsExplorerState.filters, + logsExplorerState.controls + ), + }, + }, + }, + onClose: closeAddRuleFlyout, + }); + } + }, [triggersActionsUi, pageState, state.isAddRuleFlyoutOpen]); + + return ( + <> + {state.isAddRuleFlyoutOpen && addRuleFlyout} + + + + } + isOpen={state.isPopoverOpen} + closePopover={closePopover} + panelPaddingSize="none" + anchorPosition="downLeft" + > + + + , + + + , + ]} + /> + + + ); +}; diff --git a/x-pack/plugins/observability_solution/observability_logs_explorer/public/components/discover_link.tsx b/x-pack/plugins/observability_solution/observability_logs_explorer/public/components/discover_link.tsx index b12390cc952a1..7c0b4596b4326 100644 --- a/x-pack/plugins/observability_solution/observability_logs_explorer/public/components/discover_link.tsx +++ b/x-pack/plugins/observability_solution/observability_logs_explorer/public/components/discover_link.tsx @@ -53,18 +53,22 @@ export const DiscoverLinkForValidState = React.memo( discover: DiscoverStart; pageState: InitializedPageState; }) => { - const discoverLinkParams = useMemo( - () => ({ + const discoverLinkParams = useMemo(() => { + const index = hydrateDatasetSelection(logsExplorerState.datasetSelection).toDataviewSpec(); + return { breakdownField: logsExplorerState.chart.breakdownField ?? undefined, columns: getDiscoverColumnsFromDisplayOptions(logsExplorerState), - filters: getDiscoverFiltersFromState(logsExplorerState.filters, logsExplorerState.controls), + filters: getDiscoverFiltersFromState( + index.id, + logsExplorerState.filters, + logsExplorerState.controls + ), query: logsExplorerState.query, refreshInterval: logsExplorerState.refreshInterval, timeRange: logsExplorerState.time, - dataViewSpec: hydrateDatasetSelection(logsExplorerState.datasetSelection).toDataviewSpec(), - }), - [logsExplorerState] - ); + dataViewSpec: index, + }; + }, [logsExplorerState]); return ; } diff --git a/x-pack/plugins/observability_solution/observability_logs_explorer/public/components/logs_explorer_top_nav_menu.tsx b/x-pack/plugins/observability_solution/observability_logs_explorer/public/components/logs_explorer_top_nav_menu.tsx index 9c2ea0a5e4817..0f64f586ab3fd 100644 --- a/x-pack/plugins/observability_solution/observability_logs_explorer/public/components/logs_explorer_top_nav_menu.tsx +++ b/x-pack/plugins/observability_solution/observability_logs_explorer/public/components/logs_explorer_top_nav_menu.tsx @@ -26,6 +26,7 @@ import { useKibanaContextForPlugin } from '../utils/use_kibana'; import { ConnectedDiscoverLink } from './discover_link'; import { FeedbackLink } from './feedback_link'; import { ConnectedOnboardingLink } from './onboarding_link'; +import { AlertsPopover } from './alerts_popover'; export const LogsExplorerTopNavMenu = () => { const { @@ -67,6 +68,8 @@ const ServerlessTopNav = () => { + + {ObservabilityAIAssistantActionMenuItem ? ( ) : null} @@ -143,6 +146,8 @@ const StatefulTopNav = () => { + + {ObservabilityAIAssistantActionMenuItem ? ( ) : null} diff --git a/x-pack/plugins/observability_solution/observability_logs_explorer/public/types.ts b/x-pack/plugins/observability_solution/observability_logs_explorer/public/types.ts index c3f094033f697..96754cfdab021 100644 --- a/x-pack/plugins/observability_solution/observability_logs_explorer/public/types.ts +++ b/x-pack/plugins/observability_solution/observability_logs_explorer/public/types.ts @@ -15,6 +15,11 @@ import { AppMountParameters, ScopedHistory } from '@kbn/core/public'; import { LogsSharedClientStartExports } from '@kbn/logs-shared-plugin/public'; import { DatasetQualityPluginStart } from '@kbn/dataset-quality-plugin/public'; import { ObservabilityAIAssistantPluginStart } from '@kbn/observability-ai-assistant-plugin/public'; +import { TriggersAndActionsUIPublicPluginStart } from '@kbn/triggers-actions-ui-plugin/public'; +import { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public'; +import { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public'; +import { DataViewEditorStart } from '@kbn/data-view-editor-plugin/public'; +import { LensPublicStart } from '@kbn/lens-plugin/public'; import { ObservabilityLogsExplorerLocators, ObservabilityLogsExplorerLocationState, @@ -41,6 +46,11 @@ export interface ObservabilityLogsExplorerStartDeps { observabilityAIAssistant: ObservabilityAIAssistantPluginStart; observabilityShared: ObservabilitySharedPluginStart; serverless?: ServerlessPluginStart; + triggersActionsUi?: TriggersAndActionsUIPublicPluginStart; + unifiedSearch?: UnifiedSearchPublicPluginStart; + dataViews?: DataViewsPublicPluginStart; + dataViewEditor?: DataViewEditorStart; + lens?: LensPublicStart; share: SharePluginStart; datasetQuality: DatasetQualityPluginStart; } diff --git a/x-pack/plugins/observability_solution/observability_logs_explorer/tsconfig.json b/x-pack/plugins/observability_solution/observability_logs_explorer/tsconfig.json index 7192e3001a70b..c434a418c4246 100644 --- a/x-pack/plugins/observability_solution/observability_logs_explorer/tsconfig.json +++ b/x-pack/plugins/observability_solution/observability_logs_explorer/tsconfig.json @@ -38,6 +38,13 @@ "@kbn/xstate-utils", "@kbn/router-utils", "@kbn/observability-ai-assistant-plugin", + "@kbn/rule-data-utils", + "@kbn/observability-plugin", + "@kbn/triggers-actions-ui-plugin", + "@kbn/unified-search-plugin", + "@kbn/data-views-plugin", + "@kbn/data-view-editor-plugin", + "@kbn/lens-plugin", ], "exclude": [ "target/**/*" diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/index.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/index.tsx index dfab594febf10..1a99e346ed808 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/index.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/index.tsx @@ -7,6 +7,13 @@ import { lazy } from 'react'; import { suspendedComponentWithProps } from '../../lib/suspended_component_with_props'; +import type { RuleAddComponent } from './rule_add'; +import type { RuleEditComponent } from './rule_edit'; -export const RuleAdd = suspendedComponentWithProps(lazy(() => import('./rule_add'))); -export const RuleEdit = suspendedComponentWithProps(lazy(() => import('./rule_edit'))); +export const RuleAdd = suspendedComponentWithProps( + lazy(() => import('./rule_add')) +) as RuleAddComponent; // `React.lazy` is not typed correctly to support generics so casting back to imported component + +export const RuleEdit = suspendedComponentWithProps( + lazy(() => import('./rule_edit')) +) as RuleEditComponent; // `React.lazy` is not typed correctly to support generics so casting back to imported component diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_add.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_add.tsx index 07264709dd544..19eb8da4bf0d3 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_add.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_add.tsx @@ -15,6 +15,7 @@ import { parseRuleCircuitBreakerErrorMessage } from '@kbn/alerting-plugin/common import { Rule, RuleTypeParams, + RuleTypeMetaData, RuleUpdates, RuleFlyoutCloseReason, IErrorObject, @@ -49,7 +50,12 @@ const defaultCreateRuleErrorMessage = i18n.translate( } ); -const RuleAdd = ({ +export type RuleAddComponent = typeof RuleAdd; + +const RuleAdd = < + Params extends RuleTypeParams = RuleTypeParams, + MetaData extends RuleTypeMetaData = RuleTypeMetaData +>({ consumer, ruleTypeRegistry, actionTypeRegistry, @@ -67,7 +73,7 @@ const RuleAdd = ({ useRuleProducer, initialSelectedConsumer, ...props -}: RuleAddProps) => { +}: RuleAddProps) => { const onSaveHandler = onSave ?? reloadRules; const [metadata, setMetadata] = useState(initialMetadata); const onChangeMetaData = useCallback((newMetadata) => setMetadata(newMetadata), []); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_edit.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_edit.tsx index 0aebaaaa29882..975881e516e45 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_edit.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_edit.tsx @@ -34,6 +34,8 @@ import { RuleEditProps, IErrorObject, RuleType, + RuleTypeParams, + RuleTypeMetaData, TriggersActionsUiConfig, RuleNotifyWhenType, } from '../../../types'; @@ -81,7 +83,12 @@ const cloneAndMigrateRule = (initialRule: Rule) => { return clonedRule; }; -export const RuleEdit = ({ +export type RuleEditComponent = typeof RuleEdit; + +export const RuleEdit = < + Params extends RuleTypeParams = RuleTypeParams, + MetaData extends RuleTypeMetaData = RuleTypeMetaData +>({ initialRule, onClose, reloadRules, @@ -91,7 +98,7 @@ export const RuleEdit = ({ actionTypeRegistry, metadata: initialMetadata, ...props -}: RuleEditProps) => { +}: RuleEditProps) => { const onSaveHandler = onSave ?? reloadRules; const [{ rule }, dispatch] = useReducer(ruleReducer as ConcreteRuleReducer, { rule: cloneAndMigrateRule(initialRule), diff --git a/x-pack/plugins/triggers_actions_ui/public/common/get_add_rule_flyout.tsx b/x-pack/plugins/triggers_actions_ui/public/common/get_add_rule_flyout.tsx index 23f751201d1be..c6a79b4c6e82d 100644 --- a/x-pack/plugins/triggers_actions_ui/public/common/get_add_rule_flyout.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/common/get_add_rule_flyout.tsx @@ -9,11 +9,14 @@ import React from 'react'; import { QueryClientProvider } from '@tanstack/react-query'; import { ConnectorProvider } from '../application/context/connector_context'; import { RuleAdd } from '../application/sections/rule_form'; -import type { ConnectorServices, RuleAddProps } from '../types'; +import type { ConnectorServices, RuleAddProps, RuleTypeParams, RuleTypeMetaData } from '../types'; import { queryClient } from '../application/query_client'; -export const getAddRuleFlyoutLazy = ( - props: RuleAddProps & { connectorServices: ConnectorServices } +export const getAddRuleFlyoutLazy = < + Params extends RuleTypeParams = RuleTypeParams, + MetaData extends RuleTypeMetaData = RuleTypeMetaData +>( + props: RuleAddProps & { connectorServices: ConnectorServices } ) => { return ( diff --git a/x-pack/plugins/triggers_actions_ui/public/common/get_edit_rule_flyout.tsx b/x-pack/plugins/triggers_actions_ui/public/common/get_edit_rule_flyout.tsx index 2d99e3911a168..f3fbccce267c5 100644 --- a/x-pack/plugins/triggers_actions_ui/public/common/get_edit_rule_flyout.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/common/get_edit_rule_flyout.tsx @@ -9,11 +9,14 @@ import React from 'react'; import { QueryClientProvider } from '@tanstack/react-query'; import { ConnectorProvider } from '../application/context/connector_context'; import { RuleEdit } from '../application/sections/rule_form'; -import type { ConnectorServices, RuleEditProps as AlertEditProps } from '../types'; +import type { ConnectorServices, RuleEditProps, RuleTypeParams, RuleTypeMetaData } from '../types'; import { queryClient } from '../application/query_client'; -export const getEditRuleFlyoutLazy = ( - props: AlertEditProps & { connectorServices: ConnectorServices } +export const getEditRuleFlyoutLazy = < + Params extends RuleTypeParams = RuleTypeParams, + MetaData extends RuleTypeMetaData = RuleTypeMetaData +>( + props: RuleEditProps & { connectorServices: ConnectorServices } ) => { return ( diff --git a/x-pack/plugins/triggers_actions_ui/public/mocks.ts b/x-pack/plugins/triggers_actions_ui/public/mocks.ts index 223a54205cb48..48691c15ed62f 100644 --- a/x-pack/plugins/triggers_actions_ui/public/mocks.ts +++ b/x-pack/plugins/triggers_actions_ui/public/mocks.ts @@ -15,8 +15,6 @@ import { getEditRuleFlyoutLazy } from './common/get_edit_rule_flyout'; import { TypeRegistry } from './application/type_registry'; import { ActionTypeModel, - RuleAddProps, - RuleEditProps, RuleTypeModel, AlertsTableProps, FieldBrowserProps, @@ -73,7 +71,7 @@ function createStartMock(): TriggersAndActionsUIPublicPluginStart { connectorServices, }); }, - getAddRuleFlyout: (props: Omit) => { + getAddRuleFlyout: (props) => { return getAddRuleFlyoutLazy({ ...props, actionTypeRegistry, @@ -81,7 +79,7 @@ function createStartMock(): TriggersAndActionsUIPublicPluginStart { connectorServices, }); }, - getEditRuleFlyout: (props: Omit) => { + getEditRuleFlyout: (props) => { return getEditRuleFlyoutLazy({ ...props, actionTypeRegistry, diff --git a/x-pack/plugins/triggers_actions_ui/public/plugin.ts b/x-pack/plugins/triggers_actions_ui/public/plugin.ts index 872528a9a5f85..bcd639e21a2ff 100644 --- a/x-pack/plugins/triggers_actions_ui/public/plugin.ts +++ b/x-pack/plugins/triggers_actions_ui/public/plugin.ts @@ -62,6 +62,8 @@ import type { RuleAddProps, RuleEditProps, RuleTypeModel, + RuleTypeParams, + RuleTypeMetaData, AlertsTableProps, RuleStatusDropdownProps, RuleTagFilterProps, @@ -115,12 +117,18 @@ export interface TriggersAndActionsUIPublicPluginStart { getEditConnectorFlyout: ( props: Omit ) => ReactElement; - getAddRuleFlyout: ( - props: Omit - ) => ReactElement; - getEditRuleFlyout: ( - props: Omit - ) => ReactElement; + getAddRuleFlyout: < + Params extends RuleTypeParams = RuleTypeParams, + MetaData extends RuleTypeMetaData = RuleTypeMetaData + >( + props: Omit, 'actionTypeRegistry' | 'ruleTypeRegistry'> + ) => ReactElement>; + getEditRuleFlyout: < + Params extends RuleTypeParams = RuleTypeParams, + MetaData extends RuleTypeMetaData = RuleTypeMetaData + >( + props: Omit, 'actionTypeRegistry' | 'ruleTypeRegistry'> + ) => ReactElement>; getAlertsTable: (props: AlertsTableProps) => ReactElement; getAlertsTableDefaultAlertActions:

( props: P @@ -403,7 +411,7 @@ export class Plugin connectorServices: this.connectorServices!, }); }, - getAddRuleFlyout: (props: Omit) => { + getAddRuleFlyout: (props) => { return getAddRuleFlyoutLazy({ ...props, actionTypeRegistry: this.actionTypeRegistry, @@ -411,9 +419,7 @@ export class Plugin connectorServices: this.connectorServices!, }); }, - getEditRuleFlyout: ( - props: Omit - ) => { + getEditRuleFlyout: (props) => { return getEditRuleFlyoutLazy({ ...props, actionTypeRegistry: this.actionTypeRegistry, diff --git a/x-pack/plugins/triggers_actions_ui/public/types.ts b/x-pack/plugins/triggers_actions_ui/public/types.ts index b47d80a0839e5..36cc294bbda5f 100644 --- a/x-pack/plugins/triggers_actions_ui/public/types.ts +++ b/x-pack/plugins/triggers_actions_ui/public/types.ts @@ -51,6 +51,7 @@ import { AlertingFrameworkHealth, RuleNotifyWhenType, RuleTypeParams, + RuleTypeMetaData, ActionVariable, RuleLastRun, MaintenanceWindow, @@ -127,6 +128,7 @@ export type { AlertingFrameworkHealth, RuleNotifyWhenType, RuleTypeParams, + RuleTypeMetaData, ResolvedRule, SanitizedRule, RuleStatusDropdownProps, @@ -412,8 +414,11 @@ export enum EditConnectorTabs { Test = 'test', } -export interface RuleEditProps> { - initialRule: Rule; +export interface RuleEditProps< + Params extends RuleTypeParams = RuleTypeParams, + MetaData extends RuleTypeMetaData = RuleTypeMetaData +> { + initialRule: Rule; ruleTypeRegistry: RuleTypeRegistryContract; actionTypeRegistry: ActionTypeRegistryContract; onClose: (reason: RuleFlyoutCloseReason, metadata?: MetaData) => void; @@ -425,14 +430,27 @@ export interface RuleEditProps> { ruleType?: RuleType; } -export interface RuleAddProps> { +export interface RuleAddProps< + Params extends RuleTypeParams = RuleTypeParams, + MetaData extends RuleTypeMetaData = RuleTypeMetaData +> { + /** + * ID of the feature this rule should be created for. + * + * Notes: + * - The feature needs to be registered using `featuresPluginSetup.registerKibanaFeature()` API during your plugin's setup phase. + * - The user needs to have permission to access the feature in order to create the rule. + * */ consumer: string; ruleTypeRegistry: RuleTypeRegistryContract; actionTypeRegistry: ActionTypeRegistryContract; onClose: (reason: RuleFlyoutCloseReason, metadata?: MetaData) => void; ruleTypeId?: string; + /** + * Determines whether the user should be able to change the rule type in the UI. + */ canChangeTrigger?: boolean; - initialValues?: Partial; + initialValues?: Partial>; /** @deprecated use `onSave` as a callback after an alert is saved*/ reloadRules?: () => Promise; hideGrouping?: boolean; @@ -445,8 +463,8 @@ export interface RuleAddProps> { useRuleProducer?: boolean; initialSelectedConsumer?: RuleCreationValidConsumer | null; } -export interface RuleDefinitionProps { - rule: Rule; +export interface RuleDefinitionProps { + rule: Rule; ruleTypeRegistry: RuleTypeRegistryContract; actionTypeRegistry: ActionTypeRegistryContract; onEditRule: () => Promise;