From 448fb5e8ca5a4cfd47b11c667a6a24fd5645e04e Mon Sep 17 00:00:00 2001 From: Stacey Gammon Date: Thu, 12 Oct 2017 15:13:12 -0400 Subject: [PATCH] Reactify visualize listing table (#14227) (#14447) * Reactify visualize listing table * refactor toolbar => toolBar * use prompt prop to also cover loading and didn't match search logic. * Use default alignment instead of ternary * Add keys to array elements where missing * Add align property to KuiTableHeaderCell * Fix issue with filter not showing up in search tool bar * onCheckChanged => onSelectionChanged * pagerComponent => pager, actionComponent => actions * use singular verbiage when only 1 item is selected * exit early per style guide * fix lint errors * rename columns => header * Refactor KuiTableHeaderCell into KuiListingTable isChecked => isSelected contents => content refactor KuiTableRowCell internally * fix lint errors * areAllRowsChecked => areAllRowsSelected * improve itemSelectedCount logic in KuiListingTableToolBarFooter * React.Component => Component * make header data a variable, not a function * Only consider all rows selected if rows exist and they are all selected, not if there are no rows. * Adding a few KuiListingTable tests * Give one column sort attributes in examples page --- .../listing/no_visualizations_prompt.js | 27 ++ .../visualize/listing/visualize_listing.html | 218 +-------------- .../visualize/listing/visualize_listing.js | 140 +--------- .../listing/visualize_listing_table.js | 261 ++++++++++++++++++ src/ui/public/pager/index.js | 1 + .../src/views/table/controlled_table.js | 186 ------------- .../table/controlled_table_loading_items.js | 88 ------ .../controlled_table_with_empty_prompt.js | 89 ------ .../table/controlled_table_with_no_items.js | 88 ------ .../doc_site/src/views/table/listing_table.js | 142 ++++++++++ .../table/listing_table_loading_items.js | 77 ++++++ .../table/listing_table_with_empty_prompt.js | 96 +++++++ .../table/listing_table_with_no_items.js | 79 ++++++ .../doc_site/src/views/table/table_example.js | 56 ++-- ui_framework/src/components/index.js | 5 + ui_framework/src/components/table/index.js | 7 + .../__snapshots__/listing_table.test.js.snap | 197 +++++++++++++ .../components/table/listing_table/index.js | 5 + .../table/listing_table/listing_table.js | 165 +++++++++++ .../table/listing_table/listing_table.test.js | 67 +++++ .../listing_table_create_button.js | 22 ++ .../listing_table_delete_button.js | 22 ++ .../listing_table_loading_prompt.js | 16 ++ .../listing_table_no_matches_prompt.js | 16 ++ .../table/listing_table/listing_table_row.js | 69 +++++ .../listing_table/listing_table_tool_bar.js | 31 +++ .../listing_table_tool_bar_footer.js | 39 +++ .../src/components/table/table_header_cell.js | 13 +- 28 files changed, 1400 insertions(+), 822 deletions(-) create mode 100644 src/core_plugins/kibana/public/visualize/listing/no_visualizations_prompt.js create mode 100644 src/core_plugins/kibana/public/visualize/listing/visualize_listing_table.js delete mode 100644 ui_framework/doc_site/src/views/table/controlled_table.js delete mode 100644 ui_framework/doc_site/src/views/table/controlled_table_loading_items.js delete mode 100644 ui_framework/doc_site/src/views/table/controlled_table_with_empty_prompt.js delete mode 100644 ui_framework/doc_site/src/views/table/controlled_table_with_no_items.js create mode 100644 ui_framework/doc_site/src/views/table/listing_table.js create mode 100644 ui_framework/doc_site/src/views/table/listing_table_loading_items.js create mode 100644 ui_framework/doc_site/src/views/table/listing_table_with_empty_prompt.js create mode 100644 ui_framework/doc_site/src/views/table/listing_table_with_no_items.js create mode 100644 ui_framework/src/components/table/listing_table/__snapshots__/listing_table.test.js.snap create mode 100644 ui_framework/src/components/table/listing_table/index.js create mode 100644 ui_framework/src/components/table/listing_table/listing_table.js create mode 100644 ui_framework/src/components/table/listing_table/listing_table.test.js create mode 100644 ui_framework/src/components/table/listing_table/listing_table_create_button.js create mode 100644 ui_framework/src/components/table/listing_table/listing_table_delete_button.js create mode 100644 ui_framework/src/components/table/listing_table/listing_table_loading_prompt.js create mode 100644 ui_framework/src/components/table/listing_table/listing_table_no_matches_prompt.js create mode 100644 ui_framework/src/components/table/listing_table/listing_table_row.js create mode 100644 ui_framework/src/components/table/listing_table/listing_table_tool_bar.js create mode 100644 ui_framework/src/components/table/listing_table/listing_table_tool_bar_footer.js diff --git a/src/core_plugins/kibana/public/visualize/listing/no_visualizations_prompt.js b/src/core_plugins/kibana/public/visualize/listing/no_visualizations_prompt.js new file mode 100644 index 0000000000000..4ca96c20a5d96 --- /dev/null +++ b/src/core_plugins/kibana/public/visualize/listing/no_visualizations_prompt.js @@ -0,0 +1,27 @@ +import React from 'react'; + +import { + KuiEmptyTablePrompt, + KuiEmptyTablePromptPanel, + KuiLinkButton, + KuiButtonIcon, +} from 'ui_framework/components'; + +export function NoVisualizationsPrompt() { + return ( + + } + > + Create a visualization + + } + message="Looks like you don't have any visualizations. Let's create some!" + /> + + ); +} diff --git a/src/core_plugins/kibana/public/visualize/listing/visualize_listing.html b/src/core_plugins/kibana/public/visualize/listing/visualize_listing.html index ebc1c3e71be3e..e9733826f88fa 100644 --- a/src/core_plugins/kibana/public/visualize/listing/visualize_listing.html +++ b/src/core_plugins/kibana/public/visualize/listing/visualize_listing.html @@ -26,220 +26,10 @@ - -
- -
- -
- - + - - - - -
- -
- - - -
-
- - -
-
- No visualizations matched your search. -
-
- - -
-
-
- Looks like you don’t have any visualizations. Let’s create some! -
- - -
-
- - - - - - - - - - - - - - - - - - - - - - -
-
- -
-
- - - -
-
- -
-
- - -
- - - - - - - {{ item.type.title }} - -
-
- - -
-
-
- {{ listingController.getSelectedItemsCount() }} selected -
-
- -
- - - -
-
-
diff --git a/src/core_plugins/kibana/public/visualize/listing/visualize_listing.js b/src/core_plugins/kibana/public/visualize/listing/visualize_listing.js index acea3a2b3024a..b1dc1fe552419 100644 --- a/src/core_plugins/kibana/public/visualize/listing/visualize_listing.js +++ b/src/core_plugins/kibana/public/visualize/listing/visualize_listing.js @@ -1,154 +1,40 @@ import { SavedObjectRegistryProvider } from 'ui/saved_objects/saved_object_registry'; import 'ui/pager_control'; import 'ui/pager'; +import { uiModules } from 'ui/modules'; -import { SortableProperties } from 'ui_framework/services'; +import { VisualizeListingTable } from './visualize_listing_table'; + +const app = uiModules.get('app/visualize', ['ngRoute', 'react']); +app.directive('visualizeListingTable', function (reactDirective) { + return reactDirective(VisualizeListingTable); +}); export function VisualizeListingController($injector) { - 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'); timefilter.enabled = false; - const limitTo = $filter('limitTo'); // TODO: Extract this into an external service. const services = Private(SavedObjectRegistryProvider).byLoaderPropertiesName; const visualizationService = services.visualizations; const notify = new Notifier({ location: 'Visualize' }); - let selectedItems = []; - const sortableProperties = new SortableProperties([ - { - name: 'title', - getValue: item => item.title.toLowerCase(), - isAscending: true, - }, - { - name: 'type', - getValue: item => item.type.title.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; - - visualizationService.find(this.filter, config.get('savedObjects:listingLimit')) + this.fetchItems = (filter) => { + return visualizationService.find(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(); + return result.hits; }); }; - const deselectAll = () => { - selectedItems = []; - }; - - const selectAll = () => { - selectedItems = this.pageOfItems.slice(0); - }; - - this.isAscending = (name) => sortableProperties.isAscendingByName(name); - this.getSortedProperty = () => sortableProperties.getSortedProperty(); - - this.sortOn = function sortOn(propertyName) { - sortableProperties.sortOn(propertyName); - deselectAll(); - calculateItemsOnPage(); - }; - - this.isFetchingItems = false; - this.items = []; - this.pageOfItems = []; - this.filter = ''; - - this.pager = pagerFactory.create(this.items.length, 20, 1); - - this.onFilter = (newFilter) => { - this.filter = newFilter; - deselectAll(); - fetchItems(); - }; - fetchItems(); - - this.toggleAll = function toggleAll() { - if (this.areAllItemsChecked()) { - deselectAll(); - } else { - selectAll(); - } - }; - - this.toggleItem = function toggleItem(item) { - if (this.isItemChecked(item)) { - const index = selectedItems.indexOf(item); - selectedItems.splice(index, 1); - } else { - selectedItems.push(item); - } - }; - - this.isItemChecked = function isItemChecked(item) { - return selectedItems.includes(item); - }; - - 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); - - visualizationService.delete(selectedIds) - .then(fetchItems) - .then(() => { - deselectAll(); - }) - .catch(error => notify.error(error)); - }; - - confirmModal( - 'Are you sure you want to delete the selected visualizations? This action is irreversible!', - { - confirmButtonText: 'Delete', - onConfirm: doDelete - }); - }; - - this.onPageNext = () => { - deselectAll(); - this.pager.nextPage(); - calculateItemsOnPage(); - }; - - this.onPagePrevious = () => { - deselectAll(); - this.pager.previousPage(); - calculateItemsOnPage(); - }; - - this.getUrlForItem = function getUrlForItem(item) { - return `#/visualize/edit/${item.id}`; + this.deleteSelectedItems = function deleteSelectedItems(selectedIds) { + return visualizationService.delete(selectedIds) + .catch(error => notify.error(error)); }; } diff --git a/src/core_plugins/kibana/public/visualize/listing/visualize_listing_table.js b/src/core_plugins/kibana/public/visualize/listing/visualize_listing_table.js new file mode 100644 index 0000000000000..bba3ed4e72959 --- /dev/null +++ b/src/core_plugins/kibana/public/visualize/listing/visualize_listing_table.js @@ -0,0 +1,261 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; + +import { SortableProperties } from 'ui_framework/services'; +import { Pager } from 'ui/pager'; +import { NoVisualizationsPrompt } from './no_visualizations_prompt'; + +import { + KuiPager, + KuiModalOverlay, + KuiConfirmModal, + KuiListingTableDeleteButton, + KuiListingTableCreateButton, + KuiListingTable, + KuiListingTableNoMatchesPrompt, + KuiListingTableLoadingPrompt +} from 'ui_framework/components'; + +export class VisualizeListingTable extends Component { + + constructor(props) { + super(props); + this.state = { + selectedRowIds: [], + pageOfItems: [], + showDeleteModal: false, + filter: '', + sortedColumn: '', + sortedColumnDirection: '', + pageStartNumber: 1, + isFetchingItems: false, + }; + + this.sortableProperties = new SortableProperties( + [ + { + name: 'title', + getValue: item => item.title.toLowerCase(), + isAscending: true, + }, + { + name: 'type', + getValue: item => item.type.title.toLowerCase(), + isAscending: true, + } + ], + 'title' + ); + this.items = []; + this.pager = new Pager(this.items.length, 20, 1); + } + + calculateItemsOnPage = () => { + this.items = this.sortableProperties.sortItems(this.items); + this.pager.setTotalItems(this.items.length); + const pageOfItems = this.items.slice(this.pager.startIndex, this.pager.startIndex + this.pager.pageSize); + this.setState({ pageOfItems, pageStartNumber: this.pager.startItem }); + }; + + deselectAll = () => { + this.setState({ selectedRowIds: [] }); + }; + + isAscending = (name) => this.sortableProperties.isAscendingByName(name); + getSortedProperty = () => this.sortableProperties.getSortedProperty(); + + sortOn = function sortOn(propertyName) { + this.sortableProperties.sortOn(propertyName); + this.setState({ + selectedRowIds: [], + sortedColumn: this.sortableProperties.getSortedProperty(), + sortedColumnDirection: this.sortableProperties.isCurrentSortAscending() ? 'ASC' : 'DESC', + }); + this.calculateItemsOnPage(); + }; + + fetchItems = (filter) => { + this.setState({ isFetchingItems: true }); + + this.props.fetchItems(filter) + .then(items => { + this.setState({ + isFetchingItems: false, + selectedRowIds: [], + filter, + }); + this.items = items; + this.calculateItemsOnPage(); + }); + }; + + componentDidMount() { + this.fetchItems(this.state.filter); + } + + onNextPage = () => { + this.deselectAll(); + this.pager.nextPage(); + this.calculateItemsOnPage(); + }; + + onPreviousPage = () => { + this.deselectAll(); + this.pager.previousPage(); + this.calculateItemsOnPage(); + }; + + getUrlForItem(item) { + return `#/visualize/edit/${item.id}`; + } + + renderItemTypeIcon(item) { + return item.type.image ? + : + ; + } + + sortByTitle = () => this.sortOn('title'); + sortByType = () => this.sortOn('type'); + + renderHeader() { + return [ + { + content: 'Title', + onSort: this.sortByTitle, + isSorted: this.state.sortedColumn === 'title', + isSortAscending: this.sortableProperties.isAscendingByName('title'), + }, + { + content: 'Type', + onSort: this.sortByType, + isSorted: this.state.sortedColumn === 'type', + isSortAscending: this.sortableProperties.isAscendingByName('type'), + }, + ]; + } + + renderRowCells(item) { + return [ + + {item.title} + , + + {this.renderItemTypeIcon(item)} + {item.type.title} + + ]; + } + + createRows() { + return this.state.pageOfItems.map(item => ({ + id: item.id, + cells: this.renderRowCells(item) + })); + } + + closeModal = () => { + this.setState({ showDeleteModal: false }); + }; + + renderConfirmDeleteModal() { + return ( + + + + ); + } + + onDelete = () => { + this.setState({ showDeleteModal: true }); + }; + + deleteSelectedItems = () => { + this.props.deleteSelectedItems(this.state.selectedRowIds) + .then(() => this.fetchItems(this.state.filter)) + .catch(() => {}) + .then(() => this.deselectAll()) + .then(() => this.closeModal()); + }; + + onItemSelectionChanged = (newSelectedIds) => { + this.setState({ selectedRowIds: newSelectedIds }); + }; + + onCreate() { + window.location = '#/visualize/new'; + } + + renderToolBarActions() { + return this.state.selectedRowIds.length > 0 ? + : + ; + } + + renderPager() { + return ( + + ); + } + + renderPrompt() { + if (this.state.isFetchingItems) { + return ; + } + + if (this.items.length === 0) { + if (this.state.filter) { + return ; + } + + return ; + } + + return null; + } + + render() { + return ( +
+ {this.state.showDeleteModal && this.renderConfirmDeleteModal()} + +
+ ); + } +} + +VisualizeListingTable.propTypes = { + deleteSelectedItems: PropTypes.func, + fetchItems: PropTypes.func, +}; diff --git a/src/ui/public/pager/index.js b/src/ui/public/pager/index.js index 3f8e47d5d122a..9499a273498d1 100644 --- a/src/ui/public/pager/index.js +++ b/src/ui/public/pager/index.js @@ -1 +1,2 @@ import './pager_factory'; +export { Pager } from './pager'; diff --git a/ui_framework/doc_site/src/views/table/controlled_table.js b/ui_framework/doc_site/src/views/table/controlled_table.js deleted file mode 100644 index 6ce8563f05909..0000000000000 --- a/ui_framework/doc_site/src/views/table/controlled_table.js +++ /dev/null @@ -1,186 +0,0 @@ -import React from 'react'; - -import { - KuiToolBar, - KuiToolBarSearchBox, - KuiToolBarSection, - KuiButton, - KuiButtonIcon, - KuiTable, - KuiToolBarText, - KuiControlledTable, - KuiPager, - KuiTableRow, - KuiTableRowCell, - KuiTableRowCheckBoxCell, - KuiTableHeaderCell, - KuiToolBarFooterSection, - KuiToolBarFooter, - KuiTableHeaderCheckBoxCell, - KuiTableBody, - KuiTableHeader, -} from '../../../../components'; - -import { - LEFT_ALIGNMENT, - RIGHT_ALIGNMENT -} from '../../../../services'; - -export class ControlledTable extends React.Component { - constructor(props) { - super(props); - this.state = { - rowToSelectedStateMap: new Map(), - }; - - this.rows = [ - [ - Alligator, -
, - 'Tue Dec 06 2016 12:56:15 GMT-0800 (PST)', - '1' - ], - [ - Boomerang, -
, - 'Tue Dec 06 2016 12:56:15 GMT-0800 (PST)', - '10' - ], - [ - Celebration, -
, - 'Tue Dec 06 2016 12:56:15 GMT-0800 (PST)', - '100' - ], - [ - Dog, -
, - 'Tue Dec 06 2016 12:56:15 GMT-0800 (PST)', - '1000' - ] - ]; - } - - renderPager() { - return ( - {}} - onPreviousPage={() => {}} - /> - ); - } - - toggleItem = (item) => { - this.setState(previousState => { - const rowToSelectedStateMap = new Map(previousState.rowToSelectedStateMap); - rowToSelectedStateMap.set(item, !rowToSelectedStateMap.get(item)); - return { rowToSelectedStateMap }; - }); - }; - - isItemChecked = (item) => { - return this.state.rowToSelectedStateMap.get(item); - }; - - renderTableRows() { - return this.rows.map((rowData, rowIndex) => { - return ( - - this.toggleItem(rowData)} - /> - { - rowData.map((cellData, index) => { - const align = index === rowData.length - 1 ? RIGHT_ALIGNMENT : LEFT_ALIGNMENT; - return ( - - { cellData } - - ); - }) - } - - ); - }); - } - - render() { - return ( - - - {}} /> - - - - Add - - - } - /> - - } - /> - - - - { this.renderPager() } - - - - - - this.toggleItem('header')} - /> - - Title - - - Status - - - Date created - - - Orders of magnitude - - - - - { - this.renderTableRows() - } - - - - - - - 5 Items selected - - - - - { - this.renderPager() - } - - - - - ); - } -} diff --git a/ui_framework/doc_site/src/views/table/controlled_table_loading_items.js b/ui_framework/doc_site/src/views/table/controlled_table_loading_items.js deleted file mode 100644 index 65e9e4fed8965..0000000000000 --- a/ui_framework/doc_site/src/views/table/controlled_table_loading_items.js +++ /dev/null @@ -1,88 +0,0 @@ -import React from 'react'; - -import { - KuiToolBar, - KuiToolBarSearchBox, - KuiToolBarSection, - KuiButton, - KuiButtonIcon, - KuiToolBarText, - KuiControlledTable, - KuiPager, - KuiToolBarFooterSection, - KuiToolBarFooter, - KuiTableInfo, - KuiEmptyTablePromptPanel, -} from '../../../../components'; - -export class ControlledTableLoadingItems extends React.Component { - getPager() { - return ( - {}} - onPreviousPage={() => {}} - /> - ); - } - - render() { - return ( - - - {}} /> - - - - Add - - - } - /> - - } - /> - - - - { this.getPager() } - - - - - - Loading... - - - - - - - 5 Items selected - - - - - - 1 – 20 of 33 - - { - this.getPager() - } - - - - - ); - } -} diff --git a/ui_framework/doc_site/src/views/table/controlled_table_with_empty_prompt.js b/ui_framework/doc_site/src/views/table/controlled_table_with_empty_prompt.js deleted file mode 100644 index a52f41b31e1fa..0000000000000 --- a/ui_framework/doc_site/src/views/table/controlled_table_with_empty_prompt.js +++ /dev/null @@ -1,89 +0,0 @@ -import React from 'react'; - -import { - KuiToolBar, - KuiToolBarSearchBox, - KuiToolBarSection, - KuiButton, - KuiButtonIcon, - KuiToolBarText, - KuiControlledTable, - KuiPager, - KuiToolBarFooterSection, - KuiToolBarFooter, - KuiEmptyTablePrompt, - KuiEmptyTablePromptPanel, -} from '../../../../components'; - -export class ControlledTableWithEmptyPrompt extends React.Component { - getPager() { - return ( - {}} - onPreviousPage={() => {}} - /> - ); - } - - render() { - return ( - - - {}} /> - - - - Add - - - } - /> - - } - /> - - - - { this.getPager() } - - - - - Add Items} - message="Uh oh you have no items!" - /> - - - - - - 5 Items selected - - - - - - 1 – 20 of 33 - - { - this.getPager() - } - - - - - ); - } -} diff --git a/ui_framework/doc_site/src/views/table/controlled_table_with_no_items.js b/ui_framework/doc_site/src/views/table/controlled_table_with_no_items.js deleted file mode 100644 index 87625de958a9d..0000000000000 --- a/ui_framework/doc_site/src/views/table/controlled_table_with_no_items.js +++ /dev/null @@ -1,88 +0,0 @@ -import React from 'react'; - -import { - KuiToolBar, - KuiToolBarSearchBox, - KuiToolBarSection, - KuiButton, - KuiButtonIcon, - KuiToolBarText, - KuiControlledTable, - KuiPager, - KuiToolBarFooterSection, - KuiToolBarFooter, - KuiTableInfo, - KuiEmptyTablePromptPanel, -} from '../../../../components'; - -export class ControlledTableWithNoItems extends React.Component { - getPager() { - return ( - {}} - onPreviousPage={() => {}} - /> - ); - } - - render() { - return ( - - - {}} /> - - - - Add - - - } - /> - - } - /> - - - - { this.getPager() } - - - - - - No items matched your search. - - - - - - - 5 Items selected - - - - - - 1 – 20 of 33 - - { - this.getPager() - } - - - - - ); - } -} diff --git a/ui_framework/doc_site/src/views/table/listing_table.js b/ui_framework/doc_site/src/views/table/listing_table.js new file mode 100644 index 0000000000000..e902dbb48253e --- /dev/null +++ b/ui_framework/doc_site/src/views/table/listing_table.js @@ -0,0 +1,142 @@ +import React, { Component } from 'react'; + +import { + KuiButton, + KuiButtonIcon, + KuiPager, + KuiListingTable, +} from '../../../../components'; + +import { + RIGHT_ALIGNMENT +} from '../../../../services'; + +export class ListingTable extends Component { + constructor(props) { + super(props); + this.state = { + selectedRowIds: [], + }; + + this.rows = [ + { + id: '1', + cells: [ + Alligator, +
, + 'Tue Dec 06 2016 12:56:15 GMT-0800 (PST)', + { + content: '1', + align: RIGHT_ALIGNMENT + }, + ] + }, + { + id: '2', + cells: [ + Boomerang, +
, + 'Tue Dec 06 2016 12:56:15 GMT-0800 (PST)', + { + content: '10', + align: RIGHT_ALIGNMENT + }, + ] + }, + { + id: '3', + cells: [ + Celebration, +
, + 'Tue Dec 06 2016 12:56:15 GMT-0800 (PST)', + { + content: '100', + align: RIGHT_ALIGNMENT + }, + ] + }, + { + id: '4', + cells: [ + Dog, +
, + 'Tue Dec 06 2016 12:56:15 GMT-0800 (PST)', + { + content: '1000', + align: RIGHT_ALIGNMENT + }, + ] + } + ]; + + this.header = [ + 'Title', + 'Status', + 'Date created', + { + content: 'Orders of magnitude', + onSort: () => {}, + isSorted: true, + isSortAscending: true, + align: RIGHT_ALIGNMENT, + } + ]; + } + + renderPager() { + return ( + {}} + onPreviousPage={() => {}} + /> + ); + } + + renderToolBarActions() { + return [ + + Add + , + } + />, + } + /> + ]; + } + + onItemSelectionChanged = (selectedRowIds) => { + this.setState({ selectedRowIds }); + }; + + render() { + return ( + {}} + filter="" + onItemSelectionChanged={this.onItemSelectionChanged} + /> + ); + } +} diff --git a/ui_framework/doc_site/src/views/table/listing_table_loading_items.js b/ui_framework/doc_site/src/views/table/listing_table_loading_items.js new file mode 100644 index 0000000000000..fd711d1fc4954 --- /dev/null +++ b/ui_framework/doc_site/src/views/table/listing_table_loading_items.js @@ -0,0 +1,77 @@ +import React from 'react'; + +import { + KuiButton, + KuiButtonIcon, + KuiPager, + KuiListingTable, + KuiListingTableLoadingPrompt, +} from '../../../../components'; + +import { + RIGHT_ALIGNMENT +} from '../../../../services'; + +function renderHeader() { + return [ + 'Title', + 'Status', + 'Date created', + { + content: 'Orders of magnitude', + align: RIGHT_ALIGNMENT + } + ]; +} + +function renderPager() { + return ( + {}} + onPreviousPage={() => {}} + /> + ); +} + +function renderToolBarActions() { + return [ + + Add + , + } + />, + } + /> + ]; +} + +export function ListingTableLoadingItems() { + return ( + {}} + filter="" + prompt={} + onItemSelectionChanged={() => {}} + /> + ); +} diff --git a/ui_framework/doc_site/src/views/table/listing_table_with_empty_prompt.js b/ui_framework/doc_site/src/views/table/listing_table_with_empty_prompt.js new file mode 100644 index 0000000000000..68d0433526036 --- /dev/null +++ b/ui_framework/doc_site/src/views/table/listing_table_with_empty_prompt.js @@ -0,0 +1,96 @@ +import React from 'react'; + +import { + KuiButton, + KuiButtonIcon, + KuiPager, + KuiEmptyTablePrompt, + KuiEmptyTablePromptPanel, + KuiListingTable, + KuiTableHeaderCell +} from '../../../../components'; + +function renderEmptyTablePrompt() { + return ( + + Add Items} + message="Uh oh you have no items!" + /> + + ); +} + +function renderToolBarActions() { + return [ + + Add + , + } + />, + } + /> + ]; +} + +function renderPager() { + return ( + { + }} + onPreviousPage={() => { + }} + /> + ); +} + +function renderHeader() { + return [ + + Title + , + + Status + , + + Date created + , + + Orders of magnitude + + ]; +} + +export function ListingTableWithEmptyPrompt() { + return ( + {}} + filter="" + prompt={renderEmptyTablePrompt()} + onItemSelectionChanged={() => {}} + /> + ); +} diff --git a/ui_framework/doc_site/src/views/table/listing_table_with_no_items.js b/ui_framework/doc_site/src/views/table/listing_table_with_no_items.js new file mode 100644 index 0000000000000..30b4164765681 --- /dev/null +++ b/ui_framework/doc_site/src/views/table/listing_table_with_no_items.js @@ -0,0 +1,79 @@ +import React from 'react'; + +import { + KuiButton, + KuiButtonIcon, + KuiPager, + KuiListingTable, + KuiListingTableNoMatchesPrompt, +} from '../../../../components'; + +import { + RIGHT_ALIGNMENT +} from '../../../../services'; + +function renderToolBarActions() { + return [ + + Add + , + } + />, + } + /> + ]; +} + +function renderPager() { + return ( + { + }} + onPreviousPage={() => { + }} + /> + ); +} + +function renderHeader() { + return [ + 'Title', + 'Status', + 'Date created', + { + content: 'Orders of magnitude', + align: RIGHT_ALIGNMENT + } + ]; +} + +export function ListingTableWithNoItems() { + return ( + {}} + filter="hello" + prompt={} + onItemSelectionChanged={() => {}} + /> + ); +} diff --git a/ui_framework/doc_site/src/views/table/table_example.js b/ui_framework/doc_site/src/views/table/table_example.js index c404f8e856414..f6d9268a6ad3b 100644 --- a/ui_framework/doc_site/src/views/table/table_example.js +++ b/ui_framework/doc_site/src/views/table/table_example.js @@ -21,21 +21,21 @@ import { FluidTable } from './fluid_table'; const fluidTableSource = require('!!raw!./fluid_table'); const fluidTableHtml = renderToHtml(FluidTable); -import { ControlledTable } from './controlled_table'; -const controlledTableSource = require('!!raw!./controlled_table'); -const controlledTableHtml = renderToHtml(ControlledTable); +import { ListingTable } from './listing_table'; +const listingTableSource = require('!!raw!./listing_table'); +const listingTableHtml = renderToHtml(ListingTable); -import { ControlledTableWithEmptyPrompt } from './controlled_table_with_empty_prompt'; -const controlledTableWithEmptyPromptSource = require('!!raw!./controlled_table_with_empty_prompt'); -const controlledTableWithEmptyPromptHtml = renderToHtml(ControlledTableWithEmptyPrompt); +import { ListingTableWithEmptyPrompt } from './listing_table_with_empty_prompt'; +const listingTableWithEmptyPromptSource = require('!!raw!./listing_table_with_empty_prompt'); +const listingTableWithEmptyPromptHtml = renderToHtml(ListingTableWithEmptyPrompt); -import { ControlledTableWithNoItems } from './controlled_table_with_no_items'; -const controlledTableWithNoItemsSource = require('!!raw!./controlled_table_with_no_items'); -const controlledTableWithNoItemsHtml = renderToHtml(ControlledTableWithNoItems); +import { ListingTableWithNoItems } from './listing_table_with_no_items'; +const listingTableWithNoItemsSource = require('!!raw!./listing_table_with_no_items'); +const listingTableWithNoItemsHtml = renderToHtml(ListingTableWithNoItems); -import { ControlledTableLoadingItems } from './controlled_table_loading_items'; -const controlledTableLoadingItemsSource = require('!!raw!./controlled_table_loading_items'); -const controlledTableLoadingItemsHtml = renderToHtml(ControlledTableLoadingItems); +import { ListingTableLoadingItems } from './listing_table_loading_items'; +const listingTableLoadingItemsSource = require('!!raw!./listing_table_loading_items'); +const listingTableLoadingItemsHtml = renderToHtml(ListingTableLoadingItems); export default props => ( @@ -93,62 +93,62 @@ export default props => ( - + - + - + - + diff --git a/ui_framework/src/components/index.js b/ui_framework/src/components/index.js index fb2fb59b6e3d3..9cf44483a11fc 100644 --- a/ui_framework/src/components/index.js +++ b/ui_framework/src/components/index.js @@ -151,6 +151,11 @@ export { KuiTableHeaderCheckBoxCell, KuiTableHeader, KuiTableBody, + KuiListingTable, + KuiListingTableCreateButton, + KuiListingTableDeleteButton, + KuiListingTableNoMatchesPrompt, + KuiListingTableLoadingPrompt } from './table'; export { diff --git a/ui_framework/src/components/table/index.js b/ui_framework/src/components/table/index.js index 43e6392c9d897..6687753a6b4a5 100644 --- a/ui_framework/src/components/table/index.js +++ b/ui_framework/src/components/table/index.js @@ -8,3 +8,10 @@ export { KuiTableRowCheckBoxCell } from './table_row_check_box_cell'; export { KuiTableHeaderCheckBoxCell } from './table_header_check_box_cell'; export { KuiTableBody } from './table_body'; export { KuiTableHeader } from './table_header'; +export { + KuiListingTable, + KuiListingTableCreateButton, + KuiListingTableDeleteButton, + KuiListingTableLoadingPrompt, + KuiListingTableNoMatchesPrompt, +} from './listing_table'; diff --git a/ui_framework/src/components/table/listing_table/__snapshots__/listing_table.test.js.snap b/ui_framework/src/components/table/listing_table/__snapshots__/listing_table.test.js.snap new file mode 100644 index 0000000000000..9aa81a391b507 --- /dev/null +++ b/ui_framework/src/components/table/listing_table/__snapshots__/listing_table.test.js.snap @@ -0,0 +1,197 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`renders KuiListingTable 1`] = ` +
+
+
+
+ +
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ +
+
+
+ Breed +
+
+
+ Description +
+
+
+ +
+
+
+ Bengal +
+
+
+ An athlete, spotted cat +
+
+
+ +
+
+
+ Himalayan +
+
+
+ Affectionate but discriminating +
+
+
+ +
+
+
+ Chartreux +
+
+
+ Silent but communicative and sometimes silly +
+
+
+
+
+ 0 items selected +
+
+
+
+
+`; diff --git a/ui_framework/src/components/table/listing_table/index.js b/ui_framework/src/components/table/listing_table/index.js new file mode 100644 index 0000000000000..b46f749cd8e9c --- /dev/null +++ b/ui_framework/src/components/table/listing_table/index.js @@ -0,0 +1,5 @@ +export { KuiListingTable } from './listing_table'; +export { KuiListingTableCreateButton } from './listing_table_create_button'; +export { KuiListingTableDeleteButton } from './listing_table_delete_button'; +export { KuiListingTableNoMatchesPrompt } from './listing_table_no_matches_prompt'; +export { KuiListingTableLoadingPrompt } from './listing_table_loading_prompt'; diff --git a/ui_framework/src/components/table/listing_table/listing_table.js b/ui_framework/src/components/table/listing_table/listing_table.js new file mode 100644 index 0000000000000..714cd2b38bdde --- /dev/null +++ b/ui_framework/src/components/table/listing_table/listing_table.js @@ -0,0 +1,165 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +import _ from 'lodash'; + +import { KuiListingTableToolBar } from './listing_table_tool_bar'; +import { KuiListingTableToolBarFooter } from './listing_table_tool_bar_footer'; +import { KuiListingTableRow } from './listing_table_row'; + +import { + KuiControlledTable, + KuiTableHeaderCheckBoxCell, + KuiTableBody, + KuiTableHeader, + KuiTable, + KuiTableHeaderCell, +} from '../../index'; + +import { + LEFT_ALIGNMENT, + RIGHT_ALIGNMENT, +} from '../../../services'; + +export function KuiListingTable({ + rows, + header, + pager, + toolBarActions, + onFilter, + onItemSelectionChanged, + selectedRowIds, + filter, + prompt, +}) { + + function areAllRowsSelected() { + return rows.length > 0 && rows.length === selectedRowIds.length; + } + + function toggleAll() { + if (areAllRowsSelected()) { + onItemSelectionChanged([]); + } else { + onItemSelectionChanged(rows.map(row => row.id)); + } + } + + function toggleRow(rowId) { + const selectedRowIndex = selectedRowIds.indexOf(rowId); + if (selectedRowIndex >= 0) { + onItemSelectionChanged(selectedRowIds.filter((item, index) => index !== selectedRowIndex)); + } else { + onItemSelectionChanged([ + ...selectedRowIds, + rowId + ]); + } + } + + function renderTableRows() { + return rows.map((row, rowIndex) => { + return ( + = 0} + onSelectionChanged={toggleRow} + row={row} + /> + ); + }); + } + + function renderHeader() { + return header.map((cell, index) => { + let { content, ...props } = cell; + if (React.isValidElement(cell) || !_.isObject(cell)) { + props = []; + content = cell; + } + return ( + + {content} + + ); + }); + } + + function renderInnerTable() { + return ( + + + + {renderHeader()} + + + + {renderTableRows()} + + + ); + } + + return ( + + + + {prompt ? prompt : renderInnerTable()} + + + + ); +} + +KuiListingTable.PropTypes = { + header: PropTypes.arrayOf( + PropTypes.oneOf([ + PropTypes.node, + PropTypes.shape({ + content: PropTypes.node, + align: PropTypes.oneOf([LEFT_ALIGNMENT, RIGHT_ALIGNMENT]), + onSort: PropTypes.func, + isSortAscending: PropTypes.bool, + isSorted: PropTypes.bool, + }), + ] + )), + rows: PropTypes.arrayOf(PropTypes.shape({ + id: PropTypes.string, + cells: PropTypes.arrayOf( + PropTypes.oneOf([ + PropTypes.node, + PropTypes.shape({ + content: PropTypes.node, + align: PropTypes.oneOf([LEFT_ALIGNMENT, RIGHT_ALIGNMENT]), + }), + ], + )), + })), + pager: PropTypes.node, + onItemSelectionChanged: PropTypes.func.isRequired, + selectedRowIds: PropTypes.array, + prompt: PropTypes.node, // If given, will be shown instead of a table with rows. + onFilter: PropTypes.func, + toolBarActions: PropTypes.node, + filter: PropTypes.string, +}; + +KuiListingTable.defaultProps = { + rows: [], + selectedRowIds: [], +}; diff --git a/ui_framework/src/components/table/listing_table/listing_table.test.js b/ui_framework/src/components/table/listing_table/listing_table.test.js new file mode 100644 index 0000000000000..01b274dc36270 --- /dev/null +++ b/ui_framework/src/components/table/listing_table/listing_table.test.js @@ -0,0 +1,67 @@ +import React from 'react'; +import { mount, shallow } from 'enzyme'; +import { requiredProps } from '../../../test/required_props'; +import { + takeMountedSnapshot, +} from 'ui_framework/src/test'; +import { + KuiListingTable, +} from './listing_table'; + +const getProps = (customProps) => { + const defaultProps = { + header: [ + 'Breed', + 'Description' + ], + rows: [ + { + id: '1', + cells: ['Bengal', 'An athlete, spotted cat'], + }, + { + id: '2', + cells: ['Himalayan', 'Affectionate but discriminating'], + }, + { + id: '3', + cells: ['Chartreux', 'Silent but communicative and sometimes silly'], + }, + ], + onItemSelectionChanged: jest.fn(), + selectedRowIds: [], + onFilter: jest.fn(), + }; + + return { + ...defaultProps, + ...requiredProps, + ...customProps, + }; +}; + +test('renders KuiListingTable', () => { + const component = mount(); + expect(takeMountedSnapshot(component)).toMatchSnapshot(); +}); + +test('selecting a row calls onItemSelectionChanged', () => { + const props = getProps(); + const component = shallow(); + component.find('KuiListingTableRow').at(1).prop('onSelectionChanged')('1'); + expect(props.onItemSelectionChanged).toHaveBeenCalledWith(['1']); +}); + +test('selectedRowIds is preserved when onItemSelectionChanged is called', () => { + const props = getProps({ selectedRowIds: ['3'] }); + const component = shallow(); + component.find('KuiListingTableRow').at(0).prop('onSelectionChanged')('1'); + expect(props.onItemSelectionChanged).toHaveBeenCalledWith(expect.arrayContaining(['1', '3'])); +}); + +test('onFilter is called when the search box is used', () => { + const props = getProps(); + const component = mount(); + component.find('KuiToolBarSearchBox').prop('onFilter')('a filter'); + expect(props.onFilter).toHaveBeenCalledWith('a filter'); +}); diff --git a/ui_framework/src/components/table/listing_table/listing_table_create_button.js b/ui_framework/src/components/table/listing_table/listing_table_create_button.js new file mode 100644 index 0000000000000..69ed7f8f8dbc3 --- /dev/null +++ b/ui_framework/src/components/table/listing_table/listing_table_create_button.js @@ -0,0 +1,22 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +import { + KuiButton, + KuiButtonIcon, +} from '../../'; + +export function KuiListingTableCreateButton({ onCreate, ...props }) { + return ( + } + /> + ); +} + +KuiListingTableCreateButton.propTypes = { + onCreate: PropTypes.func +}; diff --git a/ui_framework/src/components/table/listing_table/listing_table_delete_button.js b/ui_framework/src/components/table/listing_table/listing_table_delete_button.js new file mode 100644 index 0000000000000..2b9efae47352c --- /dev/null +++ b/ui_framework/src/components/table/listing_table/listing_table_delete_button.js @@ -0,0 +1,22 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +import { + KuiButton, + KuiButtonIcon, +} from '../../'; + +export function KuiListingTableDeleteButton({ onDelete, ...props }) { + return ( + } + /> + ); +} + +KuiListingTableDeleteButton.propTypes = { + onDelete: PropTypes.func +}; diff --git a/ui_framework/src/components/table/listing_table/listing_table_loading_prompt.js b/ui_framework/src/components/table/listing_table/listing_table_loading_prompt.js new file mode 100644 index 0000000000000..b5f5a2ac6ac08 --- /dev/null +++ b/ui_framework/src/components/table/listing_table/listing_table_loading_prompt.js @@ -0,0 +1,16 @@ +import React from 'react'; + +import { + KuiEmptyTablePromptPanel, + KuiTableInfo, +} from '../../'; + +export function KuiListingTableLoadingPrompt() { + return ( + + + Loading... + + + ); +} diff --git a/ui_framework/src/components/table/listing_table/listing_table_no_matches_prompt.js b/ui_framework/src/components/table/listing_table/listing_table_no_matches_prompt.js new file mode 100644 index 0000000000000..20149f90e9ff2 --- /dev/null +++ b/ui_framework/src/components/table/listing_table/listing_table_no_matches_prompt.js @@ -0,0 +1,16 @@ +import React from 'react'; + +import { + KuiEmptyTablePromptPanel, + KuiTableInfo, +} from '../../'; + +export function KuiListingTableNoMatchesPrompt() { + return ( + + + No items matched your search. + + + ); +} diff --git a/ui_framework/src/components/table/listing_table/listing_table_row.js b/ui_framework/src/components/table/listing_table/listing_table_row.js new file mode 100644 index 0000000000000..6defeeff5ae1a --- /dev/null +++ b/ui_framework/src/components/table/listing_table/listing_table_row.js @@ -0,0 +1,69 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +import _ from 'lodash'; + +import { + KuiTableRow, + KuiTableRowCell, + KuiTableRowCheckBoxCell, +} from '../'; + +import { + LEFT_ALIGNMENT, + RIGHT_ALIGNMENT, +} from '../../../services'; + +export class KuiListingTableRow extends React.PureComponent { + onSelectionChanged = () => { + this.props.onSelectionChanged(this.props.row.id); + }; + + renderCells() { + return this.props.row.cells.map((cell, index) => { + let { content, ...props } = cell; + if (React.isValidElement(cell) || !_.isObject(cell)) { + props = []; + content = cell; + } + return ( + + {content} + + ); + }); + } + + render() { + const { isSelected } = this.props; + return ( + + + {this.renderCells()} + + ); + } +} + +KuiListingTableRow.PropTypes = { + row: PropTypes.shape({ + id: PropTypes.string, + cells: PropTypes.arrayOf( + PropTypes.oneOf([ + PropTypes.node, + PropTypes.shape({ + content: PropTypes.node, + align: PropTypes.oneOf([LEFT_ALIGNMENT, RIGHT_ALIGNMENT]), + }) + ], + )), + }).isRequired, + onSelectionChanged: PropTypes.func.isRequired, + isSelected: PropTypes.bool, +}; diff --git a/ui_framework/src/components/table/listing_table/listing_table_tool_bar.js b/ui_framework/src/components/table/listing_table/listing_table_tool_bar.js new file mode 100644 index 0000000000000..727bde90410fd --- /dev/null +++ b/ui_framework/src/components/table/listing_table/listing_table_tool_bar.js @@ -0,0 +1,31 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +import { + KuiToolBar, + KuiToolBarSearchBox, + KuiToolBarSection, +} from '../../'; + +export function KuiListingTableToolBar({ pager, actions, onFilter, filter }) { + return ( + + + + + {actions} + + + + {pager} + + + ); +} + +KuiListingTableToolBar.propTypes = { + filter: PropTypes.string, + onFilter: PropTypes.func.isRequired, + pager: PropTypes.node, + actions: PropTypes.node, +}; diff --git a/ui_framework/src/components/table/listing_table/listing_table_tool_bar_footer.js b/ui_framework/src/components/table/listing_table/listing_table_tool_bar_footer.js new file mode 100644 index 0000000000000..a7f227c810048 --- /dev/null +++ b/ui_framework/src/components/table/listing_table/listing_table_tool_bar_footer.js @@ -0,0 +1,39 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +import { + KuiToolBarFooter, + KuiToolBarText, + KuiToolBarFooterSection, +} from '../../'; + + + +export function KuiListingTableToolBarFooter({ pager, itemsSelectedCount }) { + const renderText = () => { + if (itemsSelectedCount === 1) { + return '1 item selected'; + } + + return `${itemsSelectedCount} items selected`; + }; + + return ( + + + + {renderText()} + + + + + {pager} + + + ); +} + +KuiListingTableToolBarFooter.PropTypes = { + pager: PropTypes.node, + itemsSelectedCount: PropTypes.number, +}; diff --git a/ui_framework/src/components/table/table_header_cell.js b/ui_framework/src/components/table/table_header_cell.js index 1772e055a4a8e..f82c17eeb7814 100644 --- a/ui_framework/src/components/table/table_header_cell.js +++ b/ui_framework/src/components/table/table_header_cell.js @@ -2,6 +2,8 @@ import React from 'react'; import PropTypes from 'prop-types'; import classNames from 'classnames'; +import { LEFT_ALIGNMENT, RIGHT_ALIGNMENT } from '../../services'; + export const KuiTableHeaderCell = ({ children, onSort, @@ -9,10 +11,12 @@ export const KuiTableHeaderCell = ({ isSortAscending, className, ariaLabel, + align, ...rest, }) => { - const classes = classNames('kuiTableHeaderCell', className); - + const classes = classNames('kuiTableHeaderCell', className, { + 'kuiTableHeaderCell--alignRight': align === RIGHT_ALIGNMENT, + }); if (onSort) { const sortIconClasses = classNames('kuiTableSortIcon kuiIcon', { 'fa-long-arrow-up': isSortAscending, @@ -66,4 +70,9 @@ KuiTableHeaderCell.propTypes = { onSort: PropTypes.func, isSorted: PropTypes.bool, isSortAscending: PropTypes.bool, + align: PropTypes.oneOf([LEFT_ALIGNMENT, RIGHT_ALIGNMENT]), +}; + +KuiTableHeaderCell.defaultProps = { + align: LEFT_ALIGNMENT };