diff --git a/src/legacy/core_plugins/kibana/public/table_list_view/table_list_view.js b/src/legacy/core_plugins/kibana/public/table_list_view/table_list_view.js
index 97bacb45d7ad8a..5cfbca49d9a496 100644
--- a/src/legacy/core_plugins/kibana/public/table_list_view/table_list_view.js
+++ b/src/legacy/core_plugins/kibana/public/table_list_view/table_list_view.js
@@ -116,7 +116,8 @@ class TableListViewUi extends React.Component {
isDeletingItems: true
});
try {
- await this.props.deleteItems(this.state.selectedIds);
+ const itemsById = _.indexBy(this.state.items, 'id');
+ await this.props.deleteItems(this.state.selectedIds, this.state.selectedIds.map(id => itemsById[id]));
} catch (error) {
toastNotifications.addDanger({
title: (
@@ -316,6 +317,7 @@ class TableListViewUi extends React.Component {
};
const selection = this.props.deleteItems ? {
+ selectable: this.props.selectable,
onSelectionChange: (selection) => {
this.setState({
selectedIds: selection.map(item => { return item.id; })
@@ -468,6 +470,7 @@ TableListViewUi.propTypes = {
deleteItems: PropTypes.func,
createItem: PropTypes.func,
editItem: PropTypes.func,
+ selectable: PropTypes.func,
listingLimit: PropTypes.number,
initialFilter: PropTypes.string,
@@ -482,4 +485,3 @@ TableListViewUi.defaultProps = {
};
export const TableListView = injectI18n(TableListViewUi);
-
diff --git a/src/legacy/core_plugins/kibana/public/visualize/listing/visualize_listing.js b/src/legacy/core_plugins/kibana/public/visualize/listing/visualize_listing.js
index 73afccf2a288ad..c47a301b24c37c 100644
--- a/src/legacy/core_plugins/kibana/public/visualize/listing/visualize_listing.js
+++ b/src/legacy/core_plugins/kibana/public/visualize/listing/visualize_listing.js
@@ -26,12 +26,11 @@ import chrome from 'ui/chrome';
import { wrapInI18nContext } from 'ui/i18n';
import { toastNotifications } from 'ui/notify';
import { addHelpMenuToAppChrome } from '../help_menu/help_menu_util';
-
+import { SavedObjectsClientProvider } from 'ui/saved_objects';
import { VisualizeListingTable } from './visualize_listing_table';
import { NewVisModal } from '../wizard/new_vis_modal';
-import { createVisualizeEditUrl, VisualizeConstants } from '../visualize_constants';
+import { VisualizeConstants } from '../visualize_constants';
import { visualizations } from 'plugins/visualizations';
-
import { i18n } from '@kbn/i18n';
const app = uiModules.get('app/visualize', ['ngRoute', 'react']);
@@ -42,6 +41,7 @@ export function VisualizeListingController($injector, createNewVis) {
const Private = $injector.get('Private');
const config = $injector.get('config');
const kbnUrl = $injector.get('kbnUrl');
+ const savedObjectClient = Private(SavedObjectsClientProvider);
this.visTypeRegistry = Private(VisTypesRegistryProvider);
this.visTypeAliases = visualizations.types.visTypeAliasRegistry.get();
@@ -55,13 +55,13 @@ export function VisualizeListingController($injector, createNewVis) {
this.showNewVisModal = true;
};
- this.editItem = ({ id }) => {
+ this.editItem = ({ editURL }) => {
// for visualizations the edit and view URLs are the same
- kbnUrl.change(createVisualizeEditUrl(id));
+ window.location = chrome.addBasePath(editURL);
};
- this.getViewUrl = ({ id }) => {
- return chrome.addBasePath(`#${createVisualizeEditUrl(id)}`);
+ this.getViewUrl = ({ editURL }) => {
+ return chrome.addBasePath(editURL);
};
this.closeNewVisModal = () => {
@@ -83,7 +83,7 @@ export function VisualizeListingController($injector, createNewVis) {
this.fetchItems = (filter) => {
const isLabsEnabled = config.get('visualize:enableLabs');
- return visualizationService.find(filter, config.get('savedObjects:listingLimit'))
+ return visualizationService.findListItems(filter, config.get('savedObjects:listingLimit'))
.then(result => {
this.totalItems = result.total;
@@ -94,15 +94,18 @@ export function VisualizeListingController($injector, createNewVis) {
});
};
- this.deleteSelectedItems = function deleteSelectedItems(selectedIds) {
- return visualizationService.delete(selectedIds)
- .catch(error => {
- toastNotifications.addError(error, {
- title: i18n.translate('kbn.visualize.visualizeListingDeleteErrorTitle', {
- defaultMessage: 'Error deleting visualization',
- }),
- });
+ this.deleteSelectedItems = function deleteSelectedItems(_ids, selectedItems) {
+ return Promise.all(
+ selectedItems.map(item => {
+ return savedObjectClient.delete(item.savedObjectType, item.id);
+ }),
+ ).catch(error => {
+ toastNotifications.addError(error, {
+ title: i18n.translate('kbn.visualize.visualizeListingDeleteErrorTitle', {
+ defaultMessage: 'Error deleting visualization',
+ }),
});
+ });
};
chrome.breadcrumbs.set([{
diff --git a/src/legacy/core_plugins/kibana/public/visualize/listing/visualize_listing_table.js b/src/legacy/core_plugins/kibana/public/visualize/listing/visualize_listing_table.js
index 6c59c820e0897b..acc8ce7a8c2d48 100644
--- a/src/legacy/core_plugins/kibana/public/visualize/listing/visualize_listing_table.js
+++ b/src/legacy/core_plugins/kibana/public/visualize/listing/visualize_listing_table.js
@@ -21,12 +21,12 @@ import React, { Component, Fragment } from 'react';
import PropTypes from 'prop-types';
import { injectI18n, FormattedMessage } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
-import { capabilities } from 'ui/capabilities';
import { TableListView } from './../../table_list_view';
import {
EuiIcon,
EuiBetaBadge,
+ EuiButtonIcon,
EuiLink,
EuiButton,
EuiEmptyPrompt,
@@ -46,10 +46,10 @@ class VisualizeListingTableUi extends Component {
// for data exploration purposes
createItem={this.props.createItem}
findItems={this.props.findItems}
- deleteItems={capabilities.get().visualize.delete ? this.props.deleteItems : null}
- editItem={capabilities.get().visualize.save ? this.props.editItem : null}
+ deleteItems={this.props.deleteItems}
tableColumns={this.getTableColumns()}
listingLimit={this.props.listingLimit}
+ selectable={item => item.canDelete}
initialFilter={''}
noItemsFragment={this.getNoItemsMessage()}
entityName={
@@ -94,7 +94,7 @@ class VisualizeListingTableUi extends Component {
)
},
{
- field: 'type.title',
+ field: 'typeTitle',
name: intl.formatMessage({
id: 'kbn.visualize.listing.table.typeColumnName',
defaultMessage: 'Type',
@@ -103,10 +103,31 @@ class VisualizeListingTableUi extends Component {
render: (field, record) => (
{this.renderItemTypeIcon(record)}
- {record.type.title}
+ {record.typeTitle}
{this.getExperimentalBadge(record)}
)
+ },
+ {
+ field: 'canEdit',
+ name: intl.formatMessage({
+ id: 'kbn.visualize.listing.table.actionsColumnName',
+ defaultMessage: 'Actions',
+ }),
+ align: 'right',
+ width: '100px',
+ render: (_field, record) => (
+ this.props.editItem(record)}
+ iconType="pencil"
+ aria-label={intl.formatMessage({
+ id: 'kbn.visualize.listing.table.editActionDescription',
+ defaultMessage: 'Edit',
+ })}
+ disabled={!record.canEdit}
+ />
+ ),
}
];
@@ -175,13 +196,13 @@ class VisualizeListingTableUi extends Component {
renderItemTypeIcon(item) {
let icon;
- if (item.type.image) {
+ if (item.image) {
icon = (
);
} else {
@@ -199,7 +220,7 @@ class VisualizeListingTableUi extends Component {
}
getExperimentalBadge(item) {
- return item.type.shouldMarkAsExperimentalInUI() && (
+ return item.isExperimental && (
v.appExtensions && v.appExtensions.visualizations));
+ const extensionByType = extensions.reduce((acc, m) => {
+ return m.docTypes.reduce((_acc, type) => {
+ acc[type] = m;
+ return acc;
+ }, acc);
+ }, {});
+ const searchOption = (field, ...defaults) =>
+ _(extensions)
+ .pluck(field)
+ .concat(defaults)
+ .compact()
+ .flatten()
+ .uniq()
+ .value();
+ const searchOptions = {
+ type: searchOption('docTypes', 'visualization'),
+ searchFields: searchOption('searchFields', 'title^3', 'description'),
+ search: search ? `${search}*` : undefined,
+ perPage: size,
+ page: 1,
+ defaultSearchOperator: 'AND'
+ };
+
+ const { total, savedObjects } = await savedObjectsClient.find(searchOptions);
+
+ return {
+ total,
+ hits: savedObjects
+ .map((savedObject) => {
+ const config = extensionByType[savedObject.type];
+
+ if (config) {
+ return config.toListItem(savedObject);
+ } else {
+ return mapSavedObjectApiHits(savedObject);
+ }
+ })
+ };
+}
diff --git a/src/legacy/core_plugins/kibana/public/visualize/saved_visualizations/find_list_items.test.js b/src/legacy/core_plugins/kibana/public/visualize/saved_visualizations/find_list_items.test.js
new file mode 100644
index 00000000000000..ed0f6dc429ef45
--- /dev/null
+++ b/src/legacy/core_plugins/kibana/public/visualize/saved_visualizations/find_list_items.test.js
@@ -0,0 +1,185 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { findListItems } from './find_list_items';
+
+describe('saved_visualizations', () => {
+ function testProps() {
+ return {
+ visTypes: [],
+ search: '',
+ size: 10,
+ savedObjectsClient: {
+ find: jest.fn(async () => ({
+ total: 0,
+ savedObjects: [],
+ })),
+ },
+ mapSavedObjectApiHits: jest.fn(),
+ };
+ }
+
+ it('searches visualization title and description', async () => {
+ const props = testProps();
+ const { find } = props.savedObjectsClient;
+ await findListItems(props);
+ expect(find.mock.calls).toMatchObject([
+ [
+ {
+ type: ['visualization'],
+ searchFields: ['title^3', 'description'],
+ },
+ ],
+ ]);
+ });
+
+ it('searches searchFields and types specified by app extensions', async () => {
+ const props = {
+ ...testProps(),
+ visTypes: [
+ {
+ appExtensions: {
+ visualizations: {
+ docTypes: ['bazdoc', 'etc'],
+ searchFields: ['baz', 'bing'],
+ },
+ },
+ },
+ ],
+ };
+ const { find } = props.savedObjectsClient;
+ await findListItems(props);
+ expect(find.mock.calls).toMatchObject([
+ [
+ {
+ type: ['bazdoc', 'etc', 'visualization'],
+ searchFields: ['baz', 'bing', 'title^3', 'description'],
+ },
+ ],
+ ]);
+ });
+
+ it('deduplicates types and search fields', async () => {
+ const props = {
+ ...testProps(),
+ visTypes: [
+ {
+ appExtensions: {
+ visualizations: {
+ docTypes: ['bazdoc', 'bar'],
+ searchFields: ['baz', 'bing', 'barfield'],
+ },
+ },
+ },
+ {
+ appExtensions: {
+ visualizations: {
+ docTypes: ['visualization', 'foo', 'bazdoc'],
+ searchFields: ['baz', 'bing', 'foofield'],
+ },
+ },
+ },
+ ],
+ };
+ const { find } = props.savedObjectsClient;
+ await findListItems(props);
+ expect(find.mock.calls).toMatchObject([
+ [
+ {
+ type: ['bazdoc', 'bar', 'visualization', 'foo'],
+ searchFields: ['baz', 'bing', 'barfield', 'foofield', 'title^3', 'description'],
+ },
+ ],
+ ]);
+ });
+
+ it('searches the search term prefix', async () => {
+ const props = {
+ ...testProps(),
+ search: 'ahoythere',
+ };
+ const { find } = props.savedObjectsClient;
+ await findListItems(props);
+ expect(find.mock.calls).toMatchObject([
+ [
+ {
+ search: 'ahoythere*',
+ },
+ ],
+ ]);
+ });
+
+ it('uses type-specific toListItem function, if available', async () => {
+ const props = {
+ ...testProps(),
+ savedObjectsClient: {
+ find: jest.fn(async () => ({
+ total: 2,
+ savedObjects: [
+ {
+ id: 'lotr',
+ type: 'wizard',
+ attributes: { label: 'Gandalf' },
+ },
+ {
+ id: 'wat',
+ type: 'visualization',
+ attributes: { title: 'WATEVER' },
+ },
+ ],
+ })),
+ },
+ mapSavedObjectApiHits(savedObject) {
+ return {
+ id: savedObject.id,
+ title: `DEFAULT ${savedObject.attributes.title}`,
+ };
+ },
+ visTypes: [
+ {
+ appExtensions: {
+ visualizations: {
+ docTypes: ['wizard'],
+ toListItem(savedObject) {
+ return {
+ id: savedObject.id,
+ title: `${savedObject.attributes.label} THE GRAY`,
+ };
+ },
+ },
+ },
+ },
+ ],
+ };
+ const items = await findListItems(props);
+ expect(items).toEqual({
+ total: 2,
+ hits: [
+ {
+ id: 'lotr',
+ title: 'Gandalf THE GRAY',
+ },
+ {
+ id: 'wat',
+ title: 'DEFAULT WATEVER',
+ },
+ ],
+ });
+ });
+});
diff --git a/src/legacy/core_plugins/kibana/public/visualize/saved_visualizations/saved_visualizations.js b/src/legacy/core_plugins/kibana/public/visualize/saved_visualizations/saved_visualizations.js
index dee8cd7fda9ab2..c3c3abbb215da5 100644
--- a/src/legacy/core_plugins/kibana/public/visualize/saved_visualizations/saved_visualizations.js
+++ b/src/legacy/core_plugins/kibana/public/visualize/saved_visualizations/saved_visualizations.js
@@ -22,6 +22,10 @@ import { VisTypesRegistryProvider } from 'ui/registry/vis_types';
import { uiModules } from 'ui/modules';
import { SavedObjectLoader, SavedObjectsClientProvider } from 'ui/saved_objects';
import { savedObjectManagementRegistry } from '../../management/saved_object_registry';
+import { visualizations } from 'plugins/visualizations';
+import { capabilities } from 'ui/capabilities';
+import { createVisualizeEditUrl } from '../visualize_constants';
+import { findListItems } from './find_list_items';
const app = uiModules.get('app/visualize');
@@ -34,7 +38,6 @@ savedObjectManagementRegistry.register({
app.service('savedVisualizations', function (SavedVis, Private, kbnUrl, chrome) {
const visTypes = Private(VisTypesRegistryProvider);
-
const savedObjectClient = Private(SavedObjectsClientProvider);
const saveVisualizationLoader = new SavedObjectLoader(SavedVis, kbnUrl, chrome, savedObjectClient);
@@ -54,12 +57,33 @@ app.service('savedVisualizations', function (SavedVis, Private, kbnUrl, chrome)
}
source.type = visTypes.byName[typeName];
+ source.savedObjectType = 'visualization';
source.icon = source.type.icon;
+ source.image = source.type.image;
+ source.typeTitle = source.type.title;
+ source.isExperimental = source.type.shouldMarkAsExperimentalInUI();
+ source.editURL = `/app/kibana#${createVisualizeEditUrl(id)}`;
+ source.canDelete = capabilities.get().visualize.delete;
+ source.canEdit = capabilities.get().visualize.save;
+
return source;
};
saveVisualizationLoader.urlFor = function (id) {
return kbnUrl.eval('#/visualize/edit/{{id}}', { id: id });
};
+
+ // This behaves similarly to find, except it returns visualizations that are
+ // defined as appExtensions and which may not conform to type: visualization
+ saveVisualizationLoader.findListItems = function (search = '', size = 100) {
+ return findListItems({
+ search,
+ size,
+ mapSavedObjectApiHits: this.mapSavedObjectApiHits.bind(this),
+ savedObjectsClient: this.savedObjectsClient,
+ visTypes: visualizations.types.visTypeAliasRegistry.get(),
+ });
+ };
+
return saveVisualizationLoader;
});
diff --git a/src/legacy/core_plugins/visualizations/public/types/vis_type_alias_registry.ts b/src/legacy/core_plugins/visualizations/public/types/vis_type_alias_registry.ts
index 763c34a4240b78..2fb178dd09648b 100644
--- a/src/legacy/core_plugins/visualizations/public/types/vis_type_alias_registry.ts
+++ b/src/legacy/core_plugins/visualizations/public/types/vis_type_alias_registry.ts
@@ -17,12 +17,39 @@
* under the License.
*/
+export interface VisualizationListItem {
+ canDelete?: boolean;
+ canEdit?: boolean;
+ editURL: string;
+ icon: string;
+ id: string;
+ isExperimental: boolean;
+ title: string;
+ typeTitle: string;
+ savedObjectType: string;
+}
+
+export interface VisualizationsAppExtension {
+ docTypes: string[];
+ searchFields?: string[];
+ toListItem: (savedObject: {
+ id: string;
+ type: string;
+ attributes: object;
+ }) => VisualizationListItem;
+}
+
export interface VisTypeAlias {
aliasUrl: string;
name: string;
title: string;
icon: string;
description: string;
+
+ appExtensions?: {
+ visualizations: VisualizationsAppExtension;
+ [appName: string]: unknown;
+ };
}
const registry: VisTypeAlias[] = [];
diff --git a/src/legacy/plugin_discovery/types.ts b/src/legacy/plugin_discovery/types.ts
index 76b62b7eb693c5..6d7c59893dfe6c 100644
--- a/src/legacy/plugin_discovery/types.ts
+++ b/src/legacy/plugin_discovery/types.ts
@@ -58,6 +58,7 @@ export interface LegacyPluginOptions {
icon: string;
euiIconType: string;
order: number;
+ listed: boolean;
}>;
apps: any;
hacks: string[];
diff --git a/x-pack/legacy/plugins/lens/index.ts b/x-pack/legacy/plugins/lens/index.ts
index 36cc18c86b5496..175c65eccd7da3 100644
--- a/x-pack/legacy/plugins/lens/index.ts
+++ b/x-pack/legacy/plugins/lens/index.ts
@@ -25,6 +25,7 @@ export const lens: LegacyPluginInitializer = kibana => {
title: NOT_INTERNATIONALIZED_PRODUCT_NAME,
description: 'Explore and visualize data.',
main: `plugins/${PLUGIN_ID}/index`,
+ listed: false,
},
embeddableFactories: ['plugins/lens/register_embeddable'],
styleSheetPaths: resolve(__dirname, 'public/index.scss'),
@@ -64,7 +65,7 @@ export const lens: LegacyPluginInitializer = kibana => {
all: [],
read: [],
},
- ui: ['save', 'show'],
+ ui: ['save', 'show', 'delete'],
},
read: {
api: [PLUGIN_ID],
diff --git a/x-pack/legacy/plugins/lens/mappings.json b/x-pack/legacy/plugins/lens/mappings.json
index 9136447531be8d..673197a8b1247a 100644
--- a/x-pack/legacy/plugins/lens/mappings.json
+++ b/x-pack/legacy/plugins/lens/mappings.json
@@ -11,7 +11,8 @@
"type": "keyword"
},
"state": {
- "type": "text"
+ "enabled": false,
+ "type": "object"
},
"expression": {
"type": "text"
diff --git a/x-pack/legacy/plugins/lens/public/persistence/saved_object_store.test.ts b/x-pack/legacy/plugins/lens/public/persistence/saved_object_store.test.ts
index 12dff938d9be13..6952d1254ee00a 100644
--- a/x-pack/legacy/plugins/lens/public/persistence/saved_object_store.test.ts
+++ b/x-pack/legacy/plugins/lens/public/persistence/saved_object_store.test.ts
@@ -67,7 +67,7 @@ describe('LensStore', () => {
expression: '',
activeDatasourceId: 'indexpattern',
- state: JSON.stringify({
+ state: {
datasourceMetaData: { filterableIndexPatterns: [] },
datasourceStates: {
indexpattern: { type: 'index_pattern', indexPattern: '.kibana_test' },
@@ -75,7 +75,7 @@ describe('LensStore', () => {
visualization: { x: 'foo', y: 'baz' },
query: { query: '', language: 'lucene' },
filters: [],
- }),
+ },
});
});
@@ -117,45 +117,18 @@ describe('LensStore', () => {
visualizationType: 'line',
expression: '',
activeDatasourceId: 'indexpattern',
- state: JSON.stringify({
+ state: {
datasourceMetaData: { filterableIndexPatterns: [] },
datasourceStates: { indexpattern: { type: 'index_pattern', indexPattern: 'lotr' } },
visualization: { gear: ['staff', 'pointy hat'] },
query: { query: '', language: 'lucene' },
filters: [],
- }),
+ },
});
});
});
describe('load', () => {
- test('parses the visState', async () => {
- const { client, store } = testStore();
- client.get = jest.fn(async () => ({
- id: 'Paul',
- type: 'lens',
- attributes: {
- title: 'Hope clouds observation.',
- visualizationType: 'dune',
- state: '{ "datasource": { "giantWorms": true } }',
- },
- }));
- const doc = await store.load('Paul');
-
- expect(doc).toEqual({
- id: 'Paul',
- type: 'lens',
- title: 'Hope clouds observation.',
- visualizationType: 'dune',
- state: {
- datasource: { giantWorms: true },
- },
- });
-
- expect(client.get).toHaveBeenCalledTimes(1);
- expect(client.get).toHaveBeenCalledWith('lens', 'Paul');
- });
-
test('throws if an error is returned', async () => {
const { client, store } = testStore();
client.get = jest.fn(async () => ({
diff --git a/x-pack/legacy/plugins/lens/public/persistence/saved_object_store.ts b/x-pack/legacy/plugins/lens/public/persistence/saved_object_store.ts
index 681042ed34ffe4..e047e8e57c4dd0 100644
--- a/x-pack/legacy/plugins/lens/public/persistence/saved_object_store.ts
+++ b/x-pack/legacy/plugins/lens/public/persistence/saved_object_store.ts
@@ -62,18 +62,17 @@ export class SavedObjectIndexStore implements SavedObjectStore {
async save(vis: Document) {
const { id, type, ...rest } = vis;
- const attributes = {
- ...rest,
- state: JSON.stringify(rest.state),
- };
+
+ // Any is the most straighforward way out here, since the saved
+ // object client doesn't allow unknowns, and we have them in
+ // our object.
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ const attributes: any = rest;
const result = await (id
? this.client.update(DOC_TYPE, id, attributes)
: this.client.create(DOC_TYPE, attributes));
- return {
- ...vis,
- id: result.id,
- };
+ return { ...vis, id: result.id };
}
async load(id: string): Promise {
@@ -87,7 +86,6 @@ export class SavedObjectIndexStore implements SavedObjectStore {
...attributes,
id,
type,
- state: JSON.parse(((attributes as unknown) as { state: string }).state as string),
} as Document;
}
}
diff --git a/x-pack/legacy/plugins/lens/public/register_vis_type_alias.ts b/x-pack/legacy/plugins/lens/public/register_vis_type_alias.ts
index 595eb4d0e350bd..ab6b8b63deb493 100644
--- a/x-pack/legacy/plugins/lens/public/register_vis_type_alias.ts
+++ b/x-pack/legacy/plugins/lens/public/register_vis_type_alias.ts
@@ -5,12 +5,13 @@
*/
import { i18n } from '@kbn/i18n';
+import { capabilities } from 'ui/capabilities';
import { visualizations } from '../../../../../src/legacy/core_plugins/visualizations/public';
const NOT_INTERNATIONALIZED_PRODUCT_NAME = 'Lens Visualizations';
visualizations.types.visTypeAliasRegistry.add({
- aliasUrl: '/app/lens/',
+ aliasUrl: '/app/lens',
name: NOT_INTERNATIONALIZED_PRODUCT_NAME,
title: i18n.translate('xpack.lens.visTypeAlias.title', {
defaultMessage: 'Lens Visualizations',
@@ -19,4 +20,25 @@ visualizations.types.visTypeAliasRegistry.add({
defaultMessage: `Lens is a simpler way to create basic visualizations`,
}),
icon: 'faceHappy',
+ appExtensions: {
+ visualizations: {
+ docTypes: ['lens'],
+ searchFields: ['title^3'],
+ toListItem(savedObject) {
+ const { id, type, attributes } = savedObject;
+ const { title } = attributes as { title: string };
+ return {
+ id,
+ title,
+ canDelete: !!capabilities.get().visualize.delete,
+ canEdit: !!capabilities.get().visualize.save,
+ editURL: `/app/lens#/edit/${id}`,
+ icon: 'faceHappy',
+ isExperimental: true,
+ savedObjectType: type,
+ typeTitle: i18n.translate('xpack.lens.visTypeAlias.type', { defaultMessage: 'Lens' }),
+ };
+ },
+ },
+ },
});