From 5438de16591de73597a597652acf0ad7690230a8 Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Tue, 13 Mar 2018 15:20:50 -0600 Subject: [PATCH 01/21] convert dashboard listing page to react and EUI --- .../kibana/public/dashboard/dashboard_app.js | 5 + .../kibana/public/dashboard/index.js | 12 +- .../dashboard/listing/dashboard_listing.html | 268 ------------ .../dashboard/listing/dashboard_listing.js | 387 +++++++++++------- .../dashboard/listing/dashboard_listing.less | 9 - .../listing/dashboard_listing_ng_wrapper.html | 4 + 6 files changed, 250 insertions(+), 435 deletions(-) delete mode 100644 src/core_plugins/kibana/public/dashboard/listing/dashboard_listing.html delete mode 100644 src/core_plugins/kibana/public/dashboard/listing/dashboard_listing.less create mode 100644 src/core_plugins/kibana/public/dashboard/listing/dashboard_listing_ng_wrapper.html diff --git a/src/core_plugins/kibana/public/dashboard/dashboard_app.js b/src/core_plugins/kibana/public/dashboard/dashboard_app.js index 966ad23a41523..13d29f88286eb 100644 --- a/src/core_plugins/kibana/public/dashboard/dashboard_app.js +++ b/src/core_plugins/kibana/public/dashboard/dashboard_app.js @@ -25,6 +25,7 @@ import { FilterManagerProvider } from 'ui/filter_manager'; import { EmbeddableFactoriesRegistryProvider } from 'ui/embeddable/embeddable_factories_registry'; import { DashboardViewportProvider } from './viewport/dashboard_viewport_provider'; +import { DashboardListing } from './listing/dashboard_listing'; const app = uiModules.get('app/dashboard', [ 'elasticsearch', @@ -36,6 +37,10 @@ const app = uiModules.get('app/dashboard', [ 'kibana/typeahead', ]); +app.directive('dashboardListing', function (reactDirective) { + return reactDirective(DashboardListing); +}); + app.directive('dashboardViewportProvider', function (reactDirective) { return reactDirective(DashboardViewportProvider); }); diff --git a/src/core_plugins/kibana/public/dashboard/index.js b/src/core_plugins/kibana/public/dashboard/index.js index bcad8f3293a11..ca3e31085d2b1 100644 --- a/src/core_plugins/kibana/public/dashboard/index.js +++ b/src/core_plugins/kibana/public/dashboard/index.js @@ -6,14 +6,14 @@ import uiRoutes from 'ui/routes'; import { toastNotifications } from 'ui/notify'; import dashboardTemplate from './dashboard_app.html'; -import dashboardListingTemplate from './listing/dashboard_listing.html'; +import dashboardListingTemplate from './listing/dashboard_listing_ng_wrapper.html'; -import { DashboardListingController } from './listing/dashboard_listing'; import { DashboardConstants, createDashboardEditUrl } from './dashboard_constants'; import { SavedObjectNotFound } from 'ui/errors'; import { FeatureCatalogueRegistryProvider, FeatureCatalogueCategory } from 'ui/registry/feature_catalogue'; import { SavedObjectsClientProvider } from 'ui/saved_objects'; import { recentlyAccessed } from 'ui/persisted_log'; +import { SavedObjectRegistryProvider } from 'ui/saved_objects/saved_object_registry'; uiRoutes .defaults(/dashboard/, { @@ -21,8 +21,12 @@ uiRoutes }) .when(DashboardConstants.LANDING_PAGE_PATH, { template: dashboardListingTemplate, - controller: DashboardListingController, - controllerAs: 'listingController', + controller($scope, Private, config) { + const services = Private(SavedObjectRegistryProvider).byLoaderPropertiesName; + + $scope.dashboardService = services.dashboards; + $scope.listingLimit = config.get('savedObjects:listingLimit'); + }, resolve: { dash: function ($route, Private, courier, kbnUrl) { const savedObjectsClient = Private(SavedObjectsClientProvider); diff --git a/src/core_plugins/kibana/public/dashboard/listing/dashboard_listing.html b/src/core_plugins/kibana/public/dashboard/listing/dashboard_listing.html deleted file mode 100644 index c696083101565..0000000000000 --- a/src/core_plugins/kibana/public/dashboard/listing/dashboard_listing.html +++ /dev/null @@ -1,268 +0,0 @@ - - - -
- -
- Dashboard -
-
-
- -
-
-
-
-
- You have {{ listingController.totalItems }} dashboards, but your "listingLimit" setting prevents the table below from displaying more than {{ listingController.listingLimit }}. You can change this setting under Advanced Settings. -
-
-
-
- -
- -
-
-
- - -
-
- -
- - - - - - - -
- -
- - - -
-
- - -
-
- No dashboards matched your search. -
-
- - -
-
-
- Looks like you don’t have any dashboards. Let’s create some! -
- - -
-
- - - - - - - - - - - - - - - - - - - - - - - - -
-
- -
-
- - - - -
Actions
-
-
- -
-
- - -
- {{ item.description }} -
-
- -
- - -
-
-
- {{ listingController.getSelectedItemsCount() }} selected -
-
- -
- - - -
-
-
-
diff --git a/src/core_plugins/kibana/public/dashboard/listing/dashboard_listing.js b/src/core_plugins/kibana/public/dashboard/listing/dashboard_listing.js index b5b91dd275a25..bc26b176ea614 100644 --- a/src/core_plugins/kibana/public/dashboard/listing/dashboard_listing.js +++ b/src/core_plugins/kibana/public/dashboard/listing/dashboard_listing.js @@ -1,168 +1,247 @@ -import { SavedObjectRegistryProvider } from 'ui/saved_objects/saved_object_registry'; -import 'ui/pager_control'; -import 'ui/pager'; -import './dashboard_listing.less'; +import React from 'react'; +import PropTypes from 'prop-types'; +import _ from 'lodash'; +import { toastNotifications } from 'ui/notify'; +import { + EuiTitle, + EuiInMemoryTable, + EuiPage, + EuiLink, + EuiFlexGroup, + EuiFlexItem, + EuiButton, + EuiSpacer, + EuiOverlayMask, + EuiConfirmModal, + EuiCallOut, +} from '@elastic/eui'; import { DashboardConstants, createDashboardEditUrl } from '../dashboard_constants'; -import { SortableProperties } from '@elastic/eui'; -import { ConfirmationButtonTypes } from 'ui/modals'; - -export function DashboardListingController($injector, $scope, $location) { - const $filter = $injector.get('$filter'); - const confirmModal = $injector.get('confirmModal'); - const Notifier = $injector.get('Notifier'); - const pagerFactory = $injector.get('pagerFactory'); - const Private = $injector.get('Private'); - const timefilter = $injector.get('timefilter'); - const config = $injector.get('config'); - const dashboardConfig = $injector.get('dashboardConfig'); - - timefilter.disableAutoRefreshSelector(); - timefilter.disableTimeRangeSelector(); - - const limitTo = $filter('limitTo'); - // TODO: Extract this into an external service. - const services = Private(SavedObjectRegistryProvider).byLoaderPropertiesName; - const dashboardService = services.dashboards; - const notify = new Notifier({ location: 'Dashboard' }); - - let selectedItems = []; - const sortableProperties = new SortableProperties([ - { - name: 'title', - getValue: item => item.title.toLowerCase(), - isAscending: true, - }, - { - name: 'description', - getValue: item => item.description.toLowerCase(), - isAscending: true - } - ], - 'title'); - - const calculateItemsOnPage = () => { - this.items = sortableProperties.sortItems(this.items); - this.pager.setTotalItems(this.items.length); - this.pageOfItems = limitTo(this.items, this.pager.pageSize, this.pager.startIndex); - }; - - const fetchItems = () => { - this.isFetchingItems = true; - - dashboardService.find(this.filter, config.get('savedObjects:listingLimit')) - .then(result => { - this.isFetchingItems = false; - this.items = result.hits; - this.totalItems = result.total; - this.showLimitError = result.total > config.get('savedObjects:listingLimit'); - this.listingLimit = config.get('savedObjects:listingLimit'); - calculateItemsOnPage(); - }); - }; - const deselectAll = () => { - selectedItems = []; - }; +const tableColumns = [ + { + field: 'title', + name: 'Title', + sortable: true, + sortable: true, + render: (field, record) => ( + + {field} + + ) + }, + { + field: 'description', + name: 'Description', + dataType: 'string', + sortable: true + } +]; + +const EMPTY_FILTER = ''; + +export class DashboardListing extends React.Component { + + constructor(props) { + super(props); + + this.state = { + isFetchingItems: false, + showDeleteModal: false, + showLimitError: false, + filter: EMPTY_FILTER, + dashboards: [], + selectedIds: [], + }; + } - const selectAll = () => { - selectedItems = this.pageOfItems.slice(0); - }; + componentWillMount() { + this._isMounted = true; + } - this.isFetchingItems = false; - this.items = []; - this.pageOfItems = []; - this.filter = ($location.search()).filter || ''; + componentWillUnmount() { + this._isMounted = false; + this.debouncedFetch.cancel(); + } - this.pager = pagerFactory.create(this.items.length, 20, 1); + componentDidMount() { + this.fetchItems(); + } - this.hideWriteControls = dashboardConfig.getHideWriteControls(); + fetchItems = () => { + this.setState({ + isFetchingItems: true, + }, this.debouncedFetch); + } - $scope.$watch(() => this.filter, () => { - deselectAll(); - fetchItems(); - $location.search('filter', this.filter); - }); - this.isAscending = (name) => sortableProperties.isAscendingByName(name); - this.getSortedProperty = () => sortableProperties.getSortedProperty(); + debouncedFetch = _.debounce(async () => { + const response = await this.props.dashboardService.find(this.state.filter, this.props.listingLimit); - this.sortOn = function sortOn(propertyName) { - sortableProperties.sortOn(propertyName); - deselectAll(); - calculateItemsOnPage(); - }; - - this.toggleAll = function toggleAll() { - if (this.areAllItemsChecked()) { - deselectAll(); - } else { - selectAll(); + if (!this._isMounted) { + return; } - }; - this.toggleItem = function toggleItem(item) { - if (this.isItemChecked(item)) { - const index = selectedItems.indexOf(item); - selectedItems.splice(index, 1); - } else { - selectedItems.push(item); + this.setState({ + isFetchingItems: false, + dashboards: response.hits, + totalDashboards: response.total, + showLimitError: response.total > this.props.listingLimit, + }); + }, 300); + + deleteSelectedItems = async () => { + try { + await this.props.dashboardService.delete(this.state.selectedIds); + } catch (error) { + toastNotifications.addWarning({ + title: `Unable to delete dashboard(s)`, + text: `${error}`, + }); + } + this.fetchItems(); + this.setState({ + selectedIds: [] + }); + this.closeDeleteModal(); + } + + closeDeleteModal = () => { + this.setState({ showDeleteModal: false }); + }; + + openDeleteModal = () => { + this.setState({ showDeleteModal: true }); + }; + + hasFilter = () => { + return this.state.filter !== EMPTY_FILTER; + } + + renderConfirmDeleteModal() { + return ( + + +

{`You can't recover deleted dashboards.`}

+
+
+ ); + } + + renderDeleteButton() { + if (this.state.selectedIds.length === 0) { + return; } - }; - - this.isItemChecked = function isItemChecked(item) { - return selectedItems.indexOf(item) !== -1; - }; - - this.areAllItemsChecked = function areAllItemsChecked() { - return this.getSelectedItemsCount() === this.pageOfItems.length; - }; - - this.getSelectedItemsCount = function getSelectedItemsCount() { - return selectedItems.length; - }; - - this.deleteSelectedItems = function deleteSelectedItems() { - const doDelete = () => { - const selectedIds = selectedItems.map(item => item.id); - dashboardService.delete(selectedIds) - .then(fetchItems) - .then(() => { - deselectAll(); - }) - .catch(error => notify.error(error)); + return ( + + Delete selected + + ); + } + + renderListingLimitWarning() { + if (this.state.showLimitError) { + return ( + + +

+ You have {this.state.totalDashboards} dashboards, + but your listingLimit setting prevents the table below from displaying more than {this.props.listingLimit}. + You can change this setting under Advanced Settings. +

+
+ +
+ ); + } + } + + shouldFetch(newFilter, oldFilter) { + return !newFilter.includes(oldFilter); + } + + renderTable() { + const search = { + toolsLeft: this.renderDeleteButton(), + box: { + incremental: true, + }, + onChange: (query) => { + if (this.state.showLimitError || this.shouldFetch(query.text, this.state.filter)) { + this.setState({ + filter: query.text + }, this.fetchItems); + } + return true; + }, }; - - confirmModal( - `You can't recover deleted dashboards.`, - { - confirmButtonText: 'Delete', - onConfirm: doDelete, - defaultFocusedButton: ConfirmationButtonTypes.CANCEL, - title: 'Delete selected dashboards?' - }); - }; - - this.onPageNext = () => { - deselectAll(); - this.pager.nextPage(); - calculateItemsOnPage(); - }; - - this.onPagePrevious = () => { - deselectAll(); - this.pager.previousPage(); - calculateItemsOnPage(); - }; - - this.getUrlForItem = function getUrlForItem(item) { - return `#${createDashboardEditUrl(item.id)}`; - }; - - this.getEditUrlForItem = function getEditUrlForItem(item) { - return `#${createDashboardEditUrl(item.id)}?_a=(viewMode:edit)`; - }; - - this.getCreateDashboardHref = function getCreateDashboardHref() { - return `#${DashboardConstants.CREATE_NEW_DASHBOARD_URL}`; - }; + const selection = { + itemId: 'id', + onSelectionChange: (selection) => { + this.setState({ + selectedIds: selection.map(item => { return item.id; }) + }); + } + }; + return ( + + ); + } + + render() { + return ( + + + {this.state.showDeleteModal && this.renderConfirmDeleteModal()} + + + + +

+ Dashboard +

+
+
+ + + Create new dashboard + + +
+ + + {this.renderListingLimitWarning()} + + {this.renderTable()} + +
+ ); + } } + +DashboardListing.propTypes = { + dashboardService: PropTypes.shape({ + find: PropTypes.func.isRequired, + delete: PropTypes.func.isRequired, + }), + listingLimit: PropTypes.number.isRequired, +}; diff --git a/src/core_plugins/kibana/public/dashboard/listing/dashboard_listing.less b/src/core_plugins/kibana/public/dashboard/listing/dashboard_listing.less deleted file mode 100644 index 5afa8bf68d145..0000000000000 --- a/src/core_plugins/kibana/public/dashboard/listing/dashboard_listing.less +++ /dev/null @@ -1,9 +0,0 @@ -.dashboardListingTable { - .kuiTableHeaderCell { - max-width: none; - } - - .actionBtnHeaderCell { - width: 80px; - } -} diff --git a/src/core_plugins/kibana/public/dashboard/listing/dashboard_listing_ng_wrapper.html b/src/core_plugins/kibana/public/dashboard/listing/dashboard_listing_ng_wrapper.html new file mode 100644 index 0000000000000..b4e6b090cf3cf --- /dev/null +++ b/src/core_plugins/kibana/public/dashboard/listing/dashboard_listing_ng_wrapper.html @@ -0,0 +1,4 @@ + From a400eda9bcfc9313dd36534d302589707940cd6b Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Tue, 13 Mar 2018 16:17:11 -0600 Subject: [PATCH 02/21] add jest test for DashboardListing component --- .../kibana/public/dashboard/index.js | 7 +- .../dashboard_listing.test.js.snap | 81 +++++++++++++++++++ .../dashboard/listing/dashboard_listing.js | 11 ++- .../listing/dashboard_listing.test.js | 40 +++++++++ .../listing/dashboard_listing_ng_wrapper.html | 3 +- 5 files changed, 134 insertions(+), 8 deletions(-) create mode 100644 src/core_plugins/kibana/public/dashboard/listing/__snapshots__/dashboard_listing.test.js.snap create mode 100644 src/core_plugins/kibana/public/dashboard/listing/dashboard_listing.test.js diff --git a/src/core_plugins/kibana/public/dashboard/index.js b/src/core_plugins/kibana/public/dashboard/index.js index ca3e31085d2b1..a58496b5e6476 100644 --- a/src/core_plugins/kibana/public/dashboard/index.js +++ b/src/core_plugins/kibana/public/dashboard/index.js @@ -24,8 +24,13 @@ uiRoutes controller($scope, Private, config) { const services = Private(SavedObjectRegistryProvider).byLoaderPropertiesName; - $scope.dashboardService = services.dashboards; $scope.listingLimit = config.get('savedObjects:listingLimit'); + $scope.find = (search) => { + return services.dashboards.find(search, $scope.listingLimit); + }; + $scope.delete = (ids) => { + return services.dashboards.delete(ids); + }; }, resolve: { dash: function ($route, Private, courier, kbnUrl) { diff --git a/src/core_plugins/kibana/public/dashboard/listing/__snapshots__/dashboard_listing.test.js.snap b/src/core_plugins/kibana/public/dashboard/listing/__snapshots__/dashboard_listing.test.js.snap new file mode 100644 index 0000000000000..2f26cb9818798 --- /dev/null +++ b/src/core_plugins/kibana/public/dashboard/listing/__snapshots__/dashboard_listing.test.js.snap @@ -0,0 +1,81 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`renders DashboardListing 1`] = ` + + + + +

+ Dashboard +

+
+
+ + + Create new dashboard + + +
+ + +
+`; diff --git a/src/core_plugins/kibana/public/dashboard/listing/dashboard_listing.js b/src/core_plugins/kibana/public/dashboard/listing/dashboard_listing.js index bc26b176ea614..cfd54672ec69f 100644 --- a/src/core_plugins/kibana/public/dashboard/listing/dashboard_listing.js +++ b/src/core_plugins/kibana/public/dashboard/listing/dashboard_listing.js @@ -74,7 +74,7 @@ export class DashboardListing extends React.Component { } debouncedFetch = _.debounce(async () => { - const response = await this.props.dashboardService.find(this.state.filter, this.props.listingLimit); + const response = await this.props.find(this.state.filter); if (!this._isMounted) { return; @@ -90,7 +90,7 @@ export class DashboardListing extends React.Component { deleteSelectedItems = async () => { try { - await this.props.dashboardService.delete(this.state.selectedIds); + await this.props.delete(this.state.selectedIds); } catch (error) { toastNotifications.addWarning({ title: `Unable to delete dashboard(s)`, @@ -201,6 +201,7 @@ export class DashboardListing extends React.Component { loading={this.state.isFetchingItems} columns={tableColumns} sorting={true} + pagination={true} selection={selection} search={search} /> @@ -239,9 +240,7 @@ export class DashboardListing extends React.Component { } DashboardListing.propTypes = { - dashboardService: PropTypes.shape({ - find: PropTypes.func.isRequired, - delete: PropTypes.func.isRequired, - }), + find: PropTypes.func.isRequired, + delete: PropTypes.func.isRequired, listingLimit: PropTypes.number.isRequired, }; diff --git a/src/core_plugins/kibana/public/dashboard/listing/dashboard_listing.test.js b/src/core_plugins/kibana/public/dashboard/listing/dashboard_listing.test.js new file mode 100644 index 0000000000000..0f092956c0a26 --- /dev/null +++ b/src/core_plugins/kibana/public/dashboard/listing/dashboard_listing.test.js @@ -0,0 +1,40 @@ +jest.mock('ui/notify', + () => ({ + toastNotifications: { + addWarning: () => {}, + } + }), { virtual: true }); + +import React from 'react'; +import { shallow } from 'enzyme'; + +import { + DashboardListing, +} from './dashboard_listing'; + +const find = () => { + return Promise.resolve({ + total: 2, + hits: [ + { + id: 'dashboard1', + title: 'dashboard1 title', + description: 'dashboard1 desc' + }, + { + id: 'dashboard2', + title: 'dashboard2 title', + description: 'dashboard2 desc' + }, + ] + }); +}; + +test('renders DashboardListing', () => { + const component = shallow( {}} + listingLimit={1000} + />); + expect(component).toMatchSnapshot(); // eslint-disable-line +}); diff --git a/src/core_plugins/kibana/public/dashboard/listing/dashboard_listing_ng_wrapper.html b/src/core_plugins/kibana/public/dashboard/listing/dashboard_listing_ng_wrapper.html index b4e6b090cf3cf..0a32957fbe228 100644 --- a/src/core_plugins/kibana/public/dashboard/listing/dashboard_listing_ng_wrapper.html +++ b/src/core_plugins/kibana/public/dashboard/listing/dashboard_listing_ng_wrapper.html @@ -1,4 +1,5 @@ From 92bfc1ddd60a7aa0fbbc223a3a7f2baf56df7565 Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Tue, 13 Mar 2018 16:27:49 -0600 Subject: [PATCH 03/21] add data-test-subj attributes --- .../kibana/public/dashboard/listing/dashboard_listing.js | 7 +++++-- .../public/dashboard/listing/dashboard_listing.test.js | 2 +- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/core_plugins/kibana/public/dashboard/listing/dashboard_listing.js b/src/core_plugins/kibana/public/dashboard/listing/dashboard_listing.js index cfd54672ec69f..93cdcd0ddf664 100644 --- a/src/core_plugins/kibana/public/dashboard/listing/dashboard_listing.js +++ b/src/core_plugins/kibana/public/dashboard/listing/dashboard_listing.js @@ -210,7 +210,7 @@ export class DashboardListing extends React.Component { render() { return ( - + {this.state.showDeleteModal && this.renderConfirmDeleteModal()} @@ -223,7 +223,10 @@ export class DashboardListing extends React.Component { - + Create new dashboard diff --git a/src/core_plugins/kibana/public/dashboard/listing/dashboard_listing.test.js b/src/core_plugins/kibana/public/dashboard/listing/dashboard_listing.test.js index 0f092956c0a26..0c75ca55e4c12 100644 --- a/src/core_plugins/kibana/public/dashboard/listing/dashboard_listing.test.js +++ b/src/core_plugins/kibana/public/dashboard/listing/dashboard_listing.test.js @@ -36,5 +36,5 @@ test('renders DashboardListing', () => { delete={() => {}} listingLimit={1000} />); - expect(component).toMatchSnapshot(); // eslint-disable-line + expect(component).toMatchSnapshot(); }); From 17a249eac99ce24a7beda24d022559c91190e4ea Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Wed, 14 Mar 2018 13:18:36 -0600 Subject: [PATCH 04/21] clean up jest test --- .../dashboard_listing.test.js.snap | 230 +++++++++++++++++- .../dashboard/listing/dashboard_listing.js | 12 +- .../listing/dashboard_listing.test.js | 45 +++- 3 files changed, 278 insertions(+), 9 deletions(-) diff --git a/src/core_plugins/kibana/public/dashboard/listing/__snapshots__/dashboard_listing.test.js.snap b/src/core_plugins/kibana/public/dashboard/listing/__snapshots__/dashboard_listing.test.js.snap index 2f26cb9818798..0b0836cb65093 100644 --- a/src/core_plugins/kibana/public/dashboard/listing/__snapshots__/dashboard_listing.test.js.snap +++ b/src/core_plugins/kibana/public/dashboard/listing/__snapshots__/dashboard_listing.test.js.snap @@ -1,7 +1,9 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`renders DashboardListing 1`] = ` - +exports[`after fetch renders table rows 1`] = ` + + Create new dashboard + + + + + + +`; + +exports[`after fetch renders warning when listingLimit is exceeded 1`] = ` + + + + +

+ Dashboard +

+
+
+ + + Create new dashboard + + +
+ + + +

+ You have + 2 + dashboards, but your + + listingLimit + + setting prevents the table below from displaying more than + 1 + . You can change this setting under + + Advanced Settings + + . +

+
+ +
+ +
+`; + +exports[`renders table in loading state 1`] = ` + + + + +

+ Dashboard +

+
+
+ + { - this.setState({ - isFetchingItems: true, - }, this.debouncedFetch); - } - debouncedFetch = _.debounce(async () => { const response = await this.props.find(this.state.filter); @@ -88,6 +82,12 @@ export class DashboardListing extends React.Component { }); }, 300); + fetchItems = () => { + this.setState({ + isFetchingItems: true, + }, this.debouncedFetch); + } + deleteSelectedItems = async () => { try { await this.props.delete(this.state.selectedIds); diff --git a/src/core_plugins/kibana/public/dashboard/listing/dashboard_listing.test.js b/src/core_plugins/kibana/public/dashboard/listing/dashboard_listing.test.js index 0c75ca55e4c12..36a08693a6d82 100644 --- a/src/core_plugins/kibana/public/dashboard/listing/dashboard_listing.test.js +++ b/src/core_plugins/kibana/public/dashboard/listing/dashboard_listing.test.js @@ -5,6 +5,17 @@ jest.mock('ui/notify', } }), { virtual: true }); +jest.mock('lodash', + () => ({ + // mock debounce to fire immediately with no internal timer + debounce: function (func) { + function debounced(...args) { + return func.apply(this, args); + } + return debounced; + } + }), { virtual: true }); + import React from 'react'; import { shallow } from 'enzyme'; @@ -30,7 +41,7 @@ const find = () => { }); }; -test('renders DashboardListing', () => { +test('renders table in loading state', () => { const component = shallow( {}} @@ -38,3 +49,35 @@ test('renders DashboardListing', () => { />); expect(component).toMatchSnapshot(); }); + +describe('after fetch', () => { + test('renders table rows', async () => { + const component = shallow( {}} + listingLimit={1000} + />); + + // Ensure all promises resolve + await new Promise(resolve => process.nextTick(resolve)); + // Ensure the state changes are reflected + component.update(); + + expect(component).toMatchSnapshot(); + }); + + test('renders warning when listingLimit is exceeded', async () => { + const component = shallow( {}} + listingLimit={1} + />); + + // Ensure all promises resolve + await new Promise(resolve => process.nextTick(resolve)); + // Ensure the state changes are reflected + component.update(); + + expect(component).toMatchSnapshot(); + }); +}); From eb58d320b7166e80d4da3064c35b0e1d7bf3271a Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Thu, 15 Mar 2018 14:46:45 -0600 Subject: [PATCH 05/21] hideWriteControls and call to action when no dashboards exist --- .../kibana/public/dashboard/index.js | 4 +- .../dashboard_listing.test.js.snap | 214 +++++++++++++++++- .../dashboard/listing/dashboard_listing.js | 69 +++++- .../listing/dashboard_listing.test.js | 66 ++++-- .../listing/dashboard_listing_ng_wrapper.html | 1 + 5 files changed, 317 insertions(+), 37 deletions(-) diff --git a/src/core_plugins/kibana/public/dashboard/index.js b/src/core_plugins/kibana/public/dashboard/index.js index a58496b5e6476..ab334bcb9af11 100644 --- a/src/core_plugins/kibana/public/dashboard/index.js +++ b/src/core_plugins/kibana/public/dashboard/index.js @@ -21,8 +21,9 @@ uiRoutes }) .when(DashboardConstants.LANDING_PAGE_PATH, { template: dashboardListingTemplate, - controller($scope, Private, config) { + controller($injector, $scope, Private, config) { const services = Private(SavedObjectRegistryProvider).byLoaderPropertiesName; + const dashboardConfig = $injector.get('dashboardConfig'); $scope.listingLimit = config.get('savedObjects:listingLimit'); $scope.find = (search) => { @@ -31,6 +32,7 @@ uiRoutes $scope.delete = (ids) => { return services.dashboards.delete(ids); }; + $scope.hideWriteControls = dashboardConfig.getHideWriteControls(); }, resolve: { dash: function ($route, Private, courier, kbnUrl) { diff --git a/src/core_plugins/kibana/public/dashboard/listing/__snapshots__/dashboard_listing.test.js.snap b/src/core_plugins/kibana/public/dashboard/listing/__snapshots__/dashboard_listing.test.js.snap index 0b0836cb65093..b1d7a8f5dbd7a 100644 --- a/src/core_plugins/kibana/public/dashboard/listing/__snapshots__/dashboard_listing.test.js.snap +++ b/src/core_plugins/kibana/public/dashboard/listing/__snapshots__/dashboard_listing.test.js.snap @@ -1,5 +1,193 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`after fetch hideWriteControls 1`] = ` + + + + +

+ Dashboard +

+
+
+
+ + +

+ + Looks like you don't have any dashboards. + +

+ + } + pagination={true} + search={ + Object { + "box": Object { + "data-test-subj": "searchFilter", + "incremental": true, + }, + "onChange": [Function], + "toolsLeft": undefined, + } + } + selection={ + Object { + "itemId": "id", + "onSelectionChange": [Function], + } + } + sorting={true} + /> +
+`; + +exports[`after fetch renders call to action when no dashboards exist 1`] = ` + + + + +

+ Dashboard +

+
+
+ + + Create new dashboard + + +
+ + + +

+ + Looks like you don't have any dashboards. Let's create some! + +

+
+ + Create new dashboard + + + } + pagination={true} + search={ + Object { + "box": Object { + "data-test-subj": "searchFilter", + "incremental": true, + }, + "onChange": [Function], + "toolsLeft": undefined, + } + } + selection={ + Object { + "itemId": "id", + "onSelectionChange": [Function], + } + } + sorting={true} + /> +
+`; + exports[`after fetch renders table rows 1`] = ` +

+ + {`Looks like you don't have any dashboards.`} + +

+ + ); + } + + return ( + + +

+ + {`Looks like you don't have any dashboards. Let's create some!`} + +

+
+ + Create new dashboard + +
+ ); + } + + return 'No dashboards matched your search.'; + } + renderTable() { const search = { toolsLeft: this.renderDeleteButton(), box: { incremental: true, + ['data-test-subj']: 'searchFilter' }, onChange: (query) => { if (this.state.showLimitError || this.shouldFetch(query.text, this.state.filter)) { @@ -185,7 +226,7 @@ export class DashboardListing extends React.Component { }, this.fetchItems); } return true; - }, + } }; const selection = { itemId: 'id', @@ -204,11 +245,25 @@ export class DashboardListing extends React.Component { pagination={true} selection={selection} search={search} + message={this.renderMessage()} /> ); } render() { + let createButton; + if (!this.props.hideWriteControls) { + createButton = ( + + + Create new dashboard + + + ); + } return ( @@ -222,14 +277,9 @@ export class DashboardListing extends React.Component {
- - - Create new dashboard - - + + {createButton} +
@@ -246,4 +296,5 @@ DashboardListing.propTypes = { find: PropTypes.func.isRequired, delete: PropTypes.func.isRequired, listingLimit: PropTypes.number.isRequired, + hideWriteControls: PropTypes.bool.isRequired, }; diff --git a/src/core_plugins/kibana/public/dashboard/listing/dashboard_listing.test.js b/src/core_plugins/kibana/public/dashboard/listing/dashboard_listing.test.js index 36a08693a6d82..5745a111dd3ce 100644 --- a/src/core_plugins/kibana/public/dashboard/listing/dashboard_listing.test.js +++ b/src/core_plugins/kibana/public/dashboard/listing/dashboard_listing.test.js @@ -23,29 +23,27 @@ import { DashboardListing, } from './dashboard_listing'; -const find = () => { +const find = (num) => { + const hits = []; + for (let i = 0; i < num; i++) { + hits.push({ + id: `dashboard${i}`, + title: `dashboard${i} title`, + description: `dashboard${i} desc` + }); + } return Promise.resolve({ - total: 2, - hits: [ - { - id: 'dashboard1', - title: 'dashboard1 title', - description: 'dashboard1 desc' - }, - { - id: 'dashboard2', - title: 'dashboard2 title', - description: 'dashboard2 desc' - }, - ] + total: num, + hits: hits }); }; test('renders table in loading state', () => { const component = shallow( {}} listingLimit={1000} + hideWriteControls={false} />); expect(component).toMatchSnapshot(); }); @@ -53,9 +51,42 @@ test('renders table in loading state', () => { describe('after fetch', () => { test('renders table rows', async () => { const component = shallow( {}} listingLimit={1000} + hideWriteControls={false} + />); + + // Ensure all promises resolve + await new Promise(resolve => process.nextTick(resolve)); + // Ensure the state changes are reflected + component.update(); + + expect(component).toMatchSnapshot(); + }); + + test('renders call to action when no dashboards exist', async () => { + const component = shallow( {}} + listingLimit={1} + hideWriteControls={false} + />); + + // Ensure all promises resolve + await new Promise(resolve => process.nextTick(resolve)); + // Ensure the state changes are reflected + component.update(); + + expect(component).toMatchSnapshot(); + }); + + test('hideWriteControls', async () => { + const component = shallow( {}} + listingLimit={1} + hideWriteControls={true} />); // Ensure all promises resolve @@ -68,9 +99,10 @@ describe('after fetch', () => { test('renders warning when listingLimit is exceeded', async () => { const component = shallow( {}} listingLimit={1} + hideWriteControls={false} />); // Ensure all promises resolve diff --git a/src/core_plugins/kibana/public/dashboard/listing/dashboard_listing_ng_wrapper.html b/src/core_plugins/kibana/public/dashboard/listing/dashboard_listing_ng_wrapper.html index 0a32957fbe228..bf4ac571528c7 100644 --- a/src/core_plugins/kibana/public/dashboard/listing/dashboard_listing_ng_wrapper.html +++ b/src/core_plugins/kibana/public/dashboard/listing/dashboard_listing_ng_wrapper.html @@ -2,4 +2,5 @@ find="find" delete="delete" listing-limit="listingLimit" + hide-write-controls="hideWriteControls" /> From 6ccc702fd70b8644144c2cb7ba58c8b78c545f5c Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Fri, 16 Mar 2018 07:48:49 -0600 Subject: [PATCH 06/21] pass initial filter to dashboard listing, get functional tests to work --- .../kibana/public/dashboard/index.js | 4 +- .../dashboard_listing.test.js.snap | 91 +++++++++++++++++++ .../dashboard/listing/dashboard_listing.js | 12 ++- .../listing/dashboard_listing.test.js | 11 +++ .../listing/dashboard_listing_ng_wrapper.html | 1 + .../apps/dashboard/_dashboard_listing.js | 42 ++------- .../functional/page_objects/dashboard_page.js | 15 +-- 7 files changed, 130 insertions(+), 46 deletions(-) diff --git a/src/core_plugins/kibana/public/dashboard/index.js b/src/core_plugins/kibana/public/dashboard/index.js index ab334bcb9af11..a42a01635d10a 100644 --- a/src/core_plugins/kibana/public/dashboard/index.js +++ b/src/core_plugins/kibana/public/dashboard/index.js @@ -14,6 +14,7 @@ import { FeatureCatalogueRegistryProvider, FeatureCatalogueCategory } from 'ui/r import { SavedObjectsClientProvider } from 'ui/saved_objects'; import { recentlyAccessed } from 'ui/persisted_log'; import { SavedObjectRegistryProvider } from 'ui/saved_objects/saved_object_registry'; +import { EMPTY_FILTER } from './listing/dashboard_listing'; uiRoutes .defaults(/dashboard/, { @@ -21,7 +22,7 @@ uiRoutes }) .when(DashboardConstants.LANDING_PAGE_PATH, { template: dashboardListingTemplate, - controller($injector, $scope, Private, config) { + controller($injector, $location, $scope, Private, config) { const services = Private(SavedObjectRegistryProvider).byLoaderPropertiesName; const dashboardConfig = $injector.get('dashboardConfig'); @@ -33,6 +34,7 @@ uiRoutes return services.dashboards.delete(ids); }; $scope.hideWriteControls = dashboardConfig.getHideWriteControls(); + $scope.initialFilter = ($location.search()).filter || EMPTY_FILTER; }, resolve: { dash: function ($route, Private, courier, kbnUrl) { diff --git a/src/core_plugins/kibana/public/dashboard/listing/__snapshots__/dashboard_listing.test.js.snap b/src/core_plugins/kibana/public/dashboard/listing/__snapshots__/dashboard_listing.test.js.snap index b1d7a8f5dbd7a..f2d6f2c877033 100644 --- a/src/core_plugins/kibana/public/dashboard/listing/__snapshots__/dashboard_listing.test.js.snap +++ b/src/core_plugins/kibana/public/dashboard/listing/__snapshots__/dashboard_listing.test.js.snap @@ -65,6 +65,7 @@ exports[`after fetch hideWriteControls 1`] = ` "data-test-subj": "searchFilter", "incremental": true, }, + "defaultQuery": "", "onChange": [Function], "toolsLeft": undefined, } @@ -173,6 +174,7 @@ exports[`after fetch renders call to action when no dashboards exist 1`] = ` "data-test-subj": "searchFilter", "incremental": true, }, + "defaultQuery": "", "onChange": [Function], "toolsLeft": undefined, } @@ -271,6 +273,7 @@ exports[`after fetch renders table rows 1`] = ` "data-test-subj": "searchFilter", "incremental": true, }, + "defaultQuery": "", "onChange": [Function], "toolsLeft": undefined, } @@ -400,6 +403,93 @@ exports[`after fetch renders warning when listingLimit is exceeded 1`] = ` "data-test-subj": "searchFilter", "incremental": true, }, + "defaultQuery": "", + "onChange": [Function], + "toolsLeft": undefined, + } + } + selection={ + Object { + "itemId": "id", + "onSelectionChange": [Function], + } + } + sorting={true} + /> +
+`; + +exports[`initialFilter 1`] = ` + + + + +

+ Dashboard +

+
+
+ + + Create new dashboard + + +
+ +

{`You can't recover deleted dashboards.`}

@@ -143,6 +144,7 @@ export class DashboardListing extends React.Component { Delete selected @@ -214,6 +216,7 @@ export class DashboardListing extends React.Component { renderTable() { const search = { + defaultQuery: this.props.initialFilter, toolsLeft: this.renderDeleteButton(), box: { incremental: true, @@ -297,4 +300,9 @@ DashboardListing.propTypes = { delete: PropTypes.func.isRequired, listingLimit: PropTypes.number.isRequired, hideWriteControls: PropTypes.bool.isRequired, + initialFilter: PropTypes.string, +}; + +DashboardListing.defaultProps = { + initialFilter: EMPTY_FILTER, }; diff --git a/src/core_plugins/kibana/public/dashboard/listing/dashboard_listing.test.js b/src/core_plugins/kibana/public/dashboard/listing/dashboard_listing.test.js index 5745a111dd3ce..80566bfb18814 100644 --- a/src/core_plugins/kibana/public/dashboard/listing/dashboard_listing.test.js +++ b/src/core_plugins/kibana/public/dashboard/listing/dashboard_listing.test.js @@ -48,6 +48,17 @@ test('renders table in loading state', () => { expect(component).toMatchSnapshot(); }); +test('initialFilter', () => { + const component = shallow( {}} + listingLimit={1000} + hideWriteControls={false} + initialFilter="my dashboard" + />); + expect(component).toMatchSnapshot(); +}); + describe('after fetch', () => { test('renders table rows', async () => { const component = shallow( diff --git a/test/functional/apps/dashboard/_dashboard_listing.js b/test/functional/apps/dashboard/_dashboard_listing.js index bd2506ea8513a..73477faad4ed8 100644 --- a/test/functional/apps/dashboard/_dashboard_listing.js +++ b/test/functional/apps/dashboard/_dashboard_listing.js @@ -43,10 +43,12 @@ export default function ({ getService, getPageObjects }) { describe('delete', async function () { it('default confirm action is cancel', async function () { await PageObjects.dashboard.searchForDashboardWithName(''); - await PageObjects.dashboard.clickListItemCheckbox(); + await PageObjects.dashboard.checkDashboardListingSelectAllCheckbox(); await PageObjects.dashboard.clickDeleteSelectedDashboards(); - await PageObjects.common.pressEnterKey(); + // EUI defaultFocusedButton prop not working at the moment + //await PageObjects.common.pressEnterKey(); + await PageObjects.common.clickCancelOnModal(); const isConfirmOpen = await PageObjects.common.isConfirmModalOpen(); expect(isConfirmOpen).to.be(false); @@ -56,7 +58,7 @@ export default function ({ getService, getPageObjects }) { }); it('succeeds on confirmation press', async function () { - await PageObjects.dashboard.clickListItemCheckbox(); + await PageObjects.dashboard.checkDashboardListingSelectAllCheckbox(); await PageObjects.dashboard.clickDeleteSelectedDashboards(); await PageObjects.common.clickConfirmOnModal(); @@ -66,45 +68,13 @@ export default function ({ getService, getPageObjects }) { }); }); - describe('search', function () { + describe('search by title', function () { before(async () => { await PageObjects.dashboard.clearSearchValue(); await PageObjects.dashboard.clickCreateDashboardPrompt(); await PageObjects.dashboard.saveDashboard('Two Words'); }); - it('matches on the first word', async function () { - await PageObjects.dashboard.searchForDashboardWithName('Two'); - const countOfDashboards = await PageObjects.dashboard.getCountOfDashboardsInListingTable(); - expect(countOfDashboards).to.equal(1); - }); - - it('matches the second word', async function () { - await PageObjects.dashboard.searchForDashboardWithName('Words'); - const countOfDashboards = await PageObjects.dashboard.getCountOfDashboardsInListingTable(); - expect(countOfDashboards).to.equal(1); - }); - - it('matches the second word prefix', async function () { - await PageObjects.dashboard.searchForDashboardWithName('Wor'); - const countOfDashboards = await PageObjects.dashboard.getCountOfDashboardsInListingTable(); - expect(countOfDashboards).to.equal(1); - }); - - it('does not match mid word', async function () { - await PageObjects.dashboard.searchForDashboardWithName('ords'); - const countOfDashboards = await PageObjects.dashboard.getCountOfDashboardsInListingTable(); - expect(countOfDashboards).to.equal(0); - }); - - it('is case insensitive', async function () { - await PageObjects.dashboard.searchForDashboardWithName('two words'); - const countOfDashboards = await PageObjects.dashboard.getCountOfDashboardsInListingTable(); - expect(countOfDashboards).to.equal(1); - }); - }); - - describe('search by title', function () { it('loads a dashboard if title matches', async function () { const currentUrl = await remote.getCurrentUrl(); const newUrl = currentUrl + '&title=Two%20Words'; diff --git a/test/functional/page_objects/dashboard_page.js b/test/functional/page_objects/dashboard_page.js index 70d4f9dd729aa..6b284e4f31782 100644 --- a/test/functional/page_objects/dashboard_page.js +++ b/test/functional/page_objects/dashboard_page.js @@ -198,8 +198,13 @@ export function DashboardPageProvider({ getService, getPageObjects }) { return await testSubjects.exists('createDashboardPromptButton'); } - async clickListItemCheckbox() { - await testSubjects.click('dashboardListItemCheckbox'); + async checkDashboardListingSelectAllCheckbox() { + const element = await testSubjects.find('checkboxSelectAll'); + const isSelected = await element.isSelected(); + if(!isSelected) { + log.debug(`checking checkbox "checkboxSelectAll"`); + await testSubjects.click('checkboxSelectAll'); + } } async clickDeleteSelectedDashboards() { @@ -385,16 +390,12 @@ export function DashboardPageProvider({ getService, getPageObjects }) { await searchFilter.click(); // Note: this replacement of - to space is to preserve original logic but I'm not sure why or if it's needed. await searchFilter.type(dashName.replace('-', ' ')); + await PageObjects.common.pressEnterKey(); }); await PageObjects.header.waitUntilLoadingHasFinished(); } - async getCountOfDashboardsInListingTable() { - const dashboardTitles = await testSubjects.findAll('dashboardListingRow'); - return dashboardTitles.length; - } - async getDashboardCountWithName(dashName) { log.debug(`getDashboardCountWithName: ${dashName}`); From 2331d36b717ccdee45b42f12848416f3d0b9596c Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Fri, 16 Mar 2018 09:33:46 -0600 Subject: [PATCH 07/21] fix dashboard queries functional tests --- .../listing/__snapshots__/dashboard_listing.test.js.snap | 6 ++++++ .../kibana/public/dashboard/listing/dashboard_listing.js | 7 +++++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/core_plugins/kibana/public/dashboard/listing/__snapshots__/dashboard_listing.test.js.snap b/src/core_plugins/kibana/public/dashboard/listing/__snapshots__/dashboard_listing.test.js.snap index f2d6f2c877033..3f9f3860454d8 100644 --- a/src/core_plugins/kibana/public/dashboard/listing/__snapshots__/dashboard_listing.test.js.snap +++ b/src/core_plugins/kibana/public/dashboard/listing/__snapshots__/dashboard_listing.test.js.snap @@ -7,6 +7,7 @@ exports[`after fetch hideWriteControls 1`] = ` ( - + {field} ) @@ -272,7 +275,7 @@ export class DashboardListing extends React.Component { {this.state.showDeleteModal && this.renderConfirmDeleteModal()} - +

From 165b228efe81d30cea9b2293c739f4d2c2f8cf77 Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Sat, 17 Mar 2018 07:29:01 -0600 Subject: [PATCH 08/21] upgraded to EUI 0.0.29 to get defaultFocusedButton fix --- info.gif | 0 test/functional/apps/dashboard/_dashboard_listing.js | 4 +--- 2 files changed, 1 insertion(+), 3 deletions(-) create mode 100644 info.gif diff --git a/info.gif b/info.gif new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/test/functional/apps/dashboard/_dashboard_listing.js b/test/functional/apps/dashboard/_dashboard_listing.js index 73477faad4ed8..b9e7d51f94ce9 100644 --- a/test/functional/apps/dashboard/_dashboard_listing.js +++ b/test/functional/apps/dashboard/_dashboard_listing.js @@ -46,9 +46,7 @@ export default function ({ getService, getPageObjects }) { await PageObjects.dashboard.checkDashboardListingSelectAllCheckbox(); await PageObjects.dashboard.clickDeleteSelectedDashboards(); - // EUI defaultFocusedButton prop not working at the moment - //await PageObjects.common.pressEnterKey(); - await PageObjects.common.clickCancelOnModal(); + await PageObjects.common.pressEnterKey(); const isConfirmOpen = await PageObjects.common.isConfirmModalOpen(); expect(isConfirmOpen).to.be(false); From 54ae72b2a9b1219bb11a4f35db2e308280bb514c Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Mon, 19 Mar 2018 09:39:07 -0600 Subject: [PATCH 09/21] move dashboardListing directive to index --- .../kibana/public/dashboard/dashboard_app.js | 5 ----- src/core_plugins/kibana/public/dashboard/index.js | 12 +++++++++++- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/src/core_plugins/kibana/public/dashboard/dashboard_app.js b/src/core_plugins/kibana/public/dashboard/dashboard_app.js index 13d29f88286eb..966ad23a41523 100644 --- a/src/core_plugins/kibana/public/dashboard/dashboard_app.js +++ b/src/core_plugins/kibana/public/dashboard/dashboard_app.js @@ -25,7 +25,6 @@ import { FilterManagerProvider } from 'ui/filter_manager'; import { EmbeddableFactoriesRegistryProvider } from 'ui/embeddable/embeddable_factories_registry'; import { DashboardViewportProvider } from './viewport/dashboard_viewport_provider'; -import { DashboardListing } from './listing/dashboard_listing'; const app = uiModules.get('app/dashboard', [ 'elasticsearch', @@ -37,10 +36,6 @@ const app = uiModules.get('app/dashboard', [ 'kibana/typeahead', ]); -app.directive('dashboardListing', function (reactDirective) { - return reactDirective(DashboardListing); -}); - app.directive('dashboardViewportProvider', function (reactDirective) { return reactDirective(DashboardViewportProvider); }); diff --git a/src/core_plugins/kibana/public/dashboard/index.js b/src/core_plugins/kibana/public/dashboard/index.js index a42a01635d10a..cd135c05fec16 100644 --- a/src/core_plugins/kibana/public/dashboard/index.js +++ b/src/core_plugins/kibana/public/dashboard/index.js @@ -14,7 +14,17 @@ import { FeatureCatalogueRegistryProvider, FeatureCatalogueCategory } from 'ui/r import { SavedObjectsClientProvider } from 'ui/saved_objects'; import { recentlyAccessed } from 'ui/persisted_log'; import { SavedObjectRegistryProvider } from 'ui/saved_objects/saved_object_registry'; -import { EMPTY_FILTER } from './listing/dashboard_listing'; +import { DashboardListing, EMPTY_FILTER } from './listing/dashboard_listing'; +import { uiModules } from 'ui/modules'; + +const app = uiModules.get('app/dashboard', [ + 'ngRoute', + 'react', +]); + +app.directive('dashboardListing', function (reactDirective) { + return reactDirective(DashboardListing); +}); uiRoutes .defaults(/dashboard/, { From c4996ce363a66fa2bdeb66f1d1cbd13c0ed76406 Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Mon, 19 Mar 2018 09:45:53 -0600 Subject: [PATCH 10/21] spacing in if statement --- test/functional/page_objects/dashboard_page.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/functional/page_objects/dashboard_page.js b/test/functional/page_objects/dashboard_page.js index 6b284e4f31782..037af81e369c9 100644 --- a/test/functional/page_objects/dashboard_page.js +++ b/test/functional/page_objects/dashboard_page.js @@ -201,7 +201,7 @@ export function DashboardPageProvider({ getService, getPageObjects }) { async checkDashboardListingSelectAllCheckbox() { const element = await testSubjects.find('checkboxSelectAll'); const isSelected = await element.isSelected(); - if(!isSelected) { + if (!isSelected) { log.debug(`checking checkbox "checkboxSelectAll"`); await testSubjects.click('checkboxSelectAll'); } From 5f1534785ef7460e049c0da14de360c82a935e75 Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Wed, 21 Mar 2018 08:35:18 -0600 Subject: [PATCH 11/21] switch to EuiBasicTable --- .../dashboard/listing/dashboard_listing.js | 77 +++++++++---------- 1 file changed, 37 insertions(+), 40 deletions(-) diff --git a/src/core_plugins/kibana/public/dashboard/listing/dashboard_listing.js b/src/core_plugins/kibana/public/dashboard/listing/dashboard_listing.js index f41340bf75dfe..d59fe2a3e3672 100644 --- a/src/core_plugins/kibana/public/dashboard/listing/dashboard_listing.js +++ b/src/core_plugins/kibana/public/dashboard/listing/dashboard_listing.js @@ -4,7 +4,8 @@ import _ from 'lodash'; import { toastNotifications } from 'ui/notify'; import { EuiTitle, - EuiInMemoryTable, + EuiSearchBar, + EuiBasicTable, EuiPage, EuiLink, EuiFlexGroup, @@ -23,8 +24,6 @@ const tableColumns = [ { field: 'title', name: 'Title', - sortable: true, - sortable: true, render: (field, record) => ( - Delete selected - - ); - } - renderListingLimitWarning() { if (this.state.showLimitError) { return ( @@ -179,7 +161,11 @@ export class DashboardListing extends React.Component { return !newFilter.includes(oldFilter); } - renderMessage() { + renderNoItemsMessage() { + if (this.state.isFetchingItems) { + return ''; + } + if (!this.state.isFetchingItems && this.state.dashboards.length === 0 && !this.state.filter) { if (this.props.hideWriteControls) { return ( @@ -217,23 +203,36 @@ export class DashboardListing extends React.Component { return 'No dashboards matched your search.'; } - renderTable() { - const search = { - defaultQuery: this.props.initialFilter, - toolsLeft: this.renderDeleteButton(), - box: { - incremental: true, - ['data-test-subj']: 'searchFilter' - }, - onChange: (query) => { - if (this.state.showLimitError || this.shouldFetch(query.text, this.state.filter)) { + renderSearchBar() { + const toolsLeft = []; + if (this.state.selectedIds.length > 0) { + toolsLeft.push( + + Delete selected + + ); + } + + return ( + { this.setState({ filter: query.text }, this.fetchItems); - } - return true; - } - }; + }} + box={{ incremental: true, ['data-test-subj']: 'searchFilter' }} + toolsLeft={toolsLeft} + /> + ); + } + + renderTable() { const selection = { itemId: 'id', onSelectionChange: (selection) => { @@ -243,15 +242,12 @@ export class DashboardListing extends React.Component { } }; return ( - ); } @@ -291,6 +287,7 @@ export class DashboardListing extends React.Component { {this.renderListingLimitWarning()} + {this.renderSearchBar()} {this.renderTable()} From 0453013a3abf35efd4366b4ff438c00d13bbbe1f Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Wed, 21 Mar 2018 09:30:17 -0600 Subject: [PATCH 12/21] pagination --- .../dashboard/listing/dashboard_listing.js | 41 ++++++++++++++++--- 1 file changed, 36 insertions(+), 5 deletions(-) diff --git a/src/core_plugins/kibana/public/dashboard/listing/dashboard_listing.js b/src/core_plugins/kibana/public/dashboard/listing/dashboard_listing.js index d59fe2a3e3672..20566ab062f8a 100644 --- a/src/core_plugins/kibana/public/dashboard/listing/dashboard_listing.js +++ b/src/core_plugins/kibana/public/dashboard/listing/dashboard_listing.js @@ -42,6 +42,11 @@ const tableColumns = [ export const EMPTY_FILTER = ''; +// saved objects do not support sorting by title because title is only mapped as analyzed +// the legacy implementation got around this by pulling `listingLimit` items and doing client side sorting +// and not supporting server-side paging. +// This component does not try to tackle these problems (yet) and is just feature matching the legacy component +// TODO support server side sorting/paging once title and description are sortable on the server. export class DashboardListing extends React.Component { constructor(props) { @@ -54,6 +59,8 @@ export class DashboardListing extends React.Component { filter: this.props.initialFilter, dashboards: [], selectedIds: [], + page: 0, + perPage: 20, }; } @@ -119,6 +126,25 @@ export class DashboardListing extends React.Component { return this.state.filter !== EMPTY_FILTER; } + onTableChange = ({ page }) => { + this.setState({ + page: page.index, + perPage: page.size, + }); + } + + getPageOfItems = () => { + const startIndex = this.state.page * this.state.perPage; + const lastIndex = startIndex + this.state.perPage; + if (this.state.dashboards.length < startIndex) { + // server-side paging not supported - see component comment for details + throw new Error(`Requested page that does not exist. page index: ${this.state.page}, + per page: ${this.state.perPage}, total: ${this.state.dashboards.length}`); + } + // If end is greater than the length of the sequence, slice extracts through to the end of the sequence (arr.length). + return this.state.dashboards.slice(startIndex, lastIndex); + } + renderConfirmDeleteModal() { return ( @@ -157,10 +183,6 @@ export class DashboardListing extends React.Component { } } - shouldFetch(newFilter, oldFilter) { - return !newFilter.includes(oldFilter); - } - renderNoItemsMessage() { if (this.state.isFetchingItems) { return ''; @@ -233,6 +255,12 @@ export class DashboardListing extends React.Component { } renderTable() { + const pagination = { + pageIndex: this.state.page, + pageSize: this.state.perPage, + totalItemCount: this.state.dashboards.length, + pageSizeOptions: [10, 20, 50], + }; const selection = { itemId: 'id', onSelectionChange: (selection) => { @@ -241,13 +269,16 @@ export class DashboardListing extends React.Component { }); } }; + const items = this.state.dashboards.length === 0 ? [] : this.getPageOfItems(); return ( ); } From e6ca05836da7015ba60e0a1a515ae4458711e6a3 Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Wed, 21 Mar 2018 14:29:48 -0600 Subject: [PATCH 13/21] add sorting --- .../dashboard/listing/dashboard_listing.js | 65 +++++++++++++++---- 1 file changed, 54 insertions(+), 11 deletions(-) diff --git a/src/core_plugins/kibana/public/dashboard/listing/dashboard_listing.js b/src/core_plugins/kibana/public/dashboard/listing/dashboard_listing.js index 20566ab062f8a..ade3e0ce9e7c5 100644 --- a/src/core_plugins/kibana/public/dashboard/listing/dashboard_listing.js +++ b/src/core_plugins/kibana/public/dashboard/listing/dashboard_listing.js @@ -24,6 +24,7 @@ const tableColumns = [ { field: 'title', name: 'Title', + sortable: true, render: (field, record) => ( { + onTableChange = ({ page, sort }) => { + const { + index: pageIndex, + size: pageSize, + } = page; + + let { + field: sortField, + direction: sortDirection, + } = sort; + + // 3rd sorting state that is not captured by sort - native order (no sort) + // when switching from desc to asc for the same field - use native order + if (this.state.sortField === sortField + && this.state.sortDirection === 'desc' + && sortDirection === 'asc') { + sortField = null; + sortDirection = null; + } + this.setState({ - page: page.index, - perPage: page.size, + pageIndex, + pageSize, + sortField, + sortDirection, }); } + // server-side paging not supported - see component comment for details getPageOfItems = () => { - const startIndex = this.state.page * this.state.perPage; - const lastIndex = startIndex + this.state.perPage; - if (this.state.dashboards.length < startIndex) { - // server-side paging not supported - see component comment for details - throw new Error(`Requested page that does not exist. page index: ${this.state.page}, - per page: ${this.state.perPage}, total: ${this.state.dashboards.length}`); + // do not sort original list to preserve elasticsearch ranking order + const dashboardsCopy = this.state.dashboards.slice(); + + if (this.state.sortField) { + dashboardsCopy.sort((a, b) => { + const fieldA = _.get(a, this.state.sortField, ''); + const fieldB = _.get(b, this.state.sortField, ''); + let order = 1; + if (this.state.sortDirection === 'desc') { + order = -1; + } + return order * fieldA.toLowerCase().localeCompare(fieldB.toLowerCase()); + }); } + + // If begin is greater than the length of the sequence, an empty array is returned. + const startIndex = this.state.page * this.state.perPage; // If end is greater than the length of the sequence, slice extracts through to the end of the sequence (arr.length). - return this.state.dashboards.slice(startIndex, lastIndex); + const lastIndex = startIndex + this.state.perPage; + return dashboardsCopy.slice(startIndex, lastIndex); } renderConfirmDeleteModal() { @@ -269,6 +304,13 @@ export class DashboardListing extends React.Component { }); } }; + const sorting = {}; + if (this.state.sortField) { + sorting.sort = { + field: this.state.sortField, + direction: this.state.sortDirection, + }; + } const items = this.state.dashboards.length === 0 ? [] : this.getPageOfItems(); return ( ); From aee920154f37bfaa7295e05f57c2ee2f34a1cd10 Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Wed, 21 Mar 2018 14:36:24 -0600 Subject: [PATCH 14/21] fix jest test --- .../dashboard_listing.test.js.snap | 216 ++++++++++++------ .../dashboard/listing/dashboard_listing.js | 4 - 2 files changed, 144 insertions(+), 76 deletions(-) diff --git a/src/core_plugins/kibana/public/dashboard/listing/__snapshots__/dashboard_listing.test.js.snap b/src/core_plugins/kibana/public/dashboard/listing/__snapshots__/dashboard_listing.test.js.snap index 3f9f3860454d8..d3a4f77740a76 100644 --- a/src/core_plugins/kibana/public/dashboard/listing/__snapshots__/dashboard_listing.test.js.snap +++ b/src/core_plugins/kibana/public/dashboard/listing/__snapshots__/dashboard_listing.test.js.snap @@ -29,7 +29,18 @@ exports[`after fetch hideWriteControls 1`] = ` - +

} - pagination={true} - search={ + onChange={[Function]} + pagination={ Object { - "box": Object { - "data-test-subj": "searchFilter", - "incremental": true, - }, - "defaultQuery": "", - "onChange": [Function], - "toolsLeft": undefined, + "pageIndex": 0, + "pageSize": 20, + "pageSizeOptions": Array [ + 10, + 20, + 50, + ], + "totalItemCount": 0, } } selection={ @@ -77,7 +89,7 @@ exports[`after fetch hideWriteControls 1`] = ` "onSelectionChange": [Function], } } - sorting={true} + sorting={Object {}} /> `; @@ -126,7 +138,18 @@ exports[`after fetch renders call to action when no dashboards exist 1`] = ` - +

@@ -169,16 +192,17 @@ exports[`after fetch renders call to action when no dashboards exist 1`] = ` } - pagination={true} - search={ + onChange={[Function]} + pagination={ Object { - "box": Object { - "data-test-subj": "searchFilter", - "incremental": true, - }, - "defaultQuery": "", - "onChange": [Function], - "toolsLeft": undefined, + "pageIndex": 0, + "pageSize": 20, + "pageSizeOptions": Array [ + 10, + 20, + 50, + ], + "totalItemCount": 0, } } selection={ @@ -187,7 +211,7 @@ exports[`after fetch renders call to action when no dashboards exist 1`] = ` "onSelectionChange": [Function], } } - sorting={true} + sorting={Object {}} /> `; @@ -236,7 +260,18 @@ exports[`after fetch renders table rows 1`] = ` - + `; @@ -367,7 +403,18 @@ exports[`after fetch renders warning when listingLimit is exceeded 1`] = ` size="m" /> - + `; @@ -467,7 +515,18 @@ exports[`initialFilter 1`] = ` - + `; @@ -554,7 +614,18 @@ exports[`renders table in loading state 1`] = ` - + `; diff --git a/src/core_plugins/kibana/public/dashboard/listing/dashboard_listing.js b/src/core_plugins/kibana/public/dashboard/listing/dashboard_listing.js index ade3e0ce9e7c5..beade37f2e905 100644 --- a/src/core_plugins/kibana/public/dashboard/listing/dashboard_listing.js +++ b/src/core_plugins/kibana/public/dashboard/listing/dashboard_listing.js @@ -124,10 +124,6 @@ export class DashboardListing extends React.Component { this.setState({ showDeleteModal: true }); }; - hasFilter = () => { - return this.state.filter !== EMPTY_FILTER; - } - onTableChange = ({ page, sort }) => { const { index: pageIndex, From 03db41c311623991bf8b060db967430ab6891f22 Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Wed, 21 Mar 2018 15:01:45 -0600 Subject: [PATCH 15/21] handle out of order fetchs --- .../dashboard/listing/dashboard_listing.js | 22 +++++++++++-------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/src/core_plugins/kibana/public/dashboard/listing/dashboard_listing.js b/src/core_plugins/kibana/public/dashboard/listing/dashboard_listing.js index beade37f2e905..9d4a8088a8e71 100644 --- a/src/core_plugins/kibana/public/dashboard/listing/dashboard_listing.js +++ b/src/core_plugins/kibana/public/dashboard/listing/dashboard_listing.js @@ -79,25 +79,29 @@ export class DashboardListing extends React.Component { this.fetchItems(); } - debouncedFetch = _.debounce(async () => { - const response = await this.props.find(this.state.filter); + debouncedFetch = _.debounce(async (filter) => { + const response = await this.props.find(filter); if (!this._isMounted) { return; } - this.setState({ - isFetchingItems: false, - dashboards: response.hits, - totalDashboards: response.total, - showLimitError: response.total > this.props.listingLimit, - }); + // We need this check to handle the case where search results come back in a different + // order than they were sent out. Only load results for the most recent search. + if (filter === this.state.filter) { + this.setState({ + isFetchingItems: false, + dashboards: response.hits, + totalDashboards: response.total, + showLimitError: response.total > this.props.listingLimit, + }); + } }, 300); fetchItems = () => { this.setState({ isFetchingItems: true, - }, this.debouncedFetch); + }, this.debouncedFetch.bind(null, this.state.filter)); } deleteSelectedItems = async () => { From c56bf4adb88bbacb0374990758cbe22ffa0f221f Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Wed, 21 Mar 2018 15:03:53 -0600 Subject: [PATCH 16/21] remove info.gif --- info.gif | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 info.gif diff --git a/info.gif b/info.gif deleted file mode 100644 index e69de29bb2d1d..0000000000000 From 869a1ba419cb898bcf7e62d109bcf3adb1839e21 Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Wed, 21 Mar 2018 19:16:13 -0600 Subject: [PATCH 17/21] re-instate search functional test --- .../dashboard/listing/dashboard_listing.js | 1 + .../apps/dashboard/_dashboard_listing.js | 34 ++++++++++++++++++- .../functional/page_objects/dashboard_page.js | 6 ++++ 3 files changed, 40 insertions(+), 1 deletion(-) diff --git a/src/core_plugins/kibana/public/dashboard/listing/dashboard_listing.js b/src/core_plugins/kibana/public/dashboard/listing/dashboard_listing.js index 9d4a8088a8e71..812c073e4fced 100644 --- a/src/core_plugins/kibana/public/dashboard/listing/dashboard_listing.js +++ b/src/core_plugins/kibana/public/dashboard/listing/dashboard_listing.js @@ -27,6 +27,7 @@ const tableColumns = [ sortable: true, render: (field, record) => ( diff --git a/test/functional/apps/dashboard/_dashboard_listing.js b/test/functional/apps/dashboard/_dashboard_listing.js index b9e7d51f94ce9..3d9058c78b3ea 100644 --- a/test/functional/apps/dashboard/_dashboard_listing.js +++ b/test/functional/apps/dashboard/_dashboard_listing.js @@ -66,13 +66,45 @@ export default function ({ getService, getPageObjects }) { }); }); - describe('search by title', function () { + describe('search', function () { before(async () => { await PageObjects.dashboard.clearSearchValue(); await PageObjects.dashboard.clickCreateDashboardPrompt(); await PageObjects.dashboard.saveDashboard('Two Words'); }); + it('matches on the first word', async function () { + await PageObjects.dashboard.searchForDashboardWithName('Two'); + const countOfDashboards = await PageObjects.dashboard.getCountOfDashboardsInListingTable(); + expect(countOfDashboards).to.equal(1); + }); + + it('matches the second word', async function () { + await PageObjects.dashboard.searchForDashboardWithName('Words'); + const countOfDashboards = await PageObjects.dashboard.getCountOfDashboardsInListingTable(); + expect(countOfDashboards).to.equal(1); + }); + + it('matches the second word prefix', async function () { + await PageObjects.dashboard.searchForDashboardWithName('Wor'); + const countOfDashboards = await PageObjects.dashboard.getCountOfDashboardsInListingTable(); + expect(countOfDashboards).to.equal(1); + }); + + it('does not match mid word', async function () { + await PageObjects.dashboard.searchForDashboardWithName('ords'); + const countOfDashboards = await PageObjects.dashboard.getCountOfDashboardsInListingTable(); + expect(countOfDashboards).to.equal(0); + }); + + it('is case insensitive', async function () { + await PageObjects.dashboard.searchForDashboardWithName('two words'); + const countOfDashboards = await PageObjects.dashboard.getCountOfDashboardsInListingTable(); + expect(countOfDashboards).to.equal(1); + }); + }); + + describe('search by title', function () { it('loads a dashboard if title matches', async function () { const currentUrl = await remote.getCurrentUrl(); const newUrl = currentUrl + '&title=Two%20Words'; diff --git a/test/functional/page_objects/dashboard_page.js b/test/functional/page_objects/dashboard_page.js index 037af81e369c9..6964d60eb387c 100644 --- a/test/functional/page_objects/dashboard_page.js +++ b/test/functional/page_objects/dashboard_page.js @@ -371,6 +371,7 @@ export function DashboardPageProvider({ getService, getPageObjects }) { await retry.try(async () => { const searchFilter = await testSubjects.find('searchFilter'); await searchFilter.clearValue(); + await PageObjects.common.pressEnterKey(); }); } @@ -396,6 +397,11 @@ export function DashboardPageProvider({ getService, getPageObjects }) { await PageObjects.header.waitUntilLoadingHasFinished(); } + async getCountOfDashboardsInListingTable() { + const dashboardTitles = await find.allByCssSelector('.dashboardLink'); + return dashboardTitles.length; + } + async getDashboardCountWithName(dashName) { log.debug(`getDashboardCountWithName: ${dashName}`); From 2165b2beac24d91dc23fcc1ddf8c49a0e7fd9163 Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Mon, 26 Mar 2018 14:27:02 -0600 Subject: [PATCH 18/21] replace EuiSearchBar with EuiFieldSearch --- .../dashboard_listing.test.js.snap | 204 ++++++++++++------ .../dashboard/listing/dashboard_listing.js | 60 +++--- 2 files changed, 173 insertions(+), 91 deletions(-) diff --git a/src/core_plugins/kibana/public/dashboard/listing/__snapshots__/dashboard_listing.test.js.snap b/src/core_plugins/kibana/public/dashboard/listing/__snapshots__/dashboard_listing.test.js.snap index d3a4f77740a76..24ee3b6ae1850 100644 --- a/src/core_plugins/kibana/public/dashboard/listing/__snapshots__/dashboard_listing.test.js.snap +++ b/src/core_plugins/kibana/public/dashboard/listing/__snapshots__/dashboard_listing.test.js.snap @@ -29,17 +29,29 @@ exports[`after fetch hideWriteControls 1`] = ` - + + + + + - + + + + + - + + + + + - + + + + + - + + + + + - + + + + + { + onTableChange = ({ page, sort = {} }) => { const { index: pageIndex, size: pageSize, @@ -150,8 +150,8 @@ export class DashboardListing extends React.Component { } this.setState({ - pageIndex, - pageSize, + page: pageIndex, + perPage: pageSize, sortField, sortDirection, }); @@ -262,31 +262,39 @@ export class DashboardListing extends React.Component { } renderSearchBar() { - const toolsLeft = []; + let deleteBtn; if (this.state.selectedIds.length > 0) { - toolsLeft.push( - - Delete selected - + deleteBtn = ( + + + Delete selected + + ); } return ( - { - this.setState({ - filter: query.text - }, this.fetchItems); - }} - box={{ incremental: true, ['data-test-subj']: 'searchFilter' }} - toolsLeft={toolsLeft} - /> + + {deleteBtn} + + { + this.setState({ + filter: e.target.value + }, this.fetchItems); + }} + data-test-subj="searchFilter" + /> + + ); } @@ -358,11 +366,13 @@ export class DashboardListing extends React.Component { {createButton} + {this.renderListingLimitWarning()} {this.renderSearchBar()} + {this.renderTable()} From 0518ead40654ae974a124b5b649163c7cc034899 Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Mon, 26 Mar 2018 17:21:08 -0600 Subject: [PATCH 19/21] fix functional tests --- test/functional/apps/dashboard/_dashboard_listing.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/functional/apps/dashboard/_dashboard_listing.js b/test/functional/apps/dashboard/_dashboard_listing.js index 3d9058c78b3ea..b8436b256c10a 100644 --- a/test/functional/apps/dashboard/_dashboard_listing.js +++ b/test/functional/apps/dashboard/_dashboard_listing.js @@ -42,7 +42,7 @@ export default function ({ getService, getPageObjects }) { describe('delete', async function () { it('default confirm action is cancel', async function () { - await PageObjects.dashboard.searchForDashboardWithName(''); + await PageObjects.dashboard.searchForDashboardWithName(dashboardName); await PageObjects.dashboard.checkDashboardListingSelectAllCheckbox(); await PageObjects.dashboard.clickDeleteSelectedDashboards(); @@ -69,7 +69,7 @@ export default function ({ getService, getPageObjects }) { describe('search', function () { before(async () => { await PageObjects.dashboard.clearSearchValue(); - await PageObjects.dashboard.clickCreateDashboardPrompt(); + await PageObjects.dashboard.clickNewDashboard(); await PageObjects.dashboard.saveDashboard('Two Words'); }); From f9f5ad9c281c8769ff67c2b4302e8ec584c83861 Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Wed, 25 Apr 2018 11:35:18 -0600 Subject: [PATCH 20/21] update snapshot - when code rebased - new EUI version add another prop --- .../listing/__snapshots__/dashboard_listing.test.js.snap | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/core_plugins/kibana/public/dashboard/listing/__snapshots__/dashboard_listing.test.js.snap b/src/core_plugins/kibana/public/dashboard/listing/__snapshots__/dashboard_listing.test.js.snap index 24ee3b6ae1850..665d92730e99d 100644 --- a/src/core_plugins/kibana/public/dashboard/listing/__snapshots__/dashboard_listing.test.js.snap +++ b/src/core_plugins/kibana/public/dashboard/listing/__snapshots__/dashboard_listing.test.js.snap @@ -69,6 +69,7 @@ exports[`after fetch hideWriteControls 1`] = ` }, ] } + itemIdToExpandedRowMap={Object {}} items={Array []} loading={false} noItemsMessage={ @@ -190,6 +191,7 @@ exports[`after fetch renders call to action when no dashboards exist 1`] = ` }, ] } + itemIdToExpandedRowMap={Object {}} items={Array []} loading={false} noItemsMessage={ @@ -324,6 +326,7 @@ exports[`after fetch renders table rows 1`] = ` }, ] } + itemIdToExpandedRowMap={Object {}} items={ Array [ Object { @@ -479,6 +482,7 @@ exports[`after fetch renders warning when listingLimit is exceeded 1`] = ` }, ] } + itemIdToExpandedRowMap={Object {}} items={ Array [ Object { @@ -603,6 +607,7 @@ exports[`initialFilter 1`] = ` }, ] } + itemIdToExpandedRowMap={Object {}} items={Array []} loading={true} noItemsMessage="" @@ -714,6 +719,7 @@ exports[`renders table in loading state 1`] = ` }, ] } + itemIdToExpandedRowMap={Object {}} items={Array []} loading={true} noItemsMessage="" From 11e95eaa25b4813685029b4d05f817cb8caf3b2b Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Thu, 26 Apr 2018 07:11:27 -0600 Subject: [PATCH 21/21] add Edit link to actions column --- .../dashboard_listing.test.js.snap | 40 ++++++++++++ .../dashboard/listing/dashboard_listing.js | 63 ++++++++++++------- 2 files changed, 80 insertions(+), 23 deletions(-) diff --git a/src/core_plugins/kibana/public/dashboard/listing/__snapshots__/dashboard_listing.test.js.snap b/src/core_plugins/kibana/public/dashboard/listing/__snapshots__/dashboard_listing.test.js.snap index 665d92730e99d..3b999bf8a3ac6 100644 --- a/src/core_plugins/kibana/public/dashboard/listing/__snapshots__/dashboard_listing.test.js.snap +++ b/src/core_plugins/kibana/public/dashboard/listing/__snapshots__/dashboard_listing.test.js.snap @@ -189,6 +189,14 @@ exports[`after fetch renders call to action when no dashboards exist 1`] = ` "name": "Description", "sortable": true, }, + Object { + "actions": Array [ + Object { + "render": [Function], + }, + ], + "name": "Actions", + }, ] } itemIdToExpandedRowMap={Object {}} @@ -324,6 +332,14 @@ exports[`after fetch renders table rows 1`] = ` "name": "Description", "sortable": true, }, + Object { + "actions": Array [ + Object { + "render": [Function], + }, + ], + "name": "Actions", + }, ] } itemIdToExpandedRowMap={Object {}} @@ -480,6 +496,14 @@ exports[`after fetch renders warning when listingLimit is exceeded 1`] = ` "name": "Description", "sortable": true, }, + Object { + "actions": Array [ + Object { + "render": [Function], + }, + ], + "name": "Actions", + }, ] } itemIdToExpandedRowMap={Object {}} @@ -605,6 +629,14 @@ exports[`initialFilter 1`] = ` "name": "Description", "sortable": true, }, + Object { + "actions": Array [ + Object { + "render": [Function], + }, + ], + "name": "Actions", + }, ] } itemIdToExpandedRowMap={Object {}} @@ -717,6 +749,14 @@ exports[`renders table in loading state 1`] = ` "name": "Description", "sortable": true, }, + Object { + "actions": Array [ + Object { + "render": [Function], + }, + ], + "name": "Actions", + }, ] } itemIdToExpandedRowMap={Object {}} diff --git a/src/core_plugins/kibana/public/dashboard/listing/dashboard_listing.js b/src/core_plugins/kibana/public/dashboard/listing/dashboard_listing.js index e89ea53c4419f..658dcac84570c 100644 --- a/src/core_plugins/kibana/public/dashboard/listing/dashboard_listing.js +++ b/src/core_plugins/kibana/public/dashboard/listing/dashboard_listing.js @@ -20,29 +20,6 @@ import { } from '@elastic/eui'; import { DashboardConstants, createDashboardEditUrl } from '../dashboard_constants'; -const tableColumns = [ - { - field: 'title', - name: 'Title', - sortable: true, - render: (field, record) => ( - - {field} - - ) - }, - { - field: 'description', - name: 'Description', - dataType: 'string', - sortable: true, - } -]; - export const EMPTY_FILTER = ''; // saved object client does not support sorting by title because title is only mapped as analyzed @@ -299,6 +276,46 @@ export class DashboardListing extends React.Component { } renderTable() { + const tableColumns = [ + { + field: 'title', + name: 'Title', + sortable: true, + render: (field, record) => ( + + {field} + + ) + }, + { + field: 'description', + name: 'Description', + dataType: 'string', + sortable: true, + } + ]; + if (!this.props.hideWriteControls) { + tableColumns.push({ + name: 'Actions', + actions: [ + { + render: (record) => { + return ( + + Edit + + ); + } + } + ] + }); + } const pagination = { pageIndex: this.state.page, pageSize: this.state.perPage,