Use Skeleton as ItemsList loading state (#5079)

This commit is contained in:
Gabriel Dutra
2020-08-19 09:36:11 -03:00
committed by GitHub
parent fc71acdc09
commit a596d6558c
8 changed files with 199 additions and 177 deletions

View File

@@ -31,6 +31,7 @@
@import "~antd/lib/badge/style/index";
@import "~antd/lib/card/style/index";
@import "~antd/lib/spin/style/index";
@import "~antd/lib/skeleton/style/index";
@import "~antd/lib/tabs/style/index";
@import "~antd/lib/notification/style/index";
@import "~antd/lib/collapse/style/index";

View File

@@ -1,149 +1,153 @@
.table {
margin-bottom: 0;
th.sortable-column {
cursor: pointer;
margin-bottom: 0;
th.sortable-column {
cursor: pointer;
}
&:not(.table-striped) > thead > tr > th {
background-color: #fafafa;
}
[class*="bg-"] {
& > tr > th {
color: #fff;
border-bottom: 0;
background: transparent !important;
}
&:not(.table-striped) > thead > tr > th {
background-color: #FAFAFA;
& + tbody > tr:first-child > td {
border-top: 0;
}
[class*="bg-"] {
& > tr > th {
color: #fff;
border-bottom: 0;
background: transparent !important;
}
& + tbody > tr:first-child > td {
border-top: 0;
}
}
& > thead > tr > th {
vertical-align: middle;
font-weight: 500;
color: #333;
border-width: 1px;
text-transform: uppercase;
padding: 15px 10px;
}
& > thead > tr,
& > tbody > tr,
& > tfoot > tr {
& > th, & > td {
&:first-child {
padding-left: 30px;
}
&:last-child {
padding-right: 30px;
}
}
}
tbody > tr:last-child > td {
padding-bottom: 20px;
}
& > thead > tr > th {
vertical-align: middle;
font-weight: 500;
color: #333;
border-width: 1px;
text-transform: uppercase;
padding: 15px 10px;
}
& > thead > tr,
& > tbody > tr,
& > tfoot > tr {
& > th,
& > td {
&:first-child {
padding-left: 30px;
}
&:last-child {
padding-right: 30px;
}
}
}
tbody > tr:last-child > td {
padding-bottom: 20px;
}
}
.table-bordered {
border: 0;
& > tbody > tr {
& > td, & > th {
border-bottom: 0;
border-left: 0;
&:last-child {
border-right: 0;
}
}
border: 0;
& > tbody > tr {
& > td,
& > th {
border-bottom: 0;
border-left: 0;
&:last-child {
border-right: 0;
}
}
& > thead > tr > th {
border-left: 0;
&:last-child {
border-right: 0;
}
}
& > thead > tr > th {
border-left: 0;
&:last-child {
border-right: 0;
}
}
}
.table-vmiddle {
td {
vertical-align: middle !important;
}
td {
vertical-align: middle !important;
}
}
.table-responsive {
border: 0;
border: 0;
}
.tile .table {
& > thead:not([class*="bg-"]) > tr > th {
border-top: 1px solid @table-border-color;
}
.tile .table {
& > thead:not([class*="bg-"]) > tr > th {
border-top: 1px solid @table-border-color;
}
}
.table-hover > tbody > tr:hover {
background-color: #f4f4f4;
background-color: #f4f4f4;
}
.table-data {
tbody > tr > td {
padding-top: 5px !important;
}
.btn-favourite, .btn-archive {
font-size: 15px;
}
thead > tr > th {
white-space: nowrap;
}
tbody > tr > td {
padding-top: 5px !important;
}
.btn-favourite,
.btn-archive {
font-size: 15px;
}
}
.table-main-title {
font-weight: 500;
line-height: 1.7 !important;
font-weight: 500;
line-height: 1.7 !important;
}
.btn-favourite {
color: #d4d4d4;
transition: all .25s ease-in-out;
&:hover, &:focus {
color: @yellow-darker;
cursor: pointer;
}
.fa-star {
color: @yellow-darker;
}
color: #d4d4d4;
transition: all 0.25s ease-in-out;
&:hover,
&:focus {
color: @yellow-darker;
cursor: pointer;
}
.fa-star {
color: @yellow-darker;
}
}
.btn-archive {
color: #d4d4d4;
transition: all .25s ease-in-out;
&:hover, &:focus {
color: @gray-light;
}
.fa-archive {
color: @gray-light;
}
color: #d4d4d4;
transition: all 0.25s ease-in-out;
&:hover,
&:focus {
color: @gray-light;
}
.fa-archive {
color: @gray-light;
}
}
.table > thead > tr > th {
text-transform: none;
text-transform: none;
}
.table-data .label-tag {
display: inline-block;
max-width: 135px;
}
display: inline-block;
max-width: 135px;
}

View File

@@ -110,9 +110,9 @@ export function wrap(WrappedComponent, createItemsSource, createStateStorage) {
isLoaded,
isEmpty: !isLoaded || totalCount === 0,
totalItemsCount: isLoaded ? totalCount : 0,
totalItemsCount: totalCount,
pageSizeOptions: clientConfig.pageSizeOptions,
pageItems: isLoaded ? pageItems : [],
pageItems: pageItems || [],
};
}

View File

@@ -41,18 +41,24 @@ export class ItemsSource {
extend(customParams, params);
},
};
return this._beforeUpdate().then(() =>
this._fetcher
return this._beforeUpdate().then(() => {
const fetchToken = Math.random()
.toString(36)
.substr(2);
this._currentFetchToken = fetchToken;
return this._fetcher
.fetch(changes, state, context)
.then(({ results, count, allResults }) => {
this._pageItems = results;
this._allItems = allResults || null;
this._paginator.setTotalCount(count);
this._params = { ...this._params, ...customParams };
return this._afterUpdate();
if (this._currentFetchToken === fetchToken) {
this._pageItems = results;
this._allItems = allResults || null;
this._paginator.setTotalCount(count);
this._params = { ...this._params, ...customParams };
return this._afterUpdate();
}
})
.catch(error => this.handleError(error))
);
.catch(error => this.handleError(error));
});
}
constructor({ getRequest, doRequest, processResults, isPlainList = false, ...defaultState }) {

View File

@@ -1,8 +1,9 @@
import { isFunction, map, filter, extend, omit, identity } from "lodash";
import { isFunction, map, filter, extend, omit, identity, range, isEmpty } from "lodash";
import React from "react";
import PropTypes from "prop-types";
import classNames from "classnames";
import Table from "antd/lib/table";
import Skeleton from "antd/lib/skeleton";
import FavoritesControl from "@/components/FavoritesControl";
import TimeAgo from "@/components/TimeAgo";
import { durationHumanize, formatDate, formatDateTime } from "@/lib/utils";
@@ -151,8 +152,10 @@ export default class ItemsTable extends React.Component {
}
render() {
const columns = this.prepareColumns();
const rows = map(this.props.items, (item, index) => ({ key: "row" + index, item }));
const tableDataProps = {
columns: this.prepareColumns(),
dataSource: map(this.props.items, (item, index) => ({ key: "row" + index, item })),
};
// Bind events only if `onRowClick` specified
const onTableRow = isFunction(this.props.onRowClick)
@@ -164,17 +167,27 @@ export default class ItemsTable extends React.Component {
: null;
const { showHeader } = this.props;
if (this.props.loading) {
if (isEmpty(tableDataProps.dataSource)) {
tableDataProps.columns = tableDataProps.columns.map(column => ({
...column,
sorter: false,
render: () => <Skeleton active paragraph={false} />,
}));
tableDataProps.dataSource = range(10).map(key => ({ key: `${key}` }));
} else {
tableDataProps.loading = { indicator: null };
}
}
return (
<Table
className={classNames("table-data", { "ant-table-headerless": !showHeader })}
loading={this.props.loading}
columns={columns}
showHeader={showHeader}
dataSource={rows}
rowKey={row => row.key}
pagination={false}
onRow={onTableRow}
{...tableDataProps}
/>
);
}

View File

@@ -9,7 +9,6 @@ import { wrap as itemsList, ControllerType } from "@/components/items-list/Items
import { ResourceItemsSource } from "@/components/items-list/classes/ItemsSource";
import { StateStorage } from "@/components/items-list/classes/StateStorage";
import LoadingState from "@/components/items-list/components/LoadingState";
import ItemsTable, { Columns } from "@/components/items-list/components/ItemsTable";
import Alert from "@/services/alert";
@@ -49,7 +48,7 @@ class AlertsList extends React.Component {
field: "name",
}
),
Columns.custom((text, item) => item.user.name, { title: "Created By" }),
Columns.custom((text, item) => item.user.name, { title: "Created By", width: "1%" }),
Columns.custom.sortable(
(text, alert) => (
<div>
@@ -60,10 +59,11 @@ class AlertsList extends React.Component {
title: "State",
field: "state",
width: "1%",
className: "text-nowrap",
}
),
Columns.timeAgo.sortable({ title: "Last Updated At", field: "updated_at", className: "text-nowrap", width: "1%" }),
Columns.dateTime.sortable({ title: "Created At", field: "created_at", className: "text-nowrap", width: "1%" }),
Columns.timeAgo.sortable({ title: "Last Updated At", field: "updated_at", width: "1%" }),
Columns.dateTime.sortable({ title: "Created At", field: "created_at", width: "1%" }),
];
render() {
@@ -84,8 +84,7 @@ class AlertsList extends React.Component {
}
/>
<div>
{!controller.isLoaded && <LoadingState className="" />}
{controller.isLoaded && controller.isEmpty && (
{controller.isLoaded && controller.isEmpty ? (
<EmptyState
icon="fa fa-bell-o"
illustration="alert"
@@ -93,10 +92,10 @@ class AlertsList extends React.Component {
helpLink="https://redash.io/help/user-guide/alerts/"
showAlertStep
/>
)}
{controller.isLoaded && !controller.isEmpty && (
) : (
<div className="table-responsive bg-white tiled">
<ItemsTable
loading={!controller.isLoaded}
items={controller.pageItems}
columns={this.listColumns}
orderByField={controller.orderByField}

View File

@@ -8,7 +8,6 @@ import { DashboardTagsControl } from "@/components/tags-control/TagsControl";
import { wrap as itemsList, ControllerType } from "@/components/items-list/ItemsList";
import { ResourceItemsSource } from "@/components/items-list/classes/ItemsSource";
import { UrlStateStorage } from "@/components/items-list/classes/StateStorage";
import LoadingState from "@/components/items-list/components/LoadingState";
import * as Sidebar from "@/components/items-list/components/Sidebar";
import ItemsTable, { Columns } from "@/components/items-list/components/ItemsTable";
import CreateDashboardDialog from "@/components/dashboards/CreateDashboardDialog";
@@ -63,11 +62,10 @@ class DashboardList extends React.Component {
width: null,
}
),
Columns.custom((text, item) => item.user.name, { title: "Created By" }),
Columns.custom((text, item) => item.user.name, { title: "Created By", width: "1%" }),
Columns.dateTime.sortable({
title: "Created At",
field: "created_at",
className: "text-nowrap",
width: "1%",
}),
];
@@ -99,37 +97,34 @@ class DashboardList extends React.Component {
<Sidebar.Tags url="api/dashboards/tags" onChange={controller.updateSelectedTags} />
</Layout.Sidebar>
<Layout.Content>
{controller.isLoaded ? (
<div data-test="DashboardLayoutContent">
{controller.isEmpty ? (
<DashboardListEmptyState
page={controller.params.currentPage}
searchTerm={controller.searchTerm}
selectedTags={controller.selectedTags}
<div data-test="DashboardLayoutContent">
{controller.isLoaded && controller.isEmpty ? (
<DashboardListEmptyState
page={controller.params.currentPage}
searchTerm={controller.searchTerm}
selectedTags={controller.selectedTags}
/>
) : (
<div className="bg-white tiled table-responsive">
<ItemsTable
items={controller.pageItems}
loading={!controller.isLoaded}
columns={this.listColumns}
orderByField={controller.orderByField}
orderByReverse={controller.orderByReverse}
toggleSorting={controller.toggleSorting}
/>
) : (
<div className="bg-white tiled table-responsive">
<ItemsTable
items={controller.pageItems}
columns={this.listColumns}
orderByField={controller.orderByField}
orderByReverse={controller.orderByReverse}
toggleSorting={controller.toggleSorting}
/>
<Paginator
showPageSizeSelect
totalCount={controller.totalItemsCount}
pageSize={controller.itemsPerPage}
onPageSizeChange={itemsPerPage => controller.updatePagination({ itemsPerPage })}
page={controller.page}
onChange={page => controller.updatePagination({ page })}
/>
</div>
)}
</div>
) : (
<LoadingState />
)}
<Paginator
showPageSizeSelect
totalCount={controller.totalItemsCount}
pageSize={controller.itemsPerPage}
onPageSizeChange={itemsPerPage => controller.updatePagination({ itemsPerPage })}
page={controller.page}
onChange={page => controller.updatePagination({ page })}
/>
</div>
)}
</div>
</Layout.Content>
</Layout>
</div>

View File

@@ -11,7 +11,6 @@ import { wrap as itemsList, ControllerType } from "@/components/items-list/Items
import { ResourceItemsSource } from "@/components/items-list/classes/ItemsSource";
import { UrlStateStorage } from "@/components/items-list/classes/StateStorage";
import LoadingState from "@/components/items-list/components/LoadingState";
import * as Sidebar from "@/components/items-list/components/Sidebar";
import ItemsTable, { Columns } from "@/components/items-list/components/ItemsTable";
@@ -80,12 +79,18 @@ class QueriesList extends React.Component {
width: null,
}
),
Columns.custom((text, item) => item.user.name, { title: "Created By" }),
Columns.dateTime.sortable({ title: "Created At", field: "created_at" }),
Columns.dateTime.sortable({ title: "Last Executed At", field: "retrieved_at", orderByField: "executed_at" }),
Columns.custom((text, item) => item.user.name, { title: "Created By", width: "1%" }),
Columns.dateTime.sortable({ title: "Created At", field: "created_at", width: "1%" }),
Columns.dateTime.sortable({
title: "Last Executed At",
field: "retrieved_at",
orderByField: "executed_at",
width: "1%",
}),
Columns.custom.sortable((text, item) => <SchedulePhrase schedule={item.schedule} isNew={item.isNew()} />, {
title: "Refresh Schedule",
field: "schedule",
width: "1%",
}),
];
@@ -132,18 +137,17 @@ class QueriesList extends React.Component {
<Sidebar.Tags url="api/queries/tags" onChange={controller.updateSelectedTags} />
</Layout.Sidebar>
<Layout.Content>
{!controller.isLoaded && <LoadingState />}
{controller.isLoaded && controller.isEmpty && (
{controller.isLoaded && controller.isEmpty ? (
<QueriesListEmptyState
page={controller.params.currentPage}
searchTerm={controller.searchTerm}
selectedTags={controller.selectedTags}
/>
)}
{controller.isLoaded && !controller.isEmpty && (
) : (
<div className="bg-white tiled table-responsive">
<ItemsTable
items={controller.pageItems}
loading={!controller.isLoaded}
columns={this.listColumns}
orderByField={controller.orderByField}
orderByReverse={controller.orderByReverse}