This is an automated email from the ASF dual-hosted git repository. tai pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/incubator-superset.git
The following commit(s) were added to refs/heads/master by this push: new be936c2 style: listviews closer to SIP-34 (#10094) be936c2 is described below commit be936c2eb89fa09ed6147fb9f2dc4c63ed1390bd Author: ʈᵃᵢ <tdupree...@gmail.com> AuthorDate: Tue Jun 23 14:17:28 2020 -0700 style: listviews closer to SIP-34 (#10094) --- .../components/ListView/ListView_spec.jsx | 3 +- .../javascripts/views/chartList/ChartList_spec.jsx | 4 + .../views/dashboardList/DashboardList_spec.jsx | 4 + .../javascripts/welcome/DashboardTable_spec.jsx | 8 +- superset-frontend/src/components/AvatarIcon.tsx | 29 ++- .../src/components/ListView/ListView.tsx | 211 ++++++++++----------- .../src/components/ListView/ListViewStyles.less | 169 ++++++++++------- .../src/components/ListView/Pagination.tsx | 11 +- .../src/components/ListView/TableCollection.tsx | 82 ++++++-- superset-frontend/src/components/Menu/SubMenu.tsx | 16 +- superset-frontend/src/components/Pagination.tsx | 132 +++++++++++++ .../src/types/react-table-config.d.ts | 10 +- superset-frontend/src/utils/common.js | 4 + .../src/views/chartList/ChartList.tsx | 103 +++++----- .../src/views/dashboardList/DashboardList.tsx | 125 ++++++------ .../src/views/datasetList/DatasetList.tsx | 179 +++++++++-------- superset-frontend/stylesheets/less/variables.less | 4 - 17 files changed, 682 insertions(+), 412 deletions(-) diff --git a/superset-frontend/spec/javascripts/components/ListView/ListView_spec.jsx b/superset-frontend/spec/javascripts/components/ListView/ListView_spec.jsx index e5e04d5..52e15ff 100644 --- a/superset-frontend/spec/javascripts/components/ListView/ListView_spec.jsx +++ b/superset-frontend/spec/javascripts/components/ListView/ListView_spec.jsx @@ -19,13 +19,14 @@ import React from 'react'; import { mount, shallow } from 'enzyme'; import { act } from 'react-dom/test-utils'; -import { MenuItem, Pagination } from 'react-bootstrap'; +import { MenuItem } from 'react-bootstrap'; import Select from 'src/components/Select'; import { QueryParamProvider } from 'use-query-params'; import ListView from 'src/components/ListView/ListView'; import ListViewFilters from 'src/components/ListView/Filters'; import ListViewPagination from 'src/components/ListView/Pagination'; +import Pagination from 'src/components/Pagination'; import { areArraysShallowEqual } from 'src/reduxUtils'; import { ThemeProvider } from 'emotion-theming'; import { supersetTheme } from '@superset-ui/style'; diff --git a/superset-frontend/spec/javascripts/views/chartList/ChartList_spec.jsx b/superset-frontend/spec/javascripts/views/chartList/ChartList_spec.jsx index d5785bf..1ec7275 100644 --- a/superset-frontend/spec/javascripts/views/chartList/ChartList_spec.jsx +++ b/superset-frontend/spec/javascripts/views/chartList/ChartList_spec.jsx @@ -21,6 +21,8 @@ import { mount } from 'enzyme'; import thunk from 'redux-thunk'; import configureStore from 'redux-mock-store'; import fetchMock from 'fetch-mock'; +import { ThemeProvider } from 'emotion-theming'; +import { supersetTheme } from '@superset-ui/style'; import ChartList from 'src/views/chartList/ChartList'; import ListView from 'src/components/ListView/ListView'; @@ -77,6 +79,8 @@ describe('ChartList', () => { const mockedProps = {}; const wrapper = mount(<ChartList {...mockedProps} />, { context: { store }, + wrappingComponent: ThemeProvider, + wrappingComponentProps: { theme: supersetTheme }, }); it('renders', () => { diff --git a/superset-frontend/spec/javascripts/views/dashboardList/DashboardList_spec.jsx b/superset-frontend/spec/javascripts/views/dashboardList/DashboardList_spec.jsx index 086d9d1..456035e 100644 --- a/superset-frontend/spec/javascripts/views/dashboardList/DashboardList_spec.jsx +++ b/superset-frontend/spec/javascripts/views/dashboardList/DashboardList_spec.jsx @@ -21,6 +21,8 @@ import { mount } from 'enzyme'; import thunk from 'redux-thunk'; import configureStore from 'redux-mock-store'; import fetchMock from 'fetch-mock'; +import { ThemeProvider } from 'emotion-theming'; +import { supersetTheme } from '@superset-ui/style'; import DashboardList from 'src/views/dashboardList/DashboardList'; import ListView from 'src/components/ListView/ListView'; @@ -67,6 +69,8 @@ describe('DashboardList', () => { const mockedProps = {}; const wrapper = mount(<DashboardList {...mockedProps} />, { context: { store }, + wrappingComponent: ThemeProvider, + wrappingComponentProps: { theme: supersetTheme }, }); it('renders', () => { diff --git a/superset-frontend/spec/javascripts/welcome/DashboardTable_spec.jsx b/superset-frontend/spec/javascripts/welcome/DashboardTable_spec.jsx index a31761a..2fe659b 100644 --- a/superset-frontend/spec/javascripts/welcome/DashboardTable_spec.jsx +++ b/superset-frontend/spec/javascripts/welcome/DashboardTable_spec.jsx @@ -21,6 +21,8 @@ import { mount } from 'enzyme'; import thunk from 'redux-thunk'; import configureStore from 'redux-mock-store'; import fetchMock from 'fetch-mock'; +import { ThemeProvider } from 'emotion-theming'; +import { supersetTheme } from '@superset-ui/style'; import ListView from 'src/components/ListView/ListView'; import DashboardTable from 'src/welcome/DashboardTable'; @@ -36,7 +38,11 @@ fetchMock.get(dashboardsEndpoint, { result: mockDashboards }); function setup() { // use mount because data fetching is triggered on mount - return mount(<DashboardTable />, { context: { store } }); + return mount(<DashboardTable />, { + context: { store }, + wrappingComponent: ThemeProvider, + wrappingComponentProps: { theme: supersetTheme }, + }); } describe('DashboardTable', () => { diff --git a/superset-frontend/src/components/AvatarIcon.tsx b/superset-frontend/src/components/AvatarIcon.tsx index 4d86983..7dcf8f7 100644 --- a/superset-frontend/src/components/AvatarIcon.tsx +++ b/superset-frontend/src/components/AvatarIcon.tsx @@ -19,15 +19,16 @@ import React from 'react'; import styled from '@superset-ui/style'; import { getCategoricalSchemeRegistry } from '@superset-ui/color'; -import { Tooltip, OverlayTrigger } from 'react-bootstrap'; import Avatar, { ConfigProvider } from 'react-avatar'; +import TooltipWrapper from 'src/components/TooltipWrapper'; interface Props { firstName: string; - iconSize: string; lastName: string; tableName: string; userName: string; + iconSize: number; + textSize: number; } const colorList = getCategoricalSchemeRegistry().get(); @@ -42,18 +43,26 @@ export default function AvatarIcon({ lastName, userName, iconSize, + textSize, }: Props) { const uniqueKey = `${tableName}-${userName}`; const fullName = `${firstName} ${lastName}`; return ( - <ConfigProvider colors={colorList && colorList.colors}> - <OverlayTrigger - placement="right" - overlay={<Tooltip id={`${uniqueKey}-tooltip`}>{fullName}</Tooltip>} - > - <StyledAvatar key={uniqueKey} name={fullName} size={iconSize} round /> - </OverlayTrigger> - </ConfigProvider> + <TooltipWrapper + placement="bottom" + label={`${uniqueKey}-tooltip`} + tooltip={fullName} + > + <ConfigProvider colors={colorList && colorList.colors}> + <StyledAvatar + key={uniqueKey} + name={fullName} + size={String(iconSize)} + textSizeRatio={iconSize / textSize} + round + /> + </ConfigProvider> + </TooltipWrapper> ); } diff --git a/superset-frontend/src/components/ListView/ListView.tsx b/superset-frontend/src/components/ListView/ListView.tsx index 27f68cd..1017fbe 100644 --- a/superset-frontend/src/components/ListView/ListView.tsx +++ b/superset-frontend/src/components/ListView/ListView.tsx @@ -19,7 +19,8 @@ import { t } from '@superset-ui/translation'; import React, { FunctionComponent } from 'react'; import { Col, DropdownButton, MenuItem, Row } from 'react-bootstrap'; -import IndeterminateCheckbox from '../IndeterminateCheckbox'; +import Loading from 'src/components/Loading'; +import IndeterminateCheckbox from 'src/components/IndeterminateCheckbox'; import TableCollection from './TableCollection'; import Pagination from './Pagination'; import { FilterMenu, FilterInputs } from './LegacyFilters'; @@ -37,7 +38,6 @@ interface Props { fetchData: (conf: FetchDataConfig) => any; loading: boolean; className?: string; - title?: string; initialSort?: SortColumn[]; filters?: Filters; bulkActions?: Array<{ @@ -59,6 +59,7 @@ const bulkSelectColumnConfig = { /> ), id: 'selection', + size: 'sm', }; const ListView: FunctionComponent<Props> = ({ @@ -70,7 +71,6 @@ const ListView: FunctionComponent<Props> = ({ loading, initialSort = [], className = '', - title = '', filters = [], bulkActions = [], useNewUIFilters = false, @@ -116,124 +116,111 @@ const ListView: FunctionComponent<Props> = ({ } }); } - + if (loading && !data.length) { + return <Loading />; + } return ( - <div className={`superset-list-view ${className}`}> - <div className="header"> - {!useNewUIFilters && ( - <> - {title && filterable && ( - <> - <Row> - <Col md={11}> - <h2>{t(title)}</h2> - </Col> - {filterable && ( - <Col md={1}> - <FilterMenu - filters={filters} - internalFilters={internalFilters} - setInternalFilters={setInternalFilters} - /> - </Col> - )} - </Row> - <hr /> - <FilterInputs - internalFilters={internalFilters} - filters={filters} - updateInternalFilter={updateInternalFilter} - removeFilterAndApply={removeFilterAndApply} - filtersApplied={filtersApplied} - applyFilters={applyFilters} - /> - </> - )} - </> - )} - {useNewUIFilters && ( - <> - <Row> - <Col md={10}> - <h2>{t(title)}</h2> - </Col> - </Row> - <hr /> + <div className="superset-list-view-container"> + <div className={`superset-list-view ${className}`}> + <div className="header"> + {!useNewUIFilters && filterable && ( + <> + <Row> + <Col md={10} /> + <Col md={2}> + <FilterMenu + filters={filters} + internalFilters={internalFilters} + setInternalFilters={setInternalFilters} + /> + </Col> + </Row> + <hr /> + <FilterInputs + internalFilters={internalFilters} + filters={filters} + updateInternalFilter={updateInternalFilter} + removeFilterAndApply={removeFilterAndApply} + filtersApplied={filtersApplied} + applyFilters={applyFilters} + /> + </> + )} + {useNewUIFilters && filterable && ( <FilterControls filters={filters} internalFilters={internalFilters} updateFilterValue={applyFilterValue} /> - </> - )} - </div> - <div className="body"> - <TableCollection - getTableProps={getTableProps} - getTableBodyProps={getTableBodyProps} - prepareRow={prepareRow} - headerGroups={headerGroups} - rows={rows} - loading={loading} - /> - </div> - <div className="footer"> - <Row> - <Col md={2}> - <div className="form-actions-container"> - <div className="btn-group"> - {bulkActions.length > 0 && ( - <DropdownButton - id="bulk-actions" - bsSize="small" - bsStyle="default" - noCaret - title={ - <> - {t('Actions')} <span className="caret" /> - </> - } - > - {bulkActions.map(action => ( - // @ts-ignore - <MenuItem - key={action.key} - eventKey={selectedFlatRows} + )} + </div> + <div className="body"> + <TableCollection + getTableProps={getTableProps} + getTableBodyProps={getTableBodyProps} + prepareRow={prepareRow} + headerGroups={headerGroups} + rows={rows} + loading={loading} + /> + </div> + <div className="footer"> + <Row> + <Col> + <div className="form-actions-container"> + <div className="btn-group"> + {bulkActions.length > 0 && ( + <DropdownButton + id="bulk-actions" + bsSize="small" + bsStyle="default" + noCaret + title={ + <> + {t('Actions')} <span className="caret" /> + </> + } + > + {bulkActions.map(action => ( // @ts-ignore - onSelect={(selectedRows: typeof selectedFlatRows) => { - action.onSelect( - selectedRows.map((r: any) => r.original), - ); - }} - > - {action.name} - </MenuItem> - ))} - </DropdownButton> - )} + <MenuItem + key={action.key} + eventKey={selectedFlatRows} + // @ts-ignore + onSelect={(selectedRows: typeof selectedFlatRows) => { + action.onSelect( + selectedRows.map((r: any) => r.original), + ); + }} + > + {action.name} + </MenuItem> + ))} + </DropdownButton> + )} + </div> </div> - </div> - </Col> - <Col md={8} className="text-center"> - <Pagination - totalPages={pageCount || 0} - currentPage={pageCount ? pageIndex + 1 : 0} - onChange={(p: number) => gotoPage(p - 1)} - hideFirstAndLastPageLinks - /> - </Col> - <Col md={2}> - <span className="pull-right"> - {t('showing')}{' '} - <strong> - {pageSize * pageIndex + (rows.length && 1)}- - {pageSize * pageIndex + rows.length} - </strong>{' '} - {t('of')} <strong>{count}</strong> - </span> - </Col> - </Row> + </Col> + + <Col> + <span className="row-count-container"> + showing{' '} + <strong> + {pageSize * pageIndex + (rows.length && 1)}- + {pageSize * pageIndex + rows.length} + </strong>{' '} + of <strong>{count}</strong> + </span> + </Col> + </Row> + </div> </div> + <Pagination + totalPages={pageCount || 0} + currentPage={pageCount ? pageIndex + 1 : 0} + onChange={(p: number) => gotoPage(p - 1)} + hideFirstAndLastPageLinks + /> </div> ); }; diff --git a/superset-frontend/src/components/ListView/ListViewStyles.less b/superset-frontend/src/components/ListView/ListViewStyles.less index a5f3d87..f922f1b 100644 --- a/superset-frontend/src/components/ListView/ListViewStyles.less +++ b/superset-frontend/src/components/ListView/ListViewStyles.less @@ -19,92 +19,133 @@ @import '~stylesheets/less/variables.less'; -.superset-list-view { - .filter-dropdown { - margin-top: 20px; - } +.superset-list-view-container { + text-align: center; + + .superset-list-view { + text-align: left; + background-color: white; + border-radius: 4px 0; + margin: 0 16px; + padding-bottom: 48px; + + .body { + overflow: scroll; + } - .filter-column { - height: 30px; - padding: 5px; - font-size: 16px; - } + .filter-dropdown { + margin-top: 20px; + } - .filter-close { - height: 30px; - padding: 5px; + .filter-column { + height: 30px; + padding: 5px; + font-size: 16px; + } - i { - font-size: 20px; + .filter-close { + height: 30px; + padding: 5px; + + i { + font-size: 20px; + } } - } - .table-row-loader { - animation: shimmer 2s infinite; - background: linear-gradient( - to right, - #f6f7f8 0%, - #edeef1 20%, - #f6f7f8 40%, - #f6f7f8 100% - ); - background-size: 1000px 100%; - - span { - visibility: hidden; + .table-cell-loader { + position: relative; + + .loading-bar { + background-color: @brand-secondary-light4; + border-radius: 7px; + + span { + visibility: hidden; + } + } + + &:after { + position: absolute; + transform: translateY(-50%); + top: 50%; + left: 0; + content: ''; + display: block; + width: 100%; + height: 48px; + background-image: linear-gradient( + 100deg, + rgba(255, 255, 255, 0), + rgba(255, 255, 255, 0.5) 60%, + rgba(255, 255, 255, 0) 80% + ); + background-size: 200px 48px; + background-position: -100px 0; + background-repeat: no-repeat; + animation: loading-shimmer 1s infinite; + } } - } - .actions { - font-size: 20px; - white-space: nowrap; + .actions { + white-space: nowrap; + font-size: 24px; + min-width: 100px; - width: 100px; + svg, + i { + margin-right: 8px; - svg { - &:hover { - path { - fill: @primary-color; + &:hover { + path { + fill: @primary-color; + } } } } - } - .action-button { - margin: 0 8px; - } + .table-row { + &:hover { + background-color: @brand-secondary-light5; + } + } + + .table-row-selected { + background-color: @brand-secondary-light4; - .table-row { - &:hover { - background-color: @table-hover; + &:hover { + background-color: @brand-secondary-light4; + } } - } - .table-row-selected { - background-color: @table-selected; + .table-cell { + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + max-width: 300px; + } - &:hover { - background-color: @table-selected; + .sort-icon { + position: absolute; } - } - .table-cell { - max-width: 200px; - text-overflow: ellipsis; - overflow: hidden; - } + .form-actions-container { + position: absolute; + left: 28px; + } - .sort-icon { - position: absolute; + .row-count-container { + position: absolute; + right: 28px; + } } -} -@keyframes shimmer { - 0% { - background-position: -1000px 0; - } + @keyframes loading-shimmer { + 40% { + background-position: 100% 0; + } - 100% { - background-position: 1000px 0; + 100% { + background-position: 100% 0; + } } } diff --git a/superset-frontend/src/components/ListView/Pagination.tsx b/superset-frontend/src/components/ListView/Pagination.tsx index 03b8663..6d1d272 100644 --- a/superset-frontend/src/components/ListView/Pagination.tsx +++ b/superset-frontend/src/components/ListView/Pagination.tsx @@ -17,8 +17,7 @@ * under the License. */ import React from 'react'; -// @ts-ignore -import { Pagination } from 'react-bootstrap'; +import Pagination from 'src/components/Pagination'; import { createUltimatePagination, ITEM_TYPES, @@ -35,18 +34,14 @@ const ListViewPagination = createUltimatePagination({ [ITEM_TYPES.ELLIPSIS]: ({ isActive, onClick }) => ( <Pagination.Ellipsis disabled={isActive} onClick={onClick} /> ), - [ITEM_TYPES.FIRST_PAGE_LINK]: ({ isActive, onClick }) => ( - <Pagination.First disabled={isActive} onClick={onClick} /> - ), [ITEM_TYPES.PREVIOUS_PAGE_LINK]: ({ isActive, onClick }) => ( <Pagination.Prev disabled={isActive} onClick={onClick} /> ), [ITEM_TYPES.NEXT_PAGE_LINK]: ({ isActive, onClick }) => ( <Pagination.Next disabled={isActive} onClick={onClick} /> ), - [ITEM_TYPES.LAST_PAGE_LINK]: ({ isActive, onClick }) => ( - <Pagination.Last disabled={isActive} onClick={onClick} /> - ), + [ITEM_TYPES.FIRST_PAGE_LINK]: () => null, + [ITEM_TYPES.LAST_PAGE_LINK]: () => null, }, }); diff --git a/superset-frontend/src/components/ListView/TableCollection.tsx b/superset-frontend/src/components/ListView/TableCollection.tsx index cc8b8bc..b08114b 100644 --- a/superset-frontend/src/components/ListView/TableCollection.tsx +++ b/superset-frontend/src/components/ListView/TableCollection.tsx @@ -19,6 +19,7 @@ import React from 'react'; import cx from 'classnames'; import { TableInstance } from 'react-table'; +import styled from '@superset-ui/style'; import Icon from 'src/components/Icon'; interface Props { @@ -29,6 +30,56 @@ interface Props { rows: TableInstance['rows']; loading: boolean; } + +const Table = styled.table` + th { + &.xs { + min-width: 25px; + } + &.sm { + min-width: 50px; + } + &.md { + min-width: 75px; + } + &.lg { + min-width: 100px; + } + &.xl { + min-width: 150px; + } + &.xxl { + min-width: 200px; + } + + svg { + display: inline-block; + top: 6px; + position: relative; + } + } + td { + &.xs { + width: 25px; + } + &.sm { + width: 50px; + } + &.md { + width: 75px; + } + &.lg { + width: 100px; + } + &.xl { + width: 150px; + } + &.xxl { + width: 200px; + } + } +`; + export default function TableCollection({ getTableProps, getTableBodyProps, @@ -38,29 +89,31 @@ export default function TableCollection({ loading, }: Props) { return ( - <table {...getTableProps()} className="table table-hover"> + <Table {...getTableProps()} className="table table-hover"> <thead> {headerGroups.map(headerGroup => ( <tr {...headerGroup.getHeaderGroupProps()}> {headerGroup.headers.map(column => { let sortIcon = <Icon name="sort" />; - if (column.isSortedDesc) { + if (column.isSorted && column.isSortedDesc) { sortIcon = <Icon name="sort-desc" />; - } else if (!column.isSortedDesc) { + } else if (column.isSorted && !column.isSortedDesc) { sortIcon = <Icon name="sort-asc" />; } - return column.hidden ? null : ( <th {...column.getHeaderProps( column.sortable ? column.getSortByToggleProps() : {}, )} data-test="sort-header" + className={cx({ + [column.size || '']: column.size, + })} > - <span>{column.render('Header')}</span> - {column.sortable && ( - <span className="sort-icon">{sortIcon}</span> - )} + <span> + {column.render('Header')} + {column.sortable && sortIcon} + </span> </th> ); })} @@ -74,7 +127,6 @@ export default function TableCollection({ <tr {...row.getRowProps()} className={cx({ - 'table-row-loader': loading, 'table-row-selected': row.isSelected, })} onMouseEnter={() => row.setState && row.setState({ hover: true })} @@ -86,14 +138,18 @@ export default function TableCollection({ if (cell.column.hidden) return null; const columnCellProps = cell.column.cellProps || {}; - return ( <td - className="table-cell" + className={cx('table-cell', { + 'table-cell-loader': loading, + [cell.column.size || '']: cell.column.size, + })} {...cell.getCellProps()} {...columnCellProps} > - <span>{cell.render('Cell')}</span> + <span className={cx({ 'loading-bar': loading })}> + <span>{cell.render('Cell')}</span> + </span> </td> ); })} @@ -101,6 +157,6 @@ export default function TableCollection({ ); })} </tbody> - </table> + </Table> ); } diff --git a/superset-frontend/src/components/Menu/SubMenu.tsx b/superset-frontend/src/components/Menu/SubMenu.tsx index f0fa2c1..f565af0 100644 --- a/superset-frontend/src/components/Menu/SubMenu.tsx +++ b/superset-frontend/src/components/Menu/SubMenu.tsx @@ -64,11 +64,10 @@ const StyledHeader = styled.header` `; interface SubMenuProps { - createButton: { name: string; url: string | null }; - canCreate: boolean; - label: string; + createButton?: { name: string; url: string | null }; + canCreate?: boolean; name: string; - childs: Array<{ label: string; name: string; url: string }>; + childs?: Array<{ label: string; name: string; url: string }>; } interface SubMenuState { @@ -78,7 +77,10 @@ interface SubMenuState { class SubMenu extends React.PureComponent<SubMenuProps, SubMenuState> { state: SubMenuState = { - selectedMenu: this.props.childs[0] && this.props.childs[0].label, + selectedMenu: + this.props.childs && this.props.childs[0] + ? this.props.childs[0].label + : '', isModalOpen: false, }; @@ -99,7 +101,7 @@ class SubMenu extends React.PureComponent<SubMenuProps, SubMenuState> { <StyledHeader> <Navbar inverse fluid role="navigation"> <Navbar.Header> - <Navbar.Brand>{this.props.label}</Navbar.Brand> + <Navbar.Brand>{this.props.name}</Navbar.Brand> </Navbar.Header> <DatasetModal show={this.state.isModalOpen} onHide={this.onClose} /> <Nav> @@ -116,7 +118,7 @@ class SubMenu extends React.PureComponent<SubMenuProps, SubMenuState> { </MenuItem> ))} </Nav> - {this.props.canCreate && ( + {this.props.canCreate && this.props.createButton && ( <Nav className="navbar-right"> <Button onClick={this.onOpen}> <i className="fa fa-plus" /> {this.props.createButton.name} diff --git a/superset-frontend/src/components/Pagination.tsx b/superset-frontend/src/components/Pagination.tsx new file mode 100644 index 0000000..a023f09 --- /dev/null +++ b/superset-frontend/src/components/Pagination.tsx @@ -0,0 +1,132 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import React, { PureComponent } from 'react'; +import cx from 'classnames'; +import styled from '@superset-ui/style'; + +interface PaginationButton { + disabled?: boolean; + onClick: React.EventHandler<React.SyntheticEvent<HTMLElement>>; +} + +interface PaginationItemButton extends PaginationButton { + active: boolean; + children: React.ReactNode; +} + +function Prev({ disabled, onClick }: PaginationButton) { + return ( + <li className={cx({ disabled })}> + <span role="button" tabIndex={disabled ? -1 : 0} onClick={onClick}> + « + </span> + </li> + ); +} + +function Next({ disabled, onClick }: PaginationButton) { + return ( + <li className={cx({ disabled })}> + <span role="button" tabIndex={disabled ? -1 : 0} onClick={onClick}> + » + </span> + </li> + ); +} + +function Item({ active, children, onClick }: PaginationItemButton) { + return ( + <li className={cx({ active })}> + <span role="button" tabIndex={active ? -1 : 0} onClick={onClick}> + {children} + </span> + </li> + ); +} + +function Ellipsis({ disabled, onClick }: PaginationButton) { + return ( + <li className={cx({ disabled })}> + <span role="button" tabIndex={disabled ? -1 : 0} onClick={onClick}> + … + </span> + </li> + ); +} + +interface PaginationProps { + children: React.ReactNode; +} + +const PaginationList = styled.ul` + display: inline-block; + margin: 16px 0; + + li { + display: inline; + margin: 0 4px; + + span { + padding: 8px 12px; + text-decoration: none; + background-color: ${({ theme }) => theme.colors.grayscale.light5}; + border-radius: ${({ theme }) => theme.borderRadius}px; + + &:hover, + &:focus { + z-index: 2; + color: ${({ theme }) => theme.colors.grayscale.dark1}; + background-color: ${({ theme }) => theme.colors.grayscale.light3}; + } + } + + &.disabled { + span { + background-color: transparent; + cursor: default; + + &:focus { + outline: none; + } + } + } + &.active { + span { + z-index: 3; + color: ${({ theme }) => theme.colors.grayscale.light5}; + cursor: default; + background-color: ${({ theme }) => theme.colors.primary.base}; + + &:focus { + outline: none; + } + } + } + } +`; + +export default class Pagination extends PureComponent<PaginationProps> { + static Next = Next; + static Prev = Prev; + static Item = Item; + static Ellipsis = Ellipsis; + render() { + return <PaginationList> {this.props.children}</PaginationList>; + } +} diff --git a/superset-frontend/src/types/react-table-config.d.ts b/superset-frontend/src/types/react-table-config.d.ts index 5c2321a..63e9d30 100644 --- a/superset-frontend/src/types/react-table-config.d.ts +++ b/superset-frontend/src/types/react-table-config.d.ts @@ -64,8 +64,10 @@ import { UseSortByOptions, UseSortByState, } from 'react-table'; +import { ColumnSizer } from 'react-virtualized'; declare module 'react-table' { + type ColumnSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl'; export interface TableOptions<D extends object> extends UseExpandedOptions<D>, UseFiltersOptions<D>, @@ -118,13 +120,19 @@ declare module 'react-table' { hidden?: boolean; sortable?: boolean; cellProps?: any; + size?: ColumnSize; } export interface ColumnInstance<D extends object = {}> extends UseFiltersColumnProps<D>, UseGroupByColumnProps<D>, UseResizeColumnsColumnProps<D>, - UseSortByColumnProps<D> {} + UseSortByColumnProps<D> { + hidden?: boolean; + sortable?: boolean; + cellProps?: any; + size?: ColumnSize; + } export interface Cell<D extends object = {}> extends UseGroupByCellProps<D>, diff --git a/superset-frontend/src/utils/common.js b/superset-frontend/src/utils/common.js index 84ae9a1..bbde35d 100644 --- a/superset-frontend/src/utils/common.js +++ b/superset-frontend/src/utils/common.js @@ -23,6 +23,10 @@ import getClientErrorObject from './getClientErrorObject'; export const NULL_STRING = '<NULL>'; +// moment time format strings +export const SHORT_DATE = 'MMM D, YYYY'; +export const SHORT_TIME = 'h:m a'; + export function getParamFromQuery(query, param) { const vars = query.split('&'); for (let i = 0; i < vars.length; i += 1) { diff --git a/superset-frontend/src/views/chartList/ChartList.tsx b/superset-frontend/src/views/chartList/ChartList.tsx index 6ff814f..35bc452 100644 --- a/superset-frontend/src/views/chartList/ChartList.tsx +++ b/superset-frontend/src/views/chartList/ChartList.tsx @@ -26,6 +26,7 @@ import rison from 'rison'; // @ts-ignore import { Panel } from 'react-bootstrap'; import ConfirmStatusChange from 'src/components/ConfirmStatusChange'; +import SubMenu from 'src/components/Menu/SubMenu'; import ListView from 'src/components/ListView/ListView'; import { FetchDataConfig, @@ -68,7 +69,7 @@ class ChartList extends React.PureComponent<Props, State> { filterOperators: {}, filters: [], lastFetchDataConfig: null, - loading: false, + loading: true, permissions: [], sliceCurrentlyEditing: null, }; @@ -223,7 +224,7 @@ class ChartList extends React.PureComponent<Props, State> { </span> ); }, - Header: 'Actions', + Header: t('Actions'), id: 'actions', }, ]; @@ -517,58 +518,54 @@ class ChartList extends React.PureComponent<Props, State> { sliceCurrentlyEditing, } = this.state; return ( - <div className="container welcome"> - <Panel> - <Panel.Body> - {sliceCurrentlyEditing && ( - <PropertiesModal - show - onHide={this.closeChartEditModal} - onSave={this.handleChartUpdated} - slice={sliceCurrentlyEditing} + <> + <SubMenu name={t('Charts')} /> + {sliceCurrentlyEditing && ( + <PropertiesModal + show + onHide={this.closeChartEditModal} + onSave={this.handleChartUpdated} + slice={sliceCurrentlyEditing} + /> + )} + <ConfirmStatusChange + title={t('Please confirm')} + description={t( + 'Are you sure you want to delete the selected charts?', + )} + onConfirm={this.handleBulkChartDelete} + > + {confirmDelete => { + const bulkActions = []; + if (this.canDelete) { + bulkActions.push({ + key: 'delete', + name: ( + <> + <i className="fa fa-trash" /> {t('Delete')} + </> + ), + onSelect: confirmDelete, + }); + } + return ( + <ListView + className="chart-list-view" + columns={this.columns} + data={charts} + count={chartCount} + pageSize={PAGE_SIZE} + fetchData={this.fetchData} + loading={loading} + initialSort={this.initialSort} + filters={filters} + bulkActions={bulkActions} + useNewUIFilters={this.isNewUIEnabled} /> - )} - <ConfirmStatusChange - title={t('Please confirm')} - description={t( - 'Are you sure you want to delete the selected charts?', - )} - onConfirm={this.handleBulkChartDelete} - > - {confirmDelete => { - const bulkActions = []; - if (this.canDelete) { - bulkActions.push({ - key: 'delete', - name: ( - <> - <i className="fa fa-trash" /> Delete - </> - ), - onSelect: confirmDelete, - }); - } - return ( - <ListView - className="chart-list-view" - title={'Charts'} - columns={this.columns} - data={charts} - count={chartCount} - pageSize={PAGE_SIZE} - fetchData={this.fetchData} - loading={loading} - initialSort={this.initialSort} - filters={filters} - bulkActions={bulkActions} - useNewUIFilters={this.isNewUIEnabled} - /> - ); - }} - </ConfirmStatusChange> - </Panel.Body> - </Panel> - </div> + ); + }} + </ConfirmStatusChange> + </> ); } } diff --git a/superset-frontend/src/views/dashboardList/DashboardList.tsx b/superset-frontend/src/views/dashboardList/DashboardList.tsx index 0fba528..50e8bc9 100644 --- a/superset-frontend/src/views/dashboardList/DashboardList.tsx +++ b/superset-frontend/src/views/dashboardList/DashboardList.tsx @@ -25,6 +25,7 @@ import rison from 'rison'; // @ts-ignore import { Panel } from 'react-bootstrap'; import ConfirmStatusChange from 'src/components/ConfirmStatusChange'; +import SubMenu from 'src/components/Menu/SubMenu'; import ListView from 'src/components/ListView/ListView'; import ExpandableList from 'src/components/ExpandableList'; import { @@ -76,7 +77,7 @@ class DashboardList extends React.PureComponent<Props, State> { filterOperators: {}, filters: [], lastFetchDataConfig: null, - loading: false, + loading: true, permissions: [], dashboardToEdit: null, }; @@ -508,71 +509,67 @@ class DashboardList extends React.PureComponent<Props, State> { dashboardToEdit, } = this.state; return ( - <div className="container welcome"> - <Panel> - <Panel.Body> - <ConfirmStatusChange - title={t('Please confirm')} - description={t( - 'Are you sure you want to delete the selected dashboards?', - )} - onConfirm={this.handleBulkDashboardDelete} - > - {confirmDelete => { - const bulkActions = []; - if (this.canDelete) { - bulkActions.push({ - key: 'delete', - name: ( - <> - <i className="fa fa-trash" /> Delete - </> - ), - onSelect: confirmDelete, - }); - } - if (this.canExport) { - bulkActions.push({ - key: 'export', - name: ( - <> - <i className="fa fa-database" /> Export - </> - ), - onSelect: this.handleBulkDashboardExport, - }); - } - return ( + <> + <SubMenu name={t('Dashboards')} /> + <ConfirmStatusChange + title={t('Please confirm')} + description={t( + 'Are you sure you want to delete the selected dashboards?', + )} + onConfirm={this.handleBulkDashboardDelete} + > + {confirmDelete => { + const bulkActions = []; + if (this.canDelete) { + bulkActions.push({ + key: 'delete', + name: ( + <> + <i className="fa fa-trash" /> {t('Delete')} + </> + ), + onSelect: confirmDelete, + }); + } + if (this.canExport) { + bulkActions.push({ + key: 'export', + name: ( <> - {dashboardToEdit && ( - <PropertiesModal - show - dashboardId={dashboardToEdit.id} - onHide={() => this.setState({ dashboardToEdit: null })} - onDashboardSave={this.handleDashboardEdit} - /> - )} - <ListView - className="dashboard-list-view" - title={'Dashboards'} - columns={this.columns} - data={dashboards} - count={dashboardCount} - pageSize={PAGE_SIZE} - fetchData={this.fetchData} - loading={loading} - initialSort={this.initialSort} - filters={filters} - bulkActions={bulkActions} - useNewUIFilters={this.isNewUIEnabled} - /> + <i className="fa fa-database" /> {t('Export')} </> - ); - }} - </ConfirmStatusChange> - </Panel.Body> - </Panel> - </div> + ), + onSelect: this.handleBulkDashboardExport, + }); + } + return ( + <> + {dashboardToEdit && ( + <PropertiesModal + show + dashboardId={dashboardToEdit.id} + onHide={() => this.setState({ dashboardToEdit: null })} + onDashboardSave={this.handleDashboardEdit} + /> + )} + <ListView + className="dashboard-list-view" + columns={this.columns} + data={dashboards} + count={dashboardCount} + pageSize={PAGE_SIZE} + fetchData={this.fetchData} + loading={loading} + initialSort={this.initialSort} + filters={filters} + bulkActions={bulkActions} + useNewUIFilters={this.isNewUIEnabled} + /> + </> + ); + }} + </ConfirmStatusChange> + </> ); } } diff --git a/superset-frontend/src/views/datasetList/DatasetList.tsx b/superset-frontend/src/views/datasetList/DatasetList.tsx index ae0c5a3..a4ee407 100644 --- a/superset-frontend/src/views/datasetList/DatasetList.tsx +++ b/superset-frontend/src/views/datasetList/DatasetList.tsx @@ -24,6 +24,7 @@ import React from 'react'; import rison from 'rison'; // @ts-ignore import { Panel } from 'react-bootstrap'; +import { SHORT_DATE, SHORT_TIME } from 'src/utils/common'; import ConfirmStatusChange from 'src/components/ConfirmStatusChange'; import ListView from 'src/components/ListView/ListView'; import SubMenu from 'src/components/Menu/SubMenu'; @@ -87,7 +88,7 @@ class DatasetList extends React.PureComponent<Props, State> { filterOperators: {}, filters: [], lastFetchDataConfig: null, - loading: false, + loading: true, owners: [], databases: [], permissions: [], @@ -175,6 +176,7 @@ class DatasetList extends React.PureComponent<Props, State> { ); }, accessor: 'kind_icon', + size: 'xs', }, { Cell: ({ @@ -184,6 +186,7 @@ class DatasetList extends React.PureComponent<Props, State> { }: any) => datasetTitle, Header: t('Name'), accessor: 'table_name', + sortable: true, }, { Cell: ({ @@ -193,36 +196,51 @@ class DatasetList extends React.PureComponent<Props, State> { }: any) => kind[0]?.toUpperCase() + kind.slice(1), Header: t('Type'), accessor: 'kind', + size: 'md', }, { Header: t('Source'), accessor: 'database_name', + size: 'lg', }, { Header: t('Schema'), accessor: 'schema', + size: 'lg', }, { Cell: ({ row: { original: { changed_on: changedOn }, }, - }: any) => <span className="no-wrap">{moment(changedOn).fromNow()}</span>, + }: any) => { + const momentTime = moment(changedOn); + const time = momentTime.format(SHORT_DATE); + const date = momentTime.format(SHORT_TIME); + return ( + <TooltipWrapper + label="last-modified" + tooltip={time} + placement="right" + > + <span>{date}</span> + </TooltipWrapper> + ); + }, Header: t('Last Modified'), accessor: 'changed_on', sortable: true, + size: 'xl', }, { Cell: ({ row: { - original: { - changed_by_name: changedByName, - changed_by_url: changedByUrl, - }, + original: { changed_by_name: changedByName }, }, - }: any) => <a href={changedByUrl}>{changedByName}</a>, + }: any) => changedByName, Header: t('Modified By'), accessor: 'changed_by_fk', + size: 'xl', }, { accessor: 'database', @@ -241,16 +259,19 @@ class DatasetList extends React.PureComponent<Props, State> { .slice(0, 5) .map((owner: Owner) => ( <AvatarIcon + key={owner.id} tableName={tableName} firstName={owner.first_name} lastName={owner.last_name} userName={owner.username} - iconSize="20" + iconSize={24} + textSize={9} /> )); }, Header: t('Owners'), id: 'owners', + size: 'lg', }, { accessor: 'is_sqllab_view', @@ -267,14 +288,20 @@ class DatasetList extends React.PureComponent<Props, State> { <span className={`actions ${state && state.hover ? '' : 'invisible'}`} > - <a - role="button" - tabIndex={0} - className="action-button" - href={original.explore_url} + <TooltipWrapper + label="explore-action" + tooltip={t('Explore')} + placement="bottom" > - <Icon name="compass" /> - </a> + <a + role="button" + tabIndex={0} + className="action-button" + href={original.explore_url} + > + <Icon name="compass" /> + </a> + </TooltipWrapper> {this.canDelete && ( <ConfirmStatusChange title={t('Please Confirm')} @@ -287,26 +314,38 @@ class DatasetList extends React.PureComponent<Props, State> { onConfirm={handleDelete} > {confirmDelete => ( - <span - role="button" - tabIndex={0} - className="action-button" - onClick={confirmDelete} + <TooltipWrapper + label="delete-action" + tooltip={t('Delete')} + placement="bottom" > - <Icon name="trash" /> - </span> + <span + role="button" + tabIndex={0} + className="action-button" + onClick={confirmDelete} + > + <Icon name="trash" /> + </span> + </TooltipWrapper> )} </ConfirmStatusChange> )} {this.canEdit && ( - <span - role="button" - tabIndex={0} - className="action-button" - onClick={handleEdit} + <TooltipWrapper + label="edit-action" + tooltip={t('Edit')} + placement="bottom" > - <Icon name="pencil" /> - </span> + <span + role="button" + tabIndex={0} + className="action-button" + onClick={handleEdit} + > + <Icon name="pencil" /> + </span> + </TooltipWrapper> )} </span> ); @@ -317,8 +356,7 @@ class DatasetList extends React.PureComponent<Props, State> { ]; menu = { - label: 'Data', - name: 'Data', + name: t('Data'), createButton: { name: t('Dataset'), url: '/tablemodelview/add', @@ -487,49 +525,42 @@ class DatasetList extends React.PureComponent<Props, State> { return ( <> <SubMenu {...this.menu} canCreate={this.canCreate} /> - <div className="container welcome"> - <Panel> - <Panel.Body> - <ConfirmStatusChange - title={t('Please confirm')} - description={t( - 'Are you sure you want to delete the selected datasets?', - )} - onConfirm={this.handleBulkDatasetDelete} - > - {confirmDelete => { - const bulkActions = []; - if (this.canDelete) { - bulkActions.push({ - key: 'delete', - name: ( - <> - <i className="fa fa-trash" /> Delete - </> - ), - onSelect: confirmDelete, - }); - } - return ( - <ListView - className="dataset-list-view" - title={'Datasets'} - columns={this.columns} - data={datasets} - count={datasetCount} - pageSize={PAGE_SIZE} - fetchData={this.fetchData} - loading={loading} - initialSort={this.initialSort} - filters={filters} - bulkActions={bulkActions} - /> - ); - }} - </ConfirmStatusChange> - </Panel.Body> - </Panel> - </div> + <ConfirmStatusChange + title={t('Please confirm')} + description={t( + 'Are you sure you want to delete the selected datasets?', + )} + onConfirm={this.handleBulkDatasetDelete} + > + {confirmDelete => { + const bulkActions = []; + if (this.canDelete) { + bulkActions.push({ + key: 'delete', + name: ( + <> + <i className="fa fa-trash" /> {t('Delete')} + </> + ), + onSelect: confirmDelete, + }); + } + return ( + <ListView + className="dataset-list-view" + columns={this.columns} + data={datasets} + count={datasetCount} + pageSize={PAGE_SIZE} + fetchData={this.fetchData} + loading={loading} + initialSort={this.initialSort} + filters={filters} + bulkActions={bulkActions} + /> + ); + }} + </ConfirmStatusChange> </> ); } diff --git a/superset-frontend/stylesheets/less/variables.less b/superset-frontend/stylesheets/less/variables.less index f22db91..5f01c94 100644 --- a/superset-frontend/stylesheets/less/variables.less +++ b/superset-frontend/stylesheets/less/variables.less @@ -209,8 +209,4 @@ /* in favor of custom/reusable CSS wherever possible */ /************************************************************************/ -// ***************************** SIP 34 UI ******************************* -@table-hover: rgba(236, 238, 242, 0.5); -@table-selected: #eceef2; - @import '../less/cosmo/variables.less';