diff --git a/src/platform/plugins/private/vis_types/vega/public/data_model/search_api.ts b/src/platform/plugins/private/vis_types/vega/public/data_model/search_api.ts index 8b6110519f390..df5878b30dd9f 100644 --- a/src/platform/plugins/private/vis_types/vega/public/data_model/search_api.ts +++ b/src/platform/plugins/private/vis_types/vega/public/data_model/search_api.ts @@ -57,7 +57,7 @@ export class SearchAPI { public readonly inspectorAdapters?: VegaInspectorAdapters, private readonly searchSessionId?: string, private readonly executionContext?: KibanaExecutionContext, - private readonly projectRouting?: ProjectRouting + public readonly projectRouting?: ProjectRouting ) {} search(searchRequests: SearchRequest[]) { @@ -160,6 +160,7 @@ export class SearchAPI { abortSignal: this.abortSignal, sessionId: this.searchSessionId, executionContext: this.executionContext, + projectRouting: this.projectRouting, } ) .pipe( diff --git a/src/platform/plugins/private/vis_types/vega/public/lib/extract_project_routing_overrides.test.ts b/src/platform/plugins/private/vis_types/vega/public/lib/extract_project_routing_overrides.test.ts new file mode 100644 index 0000000000000..a3e19d0a68e4f --- /dev/null +++ b/src/platform/plugins/private/vis_types/vega/public/lib/extract_project_routing_overrides.test.ts @@ -0,0 +1,75 @@ +/* + * 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", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { extractProjectRoutingOverrides } from './extract_project_routing_overrides'; + +describe('extractProjectRoutingOverrides', () => { + it('should extract project routing from ES|QL query with SET instruction', () => { + const spec = { + data: { + name: 'metric', + url: { + '%type%': 'esql', + query: 'SET project_routing = "_alias:_origin"; FROM logs-* | STATS count=COUNT()', + }, + }, + }; + + const result = extractProjectRoutingOverrides(spec); + + expect(result).toEqual([{ name: 'metric', value: '_alias:_origin' }]); + }); + it('should extract project routing from ES|QL query with multiple sources', () => { + const spec = { + data: [ + { + name: 'metric', + url: { + '%type%': 'esql', + query: + 'SET project_routing = "_alias:_origin"; FROM kibana_sample_data_logs | STATS total=COUNT()', + }, + }, + { + name: 'metric2', + url: { + '%type%': 'esql', + query: + 'SET project_routing = "_alias:*"; FROM kibana_sample_data_logs | STATS total=COUNT()', + }, + }, + ], + }; + + const result = extractProjectRoutingOverrides(spec); + + expect(result).toEqual([ + { name: 'metric', value: '_alias:_origin' }, + { name: 'metric2', value: '_alias:*' }, + ]); + }); + + it('should return undefined when no ES|QL queries with project routing', () => { + const spec = { + data: [ + { + name: 'regular', + url: { + '%type%': 'elasticsearch', + index: 'logs-*', + }, + }, + ], + }; + + const result = extractProjectRoutingOverrides(spec); + + expect(result).toBeUndefined(); + }); +}); diff --git a/src/platform/plugins/private/vis_types/vega/public/lib/extract_project_routing_overrides.ts b/src/platform/plugins/private/vis_types/vega/public/lib/extract_project_routing_overrides.ts new file mode 100644 index 0000000000000..cc7f7c2936f8c --- /dev/null +++ b/src/platform/plugins/private/vis_types/vega/public/lib/extract_project_routing_overrides.ts @@ -0,0 +1,61 @@ +/* + * 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", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { getProjectRoutingFromEsqlQuery } from '@kbn/esql-utils'; +import type { ProjectRoutingOverrides } from '@kbn/presentation-publishing'; + +interface DataUrl { + '%type%'?: string; + query?: string; +} + +interface DataObject { + name?: string; + url?: string | DataUrl; +} + +interface VegaSpec { + data?: DataObject | DataObject[]; + [key: string]: unknown; +} + +/** + * Extracts project routing overrides from a Vega specification. + * Searches for ES|QL queries in the data.url objects and extracts project routing information. + */ +export function extractProjectRoutingOverrides(spec: VegaSpec): ProjectRoutingOverrides { + const overrides: Array<{ name?: string; value: string }> = []; + + const processDataObject = (dataObj: DataObject) => { + if (dataObj.url && typeof dataObj.url === 'object') { + const dataUrl = dataObj.url as DataUrl; + // Check if this is an ES|QL data source + if (dataUrl['%type%'] === 'esql' && typeof dataUrl.query === 'string') { + const projectRoutingValue = getProjectRoutingFromEsqlQuery(dataUrl.query); + if (projectRoutingValue) { + overrides.push({ + name: dataObj.name, + value: projectRoutingValue, + }); + } + } + } + }; + + // Process data field + if (spec.data) { + if (Array.isArray(spec.data)) { + spec.data.forEach(processDataObject); + } else if (typeof spec.data === 'object') { + processDataObject(spec.data); + } + } + + return overrides.length > 0 ? overrides : undefined; +} diff --git a/src/platform/plugins/private/vis_types/vega/public/vega_type.ts b/src/platform/plugins/private/vis_types/vega/public/vega_type.ts index 1fde88b37e107..429d1444f0e66 100644 --- a/src/platform/plugins/private/vis_types/vega/public/vega_type.ts +++ b/src/platform/plugins/private/vis_types/vega/public/vega_type.ts @@ -16,6 +16,7 @@ import { VIS_EVENT_TO_TRIGGER, VisGroups } from '@kbn/visualizations-plugin/publ import { getDefaultSpec } from './default_spec'; import { extractIndexPatternsFromSpec } from './lib/extract_index_pattern'; +import { extractProjectRoutingOverrides } from './lib/extract_project_routing_overrides'; import { createInspectorAdapters } from './vega_inspector'; import { toExpressionAst } from './to_ast'; import { getInfoMessage } from './components/vega_info_message'; @@ -60,6 +61,16 @@ export const vegaVisType: VisTypeDefinition = { } return []; }, + getProjectRoutingOverrides: async (visParams) => { + try { + const spec = parse(visParams.spec, { legacyRoot: false, keepWsc: true }); + + return extractProjectRoutingOverrides(spec); + } catch (e) { + // spec is invalid + } + return undefined; + }, inspectorAdapters: createInspectorAdapters, /** * This is necessary for showing actions bar in top of vega editor diff --git a/src/platform/plugins/shared/visualizations/public/embeddable/types.ts b/src/platform/plugins/shared/visualizations/public/embeddable/types.ts index 8df0de4edac92..7992b73627a58 100644 --- a/src/platform/plugins/shared/visualizations/public/embeddable/types.ts +++ b/src/platform/plugins/shared/visualizations/public/embeddable/types.ts @@ -18,6 +18,7 @@ import type { HasSupportedTriggers, PublishesDataLoading, PublishesDataViews, + PublishesProjectRoutingOverrides, PublishesRendered, PublishesTimeRange, PublishesTitle, @@ -59,6 +60,7 @@ export type VisualizeApi = Partial & PublishesDataViews & PublishesDataLoading & PublishesRendered & + PublishesProjectRoutingOverrides & Required & HasVisualizeConfig & HasInspectorAdapters & diff --git a/src/platform/plugins/shared/visualizations/public/embeddable/visualize_embeddable.tsx b/src/platform/plugins/shared/visualizations/public/embeddable/visualize_embeddable.tsx index 82507c47ae71f..d1fe58168b985 100644 --- a/src/platform/plugins/shared/visualizations/public/embeddable/visualize_embeddable.tsx +++ b/src/platform/plugins/shared/visualizations/public/embeddable/visualize_embeddable.tsx @@ -34,6 +34,7 @@ import { titleComparators, useBatchedPublishingSubjects, useStateFromPublishingSubject, + type ProjectRoutingOverrides, } from '@kbn/presentation-publishing'; import { apiPublishesSearchSession } from '@kbn/presentation-publishing/interfaces/fetch/publishes_search_session'; import { get, isEqual } from 'lodash'; @@ -88,6 +89,16 @@ export const getVisualizeEmbeddableFactory: (deps: { const initialVisInstance = await createVisInstance(runtimeState.serializedVis); const vis$ = new BehaviorSubject(initialVisInstance); + let initialProjectRoutingOverrides: ProjectRoutingOverrides; + if (initialVisInstance.type.getProjectRoutingOverrides) { + initialProjectRoutingOverrides = await initialVisInstance.type.getProjectRoutingOverrides( + initialVisInstance.params + ); + } + const projectRoutingOverrides$ = new BehaviorSubject( + initialProjectRoutingOverrides + ); + // Track UI state const onUiStateChange = () => serializedVis$.next(vis$.getValue().serialize()); @@ -100,6 +111,15 @@ export const getVisualizeEmbeddableFactory: (deps: { const vis = await createVisInstance(serializedVis); vis.uiState.on('change', onUiStateChange); vis$.next(vis); + + // Update project routing overrides when vis changes + if (vis.type.getProjectRoutingOverrides) { + const newOverrides = await vis.type.getProjectRoutingOverrides(vis.params); + if (!isEqual(projectRoutingOverrides$.getValue(), newOverrides)) { + projectRoutingOverrides$.next(newOverrides); + } + } + const { params, abortController } = await getExpressionParams(); return { params, abortController }; }) @@ -235,6 +255,7 @@ export const getVisualizeEmbeddableFactory: (deps: { defaultTitle$, dataLoading$, dataViews$: new BehaviorSubject(initialDataViews), + projectRoutingOverrides$, rendered$: hasRendered$, supportedTriggers: () => [ACTION_CONVERT_TO_LENS, APPLY_FILTER_TRIGGER, SELECT_RANGE_TRIGGER], serializeState: () => { diff --git a/src/platform/plugins/shared/visualizations/public/vis_types/base_vis_type.ts b/src/platform/plugins/shared/visualizations/public/vis_types/base_vis_type.ts index e7519729eaa03..f01c361d09b48 100644 --- a/src/platform/plugins/shared/visualizations/public/vis_types/base_vis_type.ts +++ b/src/platform/plugins/shared/visualizations/public/vis_types/base_vis_type.ts @@ -47,6 +47,7 @@ export class BaseVisType { public readonly hierarchicalData; public readonly setup; public readonly getUsedIndexPattern; + public readonly getProjectRoutingOverrides; public readonly inspectorAdapters; public readonly fetchDatatable: boolean; public readonly toExpressionAst; @@ -83,6 +84,7 @@ export class BaseVisType { this.hasPartialRows = opts.hasPartialRows ?? false; this.hierarchicalData = opts.hierarchicalData ?? false; this.getUsedIndexPattern = opts.getUsedIndexPattern; + this.getProjectRoutingOverrides = opts.getProjectRoutingOverrides; this.inspectorAdapters = opts.inspectorAdapters; this.fetchDatatable = opts.fetchDatatable ?? false; this.toExpressionAst = opts.toExpressionAst; diff --git a/src/platform/plugins/shared/visualizations/public/vis_types/types.ts b/src/platform/plugins/shared/visualizations/public/vis_types/types.ts index 6b592833d8d86..a72859d88cdac 100644 --- a/src/platform/plugins/shared/visualizations/public/vis_types/types.ts +++ b/src/platform/plugins/shared/visualizations/public/vis_types/types.ts @@ -127,6 +127,14 @@ export interface VisTypeDefinition { */ readonly getUsedIndexPattern?: (visParams: VisParams) => DataView[] | Promise; + /** + * Vega may provide project routing overrides. + * This method should return an array of project routing values extracted from the vega spec. + */ + readonly getProjectRoutingOverrides?: ( + visParams: VisParams + ) => Promise | undefined>; + readonly isAccessible?: boolean; /** * It is the visualization icon, displayed on the wizard. diff --git a/src/platform/plugins/shared/visualizations/public/visualize_app/components/visualize_byvalue_editor.tsx b/src/platform/plugins/shared/visualizations/public/visualize_app/components/visualize_byvalue_editor.tsx index 7cb0ed0848073..551abcceb9ab6 100644 --- a/src/platform/plugins/shared/visualizations/public/visualize_app/components/visualize_byvalue_editor.tsx +++ b/src/platform/plugins/shared/visualizations/public/visualize_app/components/visualize_byvalue_editor.tsx @@ -24,6 +24,7 @@ import { import type { VisualizeServices } from '../types'; import { VisualizeEditorCommon } from './visualize_editor_common'; import type { VisualizeAppProps } from '../app'; +import { useProjectRouting } from '../utils/use/use_project_routing'; export const VisualizeByValueEditor = ({ onAppLeave }: VisualizeAppProps) => { const [originatingApp, setOriginatingApp] = useState(); @@ -76,13 +77,16 @@ export const VisualizeByValueEditor = ({ onAppLeave }: VisualizeAppProps) => { eventEmitter, byValueVisInstance ); + // Initialize CPS project routing manager for Vega + const projectRoutingManager = useProjectRouting(services); const { isEmbeddableRendered, currentAppState } = useEditorUpdates( services, eventEmitter, setHasUnsavedChanges, appState, byValueVisInstance, - visEditorController + visEditorController, + projectRoutingManager ); useLinkedSearchUpdates(services, eventEmitter, appState, byValueVisInstance); useDataViewUpdates(services, eventEmitter, appState, byValueVisInstance);