mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[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:
parent
0650bd3819
commit
58bc0f759e
17 changed files with 298 additions and 41 deletions
|
@ -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';
|
||||
|
|
|
@ -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';
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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)}`;
|
|
@ -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 = {
|
||||
|
|
|
@ -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', {
|
||||
|
|
|
@ -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 {};
|
||||
}
|
||||
|
||||
|
|
9
src/plugins/discover/server/sample_data/index.ts
Normal file
9
src/plugins/discover/server/sample_data/index.ts
Normal 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';
|
|
@ -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,
|
||||
},
|
||||
]);
|
||||
}
|
||||
});
|
||||
}
|
|
@ -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"
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue