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
10 * http://www.apache.org/licenses/LICENSE-2.0
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
16 * ============LICENSE_END==========================================================================
18 import * as React from 'react';
19 import { withStyles, WithStyles, createStyles, Theme } from '@material-ui/core/styles';
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';
29 import { TableToolbar } from './tableToolbar';
30 import { EnhancedTableHead } from './tableHead';
31 import { EnhancedTableFilter } from './tableFilter';
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';
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[] };
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>>;
46 function desc(a: dataType, b: dataType, orderBy: string) {
47 if ((b[orderBy] || "") < (a[orderBy] || "")) {
50 if ((b[orderBy] || "") > (a[orderBy] || "")) {
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;
63 return stabilizedThis.map(el => el[0]);
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);
70 const styles = (theme: Theme) => createStyles({
73 marginTop: theme.spacing(3),
83 export type MaterialTableComponentState<TData = {}> = {
84 order: 'asc' | 'desc';
85 orderBy: string | null;
86 selected: any[] | null;
93 filter: { [property: string]: string };
96 export type TableApi = { forceRefresh?: () => Promise<void> };
98 type MaterialTableComponentBaseProps<TData> = WithStyles<typeof styles> & {
99 columns: ColumnModel<TData>[];
100 idProperty: keyof TData | ((data: TData) => React.Key);
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;
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;
120 type MaterialTableComponentProps<TData = {}> =
121 MaterialTableComponentPropsWithRows<TData> |
122 MaterialTableComponentPropsWithRequestData<TData> |
123 MaterialTableComponentPropsWithExternalState<TData>;
125 function isMaterialTableComponentPropsWithRows(props: MaterialTableComponentProps): props is MaterialTableComponentPropsWithRows {
126 return (props as MaterialTableComponentPropsWithRows).rows !== undefined && (props as MaterialTableComponentPropsWithRows).rows instanceof Array;
129 function isMaterialTableComponentPropsWithRequestData(props: MaterialTableComponentProps): props is MaterialTableComponentPropsWithRequestData {
130 return (props as MaterialTableComponentPropsWithRequestData).onRequestData !== undefined && (props as MaterialTableComponentPropsWithRequestData).onRequestData instanceof Function;
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
142 class MaterialTableComponent<TData extends {} = {}> extends React.Component<MaterialTableComponentProps, MaterialTableComponentState> {
144 constructor(props: MaterialTableComponentProps) {
147 const page = isMaterialTableComponentPropsWithRowsAndRequestData(this.props) ? this.props.page : 0;
148 const rowsPerPage = isMaterialTableComponentPropsWithRowsAndRequestData(this.props) ? this.props.rowsPerPage || 10 : 10;
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,
163 if (isMaterialTableComponentPropsWithRequestData(this.props)) {
166 if (this.props.tableApi) {
167 this.props.tableApi.forceRefresh = () => this.update();
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) }
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">
185 numSelected={selected && selected.length}
188 onSelectAllClick={this.handleSelectAllClick}
189 onRequestSort={this.onHandleRequestSort}
190 rowCount={rows.length}
191 enableSelection={this.props.enableSelection}
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);
202 onClick={event => this.handleClick(event, entry, entryId)}
204 aria-checked={isSelected}
207 selected={isSelected}
209 {this.props.enableSelection
210 ? <TableCell padding="checkbox" style={{ width: "50px" }}>
211 <Checkbox checked={isSelected} />
216 this.props.columns.map(
218 const style = col.width ? { width: col.width } : {};
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>
236 <TableRow style={{ height: 49 * emptyRows }}>
237 <TableCell colSpan={this.props.columns.length} />
244 rowsPerPageOptions={[5, 10, 20, 50]}
247 rowsPerPage={rowsPerPage}
249 backIconButtonProps={{
250 'aria-label': 'Previous Page',
252 nextIconButtonProps={{
253 'aria-label': 'Next Page',
255 onChangePage={this.onHandleChangePage}
256 onChangeRowsPerPage={this.onHandleChangeRowsPerPage}
262 static getDerivedStateFromProps(props: MaterialTableComponentProps, state: MaterialTableComponentState & { _rawRows: {}[] }): MaterialTableComponentState & { _rawRows: {}[] } {
263 if (isMaterialTableComponentPropsWithRowsAndRequestData(props)) {
268 orderBy: props.orderBy,
270 filter: props.filter,
271 loading: props.loading,
272 showFilter: props.showFilter,
274 rowsPerPage: props.rowsPerPage
276 } else if (isMaterialTableComponentPropsWithRows(props) && props.asynchronus && state._rawRows !== props.rows) {
277 const newState = MaterialTableComponent.updateRows(props, state);
281 _rawRows: props.rows || []
287 private static updateRows(props: MaterialTableComponentPropsWithRows, state: MaterialTableComponentState): { rows: {}[], total: number, page: number } {
289 const { page, rowsPerPage, order, orderBy, filter } = state;
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];
303 if (typeof exp === 'boolean') {
306 } else if (typeof exp === 'string') {
308 const valueAsString = value.toString();
309 if (exp.length === 0) return value;
311 const regex = new RegExp("\\*", "g");
312 const regex2 = new RegExp("\\?", "g");
314 const countStar = (exp.match(regex) || []).length;
315 const countQuestionmarks = (exp.match(regex2) || []).length;
317 if (countStar > 0 || countQuestionmarks > 0) {
318 let editableExpression = exp;
320 if (!exp.startsWith('*')) {
321 editableExpression = '^' + exp;
324 if (!exp.endsWith('*')) {
325 editableExpression = editableExpression + '$';
328 const expressionAsRegex = editableExpression.replace(/\*/g, ".*").replace(/\?/g, ".");
330 return valueAsString.match(new RegExp(expressionAsRegex, "g"));
332 else if (exp.includes('>=')) {
333 return Number(valueAsString) >= Number(exp.replace('>=', ''));
334 } else if (exp.includes('<=')) {
335 return Number(valueAsString) <= Number(exp.replace('<=', ''));
337 if (exp.includes('>')) {
338 return Number(valueAsString) > Number(exp.replace('>', ''));
339 } else if (exp.includes('<')) {
340 return Number(valueAsString) < Number(exp.replace('<', ''));
345 return (value == exp)
350 const rowCount = data.length;
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);
360 data = (orderBy && order
361 ? stableSort(data, getSorting(order, orderBy))
362 : data).slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage);
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 || {})
388 this.setState(response);
390 let updateResult = MaterialTableComponent.updateRows(this.props, this.state);
391 this.setState(updateResult);
395 private onFilterChanged = (property: string, filterTerm: string) => {
396 if (isMaterialTableComponentPropsWithRowsAndRequestData(this.props)) {
397 this.props.onFilterChanged(property, filterTerm);
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;
404 const filter = { ...this.state.filter, [property]: filterTerm };
410 private onHandleRequestSort = (event: React.SyntheticEvent, property: string) => {
411 if (isMaterialTableComponentPropsWithRowsAndRequestData(this.props)) {
412 this.props.onHandleRequestSort(property);
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;
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';
427 handleSelectAllClick: () => {};
429 private onHandleChangePage = (event: any | null, page: number) => {
430 if (isMaterialTableComponentPropsWithRowsAndRequestData(this.props)) {
431 this.props.onHandleChangePage(page);
439 private onHandleChangeRowsPerPage = (event: React.ChangeEvent<HTMLTextAreaElement | HTMLInputElement>) => {
440 if (isMaterialTableComponentPropsWithRowsAndRequestData(this.props)) {
441 this.props.onHandleChangeRowsPerPage(+(event && event.target.value));
444 const rowsPerPage = +(event && event.target.value);
445 if (rowsPerPage && rowsPerPage > 0) {
452 private isSelected(id: string | number): boolean {
453 let selected = this.state.selected || [];
454 const selectedIndex = selected.indexOf(id);
455 return (selectedIndex > -1);
458 private handleClick(event: any, rowData: TData, id: string | number): void {
459 if (this.props.onHandleClick instanceof Function) {
460 this.props.onHandleClick(event, rowData);
463 if (!this.props.enableSelection) {
466 let selected = this.state.selected || [];
467 const selectedIndex = selected.indexOf(id);
468 if (selectedIndex > -1) {
470 ...selected.slice(0, selectedIndex),
471 ...selected.slice(selectedIndex + 1)
485 private exportToCsv = async () => {
487 let data: dataType[] | null = null;
488 let csv: string[] = [];
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 || {})
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;
503 // table with local data
504 data = MaterialTableComponent.updateRows(this.props, this.state).rows;
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");
512 const properties = { type: "text/csv;charset=utf-8" }; // Specify the file's mime-type.
514 // Specify the filename using the File constructor, but ...
515 file = new File(csv, "export.csv", properties);
517 // ... fall back to the Blob constructor if that isn't supported.
518 file = new Blob(csv, properties);
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') {
528 link.download = "export.csv";
530 //Firefox requires the link to be in the body
531 document.body.appendChild(link);
536 //remove the link when done
537 document.body.removeChild(link);
539 window.open(dataUri);
542 reader.readAsDataURL(file);
544 // const url = URL.createObjectURL(file);
545 // window.location.replace(url);
549 export type MaterialTableCtorType<TData extends {} = {}> = new () => React.Component<Omit<MaterialTableComponentProps<TData>, 'classes'>>;
551 export const MaterialTable = withStyles(styles)(MaterialTableComponent);
552 export default MaterialTable;