You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by ta...@apache.org on 2020/06/23 21:17:47 UTC
[incubator-superset] branch master updated: style: listviews closer
to SIP-34 (#10094)
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: ʈᵃᵢ <td...@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';