From 34e26250f0ff7033cbe2d96fddf00264a5b65599 Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Wed, 24 May 2023 15:28:25 -0600 Subject: [PATCH] [tsvb] read only mode (#157920) part of https://github.com/elastic/kibana/issues/154307 PR adds ability to put TSVB into read only mode - preventing TSVB visualizations from being created and edited. To test: * start kibana with `yarn start --serverless=es` * add `vis_type_timeseries.readOnly: true` to kibana.yml Visualization public plugin changes: * Removes `hideTypes` from VisualizationSetup contract. Used by Maps plugin to set "hidden" to true for tile_map and region_map visualization types. In 8.0, tile_map and region_map visualization type registration moved into maps plugin so `hideTypes` no longer needed. * Renamed vis type definition `hidden` to `disableCreate`. * Added `disableEdit` to vis type definition. * Hide edit link in dashboard panel options when `disableEdit` is true * Does not display links and edit action in listing table when `disableEdit` is true Visualization server plugin changes: * Add `readOnlyVisType` registry to set up contract * Update visualization savedObject.management.getInAppUrl to return undefined when vis type has been registered as readOnly. * Prevents "readOnly "visualization types from being displayed in global search results * Prevents "readOnly "visualization types from having links in saved object management listing table. Timeseries server plugin changes: * Add `readOnly` yaml configuration * Expose `readOnly` yaml configuration to public * When `readOnly` is true, call VisualizationsServerSetup.registerReadOnlyVisType to mark vis type as read only Timeseries public plugin changes: * Set disableCreate and disableEdit to true when `readOnly` is true --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Stratoula Kalafateli --- .../src/components/item_details.tsx | 6 ++-- .../table_list/src/table_list_view.tsx | 10 ++++++ .../src/saved_objects_management.ts | 12 ++++--- .../dashboard_app/top_nav/editor_menu.tsx | 2 +- .../public/input_control_vis_type.ts | 2 +- .../vis_types/timeseries/common/constants.ts | 1 + .../timeseries/{server => }/config.ts | 11 ++++++ .../vis_types/timeseries/public/index.ts | 5 ++- .../timeseries/public/metrics_type.ts | 4 +-- .../vis_types/timeseries/public/plugin.ts | 13 +++++-- .../vis_types/timeseries/server/index.ts | 7 +++- .../vis_types/timeseries/server/plugin.ts | 10 +++++- .../embeddable/visualize_embeddable.tsx | 8 +++-- src/plugins/visualizations/public/mocks.ts | 1 - .../utils/saved_visualize_utils.test.ts | 3 ++ .../public/utils/saved_visualize_utils.ts | 2 ++ .../public/vis_types/base_vis_type.ts | 6 ++-- .../visualizations/public/vis_types/types.ts | 5 ++- .../public/vis_types/types_service.ts | 18 ---------- .../vis_types/vis_type_alias_registry.ts | 9 ++++- .../components/visualize_listing.tsx | 11 ++++-- .../utils/use/use_saved_vis_instance.ts | 10 ++++++ .../agg_based_selection.tsx | 2 +- .../group_selection/group_selection.tsx | 6 ++-- .../public/wizard/new_vis_modal.test.tsx | 3 +- src/plugins/visualizations/server/index.ts | 2 +- src/plugins/visualizations/server/plugin.ts | 8 ++--- .../saved_objects/get_in_app_url.test.ts | 36 +++++++++++++++++++ .../server/saved_objects/get_in_app_url.ts | 29 +++++++++++++++ .../server/saved_objects/index.ts | 1 + .../read_only_vis_type_registry.ts | 17 +++++++++ .../server/saved_objects/visualization.ts | 8 ++--- src/plugins/visualizations/server/types.ts | 7 ++-- .../test_suites/core_plugins/rendering.ts | 1 + .../editor_menu/editor_menu.tsx | 2 +- .../saved_objects/map_object_to_result.ts | 6 ++-- .../region_map/region_map_vis_type.tsx | 1 + .../tile_map/tile_map_vis_type.tsx | 1 + .../maps/public/maps_vis_type_alias.ts | 6 ++-- x-pack/plugins/maps/public/plugin.ts | 2 +- 40 files changed, 221 insertions(+), 73 deletions(-) rename src/plugins/vis_types/timeseries/{server => }/config.ts (75%) create mode 100644 src/plugins/visualizations/server/saved_objects/get_in_app_url.test.ts create mode 100644 src/plugins/visualizations/server/saved_objects/get_in_app_url.ts create mode 100644 src/plugins/visualizations/server/saved_objects/read_only_vis_type_registry.ts diff --git a/packages/content-management/table_list/src/components/item_details.tsx b/packages/content-management/table_list/src/components/item_details.tsx index ccfbb5e3ea55a..b7f4186438b66 100644 --- a/packages/content-management/table_list/src/components/item_details.tsx +++ b/packages/content-management/table_list/src/components/item_details.tsx @@ -7,7 +7,7 @@ */ import React, { useCallback, useMemo } from 'react'; -import { EuiText, EuiLink, EuiTitle, EuiSpacer, EuiHighlight } from '@elastic/eui'; +import { EuiText, EuiLink, EuiSpacer, EuiHighlight } from '@elastic/eui'; import { RedirectAppLinks } from '@kbn/shared-ux-link-redirect-app'; import type { Tag } from '../types'; @@ -104,9 +104,9 @@ export function ItemDetails({ return (
- {renderTitle()} + {renderTitle()} {Boolean(description) && ( - +

{description!} diff --git a/packages/content-management/table_list/src/table_list_view.tsx b/packages/content-management/table_list/src/table_list_view.tsx index 2191a3c9b7eee..030aaad0527a9 100644 --- a/packages/content-management/table_list/src/table_list_view.tsx +++ b/packages/content-management/table_list/src/table_list_view.tsx @@ -87,7 +87,14 @@ export interface Props void; createItem?(): void; deleteItems?(items: T[]): Promise; + /** + * Edit action onClick handler. Edit action not provided when property is not provided + */ editItem?(item: T): void; + /** + * Handler to set edit action visiblity per item. + */ + showEditActionForItem?(item: T): boolean; /** * Name for the column containing the "title" value. */ @@ -251,6 +258,7 @@ function TableListViewComp({ findItems, createItem, editItem, + showEditActionForItem, deleteItems, getDetailViewLink, onClickTitle, @@ -523,6 +531,7 @@ function TableListViewComp({ ), icon: 'pencil', type: 'icon', + available: (v) => (showEditActionForItem ? showEditActionForItem(v) : true), enabled: (v) => !(v as unknown as { error: string })?.error, onClick: editItem, }); @@ -577,6 +586,7 @@ function TableListViewComp({ DateFormatterComp, contentEditor, inspectItem, + showEditActionForItem, ]); const itemsById = useMemo(() => { diff --git a/packages/core/saved-objects/core-saved-objects-server/src/saved_objects_management.ts b/packages/core/saved-objects/core-saved-objects-server/src/saved_objects_management.ts index 5d72112cbb049..7d8608cd1479b 100644 --- a/packages/core/saved-objects/core-saved-objects-server/src/saved_objects_management.ts +++ b/packages/core/saved-objects/core-saved-objects-server/src/saved_objects_management.ts @@ -56,14 +56,16 @@ export interface SavedObjectsTypeManagementDefinition { * Function returning the url to use to redirect to this object from the management section. * If not defined, redirecting to the object will not be allowed. * - * @returns an object containing a `path` and `uiCapabilitiesPath` properties. the `path` is the path to + * @returns undefined or an object containing a `path` and `uiCapabilitiesPath` properties. the `path` is the path to * the object page, relative to the base path. `uiCapabilitiesPath` is the path to check in the * {@link Capabilities | uiCapabilities} to check if the user has permission to access the object. */ - getInAppUrl?: (savedObject: SavedObject) => { - path: string; - uiCapabilitiesPath: string; - }; + getInAppUrl?: (savedObject: SavedObject) => + | { + path: string; + uiCapabilitiesPath: string; + } + | undefined; /** * An optional export transform function that can be used transform the objects of the registered type during * the export process. diff --git a/src/plugins/dashboard/public/dashboard_app/top_nav/editor_menu.tsx b/src/plugins/dashboard/public/dashboard_app/top_nav/editor_menu.tsx index 9b1259ffdd3cd..806045c20eca6 100644 --- a/src/plugins/dashboard/public/dashboard_app/top_nav/editor_menu.tsx +++ b/src/plugins/dashboard/public/dashboard_app/top_nav/editor_menu.tsx @@ -96,7 +96,7 @@ export const EditorMenu = ({ createNewVisType, createNewEmbeddable }: Props) => } return 0; }) - .filter(({ hidden, stage }: BaseVisType) => !hidden); + .filter(({ disableCreate, stage }: BaseVisType) => !disableCreate); const promotedVisTypes = getSortedVisTypesByGroup(VisGroups.PROMOTED); const aggsBasedVisTypes = getSortedVisTypesByGroup(VisGroups.AGGBASED); diff --git a/src/plugins/input_control_vis/public/input_control_vis_type.ts b/src/plugins/input_control_vis/public/input_control_vis_type.ts index a0cfa84902dce..c25a1b53be33f 100644 --- a/src/plugins/input_control_vis/public/input_control_vis_type.ts +++ b/src/plugins/input_control_vis/public/input_control_vis_type.ts @@ -29,7 +29,7 @@ export function createInputControlVisTypeDefinition( defaultMessage: 'Input controls are deprecated and will be removed in a future version.', }), stage: 'experimental', - hidden: true, + disableCreate: true, isDeprecated: true, visConfig: { defaults: { diff --git a/src/plugins/vis_types/timeseries/common/constants.ts b/src/plugins/vis_types/timeseries/common/constants.ts index cbaf275cc0092..5e653df857eb9 100644 --- a/src/plugins/vis_types/timeseries/common/constants.ts +++ b/src/plugins/vis_types/timeseries/common/constants.ts @@ -20,3 +20,4 @@ export const ROUTES = { }; export const USE_KIBANA_INDEXES_KEY = 'use_kibana_indexes'; export const TSVB_DEFAULT_COLOR = '#68BC00'; +export const VIS_TYPE = 'metrics'; diff --git a/src/plugins/vis_types/timeseries/server/config.ts b/src/plugins/vis_types/timeseries/config.ts similarity index 75% rename from src/plugins/vis_types/timeseries/server/config.ts rename to src/plugins/vis_types/timeseries/config.ts index 5a44b3639a1f3..7b3dbbb0d6c2d 100644 --- a/src/plugins/vis_types/timeseries/server/config.ts +++ b/src/plugins/vis_types/timeseries/config.ts @@ -11,6 +11,13 @@ import { schema, TypeOf } from '@kbn/config-schema'; export const config = schema.object({ enabled: schema.boolean({ defaultValue: true }), + readOnly: schema.conditional( + schema.contextRef('serverless'), + true, + schema.maybe(schema.boolean({ defaultValue: false })), + schema.never() + ), + /** @deprecated **/ chartResolution: schema.number({ defaultValue: 150 }), /** @deprecated **/ @@ -18,3 +25,7 @@ export const config = schema.object({ }); export type VisTypeTimeseriesConfig = TypeOf; + +export interface VisTypeTimeseriesPublicConfig { + readOnly?: boolean; +} diff --git a/src/plugins/vis_types/timeseries/public/index.ts b/src/plugins/vis_types/timeseries/public/index.ts index d0051df4de71e..8574f4922f772 100644 --- a/src/plugins/vis_types/timeseries/public/index.ts +++ b/src/plugins/vis_types/timeseries/public/index.ts @@ -7,8 +7,11 @@ */ import { PluginInitializerContext } from '@kbn/core/public'; +import { VisTypeTimeseriesPublicConfig } from '../config'; import { MetricsPlugin as Plugin } from './plugin'; -export function plugin(initializerContext: PluginInitializerContext) { +export function plugin( + initializerContext: PluginInitializerContext +) { return new Plugin(initializerContext); } diff --git a/src/plugins/vis_types/timeseries/public/metrics_type.ts b/src/plugins/vis_types/timeseries/public/metrics_type.ts index 2390894b5d69d..4409faf7c0827 100644 --- a/src/plugins/vis_types/timeseries/public/metrics_type.ts +++ b/src/plugins/vis_types/timeseries/public/metrics_type.ts @@ -23,7 +23,7 @@ import { extractIndexPatternValues, isStringTypeIndexPattern, } from '../common/index_patterns_utils'; -import { TSVB_DEFAULT_COLOR, UI_SETTINGS } from '../common/constants'; +import { TSVB_DEFAULT_COLOR, UI_SETTINGS, VIS_TYPE } from '../common/constants'; import { toExpressionAst } from './to_ast'; import { getDataViewsStart, getUISettings } from './services'; import type { TimeseriesVisDefaultParams, TimeseriesVisParams } from './types'; @@ -99,7 +99,7 @@ async function getUsedIndexPatterns(params: VisParams): Promise { export const metricsVisDefinition: VisTypeDefinition< TimeseriesVisParams | TimeseriesVisDefaultParams > = { - name: 'metrics', + name: VIS_TYPE, title: i18n.translate('visTypeTimeseries.kbnVisTypes.metricsTitle', { defaultMessage: 'TSVB' }), description: i18n.translate('visTypeTimeseries.kbnVisTypes.metricsDescription', { defaultMessage: 'Perform advanced analysis of your time series data.', diff --git a/src/plugins/vis_types/timeseries/public/plugin.ts b/src/plugins/vis_types/timeseries/public/plugin.ts index 6054a0dcd3d3b..84d71cdfeddfd 100644 --- a/src/plugins/vis_types/timeseries/public/plugin.ts +++ b/src/plugins/vis_types/timeseries/public/plugin.ts @@ -20,6 +20,7 @@ import type { HttpSetup } from '@kbn/core-http-browser'; import type { ThemeServiceStart } from '@kbn/core-theme-browser'; import type { DocLinksStart } from '@kbn/core-doc-links-browser'; import type { IStorageWrapper } from '@kbn/kibana-utils-plugin/public'; +import { VisTypeTimeseriesPublicConfig } from '../config'; import { EditorController, TSVB_EDITOR_NAME } from './application/editor_controller'; @@ -71,13 +72,15 @@ export interface TimeseriesVisDependencies extends Partial { /** @internal */ export class MetricsPlugin implements Plugin { - initializerContext: PluginInitializerContext; + initializerContext: PluginInitializerContext; - constructor(initializerContext: PluginInitializerContext) { + constructor(initializerContext: PluginInitializerContext) { this.initializerContext = initializerContext; } public setup(core: CoreSetup, { expressions, visualizations }: MetricsPluginSetupDependencies) { + const { readOnly } = this.initializerContext.config.get(); + visualizations.visEditorsRegistry.register(TSVB_EDITOR_NAME, EditorController); expressions.registerFunction(createMetricsFn); expressions.registerRenderer( @@ -87,7 +90,11 @@ export class MetricsPlugin implements Plugin { }) ); setUISettings(core.uiSettings); - visualizations.createBaseVisualization(metricsVisDefinition); + visualizations.createBaseVisualization({ + ...metricsVisDefinition, + disableCreate: Boolean(readOnly), + disableEdit: Boolean(readOnly), + }); } public start( diff --git a/src/plugins/vis_types/timeseries/server/index.ts b/src/plugins/vis_types/timeseries/server/index.ts index ee274ecd6d859..0faecf4c03adc 100644 --- a/src/plugins/vis_types/timeseries/server/index.ts +++ b/src/plugins/vis_types/timeseries/server/index.ts @@ -7,12 +7,17 @@ */ import { PluginInitializerContext, PluginConfigDescriptor } from '@kbn/core/server'; -import { VisTypeTimeseriesConfig, config as configSchema } from './config'; +import { VisTypeTimeseriesConfig, config as configSchema } from '../config'; import { VisTypeTimeseriesPlugin } from './plugin'; export type { VisTypeTimeseriesSetup } from './plugin'; export const config: PluginConfigDescriptor = { + // exposeToBrowser specifies kibana.yml settings to expose to the browser + // the value `true` in this context signals configuration is exposed to browser + exposeToBrowser: { + readOnly: true, + }, schema: configSchema, }; diff --git a/src/plugins/vis_types/timeseries/server/plugin.ts b/src/plugins/vis_types/timeseries/server/plugin.ts index c7a642a1d404a..194c6388bac80 100644 --- a/src/plugins/vis_types/timeseries/server/plugin.ts +++ b/src/plugins/vis_types/timeseries/server/plugin.ts @@ -24,7 +24,9 @@ import type { DataViewsService } from '@kbn/data-views-plugin/common'; import type { FieldFormatsStart } from '@kbn/field-formats-plugin/server'; import type { PluginStart as DataViewsPublicPluginStart } from '@kbn/data-views-plugin/server'; import type { FieldFormatsRegistry } from '@kbn/field-formats-plugin/common'; -import { VisTypeTimeseriesConfig } from './config'; +import type { VisualizationsServerSetup } from '@kbn/visualizations-plugin/server'; +import { VIS_TYPE } from '../common/constants'; +import { VisTypeTimeseriesConfig } from '../config'; import { getVisData } from './lib/get_vis_data'; import { visDataRoutes } from './routes/vis'; import { fieldsRoutes } from './routes/fields'; @@ -47,6 +49,7 @@ export interface LegacySetup { interface VisTypeTimeseriesPluginSetupDependencies { home?: HomeServerPluginSetup; + visualizations: VisualizationsServerSetup; } interface VisTypeTimeseriesPluginStartDependencies { @@ -126,6 +129,11 @@ export class VisTypeTimeseriesPlugin implements Plugin { visDataRoutes(router, framework); fieldsRoutes(router, framework); + const { readOnly } = this.initializerContext.config.get(); + if (readOnly) { + plugins.visualizations.registerReadOnlyVisType(VIS_TYPE); + } + return { getVisData: async ( requestContext: VisTypeTimeseriesRequestHandlerContext, diff --git a/src/plugins/visualizations/public/embeddable/visualize_embeddable.tsx b/src/plugins/visualizations/public/embeddable/visualize_embeddable.tsx index a8eaca1552cd0..9d34d2fb26ca6 100644 --- a/src/plugins/visualizations/public/embeddable/visualize_embeddable.tsx +++ b/src/plugins/visualizations/public/embeddable/visualize_embeddable.tsx @@ -170,10 +170,12 @@ export class VisualizeEmbeddable this.attributeService = attributeService; if (this.attributeService) { + const readOnly = Boolean(vis.type.disableEdit); const isByValue = !this.inputIsRefType(initialInput); - const editable = - capabilities.visualizeSave || - (isByValue && capabilities.dashboardSave && capabilities.visualizeOpen); + const editable = readOnly + ? false + : capabilities.visualizeSave || + (isByValue && capabilities.dashboardSave && capabilities.visualizeOpen); this.updateOutput({ ...this.getOutput(), editable }); } diff --git a/src/plugins/visualizations/public/mocks.ts b/src/plugins/visualizations/public/mocks.ts index f34e725044146..4a711359143a3 100644 --- a/src/plugins/visualizations/public/mocks.ts +++ b/src/plugins/visualizations/public/mocks.ts @@ -33,7 +33,6 @@ import { Schema, VisualizationsSetup, VisualizationsStart } from '.'; const createSetupContract = (): VisualizationsSetup => ({ createBaseVisualization: jest.fn(), registerAlias: jest.fn(), - hideTypes: jest.fn(), visEditorsRegistry: { registerDefault: jest.fn(), register: jest.fn(), get: jest.fn() }, }); diff --git a/src/plugins/visualizations/public/utils/saved_visualize_utils.test.ts b/src/plugins/visualizations/public/utils/saved_visualize_utils.test.ts index e45bb15f49323..d5b26fe455ac6 100644 --- a/src/plugins/visualizations/public/utils/saved_visualize_utils.test.ts +++ b/src/plugins/visualizations/public/utils/saved_visualize_utils.test.ts @@ -499,6 +499,8 @@ describe('saved_visualize_utils', () => { }, { id: 'wat', + image: undefined, + readOnly: false, references: undefined, icon: undefined, savedObjectType: 'visualization', @@ -506,6 +508,7 @@ describe('saved_visualize_utils', () => { type: 'test', typeName: 'test', typeTitle: undefined, + updatedAt: undefined, title: 'WATEVER', url: '#/edit/wat', }, diff --git a/src/plugins/visualizations/public/utils/saved_visualize_utils.ts b/src/plugins/visualizations/public/utils/saved_visualize_utils.ts index 931d00c5b9d33..9232504e026d3 100644 --- a/src/plugins/visualizations/public/utils/saved_visualize_utils.ts +++ b/src/plugins/visualizations/public/utils/saved_visualize_utils.ts @@ -80,6 +80,7 @@ export function mapHitSource( image?: BaseVisType['image']; typeTitle?: BaseVisType['title']; error?: string; + readOnly?: boolean; } = { id, references, @@ -108,6 +109,7 @@ export function mapHitSource( newAttributes.image = newAttributes.type?.image; newAttributes.typeTitle = newAttributes.type?.title; newAttributes.editUrl = `/edit/${id}`; + newAttributes.readOnly = Boolean(visTypes.get(typeName as string)?.disableEdit); return newAttributes; } diff --git a/src/plugins/visualizations/public/vis_types/base_vis_type.ts b/src/plugins/visualizations/public/vis_types/base_vis_type.ts index 4253b134cb748..2625781c26429 100644 --- a/src/plugins/visualizations/public/vis_types/base_vis_type.ts +++ b/src/plugins/visualizations/public/vis_types/base_vis_type.ts @@ -38,7 +38,8 @@ export class BaseVisType { public readonly options: VisTypeOptions; public readonly visConfig; public readonly editorConfig; - public hidden; + public readonly disableCreate; + public readonly disableEdit; public readonly requiresSearch; public readonly suppressWarnings; public readonly hasPartialRows; @@ -74,7 +75,8 @@ export class BaseVisType { this.isDeprecated = opts.isDeprecated ?? false; this.group = opts.group ?? VisGroups.AGGBASED; this.titleInWizard = opts.titleInWizard ?? ''; - this.hidden = opts.hidden ?? false; + this.disableCreate = opts.disableCreate ?? false; + this.disableEdit = opts.disableEdit ?? false; this.requiresSearch = opts.requiresSearch ?? false; this.setup = opts.setup; this.hasPartialRows = opts.hasPartialRows ?? false; diff --git a/src/plugins/visualizations/public/vis_types/types.ts b/src/plugins/visualizations/public/vis_types/types.ts index 5d581a52130a5..90f64276adf76 100644 --- a/src/plugins/visualizations/public/vis_types/types.ts +++ b/src/plugins/visualizations/public/vis_types/types.ts @@ -199,7 +199,10 @@ export interface VisTypeDefinition { readonly updateVisTypeOnParamsChange?: (params: VisParams) => string | undefined; readonly setup?: (vis: Vis) => Promise>; - hidden?: boolean; + + disableCreate?: boolean; + + disableEdit?: boolean; readonly options?: Partial; diff --git a/src/plugins/visualizations/public/vis_types/types_service.ts b/src/plugins/visualizations/public/vis_types/types_service.ts index ae8ba8b8ad518..4b20dcc1569c5 100644 --- a/src/plugins/visualizations/public/vis_types/types_service.ts +++ b/src/plugins/visualizations/public/vis_types/types_service.ts @@ -18,13 +18,8 @@ import { VisGroups } from './vis_groups_enum'; */ export class TypesService { private types: Record> = {}; - private unregisteredHiddenTypes: string[] = []; private registerVisualization(visDefinition: BaseVisType) { - if (this.unregisteredHiddenTypes.includes(visDefinition.name)) { - visDefinition.hidden = true; - } - if (this.types[visDefinition.name]) { throw new Error('type already exists!'); } @@ -47,19 +42,6 @@ export class TypesService { * @param {VisTypeAlias} config - visualization alias definition */ registerAlias: visTypeAliasRegistry.add, - /** - * allows to hide specific visualization types from create visualization dialog - * @param {string[]} typeNames - list of type ids to hide - */ - hideTypes: (typeNames: string[]): void => { - typeNames.forEach((name: string) => { - if (this.types[name]) { - this.types[name].hidden = true; - } else { - this.unregisteredHiddenTypes.push(name); - } - }); - }, }; } diff --git a/src/plugins/visualizations/public/vis_types/vis_type_alias_registry.ts b/src/plugins/visualizations/public/vis_types/vis_type_alias_registry.ts index 83a2560f667af..61e36c931390e 100644 --- a/src/plugins/visualizations/public/vis_types/vis_type_alias_registry.ts +++ b/src/plugins/visualizations/public/vis_types/vis_type_alias_registry.ts @@ -44,7 +44,14 @@ export interface VisTypeAlias { note?: string; getSupportedTriggers?: () => string[]; stage: VisualizationStage; - hidden?: boolean; + /* + * Set to true to hide visualization type in create UIs. + */ + disableCreate?: boolean; + /* + * Set to true to hide edit links for visualization type in UIs. + */ + disableEdit?: boolean; isDeprecated?: boolean; appExtensions?: { diff --git a/src/plugins/visualizations/public/visualize_app/components/visualize_listing.tsx b/src/plugins/visualizations/public/visualize_app/components/visualize_listing.tsx index 85a81154f20ec..8dd9885ef5520 100644 --- a/src/plugins/visualizations/public/visualize_app/components/visualize_listing.tsx +++ b/src/plugins/visualizations/public/visualize_app/components/visualize_listing.tsx @@ -42,6 +42,7 @@ interface VisualizeUserContent extends VisualizationListItem, UserContentCommonS description?: string; editApp: string; editUrl: string; + readOnly: boolean; error?: string; }; } @@ -65,6 +66,7 @@ const toTableListViewSavedObject = (savedObject: Record): Visua description: savedObject.description as string, editApp: savedObject.editApp as string, editUrl: savedObject.editUrl as string, + readOnly: savedObject.readOnly as boolean, error: savedObject.error as string, }, }; @@ -291,6 +293,9 @@ export const VisualizeListing = () => { findItems={fetchItems} deleteItems={visualizeCapabilities.delete ? deleteItems : undefined} editItem={visualizeCapabilities.save ? editItem : undefined} + showEditActionForItem={({ attributes: { readOnly } }) => + visualizeCapabilities.save && !readOnly + } customTableColumn={getCustomColumn()} listingLimit={listingLimit} initialPageSize={initialPageSize} @@ -310,8 +315,10 @@ export const VisualizeListing = () => { tableListTitle={i18n.translate('visualizations.listing.table.listTitle', { defaultMessage: 'Visualize Library', })} - getDetailViewLink={({ attributes: { editApp, editUrl, error } }) => - getVisualizeListItemLink(core.application, kbnUrlStateStorage, editApp, editUrl, error) + getDetailViewLink={({ attributes: { editApp, editUrl, error, readOnly } }) => + readOnly + ? undefined + : getVisualizeListItemLink(core.application, kbnUrlStateStorage, editApp, editUrl, error) } > {dashboardCapabilities.createNew && ( diff --git a/src/plugins/visualizations/public/visualize_app/utils/use/use_saved_vis_instance.ts b/src/plugins/visualizations/public/visualize_app/utils/use/use_saved_vis_instance.ts index f84b7928dda39..dcd53feb5b1e9 100644 --- a/src/plugins/visualizations/public/visualize_app/utils/use/use_saved_vis_instance.ts +++ b/src/plugins/visualizations/public/visualize_app/utils/use/use_saved_vis_instance.ts @@ -83,6 +83,16 @@ export const useSavedVisInstance = ( savedVisInstance = await getVisualizationInstance(services, visualizationIdFromUrl); } + if (savedVisInstance.vis.type.disableEdit) { + throw new Error( + i18n.translate('visualizations.editVisualization.readOnlyErrorMessage', { + defaultMessage: + '{visTypeTitle} visualizations are read only and can not be opened in editor', + values: { visTypeTitle: savedVisInstance.vis.type.title }, + }) + ); + } + if (embeddableInput && embeddableInput.timeRange) { savedVisInstance.panelTimeRange = embeddableInput.timeRange; } diff --git a/src/plugins/visualizations/public/wizard/agg_based_selection/agg_based_selection.tsx b/src/plugins/visualizations/public/wizard/agg_based_selection/agg_based_selection.tsx index d4d2c1505bf8a..f4cdd05978830 100644 --- a/src/plugins/visualizations/public/wizard/agg_based_selection/agg_based_selection.tsx +++ b/src/plugins/visualizations/public/wizard/agg_based_selection/agg_based_selection.tsx @@ -100,7 +100,7 @@ class AggBasedSelection extends React.Component { // Filter out hidden visualizations and visualizations that are only aggregations based - return !type.hidden; + return !type.disableCreate; }); let entries: VisTypeListEntry[]; diff --git a/src/plugins/visualizations/public/wizard/group_selection/group_selection.tsx b/src/plugins/visualizations/public/wizard/group_selection/group_selection.tsx index e0acc20157b44..016d97d713074 100644 --- a/src/plugins/visualizations/public/wizard/group_selection/group_selection.tsx +++ b/src/plugins/visualizations/public/wizard/group_selection/group_selection.tsx @@ -57,7 +57,9 @@ function GroupSelection(props: GroupSelectionProps) { [ ...props.visTypesRegistry.getAliases(), ...props.visTypesRegistry.getByGroup(VisGroups.PROMOTED), - ], + ].filter((visDefinition) => { + return !Boolean(visDefinition.disableCreate); + }), ['promotion', 'title'], ['asc', 'asc'] ), @@ -217,7 +219,7 @@ const ToolsGroup = ({ visType, onVisTypeSelected, showExperimental }: VisCardPro }, [onVisTypeSelected, visType]); // hide both the hidden visualizations and, if lab mode is not enabled, the experimental visualizations // TODO: Remove the showExperimental logic as part of https://github.com/elastic/kibana/issues/152833 - if (visType.hidden || (!showExperimental && visType.stage === 'experimental')) { + if (visType.disableCreate || (!showExperimental && visType.stage === 'experimental')) { return null; } return ( diff --git a/src/plugins/visualizations/public/wizard/new_vis_modal.test.tsx b/src/plugins/visualizations/public/wizard/new_vis_modal.test.tsx index 0e48386a97be3..a150a94a60516 100644 --- a/src/plugins/visualizations/public/wizard/new_vis_modal.test.tsx +++ b/src/plugins/visualizations/public/wizard/new_vis_modal.test.tsx @@ -17,7 +17,8 @@ import { savedObjectsManagementPluginMock } from '@kbn/saved-objects-management- describe('NewVisModal', () => { const defaultVisTypeParams = { - hidden: false, + disableCreate: false, + disableEdit: false, requiresSearch: false, }; const _visTypes = [ diff --git a/src/plugins/visualizations/server/index.ts b/src/plugins/visualizations/server/index.ts index 7d01eadbc6dba..f40fb9885388f 100644 --- a/src/plugins/visualizations/server/index.ts +++ b/src/plugins/visualizations/server/index.ts @@ -16,4 +16,4 @@ export function plugin(initializerContext: PluginInitializerContext) { return new VisualizationsPlugin(initializerContext); } -export type { VisualizationsPluginSetup, VisualizationsPluginStart } from './types'; +export type { VisualizationsServerSetup, VisualizationsServerStart } from './types'; diff --git a/src/plugins/visualizations/server/plugin.ts b/src/plugins/visualizations/server/plugin.ts index 0dd46f4fdd448..6aa4a749ecb7a 100644 --- a/src/plugins/visualizations/server/plugin.ts +++ b/src/plugins/visualizations/server/plugin.ts @@ -19,13 +19,13 @@ import { ContentManagementServerSetup } from '@kbn/content-management-plugin/ser import { capabilitiesProvider } from './capabilities_provider'; import { VisualizationsStorage } from './content_management'; -import type { VisualizationsPluginSetup, VisualizationsPluginStart } from './types'; +import type { VisualizationsServerSetup, VisualizationsServerStart } from './types'; import { makeVisualizeEmbeddableFactory } from './embeddable/make_visualize_embeddable_factory'; -import { getVisualizationSavedObjectType } from './saved_objects'; +import { getVisualizationSavedObjectType, registerReadOnlyVisType } from './saved_objects'; import { CONTENT_ID, LATEST_VERSION } from '../common/content_management'; export class VisualizationsPlugin - implements Plugin + implements Plugin { private readonly logger: Logger; @@ -61,7 +61,7 @@ export class VisualizationsPlugin }, }); - return {}; + return { registerReadOnlyVisType }; } public start(core: CoreStart) { diff --git a/src/plugins/visualizations/server/saved_objects/get_in_app_url.test.ts b/src/plugins/visualizations/server/saved_objects/get_in_app_url.test.ts new file mode 100644 index 0000000000000..005e6d4a33613 --- /dev/null +++ b/src/plugins/visualizations/server/saved_objects/get_in_app_url.test.ts @@ -0,0 +1,36 @@ +/* + * 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 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 or the Server + * Side Public License, v 1. + */ + +import type { VisualizationSavedObject } from '../../common/content_management'; +import { registerReadOnlyVisType } from './read_only_vis_type_registry'; +import { getInAppUrl } from './get_in_app_url'; + +registerReadOnlyVisType('myLegacyVis'); + +test('should return visualize edit url', () => { + const obj = { + id: '1', + attributes: { + visState: JSON.stringify({ type: 'vega' }), + }, + } as unknown as VisualizationSavedObject; + expect(getInAppUrl(obj)).toEqual({ + path: '/app/visualize#/edit/1', + uiCapabilitiesPath: 'visualize.show', + }); +}); + +test('should return undefined when visualization type is read only', () => { + const obj = { + id: '1', + attributes: { + visState: JSON.stringify({ type: 'myLegacyVis' }), + }, + } as unknown as VisualizationSavedObject; + expect(getInAppUrl(obj)).toBeUndefined(); +}); diff --git a/src/plugins/visualizations/server/saved_objects/get_in_app_url.ts b/src/plugins/visualizations/server/saved_objects/get_in_app_url.ts new file mode 100644 index 0000000000000..11259327a242c --- /dev/null +++ b/src/plugins/visualizations/server/saved_objects/get_in_app_url.ts @@ -0,0 +1,29 @@ +/* + * 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 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 or the Server + * Side Public License, v 1. + */ + +import type { VisualizationSavedObject } from '../../common/content_management'; +import { isVisTypeReadOnly } from './read_only_vis_type_registry'; + +export function getInAppUrl(obj: VisualizationSavedObject) { + let visType: string | undefined; + if (obj.attributes.visState) { + try { + const visState = JSON.parse(obj.attributes.visState); + visType = visState?.type; + } catch (e) { + // let client display warning for unparsable visState + } + } + + return isVisTypeReadOnly(visType) + ? undefined + : { + path: `/app/visualize#/edit/${encodeURIComponent(obj.id)}`, + uiCapabilitiesPath: 'visualize.show', + }; +} diff --git a/src/plugins/visualizations/server/saved_objects/index.ts b/src/plugins/visualizations/server/saved_objects/index.ts index f5e2d26acbc0c..0c19590e5a27d 100644 --- a/src/plugins/visualizations/server/saved_objects/index.ts +++ b/src/plugins/visualizations/server/saved_objects/index.ts @@ -7,3 +7,4 @@ */ export { getVisualizationSavedObjectType } from './visualization'; +export { registerReadOnlyVisType } from './read_only_vis_type_registry'; diff --git a/src/plugins/visualizations/server/saved_objects/read_only_vis_type_registry.ts b/src/plugins/visualizations/server/saved_objects/read_only_vis_type_registry.ts new file mode 100644 index 0000000000000..1ee9bd91ac575 --- /dev/null +++ b/src/plugins/visualizations/server/saved_objects/read_only_vis_type_registry.ts @@ -0,0 +1,17 @@ +/* + * 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 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 or the Server + * Side Public License, v 1. + */ + +const registry: string[] = []; + +export function registerReadOnlyVisType(visType: string) { + registry.push(visType); +} + +export function isVisTypeReadOnly(visType?: string) { + return visType ? registry.includes(visType) : false; +} diff --git a/src/plugins/visualizations/server/saved_objects/visualization.ts b/src/plugins/visualizations/server/saved_objects/visualization.ts index bfde137be6f62..fb77f9b58f3e6 100644 --- a/src/plugins/visualizations/server/saved_objects/visualization.ts +++ b/src/plugins/visualizations/server/saved_objects/visualization.ts @@ -12,6 +12,7 @@ import { SavedObjectsType } from '@kbn/core/server'; import { MigrateFunctionsObject } from '@kbn/kibana-utils-plugin/common'; import { CONTENT_ID } from '../../common/content_management'; import { getAllMigrations } from '../migrations/visualization_saved_object_migrations'; +import { getInAppUrl } from './get_in_app_url'; export const getVisualizationSavedObjectType = ( getSearchSourceMigrations: () => MigrateFunctionsObject @@ -28,12 +29,7 @@ export const getVisualizationSavedObjectType = ( getTitle(obj) { return obj.attributes.title; }, - getInAppUrl(obj) { - return { - path: `/app/visualize#/edit/${encodeURIComponent(obj.id)}`, - uiCapabilitiesPath: 'visualize.show', - }; - }, + getInAppUrl, }, mappings: { dynamic: false, // declared here to prevent indexing root level attribute fields diff --git a/src/plugins/visualizations/server/types.ts b/src/plugins/visualizations/server/types.ts index 1256845584ae5..a6df93450689c 100644 --- a/src/plugins/visualizations/server/types.ts +++ b/src/plugins/visualizations/server/types.ts @@ -6,7 +6,8 @@ * Side Public License, v 1. */ +export interface VisualizationsServerSetup { + registerReadOnlyVisType: (visType: string) => void; +} // eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface VisualizationsPluginSetup {} -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface VisualizationsPluginStart {} +export interface VisualizationsServerStart {} diff --git a/test/plugin_functional/test_suites/core_plugins/rendering.ts b/test/plugin_functional/test_suites/core_plugins/rendering.ts index d444895d86503..4eae48e2b83d1 100644 --- a/test/plugin_functional/test_suites/core_plugins/rendering.ts +++ b/test/plugin_functional/test_suites/core_plugins/rendering.ts @@ -160,6 +160,7 @@ export default function ({ getService }: PluginFunctionalProviderContext) { 'telemetry.sendUsageTo (any)', 'usageCollection.uiCounters.debug (boolean)', 'usageCollection.uiCounters.enabled (boolean)', + 'vis_type_timeseries.readOnly (any)', 'vis_type_vega.enableExternalUrls (boolean)', 'xpack.actions.email.domain_allowlist (array)', 'xpack.apm.serviceMapEnabled (boolean)', diff --git a/x-pack/plugins/canvas/public/components/workpad_header/editor_menu/editor_menu.tsx b/x-pack/plugins/canvas/public/components/workpad_header/editor_menu/editor_menu.tsx index ed34b2818b531..154dab1fa0fec 100644 --- a/x-pack/plugins/canvas/public/components/workpad_header/editor_menu/editor_menu.tsx +++ b/x-pack/plugins/canvas/public/components/workpad_header/editor_menu/editor_menu.tsx @@ -134,7 +134,7 @@ export const EditorMenu: FC = ({ addElement }) => { } return 0; }) - .filter(({ hidden }: BaseVisType) => !hidden); + .filter(({ disableCreate }: BaseVisType) => !disableCreate); const visTypeAliases = visualizationsService .getAliases() diff --git a/x-pack/plugins/global_search_providers/server/providers/saved_objects/map_object_to_result.ts b/x-pack/plugins/global_search_providers/server/providers/saved_objects/map_object_to_result.ts index bc993129435cb..ac3296b231516 100644 --- a/x-pack/plugins/global_search_providers/server/providers/saved_objects/map_object_to_result.ts +++ b/x-pack/plugins/global_search_providers/server/providers/saved_objects/map_object_to_result.ts @@ -33,8 +33,8 @@ const isAccessible = ( if (getInAppUrl === undefined) { throw new Error('Trying to map an object from a type without management metadata'); } - const { uiCapabilitiesPath } = getInAppUrl(object); - return Boolean(get(capabilities, uiCapabilitiesPath) ?? false); + const inAppUrl = getInAppUrl(object); + return inAppUrl ? Boolean(get(capabilities, inAppUrl.uiCapabilitiesPath) ?? false) : false; }; export const mapToResult = ( @@ -52,7 +52,7 @@ export const mapToResult = ( title: getTitle ? getTitle(object) : (object.attributes as any)[defaultSearchField], type: object.type, icon: type.management?.icon ?? undefined, - url: getInAppUrl(object).path, + url: getInAppUrl(object)!.path, score: object.score, meta: { tagIds: object.references.filter((ref) => ref.type === 'tag').map(({ id }) => id), diff --git a/x-pack/plugins/maps/public/legacy_visualizations/region_map/region_map_vis_type.tsx b/x-pack/plugins/maps/public/legacy_visualizations/region_map/region_map_vis_type.tsx index b51c4a3b5a23f..2c0afd1ff3462 100644 --- a/x-pack/plugins/maps/public/legacy_visualizations/region_map/region_map_vis_type.tsx +++ b/x-pack/plugins/maps/public/legacy_visualizations/region_map/region_map_vis_type.tsx @@ -49,4 +49,5 @@ export const regionMapVisType = { }, toExpressionAst, requiresSearch: true, + disableCreate: true, } as VisTypeDefinition; diff --git a/x-pack/plugins/maps/public/legacy_visualizations/tile_map/tile_map_vis_type.tsx b/x-pack/plugins/maps/public/legacy_visualizations/tile_map/tile_map_vis_type.tsx index c0a5fe55259f4..63dc4bc630920 100644 --- a/x-pack/plugins/maps/public/legacy_visualizations/tile_map/tile_map_vis_type.tsx +++ b/x-pack/plugins/maps/public/legacy_visualizations/tile_map/tile_map_vis_type.tsx @@ -50,4 +50,5 @@ export const tileMapVisType = { }, toExpressionAst, requiresSearch: true, + disableCreate: true, } as VisTypeDefinition; diff --git a/x-pack/plugins/maps/public/maps_vis_type_alias.ts b/x-pack/plugins/maps/public/maps_vis_type_alias.ts index d26108737ab33..548098311e3c2 100644 --- a/x-pack/plugins/maps/public/maps_vis_type_alias.ts +++ b/x-pack/plugins/maps/public/maps_vis_type_alias.ts @@ -6,7 +6,7 @@ */ import { i18n } from '@kbn/i18n'; -import type { VisualizationsSetup, VisualizationStage } from '@kbn/visualizations-plugin/public'; +import type { VisualizationStage } from '@kbn/visualizations-plugin/public'; import type { MapItem } from '../common/content_management'; import { APP_ID, @@ -17,9 +17,7 @@ import { MAP_SAVED_OBJECT_TYPE, } from '../common/constants'; -export function getMapsVisTypeAlias(visualizations: VisualizationsSetup) { - visualizations.hideTypes(['region_map', 'tile_map']); - +export function getMapsVisTypeAlias() { const appDescription = i18n.translate('xpack.maps.visTypeAlias.description', { defaultMessage: 'Create and style maps with multiple layers and indices.', }); diff --git a/x-pack/plugins/maps/public/plugin.ts b/x-pack/plugins/maps/public/plugin.ts index 75c4211c54d58..2e6d203d05dae 100644 --- a/x-pack/plugins/maps/public/plugin.ts +++ b/x-pack/plugins/maps/public/plugin.ts @@ -185,7 +185,7 @@ export class MapsPlugin if (plugins.home) { plugins.home.featureCatalogue.register(featureCatalogueEntry); } - plugins.visualizations.registerAlias(getMapsVisTypeAlias(plugins.visualizations)); + plugins.visualizations.registerAlias(getMapsVisTypeAlias()); plugins.embeddable.registerEmbeddableFactory(MAP_SAVED_OBJECT_TYPE, new MapEmbeddableFactory()); core.application.register({