Unverified Commit 369e448c authored by Marcos Iglesias's avatar Marcos Iglesias Committed by GitHub

feat: Adds expandable feature to table (#654)

Signed-off-by: 's avatarMarcos Iglesias Valle <golodhros@gmail.com>
parent 253fea0c
......@@ -99,8 +99,8 @@ exports[`strict null compilation`] = {
[255, 6, 11, "No overload matches this call.\\n The last overload gave the following error.\\n Type \'(() => SubmitSearchRequest) | null\' is not assignable to type \'ActionCreator<any>\'.\\n Type \'null\' is not assignable to type \'ActionCreator<any>\'.", "2296208050"],
[270, 4, 18, "No overload matches this call.\\n The last overload gave the following error.\\n Argument of type \'(dispatch: any, ownProps: any) => ActionCreator<unknown>\' is not assignable to parameter of type \'DispatchFromProps\'.\\n Type \'(dispatch: any, ownProps: any) => ActionCreator<unknown>\' is missing the following properties from type \'DispatchFromProps\': submitSearch, onInputChange, onSelectInlineResult", "2926224796"]
],
"js/components/common/Table/index.tsx:2876532157": [
[127, 18, 13, "Type \'unknown\' is not assignable to type \'ReactNode\'.\\n Type \'unknown\' is not assignable to type \'ReactPortal\'.", "971959308"]
"js/components/common/Table/index.tsx:2645527205": [
[190, 22, 13, "Type \'unknown\' is not assignable to type \'ReactNode\'.\\n Type \'unknown\' is not assignable to type \'ReactPortal\'.", "971959308"]
],
"js/components/common/Tags/TagInput/index.tsx:3754832290": [
[63, 22, 6, "Type \'undefined\' is not assignable to type \'GetAllTagsRequest\'.", "1979467425"],
......@@ -364,8 +364,8 @@ exports[`strict null compilation`] = {
"js/utils/navigationUtils.ts:1127210474": [
[19, 50, 21, "Type \'undefined\' cannot be used as an index type.", "602535635"]
],
"webpack.common.ts:545199363": [
[33, 24, 20, "No overload matches this call.\\n Overload 1 of 2, \'(...items: ConcatArray<never>[]): never[]\', gave the following error.\\n Argument of type \'string\' is not assignable to parameter of type \'ConcatArray<never>\'.\\n Overload 2 of 2, \'(...items: ConcatArray<never>[]): never[]\', gave the following error.\\n Argument of type \'string\' is not assignable to parameter of type \'ConcatArray<never>\'.", "806093104"]
"webpack.common.ts:368637609": [
[34, 24, 20, "No overload matches this call.\\n Overload 1 of 2, \'(...items: ConcatArray<never>[]): never[]\', gave the following error.\\n Argument of type \'string\' is not assignable to parameter of type \'ConcatArray<never>\'.\\n Overload 2 of 2, \'(...items: ConcatArray<never>[]): never[]\', gave the following error.\\n Argument of type \'string\' is not assignable to parameter of type \'ConcatArray<never>\'.", "806093104"]
]
}`
};
......@@ -3,7 +3,8 @@
import * as React from 'react';
import { AlertIcon, IconSizes } from '../SVGIcons';
import { AlertIcon } from '../SVGIcons';
import { IconSizes } from '../SVGIcons/types';
import './styles.scss';
......
......@@ -6,6 +6,7 @@
.resource-list-header {
@extend %text-caption-w2;
display: flex;
flex-direction: row;
align-items: center;
......@@ -15,18 +16,22 @@
.dataset {
flex: 7;
margin-right: $spacer-2;
.dataset-text {
margin-left: $spacer-5;
}
}
.source {
flex: 2;
}
.badges {
display: flex;
flex: 3;
flex-wrap: wrap;
margin-left: $spacer-3;
.badges-text {
margin-left: $spacer-3;
}
......
......@@ -3,15 +3,10 @@
import * as React from 'react';
import { IconSizes } from '.';
import { IconSizes, IconProps } from './types';
const DEFAULT_STROKE_COLOR = 'currentColor';
export interface IconProps {
stroke?: string;
size?: number;
}
export const AlertIcon: React.FC<IconProps> = ({
stroke = DEFAULT_STROKE_COLOR,
size = IconSizes.REGULAR,
......
// Copyright Contributors to the Amundsen project.
// SPDX-License-Identifier: Apache-2.0
import * as React from 'react';
import { IconSizes, IconProps } from './types';
const DEFAULT_STROKE_COLOR = '';
const DEFAULT_FILL_COLOR = '#D6D9DB';
export const DownIcon: React.FC<IconProps> = ({
stroke = DEFAULT_STROKE_COLOR,
size = IconSizes.REGULAR,
fill = DEFAULT_FILL_COLOR,
}: IconProps) => {
return (
<svg width={size} height={size} viewBox="0 0 24 24">
<title>Down</title>
<defs>
<path
d="M11.987 13.73l4.051-3.918a1.017 1.017 0 011.426.012.983.983 0 01-.012 1.403l-4.733 4.578a1.013 1.013 0 01-.63.283 1.024 1.024 0 01-.814-.277l-4.833-4.59a.975.975 0 01-.018-1.397 1.026 1.026 0 011.432-.018l4.131 3.924z"
id="prefix__a"
/>
</defs>
<g fill="none" fillRule="evenodd">
<mask id="prefix__b" fill="#fff">
<use xlinkHref="#prefix__a" />
</mask>
<use fill={fill} xlinkHref="#prefix__a" stroke={stroke} />
</g>
</svg>
);
};
// Copyright Contributors to the Amundsen project.
// SPDX-License-Identifier: Apache-2.0
import * as React from 'react';
import { IconSizes, IconProps } from './types';
const DEFAULT_STROKE_COLOR = '';
const DEFAULT_FILL_COLOR = '#D6D9DB';
export const UpIcon: React.FC<IconProps> = ({
stroke = DEFAULT_STROKE_COLOR,
size = IconSizes.REGULAR,
fill = DEFAULT_FILL_COLOR,
}: IconProps) => {
return (
<svg width={size} height={size} viewBox="0 0 24 24">
<title>Up</title>
<defs>
<path
d="M12.097 12.12l4.049-3.898a1.02 1.02 0 011.427.014.981.981 0 01-.013 1.4l-4.73 4.553c-.18.174-.41.268-.642.283a1.026 1.026 0 01-.805-.276L6.553 9.63a.972.972 0 01-.02-1.394 1.028 1.028 0 011.434-.02l4.13 3.904z"
id="prefix__a"
/>
</defs>
<g fill="none" fillRule="evenodd">
<mask id="prefix__b" fill="#fff">
<use xlinkHref="#prefix__a" />
</mask>
<use
fill={fill}
stroke={stroke}
transform="rotate(-180 12.055 11.206)"
xlinkHref="#prefix__a"
/>
</g>
</svg>
);
};
......@@ -2,8 +2,5 @@
// SPDX-License-Identifier: Apache-2.0
export * from './AlertIcon';
export enum IconSizes {
REGULAR = 24,
SMALL = 16,
}
export * from './DownIcon';
export * from './UpIcon';
......@@ -5,7 +5,7 @@ import React from 'react';
import { storiesOf } from '@storybook/react';
import StorySection from '../StorySection';
import { AlertIcon } from '.';
import { AlertIcon, DownIcon, UpIcon } from '.';
const stories = storiesOf('Attributes/Iconography', module);
......@@ -14,5 +14,11 @@ stories.add('SVG Icons', () => (
<StorySection title="Alert">
<AlertIcon />
</StorySection>
<StorySection title="Down">
<DownIcon />
</StorySection>
<StorySection title="Up">
<UpIcon />
</StorySection>
</>
));
// Copyright Contributors to the Amundsen project.
// SPDX-License-Identifier: Apache-2.0
export enum IconSizes {
REGULAR = 24,
SMALL = 16,
}
export interface IconProps {
stroke?: string;
size?: number;
fill?: string;
}
......@@ -4,6 +4,7 @@
import * as React from 'react';
import ShimmeringResourceLoader from '../ShimmeringResourceLoader';
import { DownIcon, UpIcon } from '../SVGIcons';
import './styles.scss';
......@@ -13,12 +14,10 @@ export interface TableColumn {
title: string;
field: string;
horAlign?: TextAlignmentValues;
component?: (value: any) => React.ReactNode;
component?: (value: any, index: number) => React.ReactNode;
width?: number;
// className?: string;
// width?: number;
// sortable?: bool (false)
// data?: () => React.ReactNode ((row,index) => <div>{index}</div>)
// actions?: Action[]
}
export interface TableOptions {
......@@ -26,6 +25,7 @@ export interface TableOptions {
isLoading?: boolean;
numLoadingBlocks?: number;
rowHeight?: number;
expandRow?: (rowValue: any, index: number) => React.ReactNode;
}
export interface TableProps {
......@@ -35,9 +35,12 @@ export interface TableProps {
}
const DEFAULT_EMPTY_MESSAGE = 'No Results';
const EXPAND_ROW_TEXT = 'Expand Row';
const DEFAULT_LOADING_ITEMS = 3;
const DEFAULT_ROW_HEIGHT = 30;
const EXPANDING_CELL_WIDTH = '70px';
const DEFAULT_TEXT_ALIGNMENT = 'left';
const DEFAULT_CELL_WIDTH = 'auto';
type RowStyles = {
height: string;
......@@ -81,6 +84,45 @@ const ShimmeringBody: React.FC<ShimmeringBodyProps> = ({
</tr>
);
type ExpandingCellProps = {
index: number;
expandedRows: RowIndex[];
onClick: (index) => void;
};
const ExpandingCell: React.FC<ExpandingCellProps> = ({
index,
onClick,
expandedRows,
}: ExpandingCellProps) => {
const isExpanded = expandedRows.includes(index);
const cellStyling = { width: EXPANDING_CELL_WIDTH };
return (
<td
className="ams-table-cell ams-table-expanding-cell"
key={`expandingIndex:${index}`}
style={cellStyling}
>
<button
type="button"
className="ams-table-expanding-button"
onClick={() => {
const newExpandedRows = isExpanded
? expandedRows.filter((i) => i !== index)
: [...expandedRows, index];
onClick(newExpandedRows);
}}
>
<span className="sr-only">{EXPAND_ROW_TEXT}</span>
{isExpanded ? <UpIcon /> : <DownIcon />}
</button>
</td>
);
};
type RowIndex = number;
const Table: React.FC<TableProps> = ({
data,
columns,
......@@ -91,9 +133,11 @@ const Table: React.FC<TableProps> = ({
isLoading = false,
numLoadingBlocks = DEFAULT_LOADING_ITEMS,
rowHeight = DEFAULT_ROW_HEIGHT,
expandRow = null,
} = options;
const fields = columns.map(({ field }) => field);
const rowStyles = { height: `${rowHeight}px` };
const [expandedRows, setExpandedRows] = React.useState<RowIndex[]>([]);
let body: React.ReactNode = (
<EmptyRow colspan={fields.length} rowStyles={rowStyles} />
......@@ -102,55 +146,93 @@ const Table: React.FC<TableProps> = ({
if (data.length) {
body = data.map((item, index) => {
return (
<tr className="ams-table-row" key={`index:${index}`} style={rowStyles}>
{Object.entries(item)
.filter(([key]) => fields.includes(key))
.map(([key, value], index) => {
const columnInfo = columns.find(({ field }) => field === key);
const horAlign = columnInfo
? columnInfo.horAlign || DEFAULT_TEXT_ALIGNMENT
: DEFAULT_TEXT_ALIGNMENT;
const cellStyle = {
textAlign: `${horAlign}` as TextAlignmentValues,
};
// TODO: Improve the typing of this
let cellContent: React.ReactNode | typeof value = value;
if (columnInfo && columnInfo.component) {
cellContent = columnInfo.component(value);
}
return (
<td
className="ams-table-cell"
key={`index:${index}`}
style={cellStyle}
>
{cellContent}
</td>
);
})}
</tr>
<React.Fragment key={`index:${index}`}>
<tr
className="ams-table-row"
key={`index:${index}`}
style={rowStyles}
>
<>
{expandRow ? (
<ExpandingCell
index={index}
expandedRows={expandedRows}
onClick={setExpandedRows}
/>
) : null}
{Object.entries(item)
.filter(([key]) => fields.includes(key))
.map(([key, value], index) => {
const columnInfo = columns.find(({ field }) => field === key);
const horAlign = columnInfo
? columnInfo.horAlign || DEFAULT_TEXT_ALIGNMENT
: DEFAULT_TEXT_ALIGNMENT;
const width =
columnInfo && columnInfo.width
? `${columnInfo.width}px`
: DEFAULT_CELL_WIDTH;
const cellStyle = {
width,
textAlign: `${horAlign}` as TextAlignmentValues,
};
// TODO: Improve the typing of this
let cellContent: React.ReactNode | typeof value = value;
if (columnInfo && columnInfo.component) {
cellContent = columnInfo.component(value, index);
}
return (
<td
className="ams-table-cell"
key={`index:${index}`}
style={cellStyle}
>
{cellContent}
</td>
);
})}
</>
</tr>
{expandRow ? (
<tr
className={`ams-table-expanded-row ${
expandedRows.includes(index) ? 'is-expanded' : ''
}`}
key={`expandedIndex:${index}`}
>
<td className="ams-table-cell" colSpan={fields.length + 1}>
{expandRow(item, index)}
</td>
</tr>
) : null}
</React.Fragment>
);
});
}
let header: React.ReactNode = (
<tr>
{columns.map(({ title, horAlign = DEFAULT_TEXT_ALIGNMENT }, index) => {
const cellStyle = {
textAlign: `${horAlign}` as TextAlignmentValues,
};
return (
<th
className="ams-table-heading-cell"
key={`index:${index}`}
style={cellStyle}
>
{title}
</th>
);
})}
{expandRow && (
<th key="emptyTableHeading" className="ams-table-heading-cell" />
)}
{columns.map(
({ title, horAlign = DEFAULT_TEXT_ALIGNMENT, width = null }, index) => {
const cellStyle = {
width: width ? `${width}px` : DEFAULT_CELL_WIDTH,
textAlign: `${horAlign}` as TextAlignmentValues,
};
return (
<th
className="ams-table-heading-cell"
key={`index:${index}`}
style={cellStyle}
>
{title}
</th>
);
}
)}
</tr>
);
......
......@@ -48,6 +48,23 @@ $shimmer-block-width: 40%;
}
}
.ams-table-expanded-row {
display: none;
&.is-expanded {
display: table-row;
}
}
.ams-table-expanding-button {
border: 0;
background-color: white;
svg {
vertical-align: bottom;
}
}
// Loading State
.ams-table-heading-loading-cell {
padding: $spacer-1;
......
......@@ -14,6 +14,9 @@ const {
columns: alignedColumns,
data: alignedData,
} = dataBuilder.withAlignedColumns().build();
const {
columns: differentWidthColumns,
} = dataBuilder.withFixedWidthColumns().build();
const {
columns: customColumns,
data: customColumnsData,
......@@ -22,10 +25,23 @@ const {
columns: multipleCustomColumns,
data: multipleCustomComlumnsData,
} = dataBuilder.withMultipleComponentsColumn().build();
const {
columns: columnsWithAction,
data: dataWithAction,
} = dataBuilder.withActionCell().build();
const {
columns: columnsWithCollapsedRow,
data: dataWithCollapsedRow,
} = dataBuilder.withCollapsedRow().build();
const expandRowComponent = (rowValue, index) => (
<strong>
{index}:{rowValue.value}
</strong>
);
const stories = storiesOf('Components/Table', module);
stories.add('Table', () => (
stories.add('Table States', () => (
<>
<StorySection title="Basic Table">
<Table columns={columns} data={data} />
......@@ -36,25 +52,56 @@ stories.add('Table', () => (
<StorySection title="Loading Table">
<Table columns={[]} data={[]} options={{ isLoading: true }} />
</StorySection>
<StorySection title="Table with different column alignment">
</>
));
stories.add('Styled Table', () => (
<>
<StorySection title="with different column alignment">
<Table columns={alignedColumns} data={alignedData} />
</StorySection>
<StorySection title="Table with 50px row height">
<StorySection title="with 50px row height">
<Table columns={columns} data={data} options={{ rowHeight: 50 }} />
</StorySection>
<StorySection title="Table with custom column components">
<StorySection title="with different column widths">
<Table
columns={differentWidthColumns}
data={data}
options={{ rowHeight: 50 }}
/>
</StorySection>
</>
));
stories.add('Customized Table', () => (
<>
<StorySection title="with custom column components">
<Table
columns={customColumns}
data={customColumnsData}
options={{ rowHeight: 40 }}
/>
</StorySection>
<StorySection title="Table with multiple custom column components">
<StorySection title="with multiple custom column components">
<Table
columns={multipleCustomColumns}
data={multipleCustomComlumnsData}
options={{ rowHeight: 40 }}
/>
</StorySection>
<StorySection title="with Bootstrap dropdown as component">
<Table
columns={columnsWithAction}
data={dataWithAction}
options={{ rowHeight: 40 }}
/>
</StorySection>
<StorySection title="with Collapsed Rows">
<Table
columns={columnsWithCollapsedRow}
data={dataWithCollapsedRow}
options={{ rowHeight: 40, expandRow: expandRowComponent }}
/>
</StorySection>
</>
));
......@@ -2,6 +2,7 @@
// SPDX-License-Identifier: Apache-2.0
import * as React from 'react';
import { Dropdown, MenuItem } from 'react-bootstrap';
const defaultData = [
{ name: 'rowName', type: 'rowType', value: 1 },
......@@ -55,6 +56,28 @@ function TestDataBuilder(config = {}) {
return new this.Klass(attr);
};
this.withCollapsedRow = () => {
const attr = {
data: [
{ name: 'rowName', type: 'rowType', value: 1 },
{ name: 'rowName2', type: 'rowType2', value: 2 },
{ name: 'rowName3', type: 'rowType3', value: 3 },
],
columns: [
{
title: 'Name',
field: 'name',
},
{
title: 'Type',
field: 'type',
},
],
};
return new this.Klass(attr);
};
this.withOneComponentColumn = () => {
const attr = {
data: [...this.config.data],
......@@ -78,6 +101,49 @@ function TestDataBuilder(config = {}) {
return new this.Klass(attr);
};
this.withActionCell = () => {
const attr = {
data: [
{ name: 'rowName', type: 'rowType', value: 'Action Text' },
{ name: 'rowName2', type: 'rowType2', value: 'Action Text' },
{ name: 'rowName3', type: 'rowType3', value: 'Action Text' },
],
columns: [
{
title: 'Name',
field: 'name',
},
{
title: 'Type',
field: 'type',
},
{
title: 'Value',
field: 'value',
component: (value, index) => (
<Dropdown
id={`detail-list-item-dropdown:${index}`}
pullRight
className="column-dropdown"
>
<Dropdown.Toggle noCaret>
<span className="sr-only">More info</span>
<img className="icon icon-more" alt="" />
</Dropdown.Toggle>
<Dropdown.Menu>
<MenuItem onClick={() => console.log('index', index)}>
{value}
</MenuItem>
</Dropdown.Menu>
</Dropdown>
),
},
],
};
return new this.Klass(attr);
};
this.withMultipleComponentsColumn = () => {
const attr = {
data: [
......@@ -131,6 +197,33 @@ function TestDataBuilder(config = {}) {
return new this.Klass(attr);
};
this.withFixedWidthColumns = () => {
const attr = {
data: [...this.config.data],
columns: [
{
title: 'Name',
field: 'name',
width: 50,
horAlign: 'left',
},
{
title: 'Type',
field: 'type',
width: 200,
horAlign: 'center',
},
{
title: 'Value',
field: 'value',
horAlign: 'right',
},
],
};
return new this.Klass(attr);
};
this.withEmptyData = () => {
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