mirror of
https://github.com/getredash/redash.git
synced 2026-03-23 22:00:10 -04:00
Use Skeleton as ItemsList loading state (#5079)
This commit is contained in:
@@ -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";
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 || [],
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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 }) {
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user