mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
[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
<img width="1266" alt="Screenshot 2024-01-29 at 12 48 28"
src="0a141fa9
-726c-4a15-a085-f124d39071f2">
## 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 <maryam.saeidi@elastic.co>
This commit is contained in:
parent
5ab0240b11
commit
239b957191
34 changed files with 650 additions and 153 deletions
|
@ -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,
|
||||
|
|
|
@ -67,7 +67,7 @@ function FilterBadge({
|
|||
`}
|
||||
>
|
||||
<EuiTextBlockTruncate lines={10}>
|
||||
{!hideAlias && filter.meta.alias !== null ? (
|
||||
{filter.meta.alias && !hideAlias ? (
|
||||
<>
|
||||
<span className={marginLeftLabelCss(euiTheme)}>
|
||||
{prefix}
|
||||
|
|
|
@ -19,6 +19,7 @@ export type { ActionVariable } from '@kbn/alerting-types';
|
|||
|
||||
export type RuleTypeState = Record<string, unknown>;
|
||||
export type RuleTypeParams = Record<string, unknown>;
|
||||
export type RuleTypeMetaData = Record<string, unknown>;
|
||||
|
||||
// rule type defined alert fields to persist in alerts index
|
||||
export type RuleAlertData = Record<string, unknown>;
|
||||
|
|
|
@ -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],
|
||||
|
|
|
@ -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 */
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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<CustomThresholdRuleTypeParams>;
|
||||
|
@ -118,7 +116,6 @@ export default function AlertDetailsAppSection({
|
|||
const { euiTheme } = useEuiTheme();
|
||||
const hasLogRateAnalysisLicense = hasAtLeast('platinum');
|
||||
const [dataView, setDataView] = useState<DataView>();
|
||||
const [filterQuery, setFilterQuery] = useState<string>('');
|
||||
const [, setDataViewError] = useState<Error>();
|
||||
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({
|
|||
<RuleConditionChart
|
||||
metricExpression={criterion}
|
||||
dataView={dataView}
|
||||
filterQuery={filterQuery}
|
||||
searchConfiguration={ruleParams.searchConfiguration}
|
||||
groupBy={ruleParams.groupBy}
|
||||
annotations={annotations}
|
||||
timeRange={timeRange}
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { SerializedSearchSourceFields } from '@kbn/data-plugin/common';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { DataView } from '@kbn/data-views-plugin/common';
|
||||
import { mountWithIntl, nextTick } from '@kbn/test-jest-helpers';
|
||||
|
@ -35,7 +36,7 @@ describe('Rule condition chart', () => {
|
|||
<RuleConditionChart
|
||||
metricExpression={expression}
|
||||
dataView={dataView}
|
||||
filterQuery={''}
|
||||
searchConfiguration={{} as SerializedSearchSourceFields}
|
||||
groupBy={[]}
|
||||
error={{}}
|
||||
timeRange={{ from: 'now-15m', to: 'now' }}
|
||||
|
|
|
@ -4,8 +4,11 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { SerializedSearchSourceFields } from '@kbn/data-plugin/common';
|
||||
import { EuiEmptyPrompt, useEuiTheme } from '@elastic/eui';
|
||||
import { Query } from '@kbn/es-query';
|
||||
import { FillStyle, SeriesType } from '@kbn/lens-plugin/public';
|
||||
import { DataView } from '@kbn/data-views-plugin/common';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
|
@ -38,8 +41,8 @@ import {
|
|||
|
||||
interface RuleConditionChartProps {
|
||||
metricExpression: MetricExpression;
|
||||
searchConfiguration: SerializedSearchSourceFields;
|
||||
dataView?: DataView;
|
||||
filterQuery?: string;
|
||||
groupBy?: string | string[];
|
||||
error?: IErrorObject;
|
||||
timeRange: TimeRange;
|
||||
|
@ -47,10 +50,15 @@ interface RuleConditionChartProps {
|
|||
seriesType?: SeriesType;
|
||||
}
|
||||
|
||||
const defaultQuery: Query = {
|
||||
language: 'kuery',
|
||||
query: '',
|
||||
};
|
||||
|
||||
export function RuleConditionChart({
|
||||
metricExpression,
|
||||
searchConfiguration,
|
||||
dataView,
|
||||
filterQuery,
|
||||
groupBy,
|
||||
error,
|
||||
annotations,
|
||||
|
@ -283,7 +291,7 @@ export function RuleConditionChart({
|
|||
comparator,
|
||||
dataView,
|
||||
equation,
|
||||
filterQuery,
|
||||
searchConfiguration,
|
||||
formula,
|
||||
formulaAsync.value,
|
||||
groupBy,
|
||||
|
@ -337,10 +345,8 @@ export function RuleConditionChart({
|
|||
timeRange={timeRange}
|
||||
attributes={attributes}
|
||||
disableTriggers={true}
|
||||
query={{
|
||||
language: 'kuery',
|
||||
query: filterQuery || '',
|
||||
}}
|
||||
query={(searchConfiguration.query as Query) || defaultQuery}
|
||||
filters={searchConfiguration.filter}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -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 && (
|
||||
<EuiFormErrorText data-test-subj="thresholdRuleDataViewErrorNoTimestamp">
|
||||
|
@ -454,7 +464,7 @@ export default function Expressions(props: Props) {
|
|||
<PreviewChart
|
||||
metricExpression={e}
|
||||
dataView={dataView}
|
||||
filterQuery={(ruleParams.searchConfiguration?.query as Query)?.query as string}
|
||||
searchConfiguration={ruleParams.searchConfiguration}
|
||||
groupBy={ruleParams.groupBy}
|
||||
error={(errors[idx] as IErrorObject) || emptyError}
|
||||
timeRange={{ from: `now-${(timeSize ?? 1) * 20}${timeUnit}`, to: 'now' }}
|
||||
|
|
|
@ -6,11 +6,13 @@
|
|||
*/
|
||||
|
||||
import { ElasticsearchClient } from '@kbn/core/server';
|
||||
import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types';
|
||||
import type { Logger } from '@kbn/logging';
|
||||
import { isString, get, identity } from 'lodash';
|
||||
import { SearchConfigurationType } from '../types';
|
||||
import { CustomMetricExpressionParams } from '../../../../../common/custom_threshold_rule/types';
|
||||
import type { BucketKey } from './get_data';
|
||||
import { calculateCurrentTimeFrame, createBaseFilters } from './metric_query';
|
||||
import { calculateCurrentTimeFrame, createBoolQuery } from './metric_query';
|
||||
|
||||
export interface MissingGroupsRecord {
|
||||
key: string;
|
||||
|
@ -23,7 +25,7 @@ export const checkMissingGroups = async (
|
|||
indexPattern: string,
|
||||
timeFieldName: string,
|
||||
groupBy: string | undefined | string[],
|
||||
filterQuery: string | undefined,
|
||||
searchConfiguration: SearchConfigurationType,
|
||||
logger: Logger,
|
||||
timeframe: { start: number; end: number },
|
||||
missingGroups: MissingGroupsRecord[] = []
|
||||
|
@ -32,28 +34,31 @@ export const checkMissingGroups = async (
|
|||
return missingGroups;
|
||||
}
|
||||
const currentTimeFrame = calculateCurrentTimeFrame(metricParams, timeframe);
|
||||
const baseFilters = createBaseFilters(currentTimeFrame, timeFieldName, filterQuery);
|
||||
const groupByFields = isString(groupBy) ? [groupBy] : groupBy ? groupBy : [];
|
||||
|
||||
const searches = missingGroups.flatMap((group) => {
|
||||
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,
|
||||
},
|
||||
];
|
||||
});
|
||||
|
|
|
@ -69,7 +69,7 @@ export const evaluateRule = async <Params extends EvaluatedRuleParams = Evaluate
|
|||
dataView,
|
||||
timeFieldName,
|
||||
groupBy,
|
||||
searchConfiguration.query.query,
|
||||
searchConfiguration,
|
||||
compositeSize,
|
||||
alertOnGroupDisappear,
|
||||
calculatedTimerange,
|
||||
|
@ -83,7 +83,7 @@ export const evaluateRule = async <Params extends EvaluatedRuleParams = Evaluate
|
|||
dataView,
|
||||
timeFieldName,
|
||||
groupBy,
|
||||
searchConfiguration.query.query,
|
||||
searchConfiguration,
|
||||
logger,
|
||||
calculatedTimerange,
|
||||
missingGroups
|
||||
|
|
|
@ -9,6 +9,7 @@ import { SearchResponse, AggregationsAggregate } from '@elastic/elasticsearch/li
|
|||
import { ElasticsearchClient } from '@kbn/core/server';
|
||||
import type { Logger } from '@kbn/logging';
|
||||
import { EcsFieldsResponse } from '@kbn/rule-registry-plugin/common/search_strategy';
|
||||
import { SearchConfigurationType } from '../types';
|
||||
import { CustomMetricExpressionParams } from '../../../../../common/custom_threshold_rule/types';
|
||||
|
||||
import { UNGROUPED_FACTORY_KEY } from '../constants';
|
||||
|
@ -100,7 +101,7 @@ export const getData = async (
|
|||
index: string,
|
||||
timeFieldName: string,
|
||||
groupBy: string | undefined | string[],
|
||||
filterQuery: string | undefined,
|
||||
searchConfiguration: SearchConfigurationType,
|
||||
compositeSize: number,
|
||||
alertOnGroupDisappear: boolean,
|
||||
timeframe: { start: number; end: number },
|
||||
|
@ -159,7 +160,7 @@ export const getData = async (
|
|||
index,
|
||||
timeFieldName,
|
||||
groupBy,
|
||||
filterQuery,
|
||||
searchConfiguration,
|
||||
compositeSize,
|
||||
alertOnGroupDisappear,
|
||||
timeframe,
|
||||
|
@ -202,9 +203,9 @@ export const getData = async (
|
|||
timeFieldName,
|
||||
compositeSize,
|
||||
alertOnGroupDisappear,
|
||||
searchConfiguration,
|
||||
lastPeriodEnd,
|
||||
groupBy,
|
||||
filterQuery,
|
||||
afterKey,
|
||||
fieldsExisted
|
||||
),
|
||||
|
|
|
@ -11,6 +11,7 @@ import {
|
|||
Aggregators,
|
||||
CustomMetricExpressionParams,
|
||||
} from '../../../../../common/custom_threshold_rule/types';
|
||||
import { SearchConfigurationType } from '../types';
|
||||
import { getElasticsearchMetricQuery } from './metric_query';
|
||||
|
||||
describe("The Metric Threshold Alert's getElasticsearchMetricQuery", () => {
|
||||
|
@ -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' } },
|
||||
])
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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 filters = QueryDslQueryContainerToFilter([rangeQuery, ...additionalQueries]);
|
||||
|
||||
const parsedFilterQuery = getParsedFilterQuery(filterQuery);
|
||||
|
||||
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<string, string>,
|
||||
fieldsExisted?: Record<string, boolean> | 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,
|
||||
};
|
||||
|
|
|
@ -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<typeof createLifecycleExecutor>;
|
||||
|
|
|
@ -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<Record<string, any>> = (
|
||||
filter
|
||||
|
@ -19,3 +26,24 @@ export const getParsedFilterQuery: (filter: string | undefined) => Array<Record<
|
|||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
export const getSearchConfigurationBoolQuery: (
|
||||
searchConfiguration: SearchConfigurationType,
|
||||
additionalFilters: Filter[]
|
||||
) => { 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: [],
|
||||
},
|
||||
};
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
}): 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,
|
||||
},
|
||||
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<keyof ControlOptions>).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;
|
||||
}, [])
|
||||
: []),
|
||||
];
|
||||
};
|
||||
|
|
|
@ -23,7 +23,12 @@
|
|||
"datasetQuality"
|
||||
],
|
||||
"optionalPlugins": [
|
||||
"serverless"
|
||||
"serverless",
|
||||
"triggersActionsUi",
|
||||
"unifiedSearch",
|
||||
"dataViews",
|
||||
"dataViewEditor",
|
||||
"lens"
|
||||
],
|
||||
"requiredBundles": [
|
||||
"kibanaReact"
|
||||
|
|
|
@ -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 = ({
|
|||
<KibanaContextProviderForPlugin>
|
||||
<KbnUrlStateStorageFromRouterProvider>
|
||||
<Router history={appParams.history}>
|
||||
<Routes>
|
||||
<Route path="/" exact={true} render={() => <ObservabilityLogsExplorerMainRoute />} />
|
||||
<Route path="/dataset-quality" exact={true} render={() => <DatasetQualityRoute />} />
|
||||
</Routes>
|
||||
<EuiThemeProvider darkMode={isDarkMode}>
|
||||
<Routes>
|
||||
<Route
|
||||
path="/"
|
||||
exact={true}
|
||||
render={() => <ObservabilityLogsExplorerMainRoute />}
|
||||
/>
|
||||
<Route
|
||||
path="/dataset-quality"
|
||||
exact={true}
|
||||
render={() => <DatasetQualityRoute />}
|
||||
/>
|
||||
</Routes>
|
||||
</EuiThemeProvider>
|
||||
</Router>
|
||||
</KbnUrlStateStorageFromRouterProvider>
|
||||
</KibanaContextProviderForPlugin>
|
||||
|
|
|
@ -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<AlertParams, 'searchConfiguration'>;
|
||||
|
||||
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<ThresholdRuleTypeParams>({
|
||||
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}
|
||||
<EuiPopover
|
||||
button={
|
||||
<EuiButtonEmpty onClick={togglePopover} iconType="arrowDown" iconSide="right">
|
||||
<FormattedMessage
|
||||
id="xpack.observabilityLogsExplorer.alertsPopover.buttonLabel"
|
||||
defaultMessage="Alerts"
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
}
|
||||
isOpen={state.isPopoverOpen}
|
||||
closePopover={closePopover}
|
||||
panelPaddingSize="none"
|
||||
anchorPosition="downLeft"
|
||||
>
|
||||
<EuiContextMenuPanel
|
||||
items={[
|
||||
<EuiContextMenuItem key="createRule" icon="bell" onClick={openAddRuleFlyout}>
|
||||
<FormattedMessage
|
||||
id="xpack.observabilityLogsExplorer.alertsPopover.createRuleMenuItem"
|
||||
defaultMessage="Create rule"
|
||||
/>
|
||||
</EuiContextMenuItem>,
|
||||
<EuiContextMenuItem key="manageRules" icon="tableOfContents" {...manageRulesLinkProps}>
|
||||
<FormattedMessage
|
||||
id="xpack.observabilityLogsExplorer.alertsPopover.manageRulesMenuItem"
|
||||
defaultMessage="Manage rules"
|
||||
/>
|
||||
</EuiContextMenuItem>,
|
||||
]}
|
||||
/>
|
||||
</EuiPopover>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -53,18 +53,22 @@ export const DiscoverLinkForValidState = React.memo(
|
|||
discover: DiscoverStart;
|
||||
pageState: InitializedPageState;
|
||||
}) => {
|
||||
const discoverLinkParams = useMemo<DiscoverAppLocatorParams>(
|
||||
() => ({
|
||||
const discoverLinkParams = useMemo<DiscoverAppLocatorParams>(() => {
|
||||
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 <DiscoverLink discover={discover} discoverLinkParams={discoverLinkParams} />;
|
||||
}
|
||||
|
|
|
@ -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 = () => {
|
|||
<VerticalRule />
|
||||
<FeedbackLink />
|
||||
<VerticalRule />
|
||||
<AlertsPopover />
|
||||
<VerticalRule />
|
||||
{ObservabilityAIAssistantActionMenuItem ? (
|
||||
<ObservabilityAIAssistantActionMenuItem />
|
||||
) : null}
|
||||
|
@ -143,6 +146,8 @@ const StatefulTopNav = () => {
|
|||
<EuiHeaderLinks gutterSize="xs">
|
||||
<ConnectedDiscoverLink />
|
||||
<VerticalRule />
|
||||
<AlertsPopover />
|
||||
<VerticalRule />
|
||||
{ObservabilityAIAssistantActionMenuItem ? (
|
||||
<ObservabilityAIAssistantActionMenuItem />
|
||||
) : null}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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/**/*"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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<Params, MetaData>) => {
|
||||
const onSaveHandler = onSave ?? reloadRules;
|
||||
const [metadata, setMetadata] = useState(initialMetadata);
|
||||
const onChangeMetaData = useCallback((newMetadata) => setMetadata(newMetadata), []);
|
||||
|
|
|
@ -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<Params, MetaData>) => {
|
||||
const onSaveHandler = onSave ?? reloadRules;
|
||||
const [{ rule }, dispatch] = useReducer(ruleReducer as ConcreteRuleReducer, {
|
||||
rule: cloneAndMigrateRule(initialRule),
|
||||
|
|
|
@ -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<Params, MetaData> & { connectorServices: ConnectorServices }
|
||||
) => {
|
||||
return (
|
||||
<ConnectorProvider value={{ services: props.connectorServices }}>
|
||||
|
|
|
@ -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<Params, MetaData> & { connectorServices: ConnectorServices }
|
||||
) => {
|
||||
return (
|
||||
<ConnectorProvider value={{ services: props.connectorServices }}>
|
||||
|
|
|
@ -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<RuleAddProps, 'actionTypeRegistry' | 'ruleTypeRegistry'>) => {
|
||||
getAddRuleFlyout: (props) => {
|
||||
return getAddRuleFlyoutLazy({
|
||||
...props,
|
||||
actionTypeRegistry,
|
||||
|
@ -81,7 +79,7 @@ function createStartMock(): TriggersAndActionsUIPublicPluginStart {
|
|||
connectorServices,
|
||||
});
|
||||
},
|
||||
getEditRuleFlyout: (props: Omit<RuleEditProps, 'actionTypeRegistry' | 'ruleTypeRegistry'>) => {
|
||||
getEditRuleFlyout: (props) => {
|
||||
return getEditRuleFlyoutLazy({
|
||||
...props,
|
||||
actionTypeRegistry,
|
||||
|
|
|
@ -62,6 +62,8 @@ import type {
|
|||
RuleAddProps,
|
||||
RuleEditProps,
|
||||
RuleTypeModel,
|
||||
RuleTypeParams,
|
||||
RuleTypeMetaData,
|
||||
AlertsTableProps,
|
||||
RuleStatusDropdownProps,
|
||||
RuleTagFilterProps,
|
||||
|
@ -115,12 +117,18 @@ export interface TriggersAndActionsUIPublicPluginStart {
|
|||
getEditConnectorFlyout: (
|
||||
props: Omit<EditConnectorFlyoutProps, 'actionTypeRegistry'>
|
||||
) => ReactElement<EditConnectorFlyoutProps>;
|
||||
getAddRuleFlyout: (
|
||||
props: Omit<RuleAddProps, 'actionTypeRegistry' | 'ruleTypeRegistry'>
|
||||
) => ReactElement<RuleAddProps>;
|
||||
getEditRuleFlyout: (
|
||||
props: Omit<RuleEditProps, 'actionTypeRegistry' | 'ruleTypeRegistry'>
|
||||
) => ReactElement<RuleEditProps>;
|
||||
getAddRuleFlyout: <
|
||||
Params extends RuleTypeParams = RuleTypeParams,
|
||||
MetaData extends RuleTypeMetaData = RuleTypeMetaData
|
||||
>(
|
||||
props: Omit<RuleAddProps<Params, MetaData>, 'actionTypeRegistry' | 'ruleTypeRegistry'>
|
||||
) => ReactElement<RuleAddProps<Params, MetaData>>;
|
||||
getEditRuleFlyout: <
|
||||
Params extends RuleTypeParams = RuleTypeParams,
|
||||
MetaData extends RuleTypeMetaData = RuleTypeMetaData
|
||||
>(
|
||||
props: Omit<RuleEditProps<Params, MetaData>, 'actionTypeRegistry' | 'ruleTypeRegistry'>
|
||||
) => ReactElement<RuleEditProps<Params, MetaData>>;
|
||||
getAlertsTable: (props: AlertsTableProps) => ReactElement<AlertsTableProps>;
|
||||
getAlertsTableDefaultAlertActions: <P extends AlertActionsProps>(
|
||||
props: P
|
||||
|
@ -403,7 +411,7 @@ export class Plugin
|
|||
connectorServices: this.connectorServices!,
|
||||
});
|
||||
},
|
||||
getAddRuleFlyout: (props: Omit<RuleAddProps, 'actionTypeRegistry' | 'ruleTypeRegistry'>) => {
|
||||
getAddRuleFlyout: (props) => {
|
||||
return getAddRuleFlyoutLazy({
|
||||
...props,
|
||||
actionTypeRegistry: this.actionTypeRegistry,
|
||||
|
@ -411,9 +419,7 @@ export class Plugin
|
|||
connectorServices: this.connectorServices!,
|
||||
});
|
||||
},
|
||||
getEditRuleFlyout: (
|
||||
props: Omit<RuleEditProps, 'actionTypeRegistry' | 'ruleTypeRegistry'>
|
||||
) => {
|
||||
getEditRuleFlyout: (props) => {
|
||||
return getEditRuleFlyoutLazy({
|
||||
...props,
|
||||
actionTypeRegistry: this.actionTypeRegistry,
|
||||
|
|
|
@ -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<MetaData = Record<string, any>> {
|
||||
initialRule: Rule;
|
||||
export interface RuleEditProps<
|
||||
Params extends RuleTypeParams = RuleTypeParams,
|
||||
MetaData extends RuleTypeMetaData = RuleTypeMetaData
|
||||
> {
|
||||
initialRule: Rule<Params>;
|
||||
ruleTypeRegistry: RuleTypeRegistryContract;
|
||||
actionTypeRegistry: ActionTypeRegistryContract;
|
||||
onClose: (reason: RuleFlyoutCloseReason, metadata?: MetaData) => void;
|
||||
|
@ -425,14 +430,27 @@ export interface RuleEditProps<MetaData = Record<string, any>> {
|
|||
ruleType?: RuleType<string, string>;
|
||||
}
|
||||
|
||||
export interface RuleAddProps<MetaData = Record<string, any>> {
|
||||
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<Rule>;
|
||||
initialValues?: Partial<Rule<Params>>;
|
||||
/** @deprecated use `onSave` as a callback after an alert is saved*/
|
||||
reloadRules?: () => Promise<void>;
|
||||
hideGrouping?: boolean;
|
||||
|
@ -445,8 +463,8 @@ export interface RuleAddProps<MetaData = Record<string, any>> {
|
|||
useRuleProducer?: boolean;
|
||||
initialSelectedConsumer?: RuleCreationValidConsumer | null;
|
||||
}
|
||||
export interface RuleDefinitionProps {
|
||||
rule: Rule;
|
||||
export interface RuleDefinitionProps<Params extends RuleTypeParams = RuleTypeParams> {
|
||||
rule: Rule<Params>;
|
||||
ruleTypeRegistry: RuleTypeRegistryContract;
|
||||
actionTypeRegistry: ActionTypeRegistryContract;
|
||||
onEditRule: () => Promise<void>;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue