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