Allow other apps to list their custom visualizations in the Visualize app (#43386)
This commit is contained in:
parent
dedfd62717
commit
007fa0a4a0
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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([{
|
||||
|
|
|
@ -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) => (
|
||||
<span>
|
||||
{this.renderItemTypeIcon(record)}
|
||||
{record.type.title}
|
||||
{record.typeTitle}
|
||||
{this.getExperimentalBadge(record)}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
},
|
||||
];
|
||||
|
||||
return tableColumns;
|
||||
|
@ -175,13 +175,13 @@ class VisualizeListingTableUi extends Component {
|
|||
|
||||
renderItemTypeIcon(item) {
|
||||
let icon;
|
||||
if (item.type.image) {
|
||||
if (item.image) {
|
||||
icon = (
|
||||
<img
|
||||
className="visListingTable__typeImage"
|
||||
aria-hidden="true"
|
||||
alt=""
|
||||
src={item.type.image}
|
||||
src={item.image}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
|
@ -199,7 +199,7 @@ class VisualizeListingTableUi extends Component {
|
|||
}
|
||||
|
||||
getExperimentalBadge(item) {
|
||||
return item.type.shouldMarkAsExperimentalInUI() && (
|
||||
return item.isExperimental && (
|
||||
<EuiBetaBadge
|
||||
className="visListingTable__experimentalIcon"
|
||||
label="E"
|
||||
|
|
|
@ -0,0 +1,65 @@
|
|||
/*
|
||||
* 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 _ from 'lodash';
|
||||
|
||||
/**
|
||||
* Search for visualizations and convert them into a list display-friendly format.
|
||||
*/
|
||||
export async function findListItems({ visTypes, search, size, savedObjectsClient, mapSavedObjectApiHits }) {
|
||||
const extensions = _.compact(visTypes.map(v => 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);
|
||||
}
|
||||
})
|
||||
};
|
||||
}
|
|
@ -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',
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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;
|
||||
});
|
||||
|
|
|
@ -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[] = [];
|
||||
|
|
Loading…
Reference in a new issue