Skip to content

Commit 4c5e21d

Browse files
committed
[ES query rule] CPS POC
1 parent a3d8da5 commit 4c5e21d

12 files changed

Lines changed: 181 additions & 11 deletions

File tree

src/platform/packages/shared/kbn-cps-utils/components/project_picker.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,13 +34,15 @@ export interface ProjectPickerProps {
3434
fetchProjects: () => Promise<ProjectsData | null>;
3535
isReadonly?: boolean;
3636
settingsComponent?: React.ReactNode;
37+
shouldDisplayReadonlyCallout?: boolean;
3738
}
3839

3940
export const ProjectPicker = ({
4041
projectRouting,
4142
onProjectRoutingChange,
4243
fetchProjects,
4344
isReadonly = false,
45+
shouldDisplayReadonlyCallout = true,
4446
settingsComponent,
4547
}: ProjectPickerProps) => {
4648
const [showPopover, setShowPopover] = useState(false);
@@ -121,6 +123,7 @@ export const ProjectPicker = ({
121123
onProjectRoutingChange={onProjectRoutingChange}
122124
fetchProjects={fetchProjects}
123125
isReadonly={isReadonly}
126+
shouldDisplayReadonlyCallout={shouldDisplayReadonlyCallout}
124127
settingsComponent={settingsComponent}
125128
/>
126129
</EuiPopover>

src/platform/packages/shared/kbn-cps-utils/components/project_picker_content.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ export interface ProjectPickerContentProps {
3535
fetchProjects: () => Promise<ProjectsData | null>;
3636
isReadonly?: boolean;
3737
settingsComponent?: React.ReactNode;
38+
shouldDisplayReadonlyCallout?: boolean;
3839
}
3940

4041
const projectPickerOptions = [
@@ -55,6 +56,7 @@ export const ProjectPickerContent = ({
5556
onProjectRoutingChange,
5657
fetchProjects,
5758
isReadonly = false,
59+
shouldDisplayReadonlyCallout = true,
5860
settingsComponent,
5961
}: ProjectPickerContentProps) => {
6062
const styles = useMemoCss(projectPickerContentStyles);
@@ -83,7 +85,7 @@ export const ProjectPickerContent = ({
8385
{settingsComponent && <EuiFlexItem grow={false}>{settingsComponent}</EuiFlexItem>}
8486
</EuiFlexGroup>
8587
</EuiPopoverTitle>
86-
{isReadonly && (
88+
{isReadonly && shouldDisplayReadonlyCallout && (
8789
<EuiCallOut
8890
size="s"
8991
css={styles.callout}

src/platform/plugins/shared/cps/public/services/access_control.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,9 @@ export const ACCESS_CONTROL_CONFIG: AccessControlConfig = {
7171
lens: {
7272
defaultAccess: ProjectRoutingAccess.EDITABLE,
7373
},
74+
management: {
75+
defaultAccess: ProjectRoutingAccess.READONLY,
76+
},
7477
maps: {
7578
defaultAccess: ProjectRoutingAccess.EDITABLE,
7679
},

src/platform/plugins/shared/cps/public/services/cps_manager.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,8 +60,8 @@ export class CPSManager implements ICPSManager {
6060
)
6161
.subscribe((access) => {
6262
this.projectPickerAccess$.next(access);
63-
// Reset project routing to default when access is disabled
64-
if (access === ProjectRoutingAccess.DISABLED) {
63+
// Reset project routing to default when access is disabled or readonly
64+
if (access === ProjectRoutingAccess.DISABLED || access === ProjectRoutingAccess.READONLY) {
6565
this.projectRouting$.next(DEFAULT_PROJECT_ROUTING);
6666
}
6767
});

x-pack/platform/packages/shared/response-ops/rule_params/es_query/v1.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,14 @@ const EsQueryRuleParamsSchemaProperties = {
140140
},
141141
}
142142
),
143+
project_routing: schema.maybe(
144+
schema.string({
145+
meta: {
146+
description:
147+
'The project routing value for cross-project search. Determines which projects to include in the search.',
148+
},
149+
})
150+
),
143151
// esQuery rule params only
144152
esQuery: schema.conditional(
145153
schema.siblingRef('searchType'),
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
8+
import React, { useCallback, useEffect, useState } from 'react';
9+
import { EuiFlexGroup, EuiFlexItem, EuiSwitch, type EuiSwitchEvent } from '@elastic/eui';
10+
import { i18n } from '@kbn/i18n';
11+
import type { ProjectRouting } from '@kbn/es-query';
12+
import { ProjectPicker } from '@kbn/cps-utils';
13+
import type { CPSPluginStart } from '@kbn/cps/public';
14+
import type { DataViewSelectPopoverProps } from './data_view_select_popover';
15+
import { DataViewSelectPopover } from './data_view_select_popover';
16+
17+
export interface DataSourceSelectorProps extends DataViewSelectPopoverProps {
18+
cps?: CPSPluginStart;
19+
projectRouting?: ProjectRouting;
20+
onProjectRoutingChange: (projectRouting: ProjectRouting | undefined) => void;
21+
hasPersistedProjectRouting: boolean;
22+
}
23+
24+
export const DataSourceSelector: React.FC<DataSourceSelectorProps> = ({
25+
cps,
26+
projectRouting,
27+
onProjectRoutingChange,
28+
hasPersistedProjectRouting,
29+
dependencies,
30+
dataView,
31+
metadata,
32+
onSelectDataView,
33+
onChangeMetaData,
34+
}) => {
35+
const cpsManager = cps?.cpsManager;
36+
// Override is enabled if there's a persisted project_routing value
37+
const [isOverrideEnabled, setIsOverrideEnabled] = useState(hasPersistedProjectRouting);
38+
// Local display value for when override is disabled (follows global scope)
39+
const [displayProjectRouting, setDisplayProjectRouting] = useState<ProjectRouting | undefined>(
40+
projectRouting
41+
);
42+
43+
// Subscribe to global project routing changes when override is disabled
44+
useEffect(() => {
45+
if (!cpsManager || isOverrideEnabled) {
46+
return;
47+
}
48+
49+
const subscription = cpsManager.getProjectRouting$().subscribe((globalProjectRouting) => {
50+
// Only update local display, don't persist
51+
setDisplayProjectRouting(globalProjectRouting);
52+
});
53+
54+
return () => subscription.unsubscribe();
55+
}, [cpsManager, isOverrideEnabled]);
56+
57+
// Sync display value with prop when override is enabled
58+
useEffect(() => {
59+
if (isOverrideEnabled) {
60+
setDisplayProjectRouting(projectRouting);
61+
}
62+
}, [isOverrideEnabled, projectRouting]);
63+
64+
const handleOverrideChange = useCallback(
65+
(e: EuiSwitchEvent) => {
66+
const newOverrideValue = e.target.checked;
67+
setIsOverrideEnabled(newOverrideValue);
68+
69+
if (newOverrideValue) {
70+
// When enabling override, set the current display value as the persisted value
71+
const valueToSave = displayProjectRouting ?? cpsManager?.getProjectRouting();
72+
if (valueToSave) {
73+
onProjectRoutingChange(valueToSave);
74+
}
75+
} else {
76+
// When disabling override, clear the persisted value (set to undefined)
77+
onProjectRoutingChange(undefined);
78+
}
79+
},
80+
[cpsManager, displayProjectRouting, onProjectRoutingChange]
81+
);
82+
83+
const settingsComponent = (
84+
<EuiSwitch
85+
label={i18n.translate('xpack.stackAlerts.components.dataSourceSelector.overrideGlobalScope', {
86+
defaultMessage: 'Override global scope',
87+
})}
88+
checked={isOverrideEnabled}
89+
onChange={handleOverrideChange}
90+
compressed
91+
data-test-subj="override-global-scope-switch"
92+
/>
93+
);
94+
95+
return (
96+
<EuiFlexGroup gutterSize="s" alignItems="center" responsive={false}>
97+
{cpsManager && (
98+
<EuiFlexItem grow={false}>
99+
<ProjectPicker
100+
projectRouting={displayProjectRouting}
101+
onProjectRoutingChange={onProjectRoutingChange}
102+
fetchProjects={() => cpsManager.fetchProjects()}
103+
isReadonly={!isOverrideEnabled}
104+
shouldDisplayReadonlyCallout={false}
105+
settingsComponent={settingsComponent}
106+
/>
107+
</EuiFlexItem>
108+
)}
109+
<EuiFlexItem>
110+
<DataViewSelectPopover
111+
dependencies={dependencies}
112+
dataView={dataView}
113+
metadata={metadata}
114+
onSelectDataView={onSelectDataView}
115+
onChangeMetaData={onChangeMetaData}
116+
/>
117+
</EuiFlexItem>
118+
</EuiFlexGroup>
119+
);
120+
};

x-pack/platform/plugins/shared/stack_alerts/public/rule_types/es_query/expression/search_source_expression.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ export const SearchSourceExpression = ({
4343
termField,
4444
termSize,
4545
excludeHitsFromPreviousRun,
46+
project_routing,
4647
} = ruleParams;
4748
const { data, isServerless } = useTriggerUiActionServices();
4849

@@ -94,6 +95,7 @@ export const SearchSourceExpression = ({
9495
excludeHitsFromPreviousRun ?? DEFAULT_VALUES.EXCLUDE_PREVIOUS_HITS,
9596
// The sourceFields param is ignored
9697
sourceFields: [],
98+
project_routing,
9799
});
98100
setSearchSource(createdSearchSource);
99101
} catch (error) {

x-pack/platform/plugins/shared/stack_alerts/public/rule_types/es_query/expression/search_source_expression_form.tsx

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
import React, { Fragment, useCallback, useEffect, useMemo, useReducer, useState } from 'react';
99
import deepEqual from 'fast-deep-equal';
1010
import { lastValueFrom } from 'rxjs';
11-
import type { Filter, Query } from '@kbn/es-query';
11+
import type { Filter, ProjectRouting, Query } from '@kbn/es-query';
1212
import { FormattedMessage } from '@kbn/i18n-react';
1313
import { EuiFormRow, EuiSpacer, EuiTitle } from '@elastic/eui';
1414
import type { IErrorObject } from '@kbn/triggers-actions-ui-plugin/public';
@@ -35,7 +35,7 @@ import type {
3535
SourceField,
3636
} from '../types';
3737
import { DEFAULT_VALUES, SERVERLESS_DEFAULT_VALUES } from '../constants';
38-
import { DataViewSelectPopover } from '../../components/data_view_select_popover';
38+
import { DataSourceSelector } from '../../components/data_source_selector';
3939
import { RuleCommonExpressions } from '../rule_common_expressions';
4040
import { useTriggerUiActionServices, convertFieldSpecToFieldOption } from '../util';
4141
import { hasExpressionValidationErrors } from '../validation';
@@ -82,12 +82,26 @@ const isSearchSourceParam = (action: LocalStateAction): action is SearchSourcePa
8282
export const SearchSourceExpressionForm = (props: SearchSourceExpressionFormProps) => {
8383
const services = useTriggerUiActionServices();
8484
const unifiedSearch = services.unifiedSearch;
85+
const cps = services.cps;
8586
const { dataViews, dataViewEditor, isServerless } = useTriggerUiActionServices();
8687
const { searchSource, errors, initialSavedQuery, setParam, ruleParams } = props;
8788
const [savedQuery, setSavedQuery] = useState<SavedQuery>();
89+
const [projectRouting, setProjectRouting] = useState<ProjectRouting | undefined>(
90+
ruleParams.project_routing
91+
);
92+
// Track if we had a persisted value when the component mounted (for editing existing rules)
93+
const [hasPersistedProjectRouting] = useState(ruleParams.project_routing !== undefined);
8894

8995
useEffect(() => setSavedQuery(initialSavedQuery), [initialSavedQuery]);
9096

97+
const onProjectRoutingChange = useCallback(
98+
(newProjectRouting: ProjectRouting | undefined) => {
99+
setProjectRouting(newProjectRouting);
100+
setParam('project_routing', newProjectRouting);
101+
},
102+
[setParam]
103+
);
104+
91105
const [ruleConfiguration, dispatch] = useReducer<LocalStateReducer>(
92106
(currentState, action) => {
93107
if (isSearchSourceParam(action)) {
@@ -291,13 +305,19 @@ export const SearchSourceExpressionForm = (props: SearchSourceExpressionFormProp
291305
const isGroupAgg = isGroupAggregation(ruleParams.termField);
292306
const isCountAgg = isCountAggregation(ruleParams.aggType);
293307
const testSearchSource = createTestSearchSource();
294-
const { rawResponse } = await lastValueFrom(testSearchSource.fetch$());
308+
const { rawResponse } = await lastValueFrom(testSearchSource.fetch$({ projectRouting }));
295309
return {
296310
testResults: parseAggregationResults({ isCountAgg, isGroupAgg, esResult: rawResponse }),
297311
isGrouped: isGroupAgg,
298312
timeWindow,
299313
};
300-
}, [timeWindow, createTestSearchSource, ruleParams.aggType, ruleParams.termField]);
314+
}, [
315+
timeWindow,
316+
createTestSearchSource,
317+
ruleParams.aggType,
318+
ruleParams.termField,
319+
projectRouting,
320+
]);
301321

302322
return (
303323
<Fragment>
@@ -306,11 +326,15 @@ export const SearchSourceExpressionForm = (props: SearchSourceExpressionFormProp
306326
label={
307327
<FormattedMessage
308328
id="xpack.stackAlerts.esQuery.ui.selectDataViewPrompt"
309-
defaultMessage="Select a data view"
329+
defaultMessage="Select a data source"
310330
/>
311331
}
312332
>
313-
<DataViewSelectPopover
333+
<DataSourceSelector
334+
cps={cps}
335+
projectRouting={projectRouting}
336+
onProjectRoutingChange={onProjectRoutingChange}
337+
hasPersistedProjectRouting={hasPersistedProjectRouting}
314338
dependencies={{ dataViews, dataViewEditor }}
315339
dataView={dataView}
316340
metadata={props.metadata}

x-pack/platform/plugins/shared/stack_alerts/public/rule_types/es_query/types.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import type { RuleTypeParams } from '@kbn/alerting-plugin/common';
99
import type { SerializedSearchSourceFields } from '@kbn/data-plugin/common';
1010
import type { EuiComboBoxOptionOption } from '@elastic/eui';
1111
import type { DataView } from '@kbn/data-views-plugin/public';
12-
import type { AggregateQuery } from '@kbn/es-query';
12+
import type { AggregateQuery, ProjectRouting } from '@kbn/es-query';
1313

1414
export enum SearchType {
1515
esQuery = 'esQuery',
@@ -62,6 +62,7 @@ export interface OnlySearchSourceRuleParams {
6262
searchType?: 'searchSource';
6363
searchConfiguration?: SerializedSearchSourceFields;
6464
savedQueryId?: string;
65+
project_routing?: ProjectRouting;
6566
}
6667

6768
export interface OnlyEsqlQueryRuleParams {

x-pack/platform/plugins/shared/triggers_actions_ui/kibana.jsonc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
"uiActions",
3131
"contentManagement"
3232
],
33-
"optionalPlugins": ["cloud", "features", "home", "spaces", "serverless", "share"],
33+
"optionalPlugins": ["cloud", "cps", "features", "home", "spaces", "serverless", "share"],
3434
"requiredBundles": ["alerting", "esUiShared", "kibanaReact", "kibanaUtils", "actions"],
3535
"extraPublicDirs": ["public/common", "public/common/constants"]
3636
}

0 commit comments

Comments
 (0)