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