Merge "Add SDN-R odlux performance"
[ccsdk/features.git] / sdnr / wt / odlux / framework / src / components / material-table / index.tsx
1 import * as React from 'react';
2 import { withStyles, WithStyles, createStyles, Theme } from '@material-ui/core/styles';
3
4 import Table from '@material-ui/core/Table';
5 import TableBody from '@material-ui/core/TableBody';
6 import TableCell from '@material-ui/core/TableCell';
7 import TablePagination from '@material-ui/core/TablePagination';
8 import TableRow from '@material-ui/core/TableRow';
9 import Paper from '@material-ui/core/Paper';
10 import Checkbox from '@material-ui/core/Checkbox';
11
12 import { TableToolbar } from './tableToolbar';
13 import { EnhancedTableHead } from './tableHead';
14 import { EnhancedTableFilter } from './tableFilter';
15
16 import { ColumnModel, ColumnType } from './columnModel';
17 import { Omit } from '@material-ui/core';
18 import { SvgIconProps } from '@material-ui/core/SvgIcon/SvgIcon';
19 export { ColumnModel, ColumnType } from './columnModel';
20
21 type propType = string | number | null | undefined | (string|number)[];
22 type dataType = { [prop: string]: propType };
23 type resultType<TData = dataType> = { page: number, rowCount: number, rows: TData[] };
24
25 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>>;
26
27 function desc(a: dataType, b: dataType, orderBy: string) {
28   if ((b[orderBy] || "") < (a[orderBy] || "") ) {
29     return -1;
30   }
31   if ((b[orderBy] || "") > (a[orderBy] || "") ) {
32     return 1;
33   }
34   return 0;
35 }
36
37 function stableSort(array: dataType[], cmp: (a: dataType, b: dataType) => number) {
38   const stabilizedThis = array.map((el, index) => [el, index]) as [dataType, number][];
39   stabilizedThis.sort((a, b) => {
40     const order = cmp(a[0], b[0]);
41     if (order !== 0) return order;
42     return a[1] - b[1];
43   });
44   return stabilizedThis.map(el => el[0]);
45 }
46
47 function getSorting(order: 'asc' | 'desc' | null, orderBy: string) {
48   return order === 'desc' ? (a: dataType, b: dataType) => desc(a, b, orderBy) : (a: dataType, b: dataType) => -desc(a, b, orderBy);
49 }
50
51 const styles = (theme: Theme) => createStyles({
52   root: {
53     width: '100%',
54     marginTop: theme.spacing.unit * 3,
55   },
56   table: {
57     minWidth: 1020,
58   },
59   tableWrapper: {
60     overflowX: 'auto',
61   },
62 });
63
64 export type MaterialTableComponentState<TData = {}> = {
65   order: 'asc' | 'desc';
66   orderBy: string | null;
67   selected: any[] | null;
68   rows: TData[];
69   rowCount: number;
70   page: number;
71   rowsPerPage: number;
72   loading: boolean;
73   showFilter: boolean;
74   filter: { [property: string]: string };
75 };
76
77 export type TableApi = { forceRefresh?: () => Promise<void> };
78
79 type MaterialTableComponentBaseProps<TData> = WithStyles<typeof styles> & {
80   columns: ColumnModel<TData>[];
81   idProperty: keyof TData | ((data: TData) => React.Key );
82   title?: string;
83   enableSelection?: boolean;
84   disableSorting?: boolean;
85   disableFilter?: boolean;
86   customActionButtons?: { icon: React.ComponentType<SvgIconProps>, tooltip?: string, onClick: () => void  }[];
87   onHandleClick?(event: React.MouseEvent<HTMLTableRowElement>, rowData: TData): void;
88 };
89
90 type MaterialTableComponentPropsWithRows<TData={}> = MaterialTableComponentBaseProps<TData> & { rows: TData[]; asynchronus?: boolean; };
91 type MaterialTableComponentPropsWithRequestData<TData={}> = MaterialTableComponentBaseProps<TData> & { onRequestData: DataCallback; tableApi?: TableApi; };
92 type MaterialTableComponentPropsWithExternalState<TData={}> = MaterialTableComponentBaseProps<TData> & MaterialTableComponentState & {
93   onToggleFilter: () => void;
94   onFilterChanged: (property: string, filterTerm: string) => void;
95   onHandleChangePage: (page: number) => void;
96   onHandleChangeRowsPerPage: (rowsPerPage: number | null) => void;
97   onHandleRequestSort: (property: string) => void;
98 };
99
100 type MaterialTableComponentProps<TData = {}> =
101   MaterialTableComponentPropsWithRows<TData> |
102   MaterialTableComponentPropsWithRequestData<TData> |
103   MaterialTableComponentPropsWithExternalState<TData>;
104
105 function isMaterialTableComponentPropsWithRows(props: MaterialTableComponentProps): props is MaterialTableComponentPropsWithRows {
106   return (props as MaterialTableComponentPropsWithRows).rows !== undefined && (props as MaterialTableComponentPropsWithRows).rows instanceof Array;
107 }
108
109 function isMaterialTableComponentPropsWithRequestData(props: MaterialTableComponentProps): props is MaterialTableComponentPropsWithRequestData {
110   return (props as MaterialTableComponentPropsWithRequestData).onRequestData !== undefined && (props as MaterialTableComponentPropsWithRequestData).onRequestData instanceof Function;
111 }
112
113 function isMaterialTableComponentPropsWithRowsAndRequestData(props: MaterialTableComponentProps): props is MaterialTableComponentPropsWithExternalState {
114   const propsWithExternalState = (props as MaterialTableComponentPropsWithExternalState)
115   return propsWithExternalState.onFilterChanged instanceof Function ||
116     propsWithExternalState.onHandleChangePage instanceof Function ||
117     propsWithExternalState.onHandleChangeRowsPerPage instanceof Function ||
118     propsWithExternalState.onToggleFilter instanceof Function ||
119     propsWithExternalState.onHandleRequestSort instanceof Function
120 }
121
122 class MaterialTableComponent<TData extends {} = {}> extends React.Component<MaterialTableComponentProps, MaterialTableComponentState> {
123
124   constructor(props: MaterialTableComponentProps) {
125     super(props);
126
127     const page = isMaterialTableComponentPropsWithRowsAndRequestData(this.props) ? this.props.page : 0;
128     const rowsPerPage = isMaterialTableComponentPropsWithRowsAndRequestData(this.props) ? this.props.rowsPerPage || 10 : 10;
129
130     this.state = {
131       filter: isMaterialTableComponentPropsWithRowsAndRequestData(this.props) ? this.props.filter || {} : {},
132       showFilter: isMaterialTableComponentPropsWithRowsAndRequestData(this.props) ? this.props.showFilter : false,
133       loading: isMaterialTableComponentPropsWithRowsAndRequestData(this.props) ? this.props.loading : false,
134       order: isMaterialTableComponentPropsWithRowsAndRequestData(this.props) ? this.props.order : 'asc',
135       orderBy: isMaterialTableComponentPropsWithRowsAndRequestData(this.props) ? this.props.orderBy : null,
136       selected: isMaterialTableComponentPropsWithRowsAndRequestData(this.props) ? this.props.selected : null,
137       rows: isMaterialTableComponentPropsWithRows(this.props) && this.props.rows.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage) || [],
138       rowCount: isMaterialTableComponentPropsWithRows(this.props) && this.props.rows.length || 0,
139       page,
140       rowsPerPage,
141     };
142
143     if (isMaterialTableComponentPropsWithRequestData(this.props)) {
144       this.update();
145
146       if (this.props.tableApi) {
147         this.props.tableApi.forceRefresh = () => this.update();
148       }
149     }
150   }
151   render(): JSX.Element {
152     const { classes, columns } = this.props;
153     const { rows, rowCount, order, orderBy, selected, rowsPerPage, page, showFilter, filter } = this.state;
154     const emptyRows = rowsPerPage - Math.min(rowsPerPage, rowCount - page * rowsPerPage);
155     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;
156     const toggleFilter = isMaterialTableComponentPropsWithRowsAndRequestData(this.props) ? this.props.onToggleFilter : () => { !this.props.disableFilter && this.setState({ showFilter: !showFilter }, this.update) }
157     return (
158       <Paper className={ classes.root }>
159         <TableToolbar numSelected={ selected && selected.length } title={ this.props.title } customActionButtons={ this.props.customActionButtons } onExportToCsv={ this.exportToCsv }
160           onToggleFilter={ toggleFilter } />
161         <div className={ classes.tableWrapper }>
162           <Table className={ classes.table } aria-labelledby="tableTitle">
163             <EnhancedTableHead
164               columns={ columns }
165               numSelected={ selected && selected.length }
166               order={ order }
167               orderBy={ orderBy }
168               onSelectAllClick={ this.handleSelectAllClick }
169               onRequestSort={ this.onHandleRequestSort }
170               rowCount={ rows.length }
171               enableSelection={ this.props.enableSelection }
172             />
173             <TableBody>
174               { showFilter && <EnhancedTableFilter columns={ columns } filter={ filter } onFilterChanged={ this.onFilterChanged } enableSelection={this.props.enableSelection} /> || null }
175               { rows // may need ordering here
176                 .map((entry: TData & { [key: string]: any }) => {
177                   const entryId = getId(entry);
178                   const isSelected = this.isSelected(entryId);
179                   return (
180                     <TableRow
181                       hover
182                       onClick={ event => this.handleClick(event, entry, entryId) }
183                       role="checkbox"
184                       aria-checked={ isSelected }
185                       tabIndex={ -1 }
186                       key={ entryId }
187                       selected={ isSelected }
188                     >
189                       { this.props.enableSelection
190                        ? <TableCell padding="checkbox" style={ { width: "50px" } }>
191                           <Checkbox checked={ isSelected } />
192                         </TableCell>
193                        : null
194                       }
195                       {
196                         this.props.columns.map(
197                           col => {
198                             const style = col.width ? { width: col.width } : { };
199                             return (
200                               <TableCell key={ col.property } align={ col.type === ColumnType.numeric && !col.align ? "right": col.align } style={ style }>
201                                 { col.type === ColumnType.custom && col.customControl
202                                   ? <col.customControl className={col.className} style={col.style} rowData={ entry } />
203                                   : col.type === ColumnType.boolean
204                                     ? <span className={col.className} style={col.style}>{col.labels ? col.labels[entry[col.property] ? "true": "false"] : String(entry[col.property]) }</span>
205                                     : <span className={col.className} style={col.style}>{String(entry[col.property])}</span>
206                                 }
207                               </TableCell>
208                             );
209                           }
210                         )
211                       }
212                     </TableRow>
213                   );
214                 }) }
215               { emptyRows > 0 && (
216                 <TableRow style={ { height: 49 * emptyRows } }>
217                   <TableCell colSpan={ this.props.columns.length } />
218                 </TableRow>
219               ) }
220             </TableBody>
221           </Table>
222         </div>
223         <TablePagination
224           rowsPerPageOptions={[5, 10, 20, 50] }
225           component="div"
226           count={ rowCount }
227           rowsPerPage={ rowsPerPage }
228           page={ page }
229           backIconButtonProps={ {
230             'aria-label': 'Previous Page',
231           } }
232           nextIconButtonProps={ {
233             'aria-label': 'Next Page',
234           } }
235           onChangePage={ this.onHandleChangePage }
236           onChangeRowsPerPage={ this.onHandleChangeRowsPerPage }
237         />
238       </Paper>
239     );
240   }
241
242   static getDerivedStateFromProps(props: MaterialTableComponentProps, state: MaterialTableComponentState & { _rawRows: {}[] }): MaterialTableComponentState & { _rawRows: {}[] } {
243     if (isMaterialTableComponentPropsWithRowsAndRequestData(props)) {
244       return {
245         ...state,
246         rows: props.rows,
247         rowCount: props.rowCount,
248         orderBy: props.orderBy,
249         order: props.order,
250         filter: props.filter,
251         loading: props.loading,
252         showFilter: props.showFilter,
253         page: props.page,
254         rowsPerPage: props.rowsPerPage
255       }
256     } else if (isMaterialTableComponentPropsWithRows(props) && props.asynchronus && state._rawRows !== props.rows) {
257       const newState = MaterialTableComponent.updateRows(props, state);
258       return {
259         ...state,
260         ...newState,
261         _rawRows: props.rows || []
262       };
263     }
264     return state;
265   }
266
267   private static updateRows(props: MaterialTableComponentPropsWithRows, state: MaterialTableComponentState): { rows: {}[], rowCount: number } {
268     try {
269       const { page, rowsPerPage, order, orderBy, filter } = state;
270       let data: dataType[] = props.rows || [];
271       let filtered = false;
272       if (state.showFilter) {
273         Object.keys(filter).forEach(prop => {
274           const exp = filter[prop];
275           filtered = filtered || exp !== undefined;
276           data = exp !== undefined ? data.filter((val) => {
277             const value = val[prop];
278             return (value == exp) || (value && value.toString().indexOf(String(exp)) > -1);
279           }) : data;
280         });
281       }
282
283       const rowCount = data.length;
284
285       data = (orderBy && order
286         ? stableSort(data, getSorting(order, orderBy))
287         : data).slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage);
288
289       return {
290         rows: data,
291         rowCount
292       };
293     } catch{
294       return {
295         rows: [],
296         rowCount: 0
297       }
298     }
299   }
300
301   private async update() {
302     if (isMaterialTableComponentPropsWithRequestData(this.props)) {
303       const response = await Promise.resolve(
304         this.props.onRequestData(
305           this.state.page, this.state.rowsPerPage, this.state.orderBy, this.state.order, this.state.showFilter && this.state.filter || {})
306       );
307       this.setState(response);
308     } else {
309       this.setState(MaterialTableComponent.updateRows(this.props, this.state));
310     }
311   }
312
313   private onFilterChanged = (property: string, filterTerm: string) => {
314     if (isMaterialTableComponentPropsWithRowsAndRequestData(this.props)) {
315       this.props.onFilterChanged(property, filterTerm);
316       return;
317     }
318     if (this.props.disableFilter) return;
319     const colDefinition = this.props.columns && this.props.columns.find(col => col.property === property);
320     if (colDefinition && colDefinition.disableFilter) return;
321
322     const filter = { ...this.state.filter, [property]: filterTerm };
323     this.setState({
324       filter
325     }, this.update);
326   };
327
328   private onHandleRequestSort = (event: React.SyntheticEvent, property: string) => {
329     if (isMaterialTableComponentPropsWithRowsAndRequestData(this.props)) {
330       this.props.onHandleRequestSort(property);
331       return;
332     }
333     if (this.props.disableSorting) return;
334     const colDefinition = this.props.columns && this.props.columns.find(col => col.property === property);
335     if (colDefinition && colDefinition.disableSorting) return;
336
337     const orderBy = this.state.orderBy === property && this.state.order === 'desc' ? null : property;
338     const order = this.state.orderBy === property && this.state.order === 'asc' ? 'desc' : 'asc';
339     this.setState({
340       order,
341       orderBy
342     }, this.update);
343   };
344
345   handleSelectAllClick: () => {};
346
347   private onHandleChangePage = (event: React.MouseEvent<HTMLButtonElement> | null, page: number) => {
348     if (isMaterialTableComponentPropsWithRowsAndRequestData(this.props)) {
349       this.props.onHandleChangePage(page);
350       return;
351     }
352     this.setState({
353       page
354     }, this.update);
355   };
356
357   private onHandleChangeRowsPerPage = (event: React.ChangeEvent<HTMLTextAreaElement | HTMLInputElement>) => {
358     if (isMaterialTableComponentPropsWithRowsAndRequestData(this.props)) {
359       this.props.onHandleChangeRowsPerPage(+(event && event.target.value));
360       return;
361     }
362     const rowsPerPage = +(event && event.target.value);
363     if (rowsPerPage && rowsPerPage > 0) {
364       this.setState({
365         rowsPerPage
366       }, this.update);
367     }
368   };
369
370   private isSelected(id: string | number): boolean {
371     let selected = this.state.selected || [];
372     const selectedIndex = selected.indexOf(id);
373     return (selectedIndex > -1);
374   }
375
376   private handleClick(event: React.MouseEvent<HTMLTableRowElement>, rowData: TData, id: string | number): void {
377     if (this.props.onHandleClick instanceof Function) {
378       this.props.onHandleClick(event, rowData);
379       return;
380     }
381     if (!this.props.enableSelection){
382       return;
383     }
384     let selected = this.state.selected || [];
385     const selectedIndex = selected.indexOf(id);
386     if (selectedIndex > -1) {
387       selected = [
388         ...selected.slice(0, selectedIndex),
389         ...selected.slice(selectedIndex + 1)
390       ];
391     } else {
392       selected = [
393         ...selected,
394         id
395       ];
396     }
397     this.setState({
398       selected
399     });
400   }
401
402   private exportToCsv = async () => {
403     let file;
404     let data: dataType[] | null = null;
405     let csv: string[] = [];
406
407
408     if (isMaterialTableComponentPropsWithRequestData(this.props)) {
409       this.setState({ loading: true });
410       const result = await Promise.resolve(
411         this.props.onRequestData( 1, 1000, this.state.orderBy, this.state.order, this.state.showFilter && this.state.filter || {})
412       );
413       data = result.rows;
414       this.setState({ loading: true });
415     } else {
416       data = MaterialTableComponent.updateRows(this.props, this.state).rows;
417     }
418
419     if (data && data.length > 0) {
420       csv.push(this.props.columns.map(col => col.title || col.property).join(',') + "\r\n");
421       this.state.rows && this.state.rows.forEach((row: any) => {
422         csv.push(this.props.columns.map(col => row[col.property]).join(',') + "\r\n");
423       });
424       const properties = { type: "text/csv;charset=utf-8"  }; // Specify the file's mime-type.
425       try {
426         // Specify the filename using the File constructor, but ...
427         file = new File(csv, "export.csv", properties);
428       } catch (e) {
429         // ... fall back to the Blob constructor if that isn't supported.
430         file = new Blob(csv, properties);
431       }
432     }
433     if (!file) return;
434     var reader = new FileReader();
435     reader.onload = function (e) {
436       const dataUri = reader.result as any;
437       const link = document.createElement("a");
438       if (typeof link.download === 'string') {
439         link.href = dataUri;
440         link.download = "export.csv";
441
442         //Firefox requires the link to be in the body
443         document.body.appendChild(link);
444
445         //simulate click
446         link.click();
447
448         //remove the link when done
449         document.body.removeChild(link);
450       } else {
451         window.open(dataUri);
452       }
453     }
454     reader.readAsDataURL(file);
455
456     // const url = URL.createObjectURL(file);
457     // window.location.replace(url);
458   }
459 }
460
461 export type MaterialTableCtorType<TData extends {} = {}> = new () => React.Component<Omit<MaterialTableComponentProps<TData>, 'classes'>>;
462
463 export const MaterialTable = withStyles(styles)(MaterialTableComponent);
464 export default MaterialTable;