Unverified Commit b2230452 authored by Marcos Iglesias's avatar Marcos Iglesias Committed by GitHub

feat: Empty and Loading states for the table (#639)

Signed-off-by: 's avatarMarcos Iglesias Valle <golodhros@gmail.com>
parent 97a5e14f
......@@ -16,7 +16,7 @@ $loading-curve: cubic-bezier(0.45, 0, 0.15, 1);
}
}
.is-shimmer-animated {
%is-shimmer-animated {
animation: $loading-duration shimmer $loading-curve infinite;
background-image: linear-gradient(
to right,
......@@ -29,3 +29,7 @@ $loading-curve: cubic-bezier(0.45, 0, 0.15, 1);
background-repeat: no-repeat;
background-size: 300% 100%;
}
.is-shimmer-animated {
@extend %is-shimmer-animated;
}
......@@ -30,6 +30,83 @@ describe('Table', () => {
}).not.toThrow();
});
describe('when empty data is passed', () => {
const { columns, data } = dataBuilder.withEmptyData().build();
it('renders a table', () => {
const { wrapper } = setup({
data,
columns,
});
const expected = 1;
const actual = wrapper.find('.ams-table').length;
expect(actual).toEqual(expected);
});
describe('table header', () => {
it('renders a table header', () => {
const { wrapper } = setup({
data,
columns,
});
const expected = 1;
const actual = wrapper.find('.ams-table-header').length;
expect(actual).toEqual(expected);
});
it('renders one cell inside the header', () => {
const { wrapper } = setup({
data,
columns,
});
const expected = 1;
const actual = wrapper.find(
'.ams-table-header .ams-table-heading-cell'
).length;
expect(actual).toEqual(expected);
});
});
describe('table body', () => {
it('renders a table body', () => {
const { wrapper } = setup({
data,
columns,
});
const expected = 1;
const actual = wrapper.find('.ams-table-body').length;
expect(actual).toEqual(expected);
});
it('renders one row', () => {
const { wrapper } = setup({
data,
columns,
});
const expected = 1;
const actual = wrapper.find('.ams-table-row').length;
expect(actual).toEqual(expected);
});
it('renders an empty message', () => {
const { wrapper } = setup({
data,
columns,
});
const expected = 1;
const actual = wrapper.find('.ams-table-row .ams-empty-message-cell')
.length;
expect(actual).toEqual(expected);
});
});
});
describe('when simple data is passed', () => {
it('renders a table', () => {
const { wrapper } = setup();
......@@ -119,6 +196,137 @@ describe('Table', () => {
});
});
});
describe('options', () => {
describe('when a tableClassName is passed', () => {
it('adds the class to the table', () => {
const { wrapper } = setup({
options: { tableClassName: 'test-class' },
});
const expected = 1;
const actual = wrapper.find('.test-class').length;
expect(actual).toEqual(expected);
});
});
describe('when isLoading is active', () => {
it('renders a table', () => {
const { wrapper } = setup({
data: [],
columns: [],
options: {
isLoading: true,
numLoadingBlocks: 10,
},
});
const expected = 1;
const actual = wrapper.find('.ams-table').length;
expect(actual).toEqual(expected);
});
describe('table header', () => {
it('renders a table header', () => {
const { wrapper } = setup({
data: [],
columns: [],
options: {
isLoading: true,
},
});
const expected = 1;
const actual = wrapper.find('.ams-table-header').length;
expect(actual).toEqual(expected);
});
it('renders one cell inside the header', () => {
const { wrapper } = setup({
data: [],
columns: [],
options: {
isLoading: true,
numLoadingBlocks: 10,
},
});
const expected = 1;
const actual = wrapper.find(
'.ams-table-header .ams-table-heading-loading-cell'
).length;
expect(actual).toEqual(expected);
});
it('renders one loading block inside the header', () => {
const { wrapper } = setup({
data: [],
columns: [],
options: {
isLoading: true,
numLoadingBlocks: 10,
},
});
const expected = 1;
const actual = wrapper.find(
'.ams-table-header .ams-table-shimmer-block'
).length;
expect(actual).toEqual(expected);
});
});
describe('table body', () => {
it('renders a table body', () => {
const { wrapper } = setup({
data: [],
columns: [],
options: {
isLoading: true,
numLoadingBlocks: 10,
},
});
const expected = 1;
const actual = wrapper.find('.ams-table-body').length;
expect(actual).toEqual(expected);
});
it('renders one row', () => {
const { wrapper } = setup({
data: [],
columns: [],
options: {
isLoading: true,
numLoadingBlocks: 10,
},
});
const expected = 1;
const actual = wrapper.find('.ams-table-row').length;
expect(actual).toEqual(expected);
});
it('renders the proper number of shimmering blocks', () => {
const numOfLoadingBlocks = 10;
const { wrapper } = setup({
data: [],
columns: [],
options: {
isLoading: true,
numLoadingBlocks: numOfLoadingBlocks,
},
});
const expected = numOfLoadingBlocks;
const actual = wrapper.find(
'.ams-table-row .shimmer-resource-loader-item'
).length;
expect(actual).toEqual(expected);
});
});
});
});
});
describe('lifetime', () => {});
......
......@@ -3,42 +3,86 @@
import * as React from 'react';
import ShimmeringResourceLoader from '../ShimmeringResourceLoader';
import './styles.scss';
export interface TableColumn {
title: string;
field: string;
horAlign?: 'left' | 'right' | 'center';
// width?: number;
// className?: string;
// horAlign?: 'left' | 'right' | 'center';
// width?: number;
// sortable?: bool (false)
// data?: () => React.ReactNode ((row,index) => <div>{index}</div>)
// actions?: Action[]
}
export interface TableOptions {
tableClassName?: string;
isLoading?: boolean;
numLoadingBlocks?: number;
}
export interface TableProps {
columns: TableColumn[];
data: [];
options?: TableOptions;
}
const Table: React.FC<TableProps> = ({ data, columns }: TableProps) => {
const fields = columns.map(({ field }) => field);
const DEFAULT_EMPTY_MESSAGE = 'No Results';
const DEFAULT_LOADING_ITEMS = 3;
return (
<table className="ams-table">
<thead className="ams-table-header">
type EmptyRowProps = {
colspan: number;
};
const EmptyRow: React.FC<EmptyRowProps> = ({ colspan }: EmptyRowProps) => (
<tr className="ams-table-row">
<td className="ams-empty-message-cell" colSpan={colspan}>
{DEFAULT_EMPTY_MESSAGE}
</td>
</tr>
);
const ShimmeringHeader: React.FC = () => (
<tr>
{columns.map(({ title }, index) => {
return (
<th className="ams-table-heading-cell" key={`index:${index}`}>
{title}
<th className="ams-table-heading-loading-cell">
<div className="ams-table-shimmer-block" />
</th>
);
})}
</tr>
</thead>
<tbody className="ams-table-body">
{data.map((item, index) => {
);
type ShimmeringBodyProps = {
numLoadingBlocks: number;
};
const ShimmeringBody: React.FC<ShimmeringBodyProps> = ({
numLoadingBlocks,
}: ShimmeringBodyProps) => (
<tr className="ams-table-row">
<td className="ams-table-body-loading-cell">
<ShimmeringResourceLoader numItems={numLoadingBlocks} />
</td>
</tr>
);
const Table: React.FC<TableProps> = ({
data,
columns,
options = {},
}: TableProps) => {
const {
tableClassName = '',
isLoading = false,
numLoadingBlocks = DEFAULT_LOADING_ITEMS,
} = options;
const fields = columns.map(({ field }) => field);
let body: React.ReactNode = <EmptyRow colspan={fields.length} />;
if (data.length) {
body = data.map((item, index) => {
return (
<tr className="ams-table-row" key={`index:${index}`}>
{Object.entries(item)
......@@ -50,8 +94,30 @@ const Table: React.FC<TableProps> = ({ data, columns }: TableProps) => {
))}
</tr>
);
});
}
let header: React.ReactNode = (
<tr>
{columns.map(({ title }, index) => {
return (
<th className="ams-table-heading-cell" key={`index:${index}`}>
{title}
</th>
);
})}
</tbody>
</tr>
);
if (isLoading) {
header = <ShimmeringHeader />;
body = <ShimmeringBody numLoadingBlocks={numLoadingBlocks} />;
}
return (
<table className={`ams-table ${tableClassName || ''}`}>
<thead className="ams-table-header">{header}</thead>
<tbody className="ams-table-body">{body}</tbody>
</table>
);
};
......
// Copyright Contributors to the Amundsen project.
// SPDX-License-Identifier: Apache-2.0
@import 'variables';
@import 'typography';
@import 'animations';
$table-header-font-size: 12px;
$table-header-line-height: 16px;
$table-header-height: 33px;
$table-header-border-width: 2px;
$shimmer-block-height: 16px;
$shimmer-block-width: 40%;
.ams-table {
width: 100%;
max-width: 100%;
......@@ -24,3 +31,29 @@ $table-header-border-width: 2px;
height: $table-header-height;
text-transform: uppercase;
}
.ams-empty-message-cell {
@extend %text-body-w3;
color: $text-primary;
text-align: center;
}
.ams-table-heading-loading-cell {
padding: $spacer-1;
}
.ams-table-body-loading-cell {
padding: 0 $spacer-1;
.shimmer-resource-loader {
margin-top: -1px;
}
}
.ams-table-shimmer-block {
@extend %is-shimmer-animated;
height: $shimmer-block-height;
width: $shimmer-block-width;
}
......@@ -17,5 +17,11 @@ stories.add('Table', () => (
<StorySection title="Basic Table">
<Table columns={columns} data={data} />
</StorySection>
<StorySection title="Empty Table">
<Table columns={columns} data={[]} />
</StorySection>
<StorySection title="Loading Table">
<Table columns={[]} data={[]} options={{ isLoading: true }} />
</StorySection>
</>
));
......@@ -50,6 +50,15 @@ function TestDataBuilder(config = {}) {
return new this.Klass(attr);
};
this.withEmptyData = () => {
const attr = {
data: [],
columns: [{ title: 'Name', field: 'name' }],
};
return new this.Klass(attr);
};
this.withMoreDataThanColumns = () => {
const attr = {
data: [
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment