2 * ============LICENSE_START========================================================================
3 * ONAP : ccsdk feature sdnr wt odlux
4 * =================================================================================================
5 * Copyright (C) 2019 highstreet technologies GmbH Intellectual Property. All rights reserved.
6 * =================================================================================================
7 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
8 * in compliance with the License. You may obtain a copy of the License at
10 * http://www.apache.org/licenses/LICENSE-2.0
12 * Unless required by applicable law or agreed to in writing, software distributed under the License
13 * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
14 * or implied. See the License for the specific language governing permissions and limitations under
16 * ============LICENSE_END==========================================================================
18 import * as React from 'react';
19 import { withStyles, WithStyles, createStyles, Theme } from '@material-ui/core/styles';
21 import Table from '@material-ui/core/Table';
22 import TableBody from '@material-ui/core/TableBody';
23 import TableCell from '@material-ui/core/TableCell';
24 import TablePagination from '@material-ui/core/TablePagination';
25 import TableRow from '@material-ui/core/TableRow';
26 import Paper from '@material-ui/core/Paper';
27 import Checkbox from '@material-ui/core/Checkbox';
29 import { TableToolbar } from './tableToolbar';
30 import { EnhancedTableHead } from './tableHead';
31 import { EnhancedTableFilter } from './tableFilter';
33 import { ColumnModel, ColumnType } from './columnModel';
34 import { Omit } from '@material-ui/core';
35 import { SvgIconProps } from '@material-ui/core/SvgIcon/SvgIcon';
36 export { ColumnModel, ColumnType } from './columnModel';
38 type propType = string | number | null | undefined | (string|number)[];
39 type dataType = { [prop: string]: propType };
40 type resultType<TData = dataType> = { page: number, rowCount: number, rows: TData[] };
42 export type DataCallback<TData = dataType> = (page?: number, rowsPerPage?: number, orderBy?: string | null, order?: 'asc' | 'desc' | null, filter?: { [property: string]: string }) =>resultType<TData> | Promise<resultType<TData>>;
44 function desc(a: dataType, b: dataType, orderBy: string) {
45 if ((b[orderBy] || "") < (a[orderBy] || "") ) {
48 if ((b[orderBy] || "") > (a[orderBy] || "") ) {
54 function stableSort(array: dataType[], cmp: (a: dataType, b: dataType) => number) {
55 const stabilizedThis = array.map((el, index) => [el, index]) as [dataType, number][];
56 stabilizedThis.sort((a, b) => {
57 const order = cmp(a[0], b[0]);
58 if (order !== 0) return order;
61 return stabilizedThis.map(el => el[0]);
64 function getSorting(order: 'asc' | 'desc' | null, orderBy: string) {
65 return order === 'desc' ? (a: dataType, b: dataType) => desc(a, b, orderBy) : (a: dataType, b: dataType) => -desc(a, b, orderBy);
68 const styles = (theme: Theme) => createStyles({
71 marginTop: theme.spacing.unit * 3,
81 export type MaterialTableComponentState<TData = {}> = {
82 order: 'asc' | 'desc';
83 orderBy: string | null;
84 selected: any[] | null;
91 filter: { [property: string]: string };
94 export type TableApi = { forceRefresh?: () => Promise<void> };
96 type MaterialTableComponentBaseProps<TData> = WithStyles<typeof styles> & {
97 columns: ColumnModel<TData>[];
98 idProperty: keyof TData | ((data: TData) => React.Key );
100 enableSelection?: boolean;
101 disableSorting?: boolean;
102 disableFilter?: boolean;
103 customActionButtons?: { icon: React.ComponentType<SvgIconProps>, tooltip?: string, onClick: () => void }[];
104 onHandleClick?(event: React.MouseEvent<HTMLTableRowElement>, rowData: TData): void;
107 type MaterialTableComponentPropsWithRows<TData={}> = MaterialTableComponentBaseProps<TData> & { rows: TData[]; asynchronus?: boolean; };
108 type MaterialTableComponentPropsWithRequestData<TData={}> = MaterialTableComponentBaseProps<TData> & { onRequestData: DataCallback; tableApi?: TableApi; };
109 type MaterialTableComponentPropsWithExternalState<TData={}> = MaterialTableComponentBaseProps<TData> & MaterialTableComponentState & {
110 onToggleFilter: () => void;
111 onFilterChanged: (property: string, filterTerm: string) => void;
112 onHandleChangePage: (page: number) => void;
113 onHandleChangeRowsPerPage: (rowsPerPage: number | null) => void;
114 onHandleRequestSort: (property: string) => void;
117 type MaterialTableComponentProps<TData = {}> =
118 MaterialTableComponentPropsWithRows<TData> |
119 MaterialTableComponentPropsWithRequestData<TData> |
120 MaterialTableComponentPropsWithExternalState<TData>;
122 function isMaterialTableComponentPropsWithRows(props: MaterialTableComponentProps): props is MaterialTableComponentPropsWithRows {
123 return (props as MaterialTableComponentPropsWithRows).rows !== undefined && (props as MaterialTableComponentPropsWithRows).rows instanceof Array;
126 function isMaterialTableComponentPropsWithRequestData(props: MaterialTableComponentProps): props is MaterialTableComponentPropsWithRequestData {
127 return (props as MaterialTableComponentPropsWithRequestData).onRequestData !== undefined && (props as MaterialTableComponentPropsWithRequestData).onRequestData instanceof Function;
130 function isMaterialTableComponentPropsWithRowsAndRequestData(props: MaterialTableComponentProps): props is MaterialTableComponentPropsWithExternalState {
131 const propsWithExternalState = (props as MaterialTableComponentPropsWithExternalState)
132 return propsWithExternalState.onFilterChanged instanceof Function ||
133 propsWithExternalState.onHandleChangePage instanceof Function ||
134 propsWithExternalState.onHandleChangeRowsPerPage instanceof Function ||
135 propsWithExternalState.onToggleFilter instanceof Function ||
136 propsWithExternalState.onHandleRequestSort instanceof Function
139 class MaterialTableComponent<TData extends {} = {}> extends React.Component<MaterialTableComponentProps, MaterialTableComponentState> {
141 constructor(props: MaterialTableComponentProps) {
144 const page = isMaterialTableComponentPropsWithRowsAndRequestData(this.props) ? this.props.page : 0;
145 const rowsPerPage = isMaterialTableComponentPropsWithRowsAndRequestData(this.props) ? this.props.rowsPerPage || 10 : 10;
148 filter: isMaterialTableComponentPropsWithRowsAndRequestData(this.props) ? this.props.filter || {} : {},
149 showFilter: isMaterialTableComponentPropsWithRowsAndRequestData(this.props) ? this.props.showFilter : false,
150 loading: isMaterialTableComponentPropsWithRowsAndRequestData(this.props) ? this.props.loading : false,
151 order: isMaterialTableComponentPropsWithRowsAndRequestData(this.props) ? this.props.order : 'asc',
152 orderBy: isMaterialTableComponentPropsWithRowsAndRequestData(this.props) ? this.props.orderBy : null,
153 selected: isMaterialTableComponentPropsWithRowsAndRequestData(this.props) ? this.props.selected : null,
154 rows: isMaterialTableComponentPropsWithRows(this.props) && this.props.rows.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage) || [],
155 rowCount: isMaterialTableComponentPropsWithRows(this.props) && this.props.rows.length || 0,
160 if (isMaterialTableComponentPropsWithRequestData(this.props)) {
163 if (this.props.tableApi) {
164 this.props.tableApi.forceRefresh = () => this.update();
168 render(): JSX.Element {
169 const { classes, columns } = this.props;
170 const { rows, rowCount, order, orderBy, selected, rowsPerPage, page, showFilter, filter } = this.state;
171 const emptyRows = rowsPerPage - Math.min(rowsPerPage, rowCount - page * rowsPerPage);
172 const getId = typeof this.props.idProperty !== "function" ? (data: TData) => ((data as {[key:string]: any })[this.props.idProperty as any as string] as string | number) : this.props.idProperty;
173 const toggleFilter = isMaterialTableComponentPropsWithRowsAndRequestData(this.props) ? this.props.onToggleFilter : () => { !this.props.disableFilter && this.setState({ showFilter: !showFilter }, this.update) }
175 <Paper className={ classes.root }>
176 <TableToolbar numSelected={ selected && selected.length } title={ this.props.title } customActionButtons={ this.props.customActionButtons } onExportToCsv={ this.exportToCsv }
177 onToggleFilter={ toggleFilter } />
178 <div className={ classes.tableWrapper }>
179 <Table className={ classes.table } aria-labelledby="tableTitle">
182 numSelected={ selected && selected.length }
185 onSelectAllClick={ this.handleSelectAllClick }
186 onRequestSort={ this.onHandleRequestSort }
187 rowCount={ rows.length }
188 enableSelection={ this.props.enableSelection }
191 { showFilter && <EnhancedTableFilter columns={ columns } filter={ filter } onFilterChanged={ this.onFilterChanged } enableSelection={this.props.enableSelection} /> || null }
192 { rows // may need ordering here
193 .map((entry: TData & { [key: string]: any }) => {
194 const entryId = getId(entry);
195 const isSelected = this.isSelected(entryId);
199 onClick={ event => this.handleClick(event, entry, entryId) }
201 aria-checked={ isSelected }
204 selected={ isSelected }
206 { this.props.enableSelection
207 ? <TableCell padding="checkbox" style={ { width: "50px" } }>
208 <Checkbox checked={ isSelected } />
213 this.props.columns.map(
215 const style = col.width ? { width: col.width } : { };
217 <TableCell key={ col.property } align={ col.type === ColumnType.numeric && !col.align ? "right": col.align } style={ style }>
218 { col.type === ColumnType.custom && col.customControl
219 ? <col.customControl className={col.className} style={col.style} rowData={ entry } />
220 : col.type === ColumnType.boolean
221 ? <span className={col.className} style={col.style}>{col.labels ? col.labels[entry[col.property] ? "true": "false"] : String(entry[col.property]) }</span>
222 : <span className={col.className} style={col.style}>{String(entry[col.property])}</span>
233 <TableRow style={ { height: 49 * emptyRows } }>
234 <TableCell colSpan={ this.props.columns.length } />
241 rowsPerPageOptions={[5, 10, 20, 50] }
244 rowsPerPage={ rowsPerPage }
246 backIconButtonProps={ {
247 'aria-label': 'Previous Page',
249 nextIconButtonProps={ {
250 'aria-label': 'Next Page',
252 onChangePage={ this.onHandleChangePage }
253 onChangeRowsPerPage={ this.onHandleChangeRowsPerPage }
259 static getDerivedStateFromProps(props: MaterialTableComponentProps, state: MaterialTableComponentState & { _rawRows: {}[] }): MaterialTableComponentState & { _rawRows: {}[] } {
260 if (isMaterialTableComponentPropsWithRowsAndRequestData(props)) {
264 rowCount: props.rowCount,
265 orderBy: props.orderBy,
267 filter: props.filter,
268 loading: props.loading,
269 showFilter: props.showFilter,
271 rowsPerPage: props.rowsPerPage
273 } else if (isMaterialTableComponentPropsWithRows(props) && props.asynchronus && state._rawRows !== props.rows) {
274 const newState = MaterialTableComponent.updateRows(props, state);
278 _rawRows: props.rows || []
284 private static updateRows(props: MaterialTableComponentPropsWithRows, state: MaterialTableComponentState): { rows: {}[], rowCount: number } {
286 const { page, rowsPerPage, order, orderBy, filter } = state;
287 let data: dataType[] = props.rows || [];
288 let filtered = false;
289 if (state.showFilter) {
290 Object.keys(filter).forEach(prop => {
291 const exp = filter[prop];
292 filtered = filtered || exp !== undefined;
293 data = exp !== undefined ? data.filter((val) => {
294 const value = val[prop];
295 return (value == exp) || (value && value.toString().indexOf(String(exp)) > -1);
300 const rowCount = data.length;
302 data = (orderBy && order
303 ? stableSort(data, getSorting(order, orderBy))
304 : data).slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage);
318 private async update() {
319 if (isMaterialTableComponentPropsWithRequestData(this.props)) {
320 const response = await Promise.resolve(
321 this.props.onRequestData(
322 this.state.page, this.state.rowsPerPage, this.state.orderBy, this.state.order, this.state.showFilter && this.state.filter || {})
324 this.setState(response);
326 this.setState(MaterialTableComponent.updateRows(this.props, this.state));
330 private onFilterChanged = (property: string, filterTerm: string) => {
331 if (isMaterialTableComponentPropsWithRowsAndRequestData(this.props)) {
332 this.props.onFilterChanged(property, filterTerm);
335 if (this.props.disableFilter) return;
336 const colDefinition = this.props.columns && this.props.columns.find(col => col.property === property);
337 if (colDefinition && colDefinition.disableFilter) return;
339 const filter = { ...this.state.filter, [property]: filterTerm };
345 private onHandleRequestSort = (event: React.SyntheticEvent, property: string) => {
346 if (isMaterialTableComponentPropsWithRowsAndRequestData(this.props)) {
347 this.props.onHandleRequestSort(property);
350 if (this.props.disableSorting) return;
351 const colDefinition = this.props.columns && this.props.columns.find(col => col.property === property);
352 if (colDefinition && colDefinition.disableSorting) return;
354 const orderBy = this.state.orderBy === property && this.state.order === 'desc' ? null : property;
355 const order = this.state.orderBy === property && this.state.order === 'asc' ? 'desc' : 'asc';
362 handleSelectAllClick: () => {};
364 private onHandleChangePage = (event: React.MouseEvent<HTMLButtonElement> | null, page: number) => {
365 if (isMaterialTableComponentPropsWithRowsAndRequestData(this.props)) {
366 this.props.onHandleChangePage(page);
374 private onHandleChangeRowsPerPage = (event: React.ChangeEvent<HTMLTextAreaElement | HTMLInputElement>) => {
375 if (isMaterialTableComponentPropsWithRowsAndRequestData(this.props)) {
376 this.props.onHandleChangeRowsPerPage(+(event && event.target.value));
379 const rowsPerPage = +(event && event.target.value);
380 if (rowsPerPage && rowsPerPage > 0) {
387 private isSelected(id: string | number): boolean {
388 let selected = this.state.selected || [];
389 const selectedIndex = selected.indexOf(id);
390 return (selectedIndex > -1);
393 private handleClick(event: React.MouseEvent<HTMLTableRowElement>, rowData: TData, id: string | number): void {
394 if (this.props.onHandleClick instanceof Function) {
395 this.props.onHandleClick(event, rowData);
398 if (!this.props.enableSelection){
401 let selected = this.state.selected || [];
402 const selectedIndex = selected.indexOf(id);
403 if (selectedIndex > -1) {
405 ...selected.slice(0, selectedIndex),
406 ...selected.slice(selectedIndex + 1)
419 private exportToCsv = async () => {
421 let data: dataType[] | null = null;
422 let csv: string[] = [];
425 if (isMaterialTableComponentPropsWithRequestData(this.props)) {
426 this.setState({ loading: true });
427 const result = await Promise.resolve(
428 this.props.onRequestData( 0, 1000, this.state.orderBy, this.state.order, this.state.showFilter && this.state.filter || {})
431 this.setState({ loading: true });
433 data = MaterialTableComponent.updateRows(this.props, this.state).rows;
436 if (data && data.length > 0) {
437 csv.push(this.props.columns.map(col => col.title || col.property).join(',') + "\r\n");
438 this.state.rows && this.state.rows.forEach((row: any) => {
439 csv.push(this.props.columns.map(col => row[col.property]).join(',') + "\r\n");
441 const properties = { type: "text/csv;charset=utf-8" }; // Specify the file's mime-type.
443 // Specify the filename using the File constructor, but ...
444 file = new File(csv, "export.csv", properties);
446 // ... fall back to the Blob constructor if that isn't supported.
447 file = new Blob(csv, properties);
451 var reader = new FileReader();
452 reader.onload = function (e) {
453 const dataUri = reader.result as any;
454 const link = document.createElement("a");
455 if (typeof link.download === 'string') {
457 link.download = "export.csv";
459 //Firefox requires the link to be in the body
460 document.body.appendChild(link);
465 //remove the link when done
466 document.body.removeChild(link);
468 window.open(dataUri);
471 reader.readAsDataURL(file);
473 // const url = URL.createObjectURL(file);
474 // window.location.replace(url);
478 export type MaterialTableCtorType<TData extends {} = {}> = new () => React.Component<Omit<MaterialTableComponentProps<TData>, 'classes'>>;
480 export const MaterialTable = withStyles(styles)(MaterialTableComponent);
481 export default MaterialTable;