diff --git a/src/legacy/core_plugins/kibana/public/dashboard/index.js b/src/legacy/core_plugins/kibana/public/dashboard/index.js
index 5993c723a81fc..54db02ac90ba2 100644
--- a/src/legacy/core_plugins/kibana/public/dashboard/index.js
+++ b/src/legacy/core_plugins/kibana/public/dashboard/index.js
@@ -99,8 +99,8 @@ uiRoutes
$scope.getViewUrl = ({ id }) => {
return chrome.addBasePath(`#${createDashboardEditUrl(id)}`);
};
- $scope.delete = (ids) => {
- return services.dashboards.delete(ids);
+ $scope.delete = (dashboards) => {
+ return services.dashboards.delete(dashboards.map(d => d.id));
};
$scope.hideWriteControls = dashboardConfig.getHideWriteControls();
$scope.initialFilter = ($location.search()).filter || EMPTY_FILTER;
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 97bacb45d7ad8..d68e2854ab13f 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.map(id => itemsById[id]));
} catch (error) {
toastNotifications.addDanger({
title: (
@@ -482,4 +483,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 73afccf2a288a..d221dcfc5ecaf 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,20 @@ 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(selectedItems) {
+ return Promise.all(
+ selectedItems.map(item => {
+ return savedObjectClient.delete(item.savedObjectType, item.id);
+ }),
+ ).then(() => {
+ chrome.untrackNavLinksForDeletedSavedObjects(selectedItems.map(item => 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 6c59c820e0897..a660528cf2477 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
@@ -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,11 +103,11 @@ class VisualizeListingTableUi extends Component {
render: (field, record) => (
{this.renderItemTypeIcon(record)}
- {record.type.title}
+ {record.typeTitle}
{this.getExperimentalBadge(record)}
)
- }
+ },
];
return tableColumns;
@@ -175,13 +175,13 @@ class VisualizeListingTableUi extends Component {
renderItemTypeIcon(item) {
let icon;
- if (item.type.image) {
+ if (item.image) {
icon = (
);
} else {
@@ -199,7 +199,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 0000000000000..ed0f6dc429ef4
--- /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 dee8cd7fda9ab..cfe5eeec3834f 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,9 @@ 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 { createVisualizeEditUrl } from '../visualize_constants';
+import { findListItems } from './find_list_items';
const app = uiModules.get('app/visualize');
@@ -34,7 +37,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 +56,31 @@ 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 = `#${createVisualizeEditUrl(id)}`;
+
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 763c34a4240b7..91040cf966567 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,37 @@
* under the License.
*/
+export interface VisualizationListItem {
+ editUrl: string;
+ icon: string;
+ id: string;
+ isExperimental: boolean;
+ savedObjectType: string;
+ title: string;
+ typeTitle: 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[] = [];