diff --git a/packages/patternfly-3/patternfly-react-extensions/less/patternfly-react-extensions.less b/packages/patternfly-3/patternfly-react-extensions/less/patternfly-react-extensions.less index b57dbd8a757..e38c127be74 100644 --- a/packages/patternfly-3/patternfly-react-extensions/less/patternfly-react-extensions.less +++ b/packages/patternfly-3/patternfly-react-extensions/less/patternfly-react-extensions.less @@ -1,2 +1,3 @@ @import 'filter-side-panel'; +@import 'table-grid'; diff --git a/packages/patternfly-3/patternfly-react-extensions/less/table-grid.less b/packages/patternfly-3/patternfly-react-extensions/less/table-grid.less new file mode 100644 index 00000000000..de4b81b14a0 --- /dev/null +++ b/packages/patternfly-3/patternfly-react-extensions/less/table-grid.less @@ -0,0 +1,61 @@ +.table-grid-pf { + .row, + [class*='col-'] { + overflow-x: hidden; + text-overflow: ellipsis; + vertical-align: middle; + white-space: nowrap; + } + + .table-grid-pf-body { + min-height: 50px; + position: relative; + width: 100%; + + .row { + padding: 10px 0; + } + } + + .table-grid-pf-head { + font-size: 12px; + padding: 10px 0; + } + + .table-grid-pf-column-header { + align-items: center; + display: flex; + text-transform: uppercase; + + .btn.btn-link { + color: initial; + overflow-x: hidden; + padding: 0; + text-overflow: ellipsis; + text-transform: uppercase; + white-space: nowrap; + + &:hover, + &:focus { + outline: 0; + } + } + + &.active-sort { + .btn.btn-link { + color: @link-color; + } + } + } + + .table-grid-pf-header-sort-arrow { + margin-left: 10px; + } + + &.bordered { + .table-grid-pf-head, + .table-grid-pf-body .row { + border-bottom: solid 1px #eee; + } + } +} diff --git a/packages/patternfly-3/patternfly-react-extensions/sass/patternfly-react-extensions/_patternfly-react-extensions.scss b/packages/patternfly-3/patternfly-react-extensions/sass/patternfly-react-extensions/_patternfly-react-extensions.scss index 141f6d1bc50..45d75547e96 100644 --- a/packages/patternfly-3/patternfly-react-extensions/sass/patternfly-react-extensions/_patternfly-react-extensions.scss +++ b/packages/patternfly-3/patternfly-react-extensions/sass/patternfly-react-extensions/_patternfly-react-extensions.scss @@ -2,3 +2,4 @@ Patternfly React Extensions Partials */ @import 'filter-side-panel'; +@import 'table-grid'; diff --git a/packages/patternfly-3/patternfly-react-extensions/sass/patternfly-react-extensions/_table-grid.scss b/packages/patternfly-3/patternfly-react-extensions/sass/patternfly-react-extensions/_table-grid.scss new file mode 100644 index 00000000000..8b931de530a --- /dev/null +++ b/packages/patternfly-3/patternfly-react-extensions/sass/patternfly-react-extensions/_table-grid.scss @@ -0,0 +1,61 @@ +.table-grid-pf { + .row, + [class*='col-'] { + overflow-x: hidden; + text-overflow: ellipsis; + vertical-align: middle; + white-space: nowrap; + } + + .table-grid-pf-body { + min-height: 50px; + position: relative; + width: 100%; + + .row { + padding: 10px 0; + } + } + + .table-grid-pf-head { + font-size: 12px; + padding: 10px 0; + } + + .table-grid-pf-column-header { + align-items: center; + display: flex; + text-transform: uppercase; + + .btn.btn-link { + color: initial; + overflow-x: hidden; + padding: 0; + text-overflow: ellipsis; + text-transform: uppercase; + white-space: nowrap; + + &:hover, + &:focus { + outline: 0; + } + } + + &.active-sort { + .btn.btn-link { + color: $link-color; + } + } + } + + .table-grid-pf-header-sort-arrow { + margin-left: 10px; + } + + &.bordered { + .table-grid-pf-head, + .table-grid-pf-body .row { + border-bottom: solid 1px #eee; + } + } +} diff --git a/packages/patternfly-3/patternfly-react-extensions/src/components/TableGrid/TableGrid.js b/packages/patternfly-3/patternfly-react-extensions/src/components/TableGrid/TableGrid.js new file mode 100644 index 00000000000..a2eb8813f58 --- /dev/null +++ b/packages/patternfly-3/patternfly-react-extensions/src/components/TableGrid/TableGrid.js @@ -0,0 +1,41 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import classNames from 'classnames'; +import TableGridHead from './TableGridHead'; +import TableGridColumnHeader from './TableGridColumnHeader'; +import TableGridBody from './TableGridBody'; +import TableGridRow from './TableGridRow'; + +/** + * TableGrid Component for PatternFly + */ + +const TableGrid = ({ children, className, bordered, ...props }) => { + const classes = classNames('table-grid-pf', { bordered }, className); + return ( +
+ {children} +
+ ); +}; + +TableGrid.propTypes = { + /** Children nodes */ + children: PropTypes.node, + /** Additional css classes */ + className: PropTypes.string, + /** Flag to use a bordered grid */ + bordered: PropTypes.bool +}; +TableGrid.defaultProps = { + children: null, + className: '', + bordered: true +}; + +TableGrid.Head = TableGridHead; +TableGrid.ColumnHeader = TableGridColumnHeader; +TableGrid.Body = TableGridBody; +TableGrid.Row = TableGridRow; + +export default TableGrid; diff --git a/packages/patternfly-3/patternfly-react-extensions/src/components/TableGrid/TableGrid.stories.js b/packages/patternfly-3/patternfly-react-extensions/src/components/TableGrid/TableGrid.stories.js new file mode 100644 index 00000000000..f43d2127905 --- /dev/null +++ b/packages/patternfly-3/patternfly-react-extensions/src/components/TableGrid/TableGrid.stories.js @@ -0,0 +1,41 @@ +import React from 'react'; +import { storiesOf } from '@storybook/react'; +import { withInfo } from '@storybook/addon-info/dist/index'; +import { defaultTemplate } from 'storybook/decorators/storyTemplates'; +import { storybookPackageName, STORYBOOK_CATEGORY } from 'storybook/constants/siteConstants'; +import { MockTableGridExample, MockTableGridExampleSource } from './_mocks_/mockTableGridExample'; + +import { TableGrid, TableGridHead, TableGridColumnHeader, TableGridBody, TableGridRow } from './index'; + +import { name } from '../../../package.json'; +import { boolean, withKnobs } from '@storybook/addon-knobs'; + +const stories = storiesOf(`${storybookPackageName(name)}/${STORYBOOK_CATEGORY.CONTENT_VIEWS}/TableGrid`, module); + +stories.addDecorator( + defaultTemplate({ + title: 'Table Grid', + description: + 'The TableGrid is based on the Bootstrap Grid Layout. The TableGridColumnHeaders should have the same ' + + 'bootstrap col classes as the children of the TableGridRow component in order to maintain equal widths.' + }) +); + +stories.addDecorator(withKnobs); +stories.add( + 'TableGrid', + withInfo({ + source: false, + propTables: [TableGrid, TableGridHead, TableGridColumnHeader, TableGridBody, TableGridRow], + propTablesExclude: [MockTableGridExample], + text: ( +
+

Story Source

+
{MockTableGridExampleSource}
+
+ ) + })(() => { + const bordered = boolean('Bordered', true); + return ; + }) +); diff --git a/packages/patternfly-3/patternfly-react-extensions/src/components/TableGrid/TableGrid.test.js b/packages/patternfly-3/patternfly-react-extensions/src/components/TableGrid/TableGrid.test.js new file mode 100644 index 00000000000..5bedd6a9893 --- /dev/null +++ b/packages/patternfly-3/patternfly-react-extensions/src/components/TableGrid/TableGrid.test.js @@ -0,0 +1,56 @@ +import React from 'react'; +import { mount } from 'enzyme'; +import { Grid, Icon, noop } from 'patternfly-react'; + +import { TableGrid } from './index'; + +test('TableGrid renders properly', () => { + const component = mount( + + + + Column 1 + + Column 2 + + +
+ test 1 + and two +
+
+
+ + + + item 1 column 1 + + + item 1 column 2 + + + item 1 column 3 + + + item 1 column 4 + + + + item 2 column 1 + + item 1 + column 2 + + + + Danger + + + item 2 column 4 + + + +
+ ); + expect(component.render()).toMatchSnapshot(); +}); diff --git a/packages/patternfly-3/patternfly-react-extensions/src/components/TableGrid/TableGridBody.js b/packages/patternfly-3/patternfly-react-extensions/src/components/TableGrid/TableGridBody.js new file mode 100644 index 00000000000..8cf3f95e7bc --- /dev/null +++ b/packages/patternfly-3/patternfly-react-extensions/src/components/TableGrid/TableGridBody.js @@ -0,0 +1,29 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import classNames from 'classnames'; + +/** + * TableGridBody Component for PatternFly + */ + +const TableGridBody = ({ children, className, ...props }) => { + const classes = classNames('table-grid-pf-body', className); + return ( +
+ {children} +
+ ); +}; + +TableGridBody.propTypes = { + /** Children nodes */ + children: PropTypes.node, + /** Additional css classes */ + className: PropTypes.string +}; +TableGridBody.defaultProps = { + children: null, + className: '' +}; + +export default TableGridBody; diff --git a/packages/patternfly-3/patternfly-react-extensions/src/components/TableGrid/TableGridColumnHeader.js b/packages/patternfly-3/patternfly-react-extensions/src/components/TableGrid/TableGridColumnHeader.js new file mode 100644 index 00000000000..a97588ef286 --- /dev/null +++ b/packages/patternfly-3/patternfly-react-extensions/src/components/TableGrid/TableGridColumnHeader.js @@ -0,0 +1,64 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import classNames from 'classnames'; +import { Button, Grid, Icon, noop } from 'patternfly-react'; + +/** + * TableGridColumnHeader Component for PatternFly + */ + +const TableGridColumnHeader = ({ children, className, sortable, isSorted, isAscending, onSortToggle, ...props }) => { + const classes = classNames( + 'table-grid-pf-column-header text-nowrap', + { 'active-sort': isSorted, descending: !isAscending }, + className + ); + + return ( + + {sortable && ( + + + + )} + {!sortable && children} + + ); +}; + +TableGridColumnHeader.propTypes = { + /** Children nodes */ + children: PropTypes.node, + /** Additional css classes */ + className: PropTypes.string, + /** Flag if this column is sortable */ + sortable: PropTypes.bool, + /** Flag if this is the current sort column */ + isSorted: PropTypes.bool, + /** Flag if the sort is ascending */ + isAscending: PropTypes.bool, + /** Callback function when the user click on this column header */ + onSortToggle: PropTypes.func, + ...Grid.Col.propTypes +}; + +TableGridColumnHeader.defaultProps = { + children: null, + className: '', + sortable: false, + isSorted: false, + isAscending: true, + onSortToggle: noop, + ...Grid.Col.defaultProps +}; + +export default TableGridColumnHeader; diff --git a/packages/patternfly-3/patternfly-react-extensions/src/components/TableGrid/TableGridHead.js b/packages/patternfly-3/patternfly-react-extensions/src/components/TableGrid/TableGridHead.js new file mode 100644 index 00000000000..ae9f34d50bd --- /dev/null +++ b/packages/patternfly-3/patternfly-react-extensions/src/components/TableGrid/TableGridHead.js @@ -0,0 +1,30 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import classNames from 'classnames'; +import { Grid } from 'patternfly-react'; + +/** + * TableGridHead Component for PatternFly + */ + +const TableGridHead = ({ children, className, ...props }) => { + const classes = classNames('table-grid-pf-head', className); + return ( + + {children} + + ); +}; + +TableGridHead.propTypes = { + /** Children nodes */ + children: PropTypes.node, + /** Additional css classes */ + className: PropTypes.string +}; +TableGridHead.defaultProps = { + children: null, + className: '' +}; + +export default TableGridHead; diff --git a/packages/patternfly-3/patternfly-react-extensions/src/components/TableGrid/TableGridRow.js b/packages/patternfly-3/patternfly-react-extensions/src/components/TableGrid/TableGridRow.js new file mode 100644 index 00000000000..b41ec45af3c --- /dev/null +++ b/packages/patternfly-3/patternfly-react-extensions/src/components/TableGrid/TableGridRow.js @@ -0,0 +1,30 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import classNames from 'classnames'; +import { Grid } from 'patternfly-react'; + +/** + * TableGridRow Component for PatternFly + */ + +const TableGridRow = ({ children, className, ...props }) => { + const classes = classNames('table-grid-pf-row', className); + return ( + + {children} + + ); +}; + +TableGridRow.propTypes = { + /** Children nodes */ + children: PropTypes.node, + /** Additional css classes */ + className: PropTypes.string +}; +TableGridRow.defaultProps = { + children: null, + className: '' +}; + +export default TableGridRow; diff --git a/packages/patternfly-3/patternfly-react-extensions/src/components/TableGrid/__snapshots__/TableGrid.test.js.snap b/packages/patternfly-3/patternfly-react-extensions/src/components/TableGrid/__snapshots__/TableGrid.test.js.snap new file mode 100644 index 00000000000..40ff241e0e8 --- /dev/null +++ b/packages/patternfly-3/patternfly-react-extensions/src/components/TableGrid/__snapshots__/TableGrid.test.js.snap @@ -0,0 +1,120 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`TableGrid renders properly 1`] = ` +
+
+
+ +
+
+ Column 2 +
+
+
+
+ + test 1 + + + and two + +
+
+
+
+
+
+ item 1 column 1 +
+
+ item 1 column 2 +
+
+ item 1 column 3 +
+
+ item 1 column 4 +
+
+
+
+ item 2 column 1 +
+
+ + item 1 + + + column 2 + +
+
+
+
+ item 2 column 4 +
+
+
+
+`; diff --git a/packages/patternfly-3/patternfly-react-extensions/src/components/TableGrid/_mocks_/mockItems.js b/packages/patternfly-3/patternfly-react-extensions/src/components/TableGrid/_mocks_/mockItems.js new file mode 100644 index 00000000000..52c56c2f872 --- /dev/null +++ b/packages/patternfly-3/patternfly-react-extensions/src/components/TableGrid/_mocks_/mockItems.js @@ -0,0 +1,38 @@ +export const mockItems = [ + { + title: 'Item 1', + description: 'This is Item 1 description', + hosts: 3, + clusters: 1 + }, + { + title: 'Item 2', + description: 'This is Item 2 description', + hosts: 10, + clusters: 2 + }, + { + title: 'Item 3', + description: 'This is Item 3 description', + hosts: 13, + clusters: 11 + }, + { + title: 'Item 4', + description: 'This is a long description for item 4 that is longer than any other description.', + hosts: 7, + clusters: 3 + }, + { + title: 'Item 5 with a very long title, showing that it should be truncated when it gets to be to long.', + description: 'This is Item 5 description', + hosts: 2, + clusters: 9 + }, + { + title: 'Item 6', + description: 'This is Item 6 description', + hosts: 1, + clusters: 0 + } +]; diff --git a/packages/patternfly-3/patternfly-react-extensions/src/components/TableGrid/_mocks_/mockTableGridExample.js b/packages/patternfly-3/patternfly-react-extensions/src/components/TableGrid/_mocks_/mockTableGridExample.js new file mode 100644 index 00000000000..69fce979e8e --- /dev/null +++ b/packages/patternfly-3/patternfly-react-extensions/src/components/TableGrid/_mocks_/mockTableGridExample.js @@ -0,0 +1,265 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import TableGrid from '../TableGrid'; +import { Grid, OverlayTrigger, Tooltip } from 'patternfly-react'; +import { mockItems } from './mockItems'; + +const titleColSizes = { + md: 2, + sm: 4, + xs: 6 +}; +const descrColSizes = { + md: 6, + sm: 4, + xs: 6 +}; +const countColSizes = { + md: 2, + sm: 2, + xsHidden: true +}; + +class MockTableGridExample extends React.Component { + state = { + sortField: 'title', + isAscending: true, + items: mockItems + }; + + onSortToggle = id => { + const { items, sortField, isAscending } = this.state; + let updateAscending = true; + + if (id === sortField) { + updateAscending = !isAscending; + } + + items.sort((a, b) => { + let compVal = 0; + if (id === 'title') { + compVal = a.title.localeCompare(b.title); + } else if (id === 'hosts') { + compVal = a.hosts - b.hosts; + } else if (id === 'clusters') { + compVal = a.clusters - b.clusters; + } + + if (!updateAscending) { + compVal *= -1; + } + + return compVal; + }); + + this.setState({ items, sortField: id, isAscending: updateAscending }); + }; + + renderItemRow = (item, index) => ( + + {item.title} + {item.description} + {item.hosts} + {item.clusters} + + ); + + render() { + const { items, sortField, isAscending } = this.state; + const { bordered } = this.props; + return ( + + + this.onSortToggle('title')} + {...titleColSizes} + > + Title + + this.onSortToggle('description')} + {...descrColSizes} + > + Description + + this.onSortToggle('hosts')} + {...countColSizes} + > + Hosts} placement="top"> + Hosts + + + this.onSortToggle('clusters')} + {...countColSizes} + > + Clusters} placement="top"> + Clusters + + + + {items.map((item, index) => this.renderItemRow(item, index))} + + ); + } +} + +MockTableGridExample.propTypes = { + bordered: PropTypes.bool +}; + +MockTableGridExample.defaultProps = { + bordered: false +}; + +export { MockTableGridExample }; + +export const MockTableGridExampleSource = ` +import React from 'react'; +import PropTypes from 'prop-types'; +import TableGrid from '../TableGrid'; +import { Grid, OverlayTrigger, Tooltip } from 'patternfly-react'; +import { mockItems } from './mockItems'; + +const titleColSizes = { + md: 2, + sm: 4, + xs: 6 +}; +const descrColSizes = { + md: 6, + sm: 4, + xs: 6 +}; +const countColSizes = { + md: 2, + sm: 2, + xsHidden: true +}; + +class MockTableGridExample extends React.Component { + state = { + sortField: 'title', + isAscending: true, + items: mockItems + }; + + onSortToggle = id => { + const { items, sortField, isAscending } = this.state; + let updateAscending = true; + + if (id === sortField) { + updateAscending = !isAscending; + } + + items.sort((a, b) => { + let compVal = 0; + if (id === 'title') { + compVal = a.title.localeCompare(b.title); + } else if (id === 'hosts') { + compVal = a.hosts - b.hosts; + } else if (id === 'clusters') { + compVal = a.clusters - b.clusters; + } + + if (!updateAscending) { + compVal *= -1; + } + + return compVal; + }); + + this.setState({ items, sortField: id, isAscending: updateAscending }); + }; + + renderItemRow = (item, index) => ( + + {item.title} + {item.description} + {item.hosts} + {item.clusters} + + ); + + render() { + const { items, sortField, isAscending } = this.state; + const { bordered } = this.props; + return ( + + + this.onSortToggle('title')} + {...titleColSizes} + > + Title + + this.onSortToggle('description')} + {...descrColSizes} + > + Description + + this.onSortToggle('hosts')} + {...countColSizes} + > + Hosts} placement="top"> + Hosts + + + this.onSortToggle('clusters')} + {...countColSizes} + > + Clusters} placement="top"> + Clusters + + + + {items.map((item, index) => this.renderItemRow(item, index))} + + ); + } +} + +MockTableGridExample.propTypes = { + bordered: PropTypes.bool +}; + +MockTableGridExample.defaultProps = { + bordered: false +}; + +export { MockTableGridExample }; +`; diff --git a/packages/patternfly-3/patternfly-react-extensions/src/components/TableGrid/index.js b/packages/patternfly-3/patternfly-react-extensions/src/components/TableGrid/index.js new file mode 100644 index 00000000000..9e8bdadf8f8 --- /dev/null +++ b/packages/patternfly-3/patternfly-react-extensions/src/components/TableGrid/index.js @@ -0,0 +1,7 @@ +import TableGrid from './TableGrid'; +import TableGridHead from './TableGridHead'; +import TableGridColumnHeader from './TableGridColumnHeader'; +import TableGridBody from './TableGridBody'; +import TableGridRow from './TableGridRow'; + +export { TableGrid, TableGridHead, TableGridColumnHeader, TableGridBody, TableGridRow }; diff --git a/packages/patternfly-3/patternfly-react-extensions/src/index.js b/packages/patternfly-3/patternfly-react-extensions/src/index.js index b7719ae8c92..2290ca9bbb1 100644 --- a/packages/patternfly-3/patternfly-react-extensions/src/index.js +++ b/packages/patternfly-3/patternfly-react-extensions/src/index.js @@ -1 +1,2 @@ -export * from './components/FilterSidePanel/index'; +export * from './components/FilterSidePanel'; +export * from './components/TableGrid';