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