Sample data - allow plugins to register additional view data links (#33052) (#33198)

* Sample data - allow plugins to register additional view data links

* remove comment

* remove unused function

* review feedback
This commit is contained in:
Nathan Reese 2019-03-13 18:22:24 -06:00 committed by GitHub
parent 2804bb1f60
commit da81772bb8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 356 additions and 20 deletions

View file

@ -0,0 +1,69 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`should render popover when appLinks is not empty 1`] = `
<EuiPopover
anchorPosition="downCenter"
button={
<EuiButton
aria-label="View Sample eCommerce orders"
color="primary"
fill={false}
iconSide="right"
iconType="arrowDown"
onClick={[Function]}
type="button"
>
View data
</EuiButton>
}
closePopover={[Function]}
hasArrow={true}
id="sampleDataLinksecommerce"
isOpen={false}
ownFocus={false}
panelPaddingSize="none"
>
<EuiContextMenu
initialPanelId={0}
panels={
Array [
Object {
"id": 0,
"items": Array [
Object {
"href": "root/app/kibana#/dashboard/722b74f0-b882-11e8-a6d9-e546fe2bba5f",
"icon": <EuiIcon
size="m"
type="dashboardApp"
/>,
"name": "Dashboard",
},
Object {
"href": "rootapp/myAppPath",
"icon": <EuiIcon
size="m"
type="logoKibana"
/>,
"name": "myAppLabel",
},
],
},
]
}
/>
</EuiPopover>
`;
exports[`should render simple button when appLinks is empty 1`] = `
<EuiButton
aria-label="View Sample eCommerce orders"
color="primary"
data-test-subj="launchSampleDataSetecommerce"
fill={false}
href="root/app/kibana#/dashboard/722b74f0-b882-11e8-a6d9-e546fe2bba5f"
iconSide="left"
type="button"
>
View data
</EuiButton>
`;

View file

@ -34,6 +34,8 @@ export const UNINSTALLED_STATUS = 'not_installed';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import { SampleDataViewDataButton } from './sample_data_view_data_button';
export class SampleDataSetCard extends React.Component {
isInstalled = () => {
@ -91,21 +93,12 @@ export class SampleDataSetCard extends React.Component {
</EuiButtonEmpty>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton
href={this.props.launchUrl}
data-test-subj={`launchSampleDataSet${this.props.id}`}
aria-label={i18n.translate('kbn.home.sampleDataSetCard.viewDataButtonAriaLabel', {
defaultMessage: 'View {datasetName}',
values: {
datasetName: this.props.name,
}
})}
>
<FormattedMessage
id="kbn.home.sampleDataSetCard.viewDataButtonLabel"
defaultMessage="View data"
/>
</EuiButton>
<SampleDataViewDataButton
id={this.props.id}
name={this.props.name}
overviewDashboard={this.props.overviewDashboard}
appLinks={this.props.appLinks}
/>
</EuiFlexItem>
</EuiFlexGroup>
);
@ -207,7 +200,12 @@ SampleDataSetCard.propTypes = {
id: PropTypes.string.isRequired,
description: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
launchUrl: PropTypes.string.isRequired,
overviewDashboard: PropTypes.string.isRequired,
appLinks: PropTypes.arrayOf(PropTypes.shape({
path: PropTypes.string.isRequired,
label: PropTypes.string.isRequired,
icon: PropTypes.string.isRequired,
})).isRequired,
status: PropTypes.oneOf([
INSTALLED_STATUS,
UNINSTALLED_STATUS,

View file

@ -199,7 +199,8 @@ export class SampleDataSetCards extends React.Component {
id={sampleDataSet.id}
description={sampleDataSet.description}
name={sampleDataSet.name}
launchUrl={this.props.addBasePath(`/app/kibana#/dashboard/${sampleDataSet.overviewDashboard}`)}
overviewDashboard={sampleDataSet.overviewDashboard}
appLinks={sampleDataSet.appLinks}
status={sampleDataSet.status}
isProcessing={_.get(this.state.processingStatus, sampleDataSet.id, false)}
statusMsg={sampleDataSet.statusMsg}

View file

@ -0,0 +1,141 @@
/*
* 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 React from 'react';
import PropTypes from 'prop-types';
import {
EuiButton,
EuiContextMenu,
EuiIcon,
EuiPopover,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import chrome from 'ui/chrome';
export class SampleDataViewDataButton extends React.Component {
state = {
isPopoverOpen: false
}
togglePopoverVisibility = () => {
this.setState(prevState => ({
isPopoverOpen: !prevState.isPopoverOpen,
}));
};
closePopover = () => {
this.setState({
isPopoverOpen: false,
});
};
render() {
const viewDataButtonLabel = i18n.translate('kbn.home.sampleDataSetCard.viewDataButtonLabel', {
defaultMessage: 'View data' });
const viewDataButtonAriaLabel = i18n.translate('kbn.home.sampleDataSetCard.viewDataButtonAriaLabel', {
defaultMessage: 'View {datasetName}',
values: {
datasetName: this.props.name,
},
});
const dashboardPath = chrome.addBasePath(`/app/kibana#/dashboard/${this.props.overviewDashboard}`);
if (this.props.appLinks.length === 0) {
return (
<EuiButton
href={dashboardPath}
data-test-subj={`launchSampleDataSet${this.props.id}`}
aria-label={viewDataButtonAriaLabel}
>
{viewDataButtonLabel}
</EuiButton>
);
}
const additionalItems = this.props.appLinks.map(({ path, label, icon }) => {
return {
name: label,
icon: (
<EuiIcon
type={icon}
size="m"
/>
),
href: chrome.addBasePath(path)
};
});
const panels = [
{
id: 0,
items: [
{
name: i18n.translate('kbn.home.sampleDataSetCard.dashboardLinkLabel', {
defaultMessage: 'Dashboard' }),
icon: (
<EuiIcon
type="dashboardApp"
size="m"
/>
),
href: dashboardPath,
},
...additionalItems
]
}
];
const popoverButton = (
<EuiButton
aria-label={viewDataButtonAriaLabel}
onClick={this.togglePopoverVisibility}
iconType="arrowDown"
iconSide="right"
>
{viewDataButtonLabel}
</EuiButton>
);
return (
<EuiPopover
id={`sampleDataLinks${this.props.id}`}
button={popoverButton}
isOpen={this.state.isPopoverOpen}
closePopover={this.closePopover}
panelPaddingSize="none"
anchorPosition="downCenter"
>
<EuiContextMenu
initialPanelId={0}
panels={panels}
/>
</EuiPopover>
);
}
}
SampleDataViewDataButton.propTypes = {
id: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
overviewDashboard: PropTypes.string.isRequired,
appLinks: PropTypes.arrayOf(PropTypes.shape({
path: PropTypes.string.isRequired,
label: PropTypes.string.isRequired,
icon: PropTypes.string.isRequired,
})).isRequired,
};

View file

@ -0,0 +1,59 @@
/*
* 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.
*/
jest.mock('ui/chrome', () => {
return {
addBasePath: (path) => {
return `root${path}`;
},
};
});
import React from 'react';
import { shallow } from 'enzyme';
import { SampleDataViewDataButton } from './sample_data_view_data_button';
test('should render simple button when appLinks is empty', () => {
const component = shallow(<SampleDataViewDataButton
id="ecommerce"
name="Sample eCommerce orders"
overviewDashboard="722b74f0-b882-11e8-a6d9-e546fe2bba5f"
appLinks={[]}
/>);
expect(component).toMatchSnapshot(); // eslint-disable-line
});
test('should render popover when appLinks is not empty', () => {
const appLinks = [
{
path: 'app/myAppPath',
label: 'myAppLabel',
icon: 'logoKibana'
}
];
const component = shallow(<SampleDataViewDataButton
id="ecommerce"
name="Sample eCommerce orders"
overviewDashboard="722b74f0-b882-11e8-a6d9-e546fe2bba5f"
appLinks={appLinks}
/>);
expect(component).toMatchSnapshot(); // eslint-disable-line
});

View file

@ -44,6 +44,12 @@ const dataIndexSchema = Joi.object({
preserveDayOfWeekTimeOfDay: Joi.boolean().default(false),
});
const appLinkSchema = Joi.object({
path: Joi.string().required(),
label: Joi.string().required(),
icon: Joi.string().required(),
});
export const sampleDataSchema = {
id: Joi.string().regex(/^[a-zA-Z0-9-]+$/).required(),
name: Joi.string().required(),
@ -53,6 +59,7 @@ export const sampleDataSchema = {
// saved object id of main dashboard for sample data set
overviewDashboard: Joi.string().required(),
appLinks: Joi.array().items(appLinkSchema).default([]),
// saved object id of default index-pattern for sample data set
defaultIndex: Joi.string().required(),

View file

@ -39,6 +39,7 @@ export const createListRoute = () => ({
previewImagePath: sampleDataset.previewImagePath,
darkPreviewImagePath: sampleDataset.darkPreviewImagePath,
overviewDashboard: sampleDataset.overviewDashboard,
appLinks: sampleDataset.appLinks,
defaultIndex: sampleDataset.defaultIndex,
dataIndices: sampleDataset.dataIndices.map(({ id }) => ({ id })),
};

View file

@ -79,6 +79,18 @@ export function sampleDataMixin(kbnServer, server) {
sampleDataset.savedObjects = sampleDataset.savedObjects.concat(savedObjects);
});
server.decorate('server', 'addAppLinksToSampleDataset', (id, appLinks) => {
const sampleDataset = sampleDatasets.find(sampleDataset => {
return sampleDataset.id === id;
});
if (!sampleDataset) {
throw new Error(`Unable to find sample dataset with id: ${id}`);
}
sampleDataset.appLinks = sampleDataset.appLinks.concat(appLinks);
});
server.registerSampleDataset(flightsSpecProvider);
server.registerSampleDataset(logsSpecProvider);
server.registerSampleDataset(ecommerceSpecProvider);

View file

@ -4,6 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { i18n } from '@kbn/i18n';
import { ecommerceSavedObjects, flightsSavedObjects, webLogsSavedObjects } from './index';
export function loadSampleData(server) {
@ -20,13 +21,33 @@ export function loadSampleData(server) {
});
}
const sampleDataLinkLabel = i18n.translate('xpack.canvas.sampleDataLinkLabel', {
defaultMessage: 'Canvas',
});
server.addSavedObjectsToSampleDataset(
'ecommerce',
updateCanvasWorkpadTimestamps(ecommerceSavedObjects)
);
server.addAppLinksToSampleDataset('ecommerce', {
path: '/app/canvas#/workpad/workpad-e08b9bdb-ec14-4339-94c4-063bddfd610e',
label: sampleDataLinkLabel,
icon: 'canvasApp',
});
server.addSavedObjectsToSampleDataset(
'flights',
updateCanvasWorkpadTimestamps(flightsSavedObjects)
);
server.addAppLinksToSampleDataset('flights', {
path: '/app/canvas#/workpad/workpad-a474e74b-aedc-47c3-894a-db77e62c41e0',
label: sampleDataLinkLabel,
icon: 'canvasApp',
});
server.addSavedObjectsToSampleDataset('logs', updateCanvasWorkpadTimestamps(webLogsSavedObjects));
server.addAppLinksToSampleDataset('logs', {
path: '/app/canvas#/workpad/workpad-5563cc40-5760-4afe-bf33-9da72fac53b7',
label: sampleDataLinkLabel,
icon: 'canvasApp',
});
}

View file

@ -7,6 +7,9 @@
export const GIS_API_PATH = 'api/maps';
export const MAP_SAVED_OBJECT_TYPE = 'map';
export function createMapPath(id) {
return `/app/maps#/map/${id}`;
}
export const EMS_FILE = 'EMS_FILE';
export const ES_GEO_GRID = 'ES_GEO_GRID';

View file

@ -16,7 +16,7 @@ import { watchStatusAndLicenseToInitialize } from
'../../server/lib/watch_status_and_license_to_initialize';
import { initTelemetryCollection } from './server/maps_telemetry';
import { i18n } from '@kbn/i18n';
import { APP_ID, APP_ICON } from './common/constants';
import { APP_ID, APP_ICON, createMapPath } from './common/constants';
import { getAppTitle } from './common/i18n_getters';
export function maps(kibana) {
@ -91,9 +91,33 @@ export function maps(kibana) {
.feature(this.id)
.registerLicenseCheckResultsGenerator(checkLicense);
const sampleDataLinkLabel = i18n.translate('xpack.maps.sampleDataLinkLabel', {
defaultMessage: 'Map'
});
server.addSavedObjectsToSampleDataset('ecommerce', getEcommerceSavedObjects());
server.addAppLinksToSampleDataset('ecommerce', [
{
path: createMapPath('2c9c1f60-1909-11e9-919b-ffe5949a18d2'),
label: sampleDataLinkLabel,
icon: 'gisApp'
}
]);
server.addSavedObjectsToSampleDataset('flights', getFlightsSavedObjects());
server.addAppLinksToSampleDataset('flights', [
{
path: createMapPath('5dd88580-1906-11e9-919b-ffe5949a18d2'),
label: sampleDataLinkLabel,
icon: 'gisApp'
}
]);
server.addSavedObjectsToSampleDataset('logs', getWebLogsSavedObjects());
server.addAppLinksToSampleDataset('logs', [
{
path: createMapPath('de71f4f0-1902-11e9-919b-ffe5949a18d2'),
label: sampleDataLinkLabel,
icon: 'gisApp'
}
]);
server.injectUiAppVars('maps', async () => {
return await server.getInjectedUiAppVars('kibana');
});

View file

@ -10,7 +10,7 @@ import { EmbeddableFactory } from 'ui/embeddable';
import { MapEmbeddable } from './map_embeddable';
import { indexPatternService } from '../kibana_services';
import { i18n } from '@kbn/i18n';
import { MAP_SAVED_OBJECT_TYPE } from '../../common/constants';
import { createMapPath, MAP_SAVED_OBJECT_TYPE } from '../../common/constants';
export class MapEmbeddableFactory extends EmbeddableFactory {
@ -49,7 +49,7 @@ export class MapEmbeddableFactory extends EmbeddableFactory {
return new MapEmbeddable({
onEmbeddableStateChanged,
savedMap,
editUrl: chrome.addBasePath(`/app/maps#/map/${panelMetadata.id}`),
editUrl: chrome.addBasePath(createMapPath(panelMetadata.id)),
indexPatterns,
});
}