style: listviews closer to SIP-34 (#10094)

This commit is contained in:
ʈᵃᵢ 2020-06-23 14:17:28 -07:00 committed by GitHub
parent 4d1d40989c
commit be936c2eb8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 687 additions and 417 deletions

View File

@ -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';

View File

@ -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', () => {

View File

@ -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', () => {

View File

@ -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', () => {

View File

@ -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>
);
}

View File

@ -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>
);
};

View File

@ -19,92 +19,133 @@
@import '~stylesheets/less/variables.less';
.superset-list-view {
.filter-dropdown {
margin-top: 20px;
}
.superset-list-view-container {
text-align: center;
.filter-column {
height: 30px;
padding: 5px;
font-size: 16px;
}
.superset-list-view {
text-align: left;
background-color: white;
border-radius: 4px 0;
margin: 0 16px;
padding-bottom: 48px;
.filter-close {
height: 30px;
padding: 5px;
i {
font-size: 20px;
.body {
overflow: scroll;
}
}
.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;
.filter-dropdown {
margin-top: 20px;
}
}
.actions {
font-size: 20px;
white-space: nowrap;
.filter-column {
height: 30px;
padding: 5px;
font-size: 16px;
}
width: 100px;
.filter-close {
height: 30px;
padding: 5px;
svg {
&:hover {
path {
fill: @primary-color;
i {
font-size: 20px;
}
}
.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 {
white-space: nowrap;
font-size: 24px;
min-width: 100px;
svg,
i {
margin-right: 8px;
&:hover {
path {
fill: @primary-color;
}
}
}
}
}
.action-button {
margin: 0 8px;
}
.table-row {
&:hover {
background-color: @brand-secondary-light5;
}
}
.table-row {
&:hover {
background-color: @table-hover;
.table-row-selected {
background-color: @brand-secondary-light4;
&:hover {
background-color: @brand-secondary-light4;
}
}
.table-cell {
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
max-width: 300px;
}
.sort-icon {
position: absolute;
}
.form-actions-container {
position: absolute;
left: 28px;
}
.row-count-container {
position: absolute;
right: 28px;
}
}
.table-row-selected {
background-color: @table-selected;
@keyframes loading-shimmer {
40% {
background-position: 100% 0;
}
&:hover {
background-color: @table-selected;
100% {
background-position: 100% 0;
}
}
.table-cell {
max-width: 200px;
text-overflow: ellipsis;
overflow: hidden;
}
.sort-icon {
position: absolute;
}
}
@keyframes shimmer {
0% {
background-position: -1000px 0;
}
100% {
background-position: 1000px 0;
}
}

View File

@ -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,
},
});

View File

@ -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>
);
}

View File

@ -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}

View File

@ -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>;
}
}

View File

@ -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>,

View File

@ -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) {

View File

@ -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>
</>
);
}
}

View File

@ -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: (
<>
{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-trash" /> {t('Delete')}
</>
);
}}
</ConfirmStatusChange>
</Panel.Body>
</Panel>
</div>
),
onSelect: confirmDelete,
});
}
if (this.canExport) {
bulkActions.push({
key: 'export',
name: (
<>
<i className="fa fa-database" /> {t('Export')}
</>
),
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>
</>
);
}
}

View File

@ -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>
</>
);
}

View File

@ -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';