update odlux for notification change
[ccsdk/features.git] / sdnr / wt / odlux / framework / src / components / material-table / index.tsx
1 /**
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
9  *
10  * http://www.apache.org/licenses/LICENSE-2.0
11  *
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
15  * the License.
16  * ============LICENSE_END==========================================================================
17  */
18 import * as React from 'react';
19 import { withStyles, WithStyles, createStyles, Theme } from '@material-ui/core/styles';
20
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 TableContainer from '@material-ui/core/TableContainer';
25 import TablePagination from '@material-ui/core/TablePagination';
26 import TableRow from '@material-ui/core/TableRow';
27 import Paper from '@material-ui/core/Paper';
28 import Checkbox from '@material-ui/core/Checkbox';
29
30 import { TableToolbar } from './tableToolbar';
31 import { EnhancedTableHead } from './tableHead';
32 import { EnhancedTableFilter } from './tableFilter';
33
34 import { ColumnModel, ColumnType } from './columnModel';
35 import { Omit, Menu, makeStyles } from '@material-ui/core';
36
37 import { SvgIconProps } from '@material-ui/core/SvgIcon/SvgIcon';
38
39 import { DividerTypeMap } from '@material-ui/core/Divider';
40 import { MenuItemProps } from '@material-ui/core/MenuItem';
41 import { flexbox } from '@material-ui/system';
42 import { RowDisabled } from './utilities';
43 export { ColumnModel, ColumnType } from './columnModel';
44
45 type propType = string | number | null | undefined | (string | number)[];
46 type dataType = { [prop: string]: propType };
47 type resultType<TData = dataType> = { page: number, total: number, rows: TData[] };
48
49 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>>;
50
51 function regExpEscape(s: string) {
52   return s.replace(/[|\\{}()[\]^$+*?.]/g, '\\$&');
53 };
54
55 function wildcardCheck(input: string, pattern: string) {
56    if (!pattern) return true; 
57    const regex = new RegExp(
58      (!pattern.startsWith('*') ? '^' : '') + 
59      pattern.split(/\*+/).map(p => p.split(/\?+/).map(regExpEscape).join('.')).join('.*') + 
60      (!pattern.endsWith('*') ? '$' : '')
61    );
62    return input.match(regex) !== null && input.match(regex)!.length >= 1;
63 };
64
65 function desc(a: dataType, b: dataType, orderBy: string) {
66   if ((b[orderBy] || "") < (a[orderBy] || "")) {
67     return -1;
68   }
69   if ((b[orderBy] || "") > (a[orderBy] || "")) {
70     return 1;
71   }
72   return 0;
73 }
74
75 function stableSort(array: dataType[], cmp: (a: dataType, b: dataType) => number) {
76   const stabilizedThis = array.map((el, index) => [el, index]) as [dataType, number][];
77   stabilizedThis.sort((a, b) => {
78     const order = cmp(a[0], b[0]);
79     if (order !== 0) return order;
80     return a[1] - b[1];
81   });
82   return stabilizedThis.map(el => el[0]);
83 }
84
85 function getSorting(order: 'asc' | 'desc' | null, orderBy: string) {
86   return order === 'desc' ? (a: dataType, b: dataType) => desc(a, b, orderBy) : (a: dataType, b: dataType) => -desc(a, b, orderBy);
87 }
88
89 const styles = (theme: Theme) => createStyles({
90   root: {
91     width: '100%',
92     overflow: "hidden",
93     marginTop: theme.spacing(3),
94     position: "relative",
95     boxSizing: "border-box",
96     display: "flex",
97     flexDirection: "column",
98   },
99   container: {
100     flex: "1 1 100%"
101   },
102   pagination: {
103     overflow: "hidden"
104   }
105 });
106
107 const useTableRowExtStyles = makeStyles((theme: Theme) => createStyles({
108   disabled: {
109     color: "rgba(180, 180, 180, 0.7)",
110   },
111 }));
112
113 type GetStatelessComponentProps<T> = T extends (props: infer P & { children?: React.ReactNode }) => any ? P : any;
114 type TableRowExtProps = GetStatelessComponentProps<typeof TableRow> & { disabled: boolean };
115 const TableRowExt : React.FC<TableRowExtProps> = (props) => {
116   const [disabled, setDisabled] = React.useState(true);
117   const classes = useTableRowExtStyles();
118   
119   const onMouseDown = (ev: React.MouseEvent<HTMLElement>) => {
120       if (ev.button ===1){
121         setDisabled(!disabled);  
122         ev.preventDefault();
123         ev.stopPropagation();
124       } else if (props.disabled && disabled) {
125         ev.preventDefault();
126         ev.stopPropagation();
127       }
128   }; 
129
130   return (   
131     <TableRow {...{...props,  color: props.disabled && disabled ? '#a0a0a0' : undefined , className: props.disabled && disabled ? classes.disabled : '', onMouseDown, onContextMenu: props.disabled && disabled ? onMouseDown : props.onContextMenu } }  /> 
132   );
133 };
134
135 export type MaterialTableComponentState<TData = {}> = {
136   order: 'asc' | 'desc';
137   orderBy: string | null;
138   selected: any[] | null;
139   rows: TData[];
140   total: number;
141   page: number;
142   rowsPerPage: number;
143   loading: boolean;
144   showFilter: boolean;
145   filter: { [property: string]: string };
146 };
147
148 export type TableApi = { forceRefresh?: () => Promise<void> };
149
150 type MaterialTableComponentBaseProps<TData> = WithStyles<typeof styles> & {
151   className?: string;
152   columns: ColumnModel<TData>[];
153   idProperty: keyof TData | ((data: TData) => React.Key);
154   tableId?: string;
155   title?: string;
156   stickyHeader?: boolean;
157   defaultSortOrder?: 'asc' | 'desc';
158   defaultSortColumn?: keyof TData;
159   enableSelection?: boolean;
160   disableSorting?: boolean;
161   disableFilter?: boolean;
162   customActionButtons?: { icon: React.ComponentType<SvgIconProps>, tooltip?: string, onClick: () => void, disabled?: boolean }[];
163   onHandleClick?(event: React.MouseEvent<HTMLTableRowElement>, rowData: TData): void;
164   createContextMenu?: (row: TData) => React.ReactElement<MenuItemProps | DividerTypeMap<{}, "hr">, React.ComponentType<MenuItemProps | DividerTypeMap<{}, "hr">>>[];
165 };
166
167 type MaterialTableComponentPropsWithRows<TData = {}> = MaterialTableComponentBaseProps<TData> & { rows: TData[]; asynchronus?: boolean; };
168 type MaterialTableComponentPropsWithRequestData<TData = {}> = MaterialTableComponentBaseProps<TData> & { onRequestData: DataCallback; tableApi?: TableApi; };
169 type MaterialTableComponentPropsWithExternalState<TData = {}> = MaterialTableComponentBaseProps<TData> & MaterialTableComponentState & {
170   onToggleFilter: () => void;
171   onFilterChanged: (property: string, filterTerm: string) => void;
172   onHandleChangePage: (page: number) => void;
173   onHandleChangeRowsPerPage: (rowsPerPage: number | null) => void;
174   onHandleRequestSort: (property: string) => void;
175 };
176
177 type MaterialTableComponentProps<TData = {}> =
178   MaterialTableComponentPropsWithRows<TData> |
179   MaterialTableComponentPropsWithRequestData<TData> |
180   MaterialTableComponentPropsWithExternalState<TData>;
181
182 function isMaterialTableComponentPropsWithRows(props: MaterialTableComponentProps): props is MaterialTableComponentPropsWithRows {
183   return (props as MaterialTableComponentPropsWithRows).rows !== undefined && (props as MaterialTableComponentPropsWithRows).rows instanceof Array;
184 }
185
186 function isMaterialTableComponentPropsWithRequestData(props: MaterialTableComponentProps): props is MaterialTableComponentPropsWithRequestData {
187   return (props as MaterialTableComponentPropsWithRequestData).onRequestData !== undefined && (props as MaterialTableComponentPropsWithRequestData).onRequestData instanceof Function;
188 }
189
190 function isMaterialTableComponentPropsWithRowsAndRequestData(props: MaterialTableComponentProps): props is MaterialTableComponentPropsWithExternalState {
191   const propsWithExternalState = (props as MaterialTableComponentPropsWithExternalState)
192   return propsWithExternalState.onFilterChanged instanceof Function ||
193     propsWithExternalState.onHandleChangePage instanceof Function ||
194     propsWithExternalState.onHandleChangeRowsPerPage instanceof Function ||
195     propsWithExternalState.onToggleFilter instanceof Function ||
196     propsWithExternalState.onHandleRequestSort instanceof Function
197 }
198
199 class MaterialTableComponent<TData extends {} = {}> extends React.Component<MaterialTableComponentProps, MaterialTableComponentState & { contextMenuInfo: { index: number; mouseX?: number; mouseY?: number }; }> {
200
201   constructor(props: MaterialTableComponentProps) {
202     super(props);
203
204     const page = isMaterialTableComponentPropsWithRowsAndRequestData(this.props) ? this.props.page : 0;
205     const rowsPerPage = isMaterialTableComponentPropsWithRowsAndRequestData(this.props) ? this.props.rowsPerPage || 10 : 10;
206
207     this.state = {
208       contextMenuInfo: { index: -1 },
209       filter: isMaterialTableComponentPropsWithRowsAndRequestData(this.props) ? this.props.filter || {} : {},
210       showFilter: isMaterialTableComponentPropsWithRowsAndRequestData(this.props) ? this.props.showFilter : false,
211       loading: isMaterialTableComponentPropsWithRowsAndRequestData(this.props) ? this.props.loading : false,
212       order: isMaterialTableComponentPropsWithRowsAndRequestData(this.props) ? this.props.order : this.props.defaultSortOrder || 'asc',
213       orderBy: isMaterialTableComponentPropsWithRowsAndRequestData(this.props) ? this.props.orderBy : this.props.defaultSortColumn || null,
214       selected: isMaterialTableComponentPropsWithRowsAndRequestData(this.props) ? this.props.selected : null,
215       rows: isMaterialTableComponentPropsWithRows(this.props) && this.props.rows.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage) || [],
216       total: isMaterialTableComponentPropsWithRows(this.props) && this.props.rows.length || 0,
217       page,
218       rowsPerPage,
219     };
220
221     if (isMaterialTableComponentPropsWithRequestData(this.props)) {
222       this.update();
223
224       if (this.props.tableApi) {
225         this.props.tableApi.forceRefresh = () => this.update();
226       }
227     }
228   }
229   render(): JSX.Element {
230     const { classes, columns } = this.props;
231     const { rows, total: rowCount, order, orderBy, selected, rowsPerPage, page, showFilter, filter } = this.state;
232     const emptyRows = rowsPerPage - Math.min(rowsPerPage, rowCount - page * rowsPerPage);
233     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;
234     const toggleFilter = isMaterialTableComponentPropsWithRowsAndRequestData(this.props) ? this.props.onToggleFilter : () => { !this.props.disableFilter && this.setState({ showFilter: !showFilter }, this.update) }
235     return (
236       <Paper className={this.props.className ? `${classes.root} ${this.props.className}` : classes.root}>
237         <TableContainer className={classes.container}>
238           <TableToolbar tableId={this.props.tableId} numSelected={selected && selected.length} title={this.props.title} customActionButtons={this.props.customActionButtons} onExportToCsv={this.exportToCsv}
239             onToggleFilter={toggleFilter} />
240           <Table aria-label={this.props.tableId ? this.props.tableId : 'tableTitle'} stickyHeader={this.props.stickyHeader || false} >
241             <EnhancedTableHead
242               columns={columns}
243               numSelected={selected && selected.length}
244               order={order}
245               orderBy={orderBy}
246               onSelectAllClick={this.handleSelectAllClick}
247               onRequestSort={this.onHandleRequestSort}
248               rowCount={rows.length}
249               enableSelection={this.props.enableSelection}
250             />
251             <TableBody>
252               {showFilter && <EnhancedTableFilter columns={columns} filter={filter} onFilterChanged={this.onFilterChanged} enableSelection={this.props.enableSelection} /> || null}
253               {rows // may need ordering here
254                 .map((entry: TData & { [RowDisabled]?: boolean, [kex: string]: any }, index) => {
255                   const entryId = getId(entry);
256                   const isSelected = this.isSelected(entryId);
257                   const contextMenu = (this.props.createContextMenu && this.state.contextMenuInfo.index === index && this.props.createContextMenu(entry)) || null;
258                   return (
259                     <TableRowExt
260                       hover
261                       onClick={event => {
262                         if (this.props.createContextMenu) {
263                           this.setState({
264                             contextMenuInfo: {
265                               index: -1
266                             }
267                           });
268                         }
269                         this.handleClick(event, entry, entryId);
270                       }}
271                       onContextMenu={event => {
272                         if (this.props.createContextMenu) {
273                           event.preventDefault();
274                           event.stopPropagation();
275                           this.setState({ contextMenuInfo: { index, mouseX: event.clientX - 2, mouseY: event.clientY - 4 } });
276                         }
277                       }}
278                       role="checkbox"
279                       aria-checked={isSelected}
280                       aria-label="table-row"
281                       tabIndex={-1}
282                       key={entryId}
283                       selected={isSelected}
284                       disabled={entry[RowDisabled] || false}
285                     >
286                       {this.props.enableSelection
287                         ? <TableCell padding="checkbox" style={{ width: "50px", color:  entry[RowDisabled] || false ? "inherit" : undefined } }>
288                           <Checkbox checked={isSelected} />
289                         </TableCell>
290                         : null
291                       }
292                       {
293                         this.props.columns.map(
294                           col => {
295                             const style = col.width ? { width: col.width } : {};
296                             return (
297                               <TableCell style={ entry[RowDisabled] || false ? { ...style, color: "inherit"  } : style } aria-label={col.title? col.title.toLowerCase().replace(/\s/g, "-") : col.property.toLowerCase().replace(/\s/g, "-")} key={col.property} align={col.type === ColumnType.numeric && !col.align ? "right" : col.align} >
298                                 {col.type === ColumnType.custom && col.customControl
299                                   ? <col.customControl className={col.className} style={col.style} rowData={entry} />
300                                   : col.type === ColumnType.boolean
301                                     ? <span className={col.className} style={col.style}>{col.labels ? col.labels[entry[col.property] ? "true" : "false"] : String(entry[col.property])}</span>
302                                     : <span className={col.className} style={col.style}>{String(entry[col.property])}</span>
303                                 }
304                               </TableCell>
305                             );
306                           }
307                         )
308                       }
309                       {<Menu open={!!contextMenu} onClose={() => this.setState({ contextMenuInfo: { index: -1 } })} anchorReference="anchorPosition" keepMounted
310                         anchorPosition={this.state.contextMenuInfo.mouseY != null && this.state.contextMenuInfo.mouseX != null ? { top: this.state.contextMenuInfo.mouseY, left: this.state.contextMenuInfo.mouseX } : undefined}>
311                         {contextMenu}
312                       </Menu> || null}
313                     </TableRowExt>
314                   );
315                 })}
316               {emptyRows > 0 && (
317                 <TableRow style={{ height: 49 * emptyRows }}>
318                   <TableCell colSpan={this.props.columns.length} />
319                 </TableRow>
320               )}
321             </TableBody>
322           </Table>
323         </TableContainer>
324         <TablePagination className={classes.pagination}
325           rowsPerPageOptions={[5, 10, 20, 50]}
326           component="div"
327           count={rowCount}
328           rowsPerPage={rowsPerPage}
329           page={page}
330           aria-label="table-pagination-footer"
331           backIconButtonProps={{
332             'aria-label': 'previous-page',
333           }}
334           nextIconButtonProps={{
335             'aria-label': 'next-page',
336           }}
337           onChangePage={this.onHandleChangePage}
338           onChangeRowsPerPage={this.onHandleChangeRowsPerPage}
339         />
340       </Paper>
341     );
342   }
343
344   static getDerivedStateFromProps(props: MaterialTableComponentProps, state: MaterialTableComponentState & { _rawRows: {}[] }): MaterialTableComponentState & { _rawRows: {}[] } {
345     if (isMaterialTableComponentPropsWithRowsAndRequestData(props)) {
346       return {
347         ...state,
348         rows: props.rows,
349         total: props.total,
350         orderBy: props.orderBy,
351         order: props.order,
352         filter: props.filter,
353         loading: props.loading,
354         showFilter: props.showFilter,
355         page: props.page,
356         rowsPerPage: props.rowsPerPage
357       }
358     } else if (isMaterialTableComponentPropsWithRows(props) && props.asynchronus && state._rawRows !== props.rows) {
359       const newState = MaterialTableComponent.updateRows(props, state);
360       return {
361         ...state,
362         ...newState,
363         _rawRows: props.rows || []
364       };
365     }
366     return state;
367   }
368
369   private static updateRows(props: MaterialTableComponentPropsWithRows, state: MaterialTableComponentState): { rows: {}[], total: number, page: number } {
370
371     let data = [...props.rows as dataType[] || []];
372     const columns = props.columns;
373
374     const { page, rowsPerPage, order, orderBy, filter } = state;
375
376     try {
377       if (state.showFilter) {
378         Object.keys(filter).forEach(prop => {
379           const column = columns.find(c => c.property === prop);
380           const filterExpression = filter[prop];
381
382           if (!column) throw new Error("Filter for not existing column found.");
383
384           if (filterExpression != null) {
385             data = data.filter((val) => {
386               const dataValue = val[prop];
387
388               if (dataValue != null) {
389
390                 if (column.type === ColumnType.boolean) {
391
392                   const boolDataValue = JSON.parse(String(dataValue).toLowerCase());
393                   const boolFilterExpression = JSON.parse(String(filterExpression).toLowerCase());
394                   return boolDataValue == boolFilterExpression;
395
396                 } else if (column.type === ColumnType.text) {
397
398                   const valueAsString = String(dataValue);
399                   const filterExpressionAsString = String(filterExpression).trim();
400                   if (filterExpressionAsString.length === 0) return true;
401                   return wildcardCheck(valueAsString, filterExpressionAsString);
402
403                 } else if (column.type === ColumnType.numeric){
404                   
405                   const valueAsNumber = Number(dataValue);
406                   const filterExpressionAsString = String(filterExpression).trim();
407                   if (filterExpressionAsString.length === 0 || isNaN(valueAsNumber)) return true;
408                   
409                   if (filterExpressionAsString.startsWith('>=')) {
410                     return valueAsNumber >= Number(filterExpressionAsString.substr(2).trim());
411                   } else if (filterExpressionAsString.startsWith('<=')) {
412                     return valueAsNumber <= Number(filterExpressionAsString.substr(2).trim());
413                   } else if (filterExpressionAsString.startsWith('>')) {
414                     return valueAsNumber > Number(filterExpressionAsString.substr(1).trim());
415                   } else if (filterExpressionAsString.startsWith('<')) {
416                     return valueAsNumber < Number(filterExpressionAsString.substr(1).trim());
417                   }
418                 } else if (column.type === ColumnType.date){
419                    const valueAsString = String(dataValue);
420
421                    const convertToDate = (valueAsString: string) => {
422                     // time value needs to be padded   
423                     const hasTimeValue = /T\d{2,2}/.test(valueAsString);
424                     const indexCollon =  valueAsString.indexOf(':');
425                         if (hasTimeValue && (indexCollon === -1 || indexCollon >= valueAsString.length-2)) {
426                             valueAsString = indexCollon === -1 
427                             ? valueAsString + ":00"
428                             : indexCollon === valueAsString.length-1
429                                 ? valueAsString + "00"
430                                 : valueAsString += "0"
431                         }
432                      return new Date(Date.parse(valueAsString));   
433                    };
434                    
435                    // @ts-ignore
436                    const valueAsDate = new Date(Date.parse(dataValue));
437                    const filterExpressionAsString = String(filterExpression).trim();             
438
439                    if (filterExpressionAsString.startsWith('>=')) {
440                     return valueAsDate >= convertToDate(filterExpressionAsString.substr(2).trim());
441                   } else if (filterExpressionAsString.startsWith('<=')) {
442                     return valueAsDate <= convertToDate(filterExpressionAsString.substr(2).trim());
443                   } else if (filterExpressionAsString.startsWith('>')) {
444                     return valueAsDate > convertToDate(filterExpressionAsString.substr(1).trim());
445                   } else if (filterExpressionAsString.startsWith('<')) {
446                     return valueAsDate < convertToDate(filterExpressionAsString.substr(1).trim());
447                   }
448
449                   
450                   if (filterExpressionAsString.length === 0) return true;
451                   return wildcardCheck(valueAsString, filterExpressionAsString);
452
453                 }
454               }
455
456               return (dataValue == filterExpression)
457             });
458           };
459         });
460       }
461
462       const rowCount = data.length;
463
464       if (page > 0 && rowsPerPage * page > rowCount) { //if result is smaller than the currently shown page, new search and repaginate
465         let newPage = Math.floor(rowCount / rowsPerPage);
466         return {
467           rows: data,
468           total: rowCount,
469           page: newPage
470         };
471       } else {
472         data = (orderBy && order
473           ? stableSort(data, getSorting(order, orderBy))
474           : data).slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage);
475
476         return {
477           rows: data,
478           total: rowCount,
479           page: page
480         };
481       }
482
483
484     } catch (e) {
485       console.error(e);
486       return {
487         rows: [],
488         total: 0,
489         page: page
490       }
491     }
492   }
493
494   private async update() {
495     if (isMaterialTableComponentPropsWithRequestData(this.props)) {
496       const response = await Promise.resolve(
497         this.props.onRequestData(
498           this.state.page, this.state.rowsPerPage, this.state.orderBy, this.state.order, this.state.showFilter && this.state.filter || {})
499       );
500       this.setState(response);
501     } else {
502       let updateResult = MaterialTableComponent.updateRows(this.props, this.state);
503       this.setState(updateResult);
504     }
505   }
506
507   private onFilterChanged = (property: string, filterTerm: string) => {
508     if (isMaterialTableComponentPropsWithRowsAndRequestData(this.props)) {
509       this.props.onFilterChanged(property, filterTerm);
510       return;
511     }
512     if (this.props.disableFilter) return;
513     const colDefinition = this.props.columns && this.props.columns.find(col => col.property === property);
514     if (colDefinition && colDefinition.disableFilter) return;
515
516     const filter = { ...this.state.filter, [property]: filterTerm };
517     this.setState({
518       filter
519     }, this.update);
520   };
521
522   private onHandleRequestSort = (event: React.SyntheticEvent, property: string) => {
523     if (isMaterialTableComponentPropsWithRowsAndRequestData(this.props)) {
524       this.props.onHandleRequestSort(property);
525       return;
526     }
527     if (this.props.disableSorting) return;
528     const colDefinition = this.props.columns && this.props.columns.find(col => col.property === property);
529     if (colDefinition && colDefinition.disableSorting) return;
530
531     const orderBy = this.state.orderBy === property && this.state.order === 'desc' ? null : property;
532     const order = this.state.orderBy === property && this.state.order === 'asc' ? 'desc' : 'asc';
533     this.setState({
534       order,
535       orderBy
536     }, this.update);
537   };
538
539   handleSelectAllClick: () => {};
540
541   private onHandleChangePage = (event: any | null, page: number) => {
542     if (isMaterialTableComponentPropsWithRowsAndRequestData(this.props)) {
543       this.props.onHandleChangePage(page);
544       return;
545     }
546     this.setState({
547       page
548     }, this.update);
549   };
550
551   private onHandleChangeRowsPerPage = (event: React.ChangeEvent<HTMLTextAreaElement | HTMLInputElement>) => {
552     if (isMaterialTableComponentPropsWithRowsAndRequestData(this.props)) {
553       this.props.onHandleChangeRowsPerPage(+(event && event.target.value));
554       return;
555     }
556     const rowsPerPage = +(event && event.target.value);
557     if (rowsPerPage && rowsPerPage > 0) {
558       this.setState({
559         rowsPerPage
560       }, this.update);
561     }
562   };
563
564   private isSelected(id: string | number): boolean {
565     let selected = this.state.selected || [];
566     const selectedIndex = selected.indexOf(id);
567     return (selectedIndex > -1);
568   }
569
570   private handleClick(event: any, rowData: TData, id: string | number): void {
571     if (this.props.onHandleClick instanceof Function) {
572       this.props.onHandleClick(event, rowData);
573       return;
574     }
575     if (!this.props.enableSelection) {
576       return;
577     }
578     let selected = this.state.selected || [];
579     const selectedIndex = selected.indexOf(id);
580     if (selectedIndex > -1) {
581       selected = [
582         ...selected.slice(0, selectedIndex),
583         ...selected.slice(selectedIndex + 1)
584       ];
585     } else {
586       selected = [
587         ...selected,
588         id
589       ];
590     }
591     this.setState({
592       selected
593     });
594   }
595
596
597   private exportToCsv = async () => {
598     let file;
599     let data: dataType[] | null = null;
600     let csv: string[] = [];
601
602     if (isMaterialTableComponentPropsWithRequestData(this.props)) {
603       // table with extra request handler
604       this.setState({ loading: true });
605       const result = await Promise.resolve(
606         this.props.onRequestData(0, 1000, this.state.orderBy, this.state.order, this.state.showFilter && this.state.filter || {})
607       );
608       data = result.rows;
609       this.setState({ loading: true });
610     } else if (isMaterialTableComponentPropsWithRowsAndRequestData(this.props)) {
611       // table with generated handlers note: exports data shown on current page
612       data = this.props.rows;
613     }
614     else {
615       // table with local data
616       data = MaterialTableComponent.updateRows(this.props, this.state).rows;
617     }
618
619     if (data && data.length > 0) {
620       csv.push(this.props.columns.map(col => col.title || col.property).join(',') + "\r\n");
621       this.state.rows && this.state.rows.forEach((row: any) => {
622         csv.push(this.props.columns.map(col => row[col.property]).join(',') + "\r\n");
623       });
624       const properties = { type: "text/csv;charset=utf-8" }; // Specify the file's mime-type.
625       try {
626         // Specify the filename using the File constructor, but ...
627         file = new File(csv, "export.csv", properties);
628       } catch (e) {
629         // ... fall back to the Blob constructor if that isn't supported.
630         file = new Blob(csv, properties);
631       }
632     }
633     if (!file) return;
634     var reader = new FileReader();
635     reader.onload = function (e) {
636       const dataUri = reader.result as any;
637       const link = document.createElement("a");
638       if (typeof link.download === 'string') {
639         link.href = dataUri;
640         link.download = "export.csv";
641
642         //Firefox requires the link to be in the body
643         document.body.appendChild(link);
644
645         //simulate click
646         link.click();
647
648         //remove the link when done
649         document.body.removeChild(link);
650       } else {
651         window.open(dataUri);
652       }
653     }
654     reader.readAsDataURL(file);
655
656     // const url = URL.createObjectURL(file);
657     // window.location.replace(url);
658   }
659 }
660
661 export type MaterialTableCtorType<TData extends {} = {}> = new () => React.Component<Omit<MaterialTableComponentProps<TData>, 'classes'>>;
662
663 export const MaterialTable = withStyles(styles)(MaterialTableComponent);
664 export default MaterialTable;