[Discover] Provide direct link from sample data UI to Discover (#130108)

* [Discover] Allow to view sample data in Discover

* [Discover] Update deps format

* [Discover] Define order of items in the context menu

* [Discover] Update for tests

* [Discover] Add upgrade tests

* [Discover] Add a test for ordering appLinks

* [Discover] Use existing helpers

* [Discover] Add 7 days time range to Discover link

* [Discover] Rename the helper

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Julia Rechkunova 2022-05-05 11:34:43 +02:00 committed by GitHub
parent 0650bd3819
commit 58bc0f759e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 298 additions and 41 deletions

View file

@ -6,6 +6,7 @@
* Side Public License, v 1.
*/
export const APP_ICON = 'discoverApp';
export const DEFAULT_COLUMNS_SETTING = 'defaultColumns';
export const SAMPLE_SIZE_SETTING = 'discover:sampleSize';
export const SORT_DEFAULT_ORDER_SETTING = 'discover:sort:defaultOrder';

View file

@ -0,0 +1,9 @@
/*
* 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.
*/
export { getSavedSearchUrl, getSavedSearchFullPathUrl } from './saved_searches_url';

View file

@ -0,0 +1,25 @@
/*
* 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 { getSavedSearchUrl, getSavedSearchFullPathUrl } from './saved_searches_url';
describe('saved_searches_url', () => {
describe('getSavedSearchUrl', () => {
test('should return valid saved search url', () => {
expect(getSavedSearchUrl()).toBe('#/');
expect(getSavedSearchUrl('id')).toBe('#/view/id');
});
});
describe('getSavedSearchFullPathUrl', () => {
test('should return valid full path url', () => {
expect(getSavedSearchFullPathUrl()).toBe('/app/discover#/');
expect(getSavedSearchFullPathUrl('id')).toBe('/app/discover#/view/id');
});
});
});

View file

@ -0,0 +1,11 @@
/*
* 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.
*/
export const getSavedSearchUrl = (id?: string) => (id ? `#/view/${encodeURIComponent(id)}` : '#/');
export const getSavedSearchFullPathUrl = (id?: string) => `/app/discover${getSavedSearchUrl(id)}`;

View file

@ -7,8 +7,6 @@
*/
import {
getSavedSearchUrl,
getSavedSearchFullPathUrl,
fromSavedSearchAttributes,
toSavedSearchAttributes,
throwErrorOnSavedSearchUrlConflict,
@ -19,20 +17,6 @@ import { createSearchSourceMock } from '@kbn/data-plugin/public/mocks';
import type { SavedSearchAttributes, SavedSearch } from './types';
describe('saved_searches_utils', () => {
describe('getSavedSearchUrl', () => {
test('should return valid saved search url', () => {
expect(getSavedSearchUrl()).toBe('#/');
expect(getSavedSearchUrl('id')).toBe('#/view/id');
});
});
describe('getSavedSearchFullPathUrl', () => {
test('should return valid full path url', () => {
expect(getSavedSearchFullPathUrl()).toBe('/app/discover#/');
expect(getSavedSearchFullPathUrl('id')).toBe('/app/discover#/view/id');
});
});
describe('fromSavedSearchAttributes', () => {
test('should convert attributes into SavedSearch', () => {
const attributes: SavedSearchAttributes = {

View file

@ -8,9 +8,10 @@
import { i18n } from '@kbn/i18n';
import type { SavedSearchAttributes, SavedSearch } from './types';
export const getSavedSearchUrl = (id?: string) => (id ? `#/view/${encodeURIComponent(id)}` : '#/');
export const getSavedSearchFullPathUrl = (id?: string) => `/app/discover${getSavedSearchUrl(id)}`;
export {
getSavedSearchUrl,
getSavedSearchFullPathUrl,
} from '../../../common/services/saved_searches';
export const getSavedSearchUrlConflictMessage = async (savedSearch: SavedSearch) =>
i18n.translate('discover.savedSearchEmbeddable.legacyURLConflict.errorMessage', {

View file

@ -8,15 +8,18 @@
import { CoreSetup, CoreStart, Plugin } from '@kbn/core/server';
import type { PluginSetup as DataPluginSetup } from '@kbn/data-plugin/server';
import type { HomeServerPluginSetup } from '@kbn/home-plugin/server';
import { getUiSettings } from './ui_settings';
import { capabilitiesProvider } from './capabilities_provider';
import { getSavedSearchObjectType } from './saved_objects';
import { registerSampleData } from './sample_data';
export class DiscoverServerPlugin implements Plugin<object, object> {
public setup(
core: CoreSetup,
plugins: {
data: DataPluginSetup;
home?: HomeServerPluginSetup;
}
) {
const getSearchSourceMigrations = plugins.data.search.searchSource.getAllMigrations.bind(
@ -26,6 +29,10 @@ export class DiscoverServerPlugin implements Plugin<object, object> {
core.uiSettings.register(getUiSettings(core.docLinks));
core.savedObjects.registerType(getSavedSearchObjectType(getSearchSourceMigrations));
if (plugins.home) {
registerSampleData(plugins.home.sampleData);
}
return {};
}

View file

@ -0,0 +1,9 @@
/*
* 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.
*/
export { registerSampleData } from './register_sample_data';

View file

@ -0,0 +1,44 @@
/*
* 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 { i18n } from '@kbn/i18n';
import type { SampleDataRegistrySetup } from '@kbn/home-plugin/server';
import { APP_ICON } from '../../common';
import { getSavedSearchFullPathUrl } from '../../common/services/saved_searches';
function getDiscoverPathForSampleDataset(objId: string) {
// TODO: remove the time range from the URL query when saved search objects start supporting time range configuration
// https://github.com/elastic/kibana/issues/9761
return `${getSavedSearchFullPathUrl(objId)}?_g=(time:(from:now-7d,to:now))`;
}
export function registerSampleData(sampleDataRegistry: SampleDataRegistrySetup) {
const linkLabel = i18n.translate('discover.sampleData.viewLinkLabel', {
defaultMessage: 'Discover',
});
const { addAppLinksToSampleDataset, getSampleDatasets } = sampleDataRegistry;
const sampleDatasets = getSampleDatasets();
sampleDatasets.forEach((sampleDataset) => {
const sampleSavedSearchObject = sampleDataset.savedObjects.find(
(object) => object.type === 'search'
);
if (sampleSavedSearchObject) {
addAppLinksToSampleDataset(sampleDataset.id, [
{
sampleObject: sampleSavedSearchObject,
getPath: getDiscoverPathForSampleDataset,
label: linkLabel,
icon: APP_ICON,
order: -1,
},
]);
}
});
}

View file

@ -57,6 +57,90 @@ exports[`should render popover when appLinks is not empty 1`] = `
</EuiPopover>
`;
exports[`should render popover with ordered appLinks 1`] = `
<EuiPopover
anchorPosition="downCenter"
button={
<EuiButton
aria-label="View Sample eCommerce orders"
iconSide="right"
iconType="arrowDown"
onClick={[Function]}
>
View data
</EuiButton>
}
closePopover={[Function]}
data-test-subj="launchSampleDataSetecommerce"
display="inlineBlock"
hasArrow={true}
id="sampleDataLinksecommerce"
isOpen={false}
ownFocus={true}
panelPaddingSize="none"
>
<EuiContextMenu
initialPanelId={0}
panels={
Array [
Object {
"id": 0,
"items": Array [
Object {
"href": "rootapp/myAppPath",
"icon": <EuiIcon
size="m"
type="logoKibana"
/>,
"name": "myAppLabel[-1]",
"onClick": [Function],
},
Object {
"data-test-subj": "viewSampleDataSetecommerce-dashboard",
"href": "root/app/dashboards#/view/722b74f0-b882-11e8-a6d9-e546fe2bba5f",
"icon": <EuiIcon
size="m"
type="dashboardApp"
/>,
"name": "Dashboard",
"onClick": [Function],
},
Object {
"href": "rootapp/myAppPath",
"icon": <EuiIcon
size="m"
type="logoKibana"
/>,
"name": "myAppLabel[3]",
"onClick": [Function],
},
Object {
"href": "rootapp/myAppPath",
"icon": <EuiIcon
size="m"
type="logoKibana"
/>,
"name": "myAppLabel[5]",
"onClick": [Function],
},
Object {
"href": "rootapp/myAppPath",
"icon": <EuiIcon
size="m"
type="logoKibana"
/>,
"name": "myAppLabel",
"onClick": [Function],
},
],
},
]
}
size="m"
/>
</EuiPopover>
`;
exports[`should render simple button when appLinks is empty 1`] = `
<EuiButton
aria-label="View Sample eCommerce orders"

View file

@ -199,6 +199,7 @@ SampleDataSetCard.propTypes = {
path: PropTypes.string.isRequired,
label: PropTypes.string.isRequired,
icon: PropTypes.string.isRequired,
order: PropTypes.number,
})
).isRequired,
status: PropTypes.oneOf([INSTALLED_STATUS, UNINSTALLED_STATUS, 'unknown']).isRequired,

View file

@ -8,6 +8,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import { sortBy } from 'lodash';
import { EuiButton, EuiContextMenu, EuiIcon, EuiPopover } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
@ -47,7 +48,6 @@ export class SampleDataViewDataButton extends React.Component {
}
);
const dashboardPath = `/app/dashboards#/view/${this.props.overviewDashboard}`;
const prefixedDashboardPath = this.addBasePath(dashboardPath);
if (this.props.appLinks.length === 0) {
return (
@ -61,12 +61,24 @@ export class SampleDataViewDataButton extends React.Component {
);
}
const additionalItems = this.props.appLinks.map(({ path, label, icon }) => {
const dashboardAppLink = {
path: dashboardPath,
label: i18n.translate('home.sampleDataSetCard.dashboardLinkLabel', {
defaultMessage: 'Dashboard',
}),
icon: 'dashboardApp',
order: 0,
'data-test-subj': `viewSampleDataSet${this.props.id}-dashboard`,
};
const sortedItems = sortBy([dashboardAppLink, ...this.props.appLinks], 'order');
const items = sortedItems.map(({ path, label, icon, ...rest }) => {
return {
name: label,
icon: <EuiIcon type={icon} size="m" />,
href: this.addBasePath(path),
onClick: createAppNavigationHandler(path),
...(rest['data-test-subj'] ? { 'data-test-subj': rest['data-test-subj'] } : {}),
};
});
@ -75,18 +87,7 @@ export class SampleDataViewDataButton extends React.Component {
const panels = [
{
id: 0,
items: [
{
name: i18n.translate('home.sampleDataSetCard.dashboardLinkLabel', {
defaultMessage: 'Dashboard',
}),
icon: <EuiIcon type="dashboardApp" size="m" />,
href: prefixedDashboardPath,
onClick: createAppNavigationHandler(dashboardPath),
'data-test-subj': `viewSampleDataSet${this.props.id}-dashboard`,
},
...additionalItems,
],
items,
},
];
const popoverButton = (
@ -124,6 +125,7 @@ SampleDataViewDataButton.propTypes = {
path: PropTypes.string.isRequired,
label: PropTypes.string.isRequired,
icon: PropTypes.string.isRequired,
order: PropTypes.number,
})
).isRequired,
};

View file

@ -48,3 +48,41 @@ test('should render popover when appLinks is not empty', () => {
);
expect(component).toMatchSnapshot(); // eslint-disable-line
});
test('should render popover with ordered appLinks', () => {
const appLinks = [
{
path: 'app/myAppPath',
label: 'myAppLabel[-1]',
icon: 'logoKibana',
order: -1, // to position it above Dashboard link
},
{
path: 'app/myAppPath',
label: 'myAppLabel',
icon: 'logoKibana',
},
{
path: 'app/myAppPath',
label: 'myAppLabel[5]',
icon: 'logoKibana',
order: 5,
},
{
path: 'app/myAppPath',
label: 'myAppLabel[3]',
icon: 'logoKibana',
order: 3,
},
];
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

@ -58,4 +58,11 @@ export interface AppLinkData {
* The icon for this app link.
*/
icon: string;
/**
* Index of the links (ascending order, smallest will be displayed first).
* Used for ordering in the dropdown.
*
* @remark links without order defined will be displayed last
*/
order?: number;
}

View file

@ -35,12 +35,12 @@ export const createListRoute = (
?.foundObjectId ?? id;
const appLinks = (appLinksMap.get(sampleDataset.id) ?? []).map((data) => {
const { sampleObject, getPath, label, icon } = data;
const { sampleObject, getPath, label, icon, order } = data;
if (sampleObject === null) {
return { path: getPath(''), label, icon };
return { path: getPath(''), label, icon, order };
}
const objectId = findObjectId(sampleObject.type, sampleObject.id);
return { path: getPath(objectId), label, icon };
return { path: getPath(objectId), label, icon, order };
});
const sampleDataStatus = await getSampleDatasetStatus(
context,

View file

@ -78,6 +78,11 @@ export class HomePageObject extends FtrService {
});
}
async launchSampleDiscover(id: string) {
await this.launchSampleDataSet(id);
await this.find.clickByLinkText('Discover');
}
async launchSampleDashboard(id: string) {
await this.launchSampleDataSet(id);
await this.find.clickByLinkText('Dashboard');

View file

@ -8,7 +8,7 @@
import expect from '@kbn/expect';
import { FtrProviderContext } from '../../ftr_provider_context';
export default function ({ getPageObjects, getService }: FtrProviderContext) {
export default function ({ getPageObjects }: FtrProviderContext) {
const PageObjects = getPageObjects(['common', 'header', 'home', 'discover', 'timePicker']);
describe('upgrade discover smoke tests', function describeIndexTests() {
@ -18,9 +18,9 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
];
const discoverTests = [
{ name: 'kibana_sample_data_flights', timefield: true, hits: '' },
{ name: 'kibana_sample_data_logs', timefield: true, hits: '' },
{ name: 'kibana_sample_data_ecommerce', timefield: true, hits: '' },
{ name: 'flights', timefield: true, hits: '' },
{ name: 'logs', timefield: true, hits: '' },
{ name: 'ecommerce', timefield: true, hits: '' },
];
spaces.forEach(({ space, basePath }) => {
@ -31,7 +31,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
basePath,
});
await PageObjects.header.waitUntilLoadingHasFinished();
await PageObjects.discover.selectIndexPattern(name);
await PageObjects.discover.selectIndexPattern(`kibana_sample_data_${name}`);
await PageObjects.discover.waitUntilSearchingHasFinished();
if (timefield) {
await PageObjects.timePicker.setCommonlyUsedTime('Last_24 hours');
@ -52,6 +52,35 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
});
});
});
discoverTests.forEach(({ name, timefield, hits }) => {
describe('space: ' + space + ', name: ' + name, () => {
before(async () => {
await PageObjects.common.navigateToActualUrl('home', '/tutorial_directory/sampleData', {
basePath,
});
await PageObjects.header.waitUntilLoadingHasFinished();
await PageObjects.home.launchSampleDiscover(name);
await PageObjects.header.waitUntilLoadingHasFinished();
if (timefield) {
await PageObjects.timePicker.setCommonlyUsedTime('Last_24 hours');
await PageObjects.discover.waitUntilSearchingHasFinished();
}
});
it('shows hit count greater than zero', async () => {
const hitCount = await PageObjects.discover.getHitCount();
if (hits === '') {
expect(hitCount).to.be.greaterThan(0);
} else {
expect(hitCount).to.be.equal(hits);
}
});
it('shows table rows not empty', async () => {
const tableRows = await PageObjects.discover.getDocTableRows();
expect(tableRows.length).to.be.greaterThan(0);
});
});
});
});
});
}