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