diff --git a/package.json b/package.json index 024d984cd58959..391b8d31127ed1 100644 --- a/package.json +++ b/package.json @@ -742,7 +742,7 @@ "@turf/distance": "6.0.1", "@turf/helpers": "6.0.1", "@turf/length": "^6.0.2", - "@xstate/react": "^3.2.1", + "@xstate/react": "^3.2.2", "JSONStream": "1.3.5", "abort-controller": "^3.0.0", "adm-zip": "^0.5.9", @@ -965,7 +965,7 @@ "vinyl": "^2.2.0", "whatwg-fetch": "^3.0.0", "xml2js": "^0.4.22", - "xstate": "^4.37.1", + "xstate": "^4.37.2", "xterm": "^5.1.0", "yauzl": "^2.10.0", "yazl": "^2.5.1" diff --git a/src/plugins/visualizations/public/actions/edit_in_lens_action.tsx b/src/plugins/visualizations/public/actions/edit_in_lens_action.tsx index 07268726359bad..b86e2b2dbb37a5 100644 --- a/src/plugins/visualizations/public/actions/edit_in_lens_action.tsx +++ b/src/plugins/visualizations/public/actions/edit_in_lens_action.tsx @@ -17,7 +17,13 @@ import { IEmbeddable, ViewMode } from '@kbn/embeddable-plugin/public'; import { Action } from '@kbn/ui-actions-plugin/public'; import { VisualizeEmbeddable } from '../embeddable'; import { DASHBOARD_VISUALIZATION_PANEL_TRIGGER } from '../triggers'; -import { getUiActions, getApplication, getEmbeddable, getUsageCollection } from '../services'; +import { + getUiActions, + getApplication, + getEmbeddable, + getUsageCollection, + getCapabilities, +} from '../services'; export const ACTION_EDIT_IN_LENS = 'ACTION_EDIT_IN_LENS'; @@ -116,7 +122,8 @@ export class EditInLensAction implements Action { async isCompatible(context: ActionExecutionContext) { const { embeddable } = context; - if (!isVisualizeEmbeddable(embeddable)) { + const { visualize } = getCapabilities(); + if (!isVisualizeEmbeddable(embeddable) || !visualize.show) { return false; } const vis = embeddable.getVis(); diff --git a/src/plugins/visualizations/public/embeddable/create_vis_embeddable_from_object.ts b/src/plugins/visualizations/public/embeddable/create_vis_embeddable_from_object.ts index 8fb107f827cd6a..fdcdc25d21043f 100644 --- a/src/plugins/visualizations/public/embeddable/create_vis_embeddable_from_object.ts +++ b/src/plugins/visualizations/public/embeddable/create_vis_embeddable_from_object.ts @@ -62,6 +62,7 @@ export const createVisEmbeddableFromObject = const capabilities = { visualizeSave: Boolean(getCapabilities().visualize.save), dashboardSave: Boolean(getCapabilities().dashboard?.showWriteControls), + visualizeOpen: Boolean(getCapabilities().visualize?.show), }; return createVisualizeEmbeddableAsync( diff --git a/src/plugins/visualizations/public/embeddable/visualize_embeddable.tsx b/src/plugins/visualizations/public/embeddable/visualize_embeddable.tsx index a4cf5466422b29..a8eaca1552cd07 100644 --- a/src/plugins/visualizations/public/embeddable/visualize_embeddable.tsx +++ b/src/plugins/visualizations/public/embeddable/visualize_embeddable.tsx @@ -62,7 +62,7 @@ export interface VisualizeEmbeddableConfiguration { indexPatterns?: DataView[]; editPath: string; editUrl: string; - capabilities: { visualizeSave: boolean; dashboardSave: boolean }; + capabilities: { visualizeSave: boolean; dashboardSave: boolean; visualizeOpen: boolean }; deps: VisualizeEmbeddableFactoryDeps; } @@ -171,7 +171,9 @@ export class VisualizeEmbeddable if (this.attributeService) { const isByValue = !this.inputIsRefType(initialInput); - const editable = capabilities.visualizeSave || (isByValue && capabilities.dashboardSave); + const editable = + capabilities.visualizeSave || + (isByValue && capabilities.dashboardSave && capabilities.visualizeOpen); this.updateOutput({ ...this.getOutput(), editable }); } diff --git a/x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_collection_view/analytics_collection_integrate/analytics_collection_integrate_javascript_client_embed.tsx b/x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_collection_view/analytics_collection_integrate/analytics_collection_integrate_javascript_client_embed.tsx index c240d941c74b5b..f5e07851ee4fea 100644 --- a/x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_collection_view/analytics_collection_integrate/analytics_collection_integrate_javascript_client_embed.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_collection_view/analytics_collection_integrate/analytics_collection_integrate_javascript_client_embed.tsx @@ -173,15 +173,26 @@ const SearchResult = ({ hit }) => { const clickHandler = () => { trackSearchClick({ document: { id: hit.id, index: "products" }, + page: { + url: "http://my-website.com/products/123" + }, search: { query: "search term", - filters: [], + filters: {}, page: { current: 1, size: 10 }, - results: { + results: { items: [ - { id: "123", index: "products" } + { + document: { + id: "123", + index: "products", + }, + page: { + url: "http://my-website.com/products/123", + }, + }, ], - total_results: 10 + total_results: 10 }, sort: { name: "relevance", diff --git a/x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_collection_view/analytics_collection_integrate/analytics_collection_integrate_javascript_embed.tsx b/x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_collection_view/analytics_collection_integrate/analytics_collection_integrate_javascript_embed.tsx index b44b19042d60cf..87295fd8eff6b0 100644 --- a/x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_collection_view/analytics_collection_integrate/analytics_collection_integrate_javascript_embed.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_collection_view/analytics_collection_integrate/analytics_collection_integrate_javascript_embed.tsx @@ -115,10 +115,10 @@ window.elasticAnalytics.createTracker({ {`window.elasticAnalytics.trackSearch({ search: { query: "laptop", - filters: [ - { field: "brand", value: ["apple"] }, - { field: "price", value: ["1000-2000"] }, - ], + filters: { + brand: ["apple"], + price: ["1000-2000"], + }, page: { current: 1, size: 10, diff --git a/x-pack/plugins/lens/public/embeddable/embeddable.test.tsx b/x-pack/plugins/lens/public/embeddable/embeddable.test.tsx index 5e48e1f46dd638..1ab012ef844835 100644 --- a/x-pack/plugins/lens/public/embeddable/embeddable.test.tsx +++ b/x-pack/plugins/lens/public/embeddable/embeddable.test.tsx @@ -166,6 +166,7 @@ describe('embeddable', () => { capabilities: { canSaveDashboards: true, canSaveVisualizations: true, + canOpenVisualizations: true, discover: {}, navLinks: {}, }, @@ -361,6 +362,7 @@ describe('embeddable', () => { capabilities: { canSaveDashboards: true, canSaveVisualizations: true, + canOpenVisualizations: true, discover: {}, navLinks: {}, }, @@ -413,6 +415,7 @@ describe('embeddable', () => { capabilities: { canSaveDashboards: true, canSaveVisualizations: true, + canOpenVisualizations: true, discover: {}, navLinks: {}, }, @@ -940,6 +943,7 @@ describe('embeddable', () => { capabilities: { canSaveDashboards: true, canSaveVisualizations: true, + canOpenVisualizations: true, discover: {}, navLinks: {}, }, @@ -1039,6 +1043,7 @@ describe('embeddable', () => { capabilities: { canSaveDashboards: true, canSaveVisualizations: true, + canOpenVisualizations: true, discover: {}, navLinks: {}, }, @@ -1135,6 +1140,7 @@ describe('embeddable', () => { capabilities: { canSaveDashboards: true, canSaveVisualizations: true, + canOpenVisualizations: true, discover: {}, navLinks: {}, }, @@ -1186,4 +1192,25 @@ describe('embeddable', () => { }) ); }); + + it('should not be editable for no visualize library privileges', async () => { + const embeddable = new Embeddable( + getEmbeddableProps({ + capabilities: { + canSaveDashboards: false, + canSaveVisualizations: true, + canOpenVisualizations: false, + discover: {}, + navLinks: {}, + }, + }), + { + timeRange: { + from: 'now-15m', + to: 'now', + }, + } as LensEmbeddableInput + ); + expect(embeddable.getOutput().editable).toBeUndefined(); + }); }); diff --git a/x-pack/plugins/lens/public/embeddable/embeddable.tsx b/x-pack/plugins/lens/public/embeddable/embeddable.tsx index 787200e53315b0..43a871ccf326f6 100644 --- a/x-pack/plugins/lens/public/embeddable/embeddable.tsx +++ b/x-pack/plugins/lens/public/embeddable/embeddable.tsx @@ -208,6 +208,7 @@ export interface LensEmbeddableDeps { getTriggerCompatibleActions?: UiActionsStart['getTriggerCompatibleActions']; capabilities: { canSaveVisualizations: boolean; + canOpenVisualizations: boolean; canSaveDashboards: boolean; navLinks: Capabilities['navLinks']; discover: Capabilities['discover']; @@ -1353,7 +1354,9 @@ export class Embeddable private getIsEditable() { return ( this.deps.capabilities.canSaveVisualizations || - (!this.inputIsRefType(this.getInput()) && this.deps.capabilities.canSaveDashboards) + (!this.inputIsRefType(this.getInput()) && + this.deps.capabilities.canSaveDashboards && + this.deps.capabilities.canOpenVisualizations) ); } diff --git a/x-pack/plugins/lens/public/embeddable/embeddable_factory.ts b/x-pack/plugins/lens/public/embeddable/embeddable_factory.ts index f57e532bc13804..f31e8dd7dcad71 100644 --- a/x-pack/plugins/lens/public/embeddable/embeddable_factory.ts +++ b/x-pack/plugins/lens/public/embeddable/embeddable_factory.ts @@ -140,6 +140,7 @@ export class EmbeddableFactory implements EmbeddableFactoryDefinition { capabilities: { canSaveDashboards: Boolean(capabilities.dashboard?.showWriteControls), canSaveVisualizations: Boolean(capabilities.visualize.save), + canOpenVisualizations: Boolean(capabilities.visualize.show), navLinks: capabilities.navLinks, discover: capabilities.discover, }, diff --git a/x-pack/plugins/ml/public/application/app.tsx b/x-pack/plugins/ml/public/application/app.tsx index b15abc1d2cddbc..98f971f72e2e72 100644 --- a/x-pack/plugins/ml/public/application/app.tsx +++ b/x-pack/plugins/ml/public/application/app.tsx @@ -20,6 +20,7 @@ import { toMountPoint, wrapWithTheme } from '@kbn/kibana-react-plugin/public'; import { KibanaContextProvider, KibanaThemeProvider } from '@kbn/kibana-react-plugin/public'; import { StorageContextProvider } from '@kbn/ml-local-storage'; +import { firstValueFrom } from 'rxjs'; import { mlCapabilities } from './capabilities/check_capabilities'; import { ML_STORAGE_KEYS } from '../../common/types/storage'; import { ML_APP_LOCATOR, ML_PAGES } from '../../common/constants/locator'; @@ -74,10 +75,19 @@ export type MlGlobalServices = ReturnType; const App: FC = ({ coreStart, deps, appMountParams }) => { const redirectToMlAccessDeniedPage = async () => { - const accessDeniedPageUrl = await deps.share.url.locators.get(ML_APP_LOCATOR)!.getUrl({ - page: ML_PAGES.ACCESS_DENIED, - }); - await coreStart.application.navigateToUrl(accessDeniedPageUrl); + // access maybe be denied due to an expired license, so check the license status first + // if the license has expired, redirect to the license management page + const license = await firstValueFrom(deps.licensing.license$); + const redirectPage = + license.status === 'expired' + ? deps.share.url.locators.get('LICENSE_MANAGEMENT_LOCATOR')!.getUrl({ + page: 'dashboard', + }) + : deps.share.url.locators.get(ML_APP_LOCATOR)!.getUrl({ + page: ML_PAGES.ACCESS_DENIED, + }); + + await coreStart.application.navigateToUrl(await redirectPage); }; const pageDeps = { diff --git a/x-pack/plugins/ml/server/saved_objects/checks.ts b/x-pack/plugins/ml/server/saved_objects/checks.ts index 544c927b5f6c51..1985115d6ebf15 100644 --- a/x-pack/plugins/ml/server/saved_objects/checks.ts +++ b/x-pack/plugins/ml/server/saved_objects/checks.ts @@ -20,9 +20,8 @@ import type { MlSavedObjectType, } from '../../common/types/saved_objects'; -import type { DataFrameAnalyticsConfig } from '../../common/types/data_frame_analytics'; import type { ResolveMlCapabilities } from '../../common/types/capabilities'; -import { getJobDetailsFromTrainedModel } from './util'; +import { getJobDetailsFromTrainedModel, getJobsAndModels } from './util'; export interface JobSavedObjectStatus { jobId: string; @@ -79,7 +78,7 @@ export function checksFactory( mlSavedObjectService: MLSavedObjectService ) { async function checkStatus(): Promise { - const [ + const { jobObjects, allJobObjects, modelObjects, @@ -88,18 +87,7 @@ export function checksFactory( datafeeds, dfaJobs, models, - ] = await Promise.all([ - mlSavedObjectService.getAllJobObjects(undefined, false), - mlSavedObjectService.getAllJobObjectsForAllSpaces(), - mlSavedObjectService.getAllTrainedModelObjects(false), - mlSavedObjectService.getAllTrainedModelObjectsForAllSpaces(), - client.asInternalUser.ml.getJobs(), - client.asInternalUser.ml.getDatafeeds(), - client.asInternalUser.ml.getDataFrameAnalytics() as unknown as { - data_frame_analytics: DataFrameAnalyticsConfig[]; - }, - client.asInternalUser.ml.getTrainedModels(), - ]); + } = await getJobsAndModels(client, mlSavedObjectService); const jobSavedObjectsStatus: JobSavedObjectStatus[] = jobObjects.map( ({ attributes, namespaces }) => { @@ -111,10 +99,10 @@ export function checksFactory( let datafeedExists: boolean | undefined; if (type === 'anomaly-detector') { - jobExists = adJobs.jobs.some((j) => j.job_id === jobId); - datafeedExists = datafeeds.datafeeds.some((d) => d.job_id === jobId); + jobExists = adJobs.some((j) => j.job_id === jobId); + datafeedExists = datafeeds.some((d) => d.job_id === jobId); } else { - jobExists = dfaJobs.data_frame_analytics.some((j) => j.id === jobId); + jobExists = dfaJobs.some((j) => j.id === jobId); } return { @@ -130,12 +118,12 @@ export function checksFactory( } ); - const dfaJobsCreateTimeMap = dfaJobs.data_frame_analytics.reduce((acc, cur) => { + const dfaJobsCreateTimeMap = dfaJobs.reduce((acc, cur) => { acc.set(cur.id, cur.create_time!); return acc; }, new Map()); - const modelJobExits = models.trained_model_configs.reduce((acc, cur) => { + const modelJobExits = models.reduce((acc, cur) => { const job = getJobDetailsFromTrainedModel(cur); if (job === null) { return acc; @@ -152,7 +140,7 @@ export function checksFactory( const modelSavedObjectsStatus: TrainedModelSavedObjectStatus[] = modelObjects.map( ({ attributes: { job, model_id: modelId }, namespaces }) => { - const trainedModelExists = models.trained_model_configs.some((m) => m.model_id === modelId); + const trainedModelExists = models.some((m) => m.model_id === modelId); const dfaJobExists = modelJobExits.get(modelId) ?? null; return { @@ -194,14 +182,14 @@ export function checksFactory( ); const modelObjectIds = new Set(modelSavedObjectsStatus.map(({ modelId }) => modelId)); - const anomalyDetectorsStatus = adJobs.jobs + const anomalyDetectorsStatus = adJobs .filter(({ job_id: jobId }) => { // only list jobs which are in the current space (adObjectIds) // or are not in any spaces (nonSpaceADObjectIds) return adObjectIds.has(jobId) === true || nonSpaceADObjectIds.has(jobId) === false; }) .map(({ job_id: jobId }) => { - const datafeedId = datafeeds.datafeeds.find((df) => df.job_id === jobId)?.datafeed_id; + const datafeedId = datafeeds.find((df) => df.job_id === jobId)?.datafeed_id; return { jobId, datafeedId: datafeedId ?? null, @@ -211,7 +199,7 @@ export function checksFactory( }; }); - const dataFrameAnalyticsStatus = dfaJobs.data_frame_analytics + const dataFrameAnalyticsStatus = dfaJobs .filter(({ id: jobId }) => { // only list jobs which are in the current space (dfaObjectIds) // or are not in any spaces (nonSpaceDFAObjectIds) @@ -227,7 +215,7 @@ export function checksFactory( }; }); - const modelsStatus = models.trained_model_configs + const modelsStatus = models .filter(({ model_id: modelId }) => { // only list jobs which are in the current space (adObjectIds) // or are not in any spaces (nonSpaceADObjectIds) diff --git a/x-pack/plugins/ml/server/saved_objects/util.ts b/x-pack/plugins/ml/server/saved_objects/util.ts index 561c09af75c985..bbd4e17d7a47dd 100644 --- a/x-pack/plugins/ml/server/saved_objects/util.ts +++ b/x-pack/plugins/ml/server/saved_objects/util.ts @@ -6,10 +6,14 @@ */ import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import { SavedObjectsServiceStart, KibanaRequest } from '@kbn/core/server'; -import { SavedObjectsClient } from '@kbn/core/server'; +import { + type SavedObjectsServiceStart, + type KibanaRequest, + type IScopedClusterClient, + SavedObjectsClient, +} from '@kbn/core/server'; +import type { TrainedModelJob, MLSavedObjectService } from './service'; import { ML_JOB_SAVED_OBJECT_TYPE } from '../../common/types/saved_objects'; -import type { TrainedModelJob } from './service'; export function savedObjectClientsFactory( getSavedObjectsStart: () => SavedObjectsServiceStart | null @@ -57,3 +61,98 @@ export function getJobDetailsFromTrainedModel( const createTime: number = model.metadata.analytics_config.create_time; return { job_id: jobId, create_time: createTime }; } + +/* + * Function for calling elasticsearch APIs for retrieving ML jobs and models. + * The elasticsearch api may be missing in a serverless environment, in which case + * we return null. + */ + +function mlFunctionsFactory(client: IScopedClusterClient) { + return { + async getJobs() { + try { + return client.asInternalUser.ml.getJobs(); + } catch (error) { + return null; + } + }, + async getDatafeeds() { + try { + return client.asInternalUser.ml.getDatafeeds(); + } catch (error) { + return null; + } + }, + async getTrainedModels() { + try { + return client.asInternalUser.ml.getTrainedModels(); + } catch (error) { + return null; + } + }, + async getDataFrameAnalytics() { + try { + return client.asInternalUser.ml.getDataFrameAnalytics(); + } catch (error) { + return null; + } + }, + }; +} + +/* + * Function for retrieving lists of jobs, models and saved objects. + * If any of the elasticsearch APIs are missing, it returns empty arrays + * so that the sync process does not create or delete any saved objects. + */ + +export async function getJobsAndModels( + client: IScopedClusterClient, + mlSavedObjectService: MLSavedObjectService +) { + const { getJobs, getDatafeeds, getTrainedModels, getDataFrameAnalytics } = + mlFunctionsFactory(client); + + const [ + jobObjects, + allJobObjects, + modelObjects, + allModelObjects, + adJobs, + datafeeds, + dfaJobs, + models, + ] = await Promise.all([ + mlSavedObjectService.getAllJobObjects(undefined, false), + mlSavedObjectService.getAllJobObjectsForAllSpaces(), + mlSavedObjectService.getAllTrainedModelObjects(false), + mlSavedObjectService.getAllTrainedModelObjectsForAllSpaces(), + getJobs(), + getDatafeeds(), + getDataFrameAnalytics(), + getTrainedModels(), + ]); + + const adJobObjects = + adJobs !== null ? jobObjects.filter((j) => j.attributes.type === 'anomaly-detector') : []; + const adAllJobObjects = + adJobs !== null ? allJobObjects.filter((j) => j.attributes.type === 'anomaly-detector') : []; + const dfaJobObjects = + dfaJobs !== null ? jobObjects.filter((j) => j.attributes.type === 'data-frame-analytics') : []; + const dfaAllJobObjects = + dfaJobs !== null + ? allJobObjects.filter((j) => j.attributes.type === 'data-frame-analytics') + : []; + + return { + jobObjects: [...adJobObjects, ...dfaJobObjects], + allJobObjects: [...adAllJobObjects, ...dfaAllJobObjects], + modelObjects: models === null ? [] : modelObjects, + allModelObjects: models === null ? [] : allModelObjects, + adJobs: adJobs === null ? [] : adJobs.jobs, + datafeeds: datafeeds === null ? [] : datafeeds.datafeeds, + dfaJobs: dfaJobs === null ? [] : dfaJobs.data_frame_analytics, + models: models === null ? [] : models.trained_model_configs, + }; +} diff --git a/yarn.lock b/yarn.lock index 8819b232d4ac20..287ebf23462f97 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9903,7 +9903,7 @@ resolved "https://registry.yarnpkg.com/@xobotyi/scrollbar-width/-/scrollbar-width-1.9.5.tgz#80224a6919272f405b87913ca13b92929bdf3c4d" integrity sha512-N8tkAACJx2ww8vFMneJmaAgmjAG1tnVBZJRLRcx061tmsLRZHSEZSLuGWnwPtunsSLvSqXQ2wfp7Mgqg1I+2dQ== -"@xstate/react@^3.2.1": +"@xstate/react@^3.2.2": version "3.2.2" resolved "https://registry.yarnpkg.com/@xstate/react/-/react-3.2.2.tgz#ddf0f9d75e2c19375b1e1b7335e72cb99762aed8" integrity sha512-feghXWLedyq8JeL13yda3XnHPZKwYDN5HPBLykpLeuNpr9178tQd2/3d0NrH6gSd0sG5mLuLeuD+ck830fgzLQ== @@ -29656,7 +29656,7 @@ xpath@0.0.32: resolved "https://registry.yarnpkg.com/xpath/-/xpath-0.0.32.tgz#1b73d3351af736e17ec078d6da4b8175405c48af" integrity sha512-rxMJhSIoiO8vXcWvSifKqhvV96GjiD5wYb8/QHdoRyQvraTpp4IEv944nhGausZZ3u7dhQXteZuZbaqfpB7uYw== -xstate@^4.37.1: +xstate@^4.37.2: version "4.37.2" resolved "https://registry.yarnpkg.com/xstate/-/xstate-4.37.2.tgz#c5f4c1d8062784238b91e2dfddca05f821cb4eac" integrity sha512-Qm337O49CRTZ3PRyRuK6b+kvI+D3JGxXIZCTul+xEsyFCVkTFDt5jixaL1nBWcUBcaTQ9um/5CRGVItPi7fveg==