[AI4DSOC] [REVERT] Disable Visualize, Lens and Maps for Search AI Lake Tier #218089 (#221141)

Reverts: https://github.com/elastic/kibana/pull/218089 

Previous PR: 
<img width="199" alt="Screenshot 2025-05-21 at 17 52 10"
src="https://github.com/user-attachments/assets/51e87073-eec5-47fc-8c20-59399a9e1042"
/>

This PR:
<img width="215" alt="Screenshot 2025-05-21 at 18 04 15"
src="https://github.com/user-attachments/assets/30e5bc50-b9e4-486f-af17-e42ca5c6b84c"
/>


-----

We realized that there are so many dependencies on `lens`, and thus
`visualizations` that we decided to revert this, and find another way to
hide the features in AI_SOC plugin. Hopefully approach with `overrides`
would be just enough for our need.

Thank you everyone for the initial help and reviews.
This commit is contained in:
Tomasz Ciecierski 2025-05-21 20:39:09 +02:00 committed by GitHub
parent 6f41f41626
commit 4cfcf3b190
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
61 changed files with 103 additions and 243 deletions

View file

@ -9,12 +9,6 @@ xpack.serverless.chat.enabled: true
## Cloud settings ## Cloud settings
xpack.cloud.serverless.project_type: search xpack.cloud.serverless.project_type: search
## Fine-tune the search solution feature privileges. Also, refer to `serverless.yml` for the project-agnostic overrides.
xpack.features.overrides:
### Not sure if CHAT solution uses dashboard or maps
### Maps feature is hidden in Role management since it's automatically granted by Dashboard feature.
maps_v2.hidden: true
## Set the home route ## Set the home route
uiSettings.overrides.defaultRoute: /app/workchat uiSettings.overrides.defaultRoute: /app/workchat

View file

@ -50,8 +50,6 @@ xpack.features.overrides:
dev_tools.category: "enterpriseSearch" dev_tools.category: "enterpriseSearch"
### Discover feature is moved from Analytics category to the Search one. ### Discover feature is moved from Analytics category to the Search one.
discover_v2.category: "enterpriseSearch" discover_v2.category: "enterpriseSearch"
### Maps feature is hidden in Role management since it's automatically granted by Dashboard feature.
maps_v2.hidden: true
### Machine Learning feature is moved from Analytics category to the Management one. ### Machine Learning feature is moved from Analytics category to the Management one.
ml.category: "management" ml.category: "management"
### Stack Alerts feature is moved from Analytics category to the Search one renamed to simply `Alerts`. ### Stack Alerts feature is moved from Analytics category to the Search one renamed to simply `Alerts`.

View file

@ -84,8 +84,6 @@ xpack.features.overrides:
privileges: [ "read" ] privileges: [ "read" ]
### Logs feature is hidden in Role management since it's automatically granted by either Infrastructure, or Applications features. ### Logs feature is hidden in Role management since it's automatically granted by either Infrastructure, or Applications features.
logs.hidden: true logs.hidden: true
### Maps feature is hidden in Role management since it's automatically granted by Dashboard feature.
maps_v2.hidden: true
### Machine Learning feature should be moved from Analytics category to the Observability one and renamed to `AI Ops`. ### Machine Learning feature should be moved from Analytics category to the Observability one and renamed to `AI Ops`.
ml: ml:
category: "observability" category: "observability"

View file

@ -1,4 +1 @@
# Security Complete tier config # Security Complete tier config
xpack.features.overrides:
### The following features are hidden in Role management since they're automatically granted by SIEM feature.
maps_v2.hidden: true

View file

@ -1,4 +1,2 @@
# Security Essentials tier config # Security Essentials tier config
xpack.features.overrides:
### The following features are hidden in Role management since they're automatically granted by SIEM feature.
maps_v2.hidden: true

View file

@ -1,14 +1,9 @@
# Security Search AI Lake tier config # Security Search AI Lake tier config
## Disable xpack plugins ## Disable plugins
xpack.osquery.enabled: false xpack.osquery.enabled: false
xpack.maps.enabled: false
xpack.ml.ad.enabled: false xpack.ml.ad.enabled: false
xpack.ml.dfa.enabled: false xpack.ml.dfa.enabled: false
xpack.lens.enabled: false
### Disable shared plugins
visualizations.enabled: false
## Disable plugin features ## Disable plugin features
xpack.alerting.maintenanceWindow.enabled: false xpack.alerting.maintenanceWindow.enabled: false
@ -23,51 +18,6 @@ xpack.features.overrides:
siemV2.description: null siemV2.description: null
securitySolutionSiemMigrations.hidden: true securitySolutionSiemMigrations.hidden: true
## Fine-tune the security solution essentials feature privileges. These feature privilege overrides are set individually for each project type. Also, refer to `serverless.yml` for the project-agnostic overrides.
dashboard:
privileges:
## We do not need to compose dashboard from maps and visualizations because these functionalities are disabled in this tier
## Setting to empty array so the values from serverless.yml or serverless.security.yml are overwritten
all.composedOf: []
read.composedOf: []
dashboard_v2:
privileges:
## Setting to empty array so the values from serverless.yml or serverless.security.yml are overwritten
## We do not need to compose dashboard from maps and visualizations because these functionalities are disabled in this tier
all.composedOf: []
read.composedOf: []
siemV2:
privileges:
all.composedOf:
## Limited values so the fields from serverless.yml or serverless.security.yml are overwritten
## We do not need to compose siemV2 from maps and visualizations because these functionalities are disabled in this tier
- feature: "discover_v2"
privileges: [ "all" ]
- feature: "dashboard_v2"
privileges: [ "all" ]
read.composedOf:
- feature: "discover_v2"
privileges: [ "read" ]
- feature: "dashboard_v2"
privileges: [ "read" ]
siem:
privileges:
all.composedOf:
## Limited values so the fields from serverless.yml or serverless.security.yml are overwritten
## We do not need to compose siemV2 from maps and visualizations because these functionalities are disabled in this tier
- feature: "discover_v2"
privileges: [ "all" ]
- feature: "dashboard_v2"
privileges: [ "all" ]
- feature: "savedQueryManagement"
privileges: [ "all" ]
read.composedOf:
- feature: "discover_v2"
privileges: [ "read" ]
- feature: "dashboard_v2"
privileges: [ "read" ]
- feature: "savedQueryManagement"
privileges: [ "read" ]
# Custom integrations/fleet settings # Custom integrations/fleet settings
xpack.fleet.agentless.isDefault: true xpack.fleet.agentless.isDefault: true
xpack.fleet.integrationsHomeOverride: '/app/security/configurations/integrations' xpack.fleet.integrationsHomeOverride: '/app/security/configurations/integrations'

View file

@ -18,6 +18,8 @@ xpack.features.overrides:
dashboard_v2.hidden: true dashboard_v2.hidden: true
visualize.hidden: true visualize.hidden: true
visualize_v2.hidden: true visualize_v2.hidden: true
maps.hidden: true
maps_v2.hidden: true
### Machine Learning feature is moved from Analytics category to the Security one as the last item. ### Machine Learning feature is moved from Analytics category to the Security one as the last item.
ml: ml:
category: "security" category: "security"

View file

@ -81,6 +81,8 @@ xpack.features.overrides:
includeIn: "read" includeIn: "read"
### Shared images feature is hidden in Role management since it's not needed. ### Shared images feature is hidden in Role management since it's not needed.
filesSharedImage.hidden: true filesSharedImage.hidden: true
### Maps feature is hidden in Role management since it's automatically granted by Dashboard feature.
maps_v2.hidden: true
### Reporting feature is supposed to give access to reporting capabilities across different features. ### Reporting feature is supposed to give access to reporting capabilities across different features.
reporting: reporting:
privileges: privileges:

View file

@ -11,6 +11,7 @@ import { offeringBasedSchema, schema, TypeOf } from '@kbn/config-schema';
export const configSchema = schema.object({ export const configSchema = schema.object({
enabled: schema.boolean({ defaultValue: true }), enabled: schema.boolean({ defaultValue: true }),
readOnly: offeringBasedSchema({ readOnly: offeringBasedSchema({
serverless: schema.boolean({ defaultValue: false }), serverless: schema.boolean({ defaultValue: false }),
}), }),

View file

@ -51,7 +51,7 @@ export const createVisEmbeddableFromObject =
} }
const capabilities = { const capabilities = {
visualizeSave: Boolean(getCapabilities().visualize_v2?.save), visualizeSave: Boolean(getCapabilities().visualize_v2.save),
dashboardSave: Boolean(getCapabilities().dashboard_v2?.showWriteControls), dashboardSave: Boolean(getCapabilities().dashboard_v2?.showWriteControls),
visualizeOpen: Boolean(getCapabilities().visualize_v2?.show), visualizeOpen: Boolean(getCapabilities().visualize_v2?.show),
}; };

View file

@ -1,14 +0,0 @@
/*
* 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 { schema } from '@kbn/config-schema';
export const configSchema = schema.object({
enabled: schema.boolean({ defaultValue: true }),
});

View file

@ -7,19 +7,11 @@
* License v3.0 only", or the "Server Side Public License, v 1". * License v3.0 only", or the "Server Side Public License, v 1".
*/ */
import { PluginConfigDescriptor, PluginInitializerContext } from '@kbn/core/server'; import { PluginInitializerContext } from '@kbn/core/server';
import { configSchema } from './config';
// This exports static code and TypeScript types, // This exports static code and TypeScript types,
// as well as, Kibana Platform `plugin()` initializer. // as well as, Kibana Platform `plugin()` initializer.
export const config: PluginConfigDescriptor<any> = {
schema: configSchema,
exposeToBrowser: {
visualize_v2: { enabled: true },
},
};
export async function plugin(initializerContext: PluginInitializerContext) { export async function plugin(initializerContext: PluginInitializerContext) {
const { VisualizationsPlugin } = await import('./plugin'); const { VisualizationsPlugin } = await import('./plugin');
return new VisualizationsPlugin(initializerContext); return new VisualizationsPlugin(initializerContext);

View file

@ -14,6 +14,7 @@
"requiredPlugins": [ "requiredPlugins": [
"charts", "charts",
"data", "data",
"lens",
"licensing", "licensing",
"uiActions", "uiActions",
"embeddable", "embeddable",
@ -26,8 +27,7 @@
"optionalPlugins": [ "optionalPlugins": [
"cases", "cases",
"observabilityAIAssistant", "observabilityAIAssistant",
"usageCollection", "usageCollection"
"lens"
], ],
"requiredBundles": [ "requiredBundles": [
"dataViews", "dataViews",

View file

@ -20,6 +20,7 @@
"actions", "actions",
"data", "data",
"embeddable", "embeddable",
"lens",
"licensing", "licensing",
"features", "features",
"triggersActionsUi", "triggersActionsUi",
@ -38,15 +39,13 @@
"taskManager", "taskManager",
"usageCollection", "usageCollection",
"spaces", "spaces",
"serverless", "serverless"
"lens"
], ],
"requiredBundles": [ "requiredBundles": [
"esUiShared", "esUiShared",
"kibanaReact", "kibanaReact",
"kibanaUtils", "kibanaUtils",
"savedObjectsFinder", "savedObjectsFinder"
"lens"
], ],
"extraPublicDirs": [ "extraPublicDirs": [
"common" "common"

View file

@ -27,11 +27,7 @@ interface StartServiceArgs {
license?: ILicense | null; license?: ILicense | null;
} }
type StartServicesWithRequiredLens = StartServices & Required<Pick<StartServices, 'lens'>>; export const createStartServicesMock = ({ license }: StartServiceArgs = {}): StartServices => {
export const createStartServicesMock = ({
license,
}: StartServiceArgs = {}): StartServicesWithRequiredLens => {
const licensingPluginMock = licensingMock.createStart(); const licensingPluginMock = licensingMock.createStart();
const triggersActionsUi = triggersActionsUiMock.createStart(); const triggersActionsUi = triggersActionsUiMock.createStart();
@ -52,7 +48,7 @@ export const createStartServicesMock = ({
license != null license != null
? { ...licensingPluginMock, license$: new BehaviorSubject(license) } ? { ...licensingPluginMock, license$: new BehaviorSubject(license) }
: licensingPluginMock, : licensingPluginMock,
} as unknown as StartServicesWithRequiredLens; } as unknown as StartServices;
services.application.currentAppId$ = new BehaviorSubject<string>('testAppId'); services.application.currentAppId$ = new BehaviorSubject<string>('testAppId');
services.application.applications$ = new BehaviorSubject<Map<string, PublicAppInfo>>( services.application.applications$ = new BehaviorSubject<Map<string, PublicAppInfo>>(

View file

@ -10,7 +10,7 @@ import type { CasesUiConfigType } from '../../../../common/ui/types';
import type { CasesPublicStartDependencies } from '../../../types'; import type { CasesPublicStartDependencies } from '../../../types';
type GlobalServices = Pick<CoreStart, 'application' | 'http' | 'theme' | 'userProfile'> & type GlobalServices = Pick<CoreStart, 'application' | 'http' | 'theme' | 'userProfile'> &
Pick<CasesPublicStartDependencies, 'serverless' | 'lens'>; Pick<CasesPublicStartDependencies, 'serverless'>;
export class KibanaServices { export class KibanaServices {
private static kibanaVersion?: string; private static kibanaVersion?: string;

View file

@ -21,11 +21,14 @@ export const useLensOpenVisualization = ({ comment }: { comment: string }) => {
const parsedComment = parseCommentString(comment); const parsedComment = parseCommentString(comment);
const lensVisualization = getLensVisualizations(parsedComment?.children ?? []); const lensVisualization = getLensVisualizations(parsedComment?.children ?? []);
const { lens } = useKibana().services; const {
const hasLensPermissions = lens?.canUseEditor(); lens: { navigateToPrefilledEditor, canUseEditor },
} = useKibana().services;
const hasLensPermissions = canUseEditor();
const handleClick = useCallback(() => { const handleClick = useCallback(() => {
lens?.navigateToPrefilledEditor( navigateToPrefilledEditor(
{ {
id: '', id: '',
timeRange: lensVisualization[0].timeRange, timeRange: lensVisualization[0].timeRange,
@ -36,7 +39,7 @@ export const useLensOpenVisualization = ({ comment }: { comment: string }) => {
openInNewTab: true, openInNewTab: true,
} }
); );
}, [lens, lensVisualization]); }, [lensVisualization, navigateToPrefilledEditor]);
if (!lensVisualization.length || lensVisualization?.[0]?.attributes == null) { if (!lensVisualization.length || lensVisualization?.[0]?.attributes == null) {
return { canUseEditor: hasLensPermissions, actionConfig: null }; return { canUseEditor: hasLensPermissions, actionConfig: null };

View file

@ -13,13 +13,12 @@ import {
import { useMemo } from 'react'; import { useMemo } from 'react';
import { useTimelineContext } from '../timeline_context/use_timeline_context'; import { useTimelineContext } from '../timeline_context/use_timeline_context';
import type { TemporaryProcessingPluginsType } from './types'; import type { TemporaryProcessingPluginsType } from './types';
import { KibanaServices, useApplicationCapabilities, useKibana } from '../../common/lib/kibana'; import { KibanaServices, useApplicationCapabilities } from '../../common/lib/kibana';
import * as lensMarkdownPlugin from './plugins/lens'; import * as lensMarkdownPlugin from './plugins/lens';
import { ID as LensPluginId } from './plugins/lens/constants'; import { ID as LensPluginId } from './plugins/lens/constants';
export const usePlugins = (disabledPlugins?: string[]) => { export const usePlugins = (disabledPlugins?: string[]) => {
const kibanaConfig = KibanaServices.getConfig(); const kibanaConfig = KibanaServices.getConfig();
const { services } = useKibana();
const timelinePlugins = useTimelineContext()?.editor_plugins; const timelinePlugins = useTimelineContext()?.editor_plugins;
const appCapabilities = useApplicationCapabilities(); const appCapabilities = useApplicationCapabilities();
@ -39,7 +38,6 @@ export const usePlugins = (disabledPlugins?: string[]) => {
} }
if ( if (
services.lens !== undefined &&
kibanaConfig?.markdownPlugins?.lens && kibanaConfig?.markdownPlugins?.lens &&
!disabledPlugins?.includes(LensPluginId) && !disabledPlugins?.includes(LensPluginId) &&
appCapabilities?.visualize.crud appCapabilities?.visualize.crud
@ -60,7 +58,6 @@ export const usePlugins = (disabledPlugins?: string[]) => {
appCapabilities?.visualize.crud, appCapabilities?.visualize.crud,
disabledPlugins, disabledPlugins,
kibanaConfig?.markdownPlugins?.lens, kibanaConfig?.markdownPlugins?.lens,
services.lens,
timelinePlugins, timelinePlugins,
]); ]);
}; };

View file

@ -15,14 +15,11 @@ import type { LensProps } from './types';
const LENS_VISUALIZATION_HEIGHT = 200; const LENS_VISUALIZATION_HEIGHT = 200;
const LensRendererComponent: React.FC<LensProps> = ({ attributes, timeRange, metadata }) => { const LensRendererComponent: React.FC<LensProps> = ({ attributes, timeRange, metadata }) => {
const { lens } = useKibana().services; const {
lens: { EmbeddableComponent },
} = useKibana().services;
const { euiTheme } = useEuiTheme(); const { euiTheme } = useEuiTheme();
if (!lens) {
return null;
}
const { EmbeddableComponent } = lens;
if (!attributes) { if (!attributes) {
return null; return null;
} }

View file

@ -15,10 +15,12 @@ import type { LensProps } from './types';
type Props = LensProps & { attachmentId: string }; type Props = LensProps & { attachmentId: string };
const OpenLensButtonComponent: React.FC<Props> = ({ attachmentId, attributes, timeRange }) => { const OpenLensButtonComponent: React.FC<Props> = ({ attachmentId, attributes, timeRange }) => {
const { lens } = useKibana().services; const {
lens: { navigateToPrefilledEditor, canUseEditor },
} = useKibana().services;
const onClick = useCallback(() => { const onClick = useCallback(() => {
lens?.navigateToPrefilledEditor( navigateToPrefilledEditor(
{ {
id: attachmentId, id: attachmentId,
timeRange, timeRange,
@ -28,9 +30,9 @@ const OpenLensButtonComponent: React.FC<Props> = ({ attachmentId, attributes, ti
openInNewTab: true, openInNewTab: true,
} }
); );
}, [attachmentId, attributes, lens, timeRange]); }, [attachmentId, attributes, navigateToPrefilledEditor, timeRange]);
const hasLensPermissions = lens?.canUseEditor(); const hasLensPermissions = canUseEditor();
const isESQLQuery = isOfAggregateQueryType(attributes.state.query); const isESQLQuery = isOfAggregateQueryType(attributes.state.query);
if (!hasLensPermissions || isESQLQuery) { if (!hasLensPermissions || isESQLQuery) {

View file

@ -79,10 +79,7 @@ export interface CasesPublicStartDependencies {
embeddable: EmbeddableStart; embeddable: EmbeddableStart;
features: FeaturesPluginStart; features: FeaturesPluginStart;
files: FilesStart; files: FilesStart;
/** lens: LensPublicStart;
* Lens is not supported in all serverless tiers
*/
lens?: LensPublicStart;
/** /**
* Cases in used by other plugins. Plugins pass the * Cases in used by other plugins. Plugins pass the
* service to their KibanaContext. ML does not pass * service to their KibanaContext. ML does not pass

View file

@ -364,7 +364,7 @@ export const extractLensReferencesFromCommentString = (
lensEmbeddableFactory: LensServerPluginSetup['lensEmbeddableFactory'], lensEmbeddableFactory: LensServerPluginSetup['lensEmbeddableFactory'],
comment: string comment: string
): SavedObjectReference[] => { ): SavedObjectReference[] => {
const extract = lensEmbeddableFactory && lensEmbeddableFactory().extract; const extract = lensEmbeddableFactory()?.extract;
if (extract) { if (extract) {
const parsedComment = parseCommentString(comment); const parsedComment = parseCommentString(comment);

View file

@ -62,7 +62,7 @@ export class CasePlugin
private readonly kibanaVersion: PluginInitializerContext['env']['packageInfo']['version']; private readonly kibanaVersion: PluginInitializerContext['env']['packageInfo']['version'];
private clientFactory: CasesClientFactory; private clientFactory: CasesClientFactory;
private securityPluginSetup?: SecurityPluginSetup; private securityPluginSetup?: SecurityPluginSetup;
private lensEmbeddableFactory: LensServerPluginSetup['lensEmbeddableFactory']; private lensEmbeddableFactory?: LensServerPluginSetup['lensEmbeddableFactory'];
private persistableStateAttachmentTypeRegistry: PersistableStateAttachmentTypeRegistry; private persistableStateAttachmentTypeRegistry: PersistableStateAttachmentTypeRegistry;
private externalReferenceAttachmentTypeRegistry: ExternalReferenceAttachmentTypeRegistry; private externalReferenceAttachmentTypeRegistry: ExternalReferenceAttachmentTypeRegistry;
private userProfileService: UserProfileService; private userProfileService: UserProfileService;
@ -92,7 +92,7 @@ export class CasePlugin
registerCaseFileKinds(this.caseConfig.files, plugins.files, core.security.fips.isEnabled()); registerCaseFileKinds(this.caseConfig.files, plugins.files, core.security.fips.isEnabled());
this.securityPluginSetup = plugins.security; this.securityPluginSetup = plugins.security;
this.lensEmbeddableFactory = plugins.lens?.lensEmbeddableFactory; this.lensEmbeddableFactory = plugins.lens.lensEmbeddableFactory;
if (this.caseConfig.stack.enabled) { if (this.caseConfig.stack.enabled) {
// V1 is deprecated, but has to be maintained for the time being // V1 is deprecated, but has to be maintained for the time being
@ -209,7 +209,12 @@ export class CasePlugin
featuresPluginStart: plugins.features, featuresPluginStart: plugins.features,
actionsPluginStart: plugins.actions, actionsPluginStart: plugins.actions,
licensingPluginStart: plugins.licensing, licensingPluginStart: plugins.licensing,
lensEmbeddableFactory: this.lensEmbeddableFactory, /**
* Lens will be always defined as
* it is declared as required plugin in kibana.json
*/
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
lensEmbeddableFactory: this.lensEmbeddableFactory!,
persistableStateAttachmentTypeRegistry: this.persistableStateAttachmentTypeRegistry, persistableStateAttachmentTypeRegistry: this.persistableStateAttachmentTypeRegistry,
externalReferenceAttachmentTypeRegistry: this.externalReferenceAttachmentTypeRegistry, externalReferenceAttachmentTypeRegistry: this.externalReferenceAttachmentTypeRegistry,
publicBaseUrl: core.http.basePath.publicBaseUrl, publicBaseUrl: core.http.basePath.publicBaseUrl,

View file

@ -98,7 +98,7 @@ export const getLensMigrations = <T>({
lensEmbeddableFactory, lensEmbeddableFactory,
migratorFactory, migratorFactory,
}: GetLensMigrationsArgs<T>) => { }: GetLensMigrationsArgs<T>) => {
const lensMigrations = lensEmbeddableFactory?.().migrations; const lensMigrations = lensEmbeddableFactory().migrations;
const lensMigrationObject = isFunction(lensMigrations) ? lensMigrations() : lensMigrations || {}; const lensMigrationObject = isFunction(lensMigrations) ? lensMigrations() : lensMigrations || {};
const embeddableMigrations = mapValues<MigrateFunctionsObject, SavedObjectMigrationParams<T, T>>( const embeddableMigrations = mapValues<MigrateFunctionsObject, SavedObjectMigrationParams<T, T>>(

View file

@ -39,7 +39,7 @@ import type { PersistableStateAttachmentTypeRegistry } from './attachment_framew
export interface CasesServerSetupDependencies { export interface CasesServerSetupDependencies {
alerting: AlertingServerSetup; alerting: AlertingServerSetup;
actions: ActionsPluginSetup; actions: ActionsPluginSetup;
lens?: LensServerPluginSetup; lens: LensServerPluginSetup;
features: FeaturesPluginSetup; features: FeaturesPluginSetup;
files: FilesSetup; files: FilesSetup;
security: SecurityPluginSetup; security: SecurityPluginSetup;

View file

@ -25,13 +25,13 @@
"fleet", "fleet",
"fieldFormats", "fieldFormats",
"dataViews", "dataViews",
"lens",
"fieldsMetadata", "fieldsMetadata",
"taskManager", "taskManager",
"usageCollection" "usageCollection"
], ],
"optionalPlugins": [ "optionalPlugins": [
"telemetry", "telemetry"
"lens"
], ],
"requiredBundles": [ "requiredBundles": [
"discover" "discover"

View file

@ -19,6 +19,7 @@
"inspector", "inspector",
"navigation", "navigation",
"urlForwarding", "urlForwarding",
"visualizations",
"uiActions", "uiActions",
"uiActionsEnhanced", "uiActionsEnhanced",
"embeddable", "embeddable",
@ -47,7 +48,6 @@
"serverless", "serverless",
"licensing", "licensing",
"fieldsMetadata", "fieldsMetadata",
"visualizations"
], ],
"requiredBundles": [ "requiredBundles": [
"unifiedSearch", "unifiedSearch",
@ -58,8 +58,7 @@
"fieldFormats", "fieldFormats",
"charts", "charts",
"esqlDataGrid", "esqlDataGrid",
"visualizations", "esql",
"esql"
], ],
"extraPublicDirs": ["common/constants"] "extraPublicDirs": ["common/constants"]
} }

View file

@ -200,7 +200,7 @@ export function App({
useEffect(() => { useEffect(() => {
onAppLeave((actions) => { onAppLeave((actions) => {
if ( if (
application.capabilities.visualize_v2?.save && application.capabilities.visualize_v2.save &&
!isLensEqualWrapper(persistedDoc) && !isLensEqualWrapper(persistedDoc) &&
(isSaveable || persistedDoc) (isSaveable || persistedDoc)
) { ) {
@ -226,7 +226,7 @@ export function App({
lastKnownDoc, lastKnownDoc,
isSaveable, isSaveable,
persistedDoc, persistedDoc,
application.capabilities.visualize_v2?.save, application.capabilities.visualize_v2.save,
data.query.filterManager, data.query.filterManager,
datasourceMap, datasourceMap,
visualizationMap, visualizationMap,

View file

@ -481,7 +481,7 @@ export const LensTopNavMenu = ({
const { from, to } = data.query.timefilter.timefilter.getTime(); const { from, to } = data.query.timefilter.timefilter.getTime();
const savingToLibraryPermitted = Boolean( const savingToLibraryPermitted = Boolean(
isSaveable && application.capabilities.visualize_v2?.save isSaveable && application.capabilities.visualize_v2.save
); );
const savingToDashboardPermitted = Boolean( const savingToDashboardPermitted = Boolean(
isSaveable && application.capabilities.dashboard_v2?.showWriteControls isSaveable && application.capabilities.dashboard_v2?.showWriteControls

View file

@ -170,7 +170,7 @@ export async function mountApp(
const embeddableEditorIncomingState = stateTransfer?.getIncomingEditorState(APP_ID); const embeddableEditorIncomingState = stateTransfer?.getIncomingEditorState(APP_ID);
addHelpMenuToAppChrome(coreStart.chrome, coreStart.docLinks); addHelpMenuToAppChrome(coreStart.chrome, coreStart.docLinks);
if (!lensServices.application.capabilities.visualize_v2?.save) { if (!lensServices.application.capabilities.visualize_v2.save) {
coreStart.chrome.setBadge({ coreStart.chrome.setBadge({
text: i18n.translate('xpack.lens.badge.readOnly.text', { text: i18n.translate('xpack.lens.badge.readOnly.text', {
defaultMessage: 'Read only', defaultMessage: 'Read only',

View file

@ -164,7 +164,7 @@ export function SaveModalContainer({
} }
const savingToLibraryPermitted = Boolean( const savingToLibraryPermitted = Boolean(
isSaveable && application.capabilities.visualize_v2?.save isSaveable && application.capabilities.visualize_v2.save
); );
return ( return (

View file

@ -189,8 +189,8 @@ function loadViewUnderlyingDataArgs(
activeData, activeData,
capabilities: { capabilities: {
canSaveDashboards: Boolean(capabilities.dashboard_v2?.showWriteControls), canSaveDashboards: Boolean(capabilities.dashboard_v2?.showWriteControls),
canSaveVisualizations: Boolean(capabilities.visualize_v2?.save), canSaveVisualizations: Boolean(capabilities.visualize_v2.save),
canOpenVisualizations: Boolean(capabilities.visualize_v2?.show), canOpenVisualizations: Boolean(capabilities.visualize_v2.show),
navLinks: capabilities.navLinks, navLinks: capabilities.navLinks,
discover_v2: capabilities.discover_v2, discover_v2: capabilities.discover_v2,
}, },

View file

@ -211,10 +211,10 @@ export function initializeEditApi(
return false; return false;
} }
return ( return (
Boolean(capabilities.visualize_v2?.save) || Boolean(capabilities.visualize_v2.save) ||
(!getState().savedObjectId && (!getState().savedObjectId &&
Boolean(capabilities.dashboard_v2?.showWriteControls) && Boolean(capabilities.dashboard_v2?.showWriteControls) &&
Boolean(capabilities.visualize_v2?.show)) Boolean(capabilities.visualize_v2.show))
); );
}; };

View file

@ -17,7 +17,7 @@ export const convertToLensActionFactory =
type: ACTION_CONVERT_TO_LENS, type: ACTION_CONVERT_TO_LENS,
id, id,
getDisplayName: () => displayName, getDisplayName: () => displayName,
isCompatible: async () => !!application.capabilities.visualize_v2?.show, isCompatible: async () => !!application.capabilities.visualize_v2.show,
execute: async (context: { [key: string]: VisualizeEditorContext }) => { execute: async (context: { [key: string]: VisualizeEditorContext }) => {
const table = Object.values(context.layers); const table = Object.values(context.layers);
const payload = { const payload = {

View file

@ -22,7 +22,7 @@ export const visualizeDashboardVisualizePanelction = (application: ApplicationSt
i18n.translate('xpack.lens.visualizeLegacyVisualizationChart', { i18n.translate('xpack.lens.visualizeLegacyVisualizationChart', {
defaultMessage: 'Visualize legacy visualization chart', defaultMessage: 'Visualize legacy visualization chart',
}), }),
isCompatible: async () => !!application.capabilities.visualize_v2?.show, isCompatible: async () => !!application.capabilities.visualize_v2.show,
execute: async (context: { [key: string]: VisualizeEditorContext }) => { execute: async (context: { [key: string]: VisualizeEditorContext }) => {
const table = Object.values(context.layers); const table = Object.values(context.layers);
const payload = { const payload = {

View file

@ -22,7 +22,7 @@ export const visualizeAggBasedVisAction = (application: ApplicationStart) =>
i18n.translate('xpack.lens.visualizeAggBasedLegend', { i18n.translate('xpack.lens.visualizeAggBasedLegend', {
defaultMessage: 'Visualize agg based chart', defaultMessage: 'Visualize agg based chart',
}), }),
isCompatible: async () => !!application.capabilities.visualize_v2?.show, isCompatible: async () => !!application.capabilities.visualize_v2.show,
execute: async (context: { [key: string]: VisualizeEditorContext }) => { execute: async (context: { [key: string]: VisualizeEditorContext }) => {
const table = Object.values(context.layers); const table = Object.values(context.layers);
const payload = { const payload = {

View file

@ -5,14 +5,8 @@
* 2.0. * 2.0.
*/ */
import type { PluginConfigDescriptor, PluginInitializerContext } from '@kbn/core-plugins-server'; import type { PluginInitializerContext } from '@kbn/core-plugins-server';
export type { LensServerPluginSetup } from './plugin'; export type { LensServerPluginSetup } from './plugin';
import type { ConfigType } from '../common/config';
import { ConfigSchema } from '../common/config';
export const config: PluginConfigDescriptor<ConfigType> = {
schema: ConfigSchema,
};
export const plugin = async (initContext: PluginInitializerContext) => { export const plugin = async (initContext: PluginInitializerContext) => {
const { LensServerPlugin } = await import('./plugin'); const { LensServerPlugin } = await import('./plugin');

View file

@ -51,7 +51,7 @@ export interface LensServerPluginSetup {
/** /**
* Server side embeddable definition which provides migrations to run if Lens state is embedded into another saved object somewhere * Server side embeddable definition which provides migrations to run if Lens state is embedded into another saved object somewhere
*/ */
lensEmbeddableFactory: ReturnType<typeof makeLensEmbeddableFactory> | undefined; lensEmbeddableFactory: ReturnType<typeof makeLensEmbeddableFactory>;
/** /**
* Register custom migration functions for custom third party Lens visualizations * Register custom migration functions for custom third party Lens visualizations
*/ */

View file

@ -17,6 +17,7 @@
"requiredPlugins": [ "requiredPlugins": [
"controls", "controls",
"unifiedSearch", "unifiedSearch",
"lens",
"licensing", "licensing",
"features", "features",
"inspector", "inspector",
@ -26,6 +27,7 @@
"uiActions", "uiActions",
"navigation", "navigation",
"expressions", "expressions",
"visualizations",
"embeddable", "embeddable",
"mapsEms", "mapsEms",
"share", "share",
@ -37,26 +39,22 @@
"customIntegrations", "customIntegrations",
"embeddableEnhanced", "embeddableEnhanced",
"home", "home",
"lens",
"savedObjectsTagging", "savedObjectsTagging",
"charts", "charts",
"screenshotMode", "screenshotMode",
"security", "security",
"spaces", "spaces",
"usageCollection", "usageCollection",
"serverless", "serverless"
"visualizations"
], ],
"requiredBundles": [ "requiredBundles": [
"kibanaReact", "kibanaReact",
"kibanaUtils", "kibanaUtils",
"lens",
"usageCollection", "usageCollection",
"unifiedSearch", "unifiedSearch",
"fieldFormats", "fieldFormats",
"esql", "esql",
"savedObjects", "savedObjects"
"visualizations"
], ],
"extraPublicDirs": [ "extraPublicDirs": [
"common" "common"

View file

@ -93,7 +93,7 @@ export interface MapsPluginSetupDependencies {
inspector: InspectorSetupContract; inspector: InspectorSetupContract;
home?: HomePublicPluginSetup; home?: HomePublicPluginSetup;
lens: LensPublicSetup; lens: LensPublicSetup;
visualizations?: VisualizationsSetup; visualizations: VisualizationsSetup;
embeddable: EmbeddableSetup; embeddable: EmbeddableSetup;
share: SharePluginSetup; share: SharePluginSetup;
licensing: LicensingPluginSetup; licensing: LicensingPluginSetup;
@ -188,7 +188,7 @@ export class MapsPlugin
if (plugins.home) { if (plugins.home) {
plugins.home.featureCatalogue.register(featureCatalogueEntry); plugins.home.featureCatalogue.register(featureCatalogueEntry);
} }
plugins.visualizations?.registerAlias(mapsVisTypeAlias); plugins.visualizations.registerAlias(mapsVisTypeAlias);
core.application.register({ core.application.register({
id: APP_ID, id: APP_ID,
@ -230,10 +230,10 @@ export class MapsPlugin
plugins.data.search.aggs.types.registerLegacy(GEOHASH_GRID, getGeoHashBucketAgg); plugins.data.search.aggs.types.registerLegacy(GEOHASH_GRID, getGeoHashBucketAgg);
plugins.expressions.registerFunction(createRegionMapFn); plugins.expressions.registerFunction(createRegionMapFn);
plugins.expressions.registerRenderer(regionMapRenderer); plugins.expressions.registerRenderer(regionMapRenderer);
plugins.visualizations?.createBaseVisualization(regionMapVisType); plugins.visualizations.createBaseVisualization(regionMapVisType);
plugins.expressions.registerFunction(createTileMapFn); plugins.expressions.registerFunction(createTileMapFn);
plugins.expressions.registerRenderer(tileMapRenderer); plugins.expressions.registerRenderer(tileMapRenderer);
plugins.visualizations?.createBaseVisualization(tileMapVisType); plugins.visualizations.createBaseVisualization(tileMapVisType);
setIsCloudEnabled(!!plugins.cloud?.isCloudEnabled); setIsCloudEnabled(!!plugins.cloud?.isCloudEnabled);

View file

@ -36,7 +36,7 @@ export function initializeEditApi(
}); });
}, },
isEditingEnabled: () => { isEditingEnabled: () => {
return getMapsCapabilities()?.save as boolean; return getMapsCapabilities().save as boolean;
}, },
getEditHref: async () => { getEditHref: async () => {
return getHttp().basePath.prepend(getFullPath(savedObjectId)); return getHttp().basePath.prepend(getFullPath(savedObjectId));

View file

@ -36,7 +36,7 @@ export function initializeLibraryTransforms(
return { return {
canLinkToLibrary: async () => { canLinkToLibrary: async () => {
const { maps_v2: maps } = getCore().application.capabilities; const { maps_v2: maps } = getCore().application.capabilities;
return maps?.save && savedMap.getSavedObjectId() === undefined; return maps.save && savedMap.getSavedObjectId() === undefined;
}, },
saveToLibrary: async (title: string) => { saveToLibrary: async (title: string) => {
const state = serializeState(); const state = serializeState();

View file

@ -50,7 +50,7 @@ export const DEFAULT_MAP_UI_STATE = {
flyoutDisplay: FLYOUT_STATE.NONE, flyoutDisplay: FLYOUT_STATE.NONE,
drawMode: DRAW_MODE.NONE, drawMode: DRAW_MODE.NONE,
isFullScreen: false, isFullScreen: false,
isReadOnly: !getMapsCapabilities()?.save, isReadOnly: !getMapsCapabilities().save,
isLayerTOCOpen: DEFAULT_IS_LAYER_TOC_OPEN, isLayerTOCOpen: DEFAULT_IS_LAYER_TOC_OPEN,
isTimesliderOpen: false, isTimesliderOpen: false,
// storing TOC detail visibility outside of map.layerList because its UI state and not map rendering state. // storing TOC detail visibility outside of map.layerList because its UI state and not map rendering state.

View file

@ -29,7 +29,7 @@ import { registerLayerWizards } from './classes/layers/wizards/load_layer_wizard
import { MapSerializedState } from './react_embeddable/types'; import { MapSerializedState } from './react_embeddable/types';
function setAppChrome() { function setAppChrome() {
if (!getMapsCapabilities()?.save) { if (!getMapsCapabilities().save) {
getCoreChrome().setBadge({ getCoreChrome().setBadge({
text: i18n.translate('xpack.maps.badge.readOnly.text', { text: i18n.translate('xpack.maps.badge.readOnly.text', {
defaultMessage: 'Read only', defaultMessage: 'Read only',

View file

@ -69,7 +69,7 @@ function MapsListViewComp({ history }: Props) {
page: 'list', page: 'list',
}); });
const isReadOnly = !getMapsCapabilities()?.save; const isReadOnly = !getMapsCapabilities().save;
const initialPageSize = getUiSettings().get(SAVED_OBJECTS_PER_PAGE_SETTING); const initialPageSize = getUiSettings().get(SAVED_OBJECTS_PER_PAGE_SETTING);
// TLDR; render should be side effect free // TLDR; render should be side effect free

View file

@ -112,7 +112,7 @@ export function getTopNavConfig({
}); });
} }
if (getMapsCapabilities()?.save) { if (getMapsCapabilities().save) {
const hasSaveAndReturnConfig = savedMap.hasSaveAndReturnConfig(); const hasSaveAndReturnConfig = savedMap.hasSaveAndReturnConfig();
const mapDescription = savedMap.getAttributes().description const mapDescription = savedMap.getAttributes().description
? savedMap.getAttributes().description! ? savedMap.getAttributes().description!

View file

@ -26,7 +26,7 @@ export const visualizeGeoFieldAction = createAction<VisualizeFieldContext>({
defaultMessage: 'Visualize in Maps', defaultMessage: 'Visualize in Maps',
}), }),
isCompatible: async (context) => { isCompatible: async (context) => {
return Boolean(!!getVisualizeCapabilities()?.show && context.dataViewSpec && context.fieldName); return Boolean(!!getVisualizeCapabilities().show && context.dataViewSpec && context.fieldName);
}, },
getHref: async (context) => { getHref: async (context) => {
const { app, path } = await getMapsLink(context); const { app, path } = await getMapsLink(context);

View file

@ -13,7 +13,6 @@ export interface MapsConfigType {
} }
export const configSchema = schema.object({ export const configSchema = schema.object({
enabled: schema.boolean({ defaultValue: true }),
// flag used in functional testing // flag used in functional testing
showMapsInspectorAdapter: schema.boolean({ defaultValue: false }), showMapsInspectorAdapter: schema.boolean({ defaultValue: false }),
// flag used in functional testing // flag used in functional testing

View file

@ -22,6 +22,7 @@
"embeddable", "embeddable",
"features", "features",
"fieldFormats", "fieldFormats",
"lens",
"licensing", "licensing",
"share", "share",
"taskManager", "taskManager",
@ -38,7 +39,6 @@
"alerting", "alerting",
"dashboard", "dashboard",
"home", "home",
"lens",
"licenseManagement", "licenseManagement",
"management", "management",
"maps", "maps",

View file

@ -29,6 +29,7 @@
"dashboard", "dashboard",
"licensing", "licensing",
"expressions", "expressions",
"lens",
"controls", "controls",
"embeddable", "embeddable",
"fieldsMetadata", "fieldsMetadata",
@ -38,7 +39,6 @@
"cloud", "cloud",
"features", "features",
"home", "home",
"lens",
"spaces", "spaces",
"serverless", "serverless",
"share" "share"

View file

@ -23,6 +23,7 @@
"features", "features",
"files", "files",
"inspector", "inspector",
"lens",
"observabilityShared", "observabilityShared",
"security", "security",
"share", "share",
@ -33,7 +34,6 @@
"discover", "discover",
"embeddable", "embeddable",
"home", "home",
"lens",
"licensing", "licensing",
"spaces", "spaces",
"usageCollection", "usageCollection",

View file

@ -23,6 +23,7 @@
"embeddable", "embeddable",
"features", "features",
"fieldFormats", "fieldFormats",
"lens",
"logsShared", "logsShared",
"metricsDataAccess", "metricsDataAccess",
"observability", "observability",
@ -41,7 +42,6 @@
"fieldsMetadata" "fieldsMetadata"
], ],
"optionalPlugins": [ "optionalPlugins": [
"lens",
"spaces", "spaces",
"ml", "ml",
"home", "home",

View file

@ -32,12 +32,14 @@
"features", "features",
"files", "files",
"inspector", "inspector",
"lens",
"observabilityShared", "observabilityShared",
"ruleRegistry", "ruleRegistry",
"triggersActionsUi", "triggersActionsUi",
"security", "security",
"share", "share",
"unifiedSearch", "unifiedSearch",
"visualizations",
"dashboard", "dashboard",
"expressions", "expressions",
"logsShared", "logsShared",
@ -48,7 +50,6 @@
"optionalPlugins": [ "optionalPlugins": [
"discover", "discover",
"home", "home",
"lens",
"usageCollection", "usageCollection",
"cloud", "cloud",
"spaces", "spaces",
@ -56,8 +57,7 @@
"guidedOnboarding", "guidedOnboarding",
"observabilityAIAssistant", "observabilityAIAssistant",
"investigate", "investigate",
"streams", "streams"
"visualizations"
], ],
"requiredBundles": [ "requiredBundles": [
"data", "data",

View file

@ -22,6 +22,7 @@
"actions", "actions",
"data", "data",
"dataViews", "dataViews",
"lens",
"ruleRegistry", "ruleRegistry",
"uiActions", "uiActions",
"triggersActionsUi", "triggersActionsUi",
@ -37,8 +38,7 @@
"llmTasks" "llmTasks"
], ],
"optionalPlugins": [ "optionalPlugins": [
"cloud", "cloud"
"lens"
], ],
"requiredBundles": [ "requiredBundles": [
"kibanaReact", "kibanaReact",

View file

@ -28,15 +28,15 @@
"searchprofiler", "searchprofiler",
"security", "security",
"serverless", "serverless",
"share" "share",
"visualizations"
], ],
"optionalPlugins": [ "optionalPlugins": [
"indexManagement", "indexManagement",
"contentConnectors", "contentConnectors",
"searchInferenceEndpoints", "searchInferenceEndpoints",
"searchPlayground", "searchPlayground",
"usageCollection", "usageCollection"
"visualizations"
], ],
"requiredBundles": [ "requiredBundles": [
"kibanaReact" "kibanaReact"

View file

@ -31,7 +31,9 @@
"features", "features",
"fieldFormats", "fieldFormats",
"inspector", "inspector",
"lens",
"licensing", "licensing",
"maps",
"navigation", "navigation",
"ruleRegistry", "ruleRegistry",
"sessionView", "sessionView",
@ -68,9 +70,7 @@
"usageCollection", "usageCollection",
"lists", "lists",
"home", "home",
"lens",
"management", "management",
"maps",
"dataViewFieldEditor", "dataViewFieldEditor",
"osquery", "osquery",
"savedObjectsTaggingOss", "savedObjectsTaggingOss",
@ -84,12 +84,10 @@
"kibanaUtils", "kibanaUtils",
"kibanaReact", "kibanaReact",
"usageCollection", "usageCollection",
"lens",
"lists", "lists",
"ml", "ml",
"unifiedSearch", "unifiedSearch",
"esql", "esql"
"maps"
], ],
"extraPublicDirs": [ "extraPublicDirs": [
"common" "common"

View file

@ -1,30 +0,0 @@
/*
* 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 { login } from '../../../tasks/login';
import { visit } from '../../../tasks/navigation';
import { APP_NOT_FOUND_PAGE } from '../../../screens/ai_soc';
import { VISUALIZE_URL, MAPS_URL, LENS_URL } from '../../../urls/navigation';
describe('Disabled features', { tags: '@serverless' }, () => {
beforeEach(() => {
login('admin');
});
it('visualize app should not be available', () => {
visit(VISUALIZE_URL);
cy.get(APP_NOT_FOUND_PAGE).should('exist');
});
it('maps app should not be available', () => {
visit(MAPS_URL);
cy.get(APP_NOT_FOUND_PAGE).should('exist');
});
it('lens app should not be available', () => {
visit(LENS_URL);
cy.get(APP_NOT_FOUND_PAGE).should('exist');
});
});

View file

@ -86,6 +86,7 @@ describe('Capabilities', { tags: '@serverless' }, () => {
after(() => role.teardown()); after(() => role.teardown());
// Individual test cases with clear descriptions
it('should show alert summary prompt when visiting alert summary page', () => { it('should show alert summary prompt when visiting alert summary page', () => {
visit(ALERT_SUMMARY_URL); visit(ALERT_SUMMARY_URL);
cy.get(ALERTS_SUMMARY_PROMPT).should('exist'); cy.get(ALERTS_SUMMARY_PROMPT).should('exist');

View file

@ -4,12 +4,3 @@
* 2.0; you may not use this file except in compliance with the Elastic License * 2.0; you may not use this file except in compliance with the Elastic License
* 2.0. * 2.0.
*/ */
import type { TypeOf } from '@kbn/config-schema';
import { schema } from '@kbn/config-schema';
export const ConfigSchema = schema.object({
enabled: schema.boolean({ defaultValue: true }),
});
export type ConfigType = TypeOf<typeof ConfigSchema>;

View file

@ -9,4 +9,3 @@ export const ALERTS_SUMMARY_PROMPT = '[data-test-subj="alert-summary-landing-pag
export const GET_STARTED_PAGE = '[data-test-subj="onboarding-hub-page"]'; export const GET_STARTED_PAGE = '[data-test-subj="onboarding-hub-page"]';
export const AI_SOC_NAVIGATION = export const AI_SOC_NAVIGATION =
'[data-test-subj="nav-item nav-item-security_solution_ai_nav nav-item-id-security_solution_ai_nav"]'; '[data-test-subj="nav-item nav-item-security_solution_ai_nav nav-item-id-security_solution_ai_nav"]';
export const APP_NOT_FOUND_PAGE = '[data-test-subj="appNotFoundPageContent"]';

View file

@ -91,9 +91,6 @@ export const exceptionsListDetailsUrl = (listId: string) =>
export const DISCOVER_URL = '/app/discover'; export const DISCOVER_URL = '/app/discover';
export const OSQUERY_URL = '/app/osquery'; export const OSQUERY_URL = '/app/osquery';
export const FLEET_URL = '/app/fleet'; export const FLEET_URL = '/app/fleet';
export const VISUALIZE_URL = '/app/visualize';
export const MAPS_URL = '/app/maps';
export const LENS_URL = '/app/lens';
// Entity Analytics // Entity Analytics
export const ENTITY_ANALYTICS_DASHBOARD_URL = '/app/security/entity_analytics'; export const ENTITY_ANALYTICS_DASHBOARD_URL = '/app/security/entity_analytics';