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 TableContainer from '@material-ui/core/TableContainer';
25 import TablePagination from '@material-ui/core/TablePagination';
26 import TableRow from '@material-ui/core/TableRow';
27 import Paper from '@material-ui/core/Paper';
28 import Checkbox from '@material-ui/core/Checkbox';
30 import { TableToolbar } from './tableToolbar';
31 import { EnhancedTableHead } from './tableHead';
32 import { EnhancedTableFilter } from './tableFilter';
34 import { ColumnModel, ColumnType } from './columnModel';
35 import { Omit, Menu, makeStyles } from '@material-ui/core';
37 import { SvgIconProps } from '@material-ui/core/SvgIcon/SvgIcon';
39 import { DividerTypeMap } from '@material-ui/core/Divider';
40 import { MenuItemProps } from '@material-ui/core/MenuItem';
41 import { flexbox } from '@material-ui/system';
42 import { RowDisabled } from './utilities';
43 export { ColumnModel, ColumnType } from './columnModel';
45 type propType = string | number | null | undefined | (string | number)[];
46 type dataType = { [prop: string]: propType };
47 type resultType<TData = dataType> = { page: number, total: number, rows: TData[] };
49 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>>;
51 function regExpEscape(s: string) {
52 return s.replace(/[|\\{}()[\]^$+*?.]/g, '\\$&');
55 function wildcardCheck(input: string, pattern: string) {
56 if (!pattern) return true;
57 const regex = new RegExp(
58 (!pattern.startsWith('*') ? '^' : '') +
59 pattern.split(/\*+/).map(p => p.split(/\?+/).map(regExpEscape).join('.')).join('.*') +
60 (!pattern.endsWith('*') ? '$' : '')
62 return input.match(regex) !== null && input.match(regex)!.length >= 1;
65 function desc(a: dataType, b: dataType, orderBy: string) {
66 if ((b[orderBy] || "") < (a[orderBy] || "")) {
69 if ((b[orderBy] || "") > (a[orderBy] || "")) {
75 function stableSort(array: dataType[], cmp: (a: dataType, b: dataType) => number) {
76 const stabilizedThis = array.map((el, index) => [el, index]) as [dataType, number][];
77 stabilizedThis.sort((a, b) => {
78 const order = cmp(a[0], b[0]);
79 if (order !== 0) return order;
82 return stabilizedThis.map(el => el[0]);
85 function getSorting(order: 'asc' | 'desc' | null, orderBy: string) {
86 return order === 'desc' ? (a: dataType, b: dataType) => desc(a, b, orderBy) : (a: dataType, b: dataType) => -desc(a, b, orderBy);
89 const styles = (theme: Theme) => createStyles({
93 marginTop: theme.spacing(3),
95 boxSizing: "border-box",
97 flexDirection: "column",
107 const useTableRowExtStyles = makeStyles((theme: Theme) => createStyles({
109 color: "rgba(180, 180, 180, 0.7)",
113 type GetStatelessComponentProps<T> = T extends (props: infer P & { children?: React.ReactNode }) => any ? P : any;
114 type TableRowExtProps = GetStatelessComponentProps<typeof TableRow> & { disabled: boolean };
115 const TableRowExt : React.FC<TableRowExtProps> = (props) => {
116 const [disabled, setDisabled] = React.useState(true);
117 const classes = useTableRowExtStyles();
119 const onMouseDown = (ev: React.MouseEvent<HTMLElement>) => {
121 setDisabled(!disabled);
123 ev.stopPropagation();
124 } else if (props.disabled && disabled) {
126 ev.stopPropagation();
131 <TableRow {...{...props, color: props.disabled && disabled ? '#a0a0a0' : undefined , className: props.disabled && disabled ? classes.disabled : '', onMouseDown, onContextMenu: props.disabled && disabled ? onMouseDown : props.onContextMenu } } />
135 export type MaterialTableComponentState<TData = {}> = {
136 order: 'asc' | 'desc';
137 orderBy: string | null;
138 selected: any[] | null;
145 filter: { [property: string]: string };
148 export type TableApi = { forceRefresh?: () => Promise<void> };
150 type MaterialTableComponentBaseProps<TData> = WithStyles<typeof styles> & {
152 columns: ColumnModel<TData>[];
153 idProperty: keyof TData | ((data: TData) => React.Key);
156 stickyHeader?: boolean;
157 defaultSortOrder?: 'asc' | 'desc';
158 defaultSortColumn?: keyof TData;
159 enableSelection?: boolean;
160 disableSorting?: boolean;
161 disableFilter?: boolean;
162 customActionButtons?: { icon: React.ComponentType<SvgIconProps>, tooltip?: string, onClick: () => void, disabled?: boolean }[];
163 onHandleClick?(event: React.MouseEvent<HTMLTableRowElement>, rowData: TData): void;
164 createContextMenu?: (row: TData) => React.ReactElement<MenuItemProps | DividerTypeMap<{}, "hr">, React.ComponentType<MenuItemProps | DividerTypeMap<{}, "hr">>>[];
167 type MaterialTableComponentPropsWithRows<TData = {}> = MaterialTableComponentBaseProps<TData> & { rows: TData[]; asynchronus?: boolean; };
168 type MaterialTableComponentPropsWithRequestData<TData = {}> = MaterialTableComponentBaseProps<TData> & { onRequestData: DataCallback; tableApi?: TableApi; };
169 type MaterialTableComponentPropsWithExternalState<TData = {}> = MaterialTableComponentBaseProps<TData> & MaterialTableComponentState & {
170 onToggleFilter: () => void;
171 onFilterChanged: (property: string, filterTerm: string) => void;
172 onHandleChangePage: (page: number) => void;
173 onHandleChangeRowsPerPage: (rowsPerPage: number | null) => void;
174 onHandleRequestSort: (property: string) => void;
177 type MaterialTableComponentProps<TData = {}> =
178 MaterialTableComponentPropsWithRows<TData> |
179 MaterialTableComponentPropsWithRequestData<TData> |
180 MaterialTableComponentPropsWithExternalState<TData>;
182 function isMaterialTableComponentPropsWithRows(props: MaterialTableComponentProps): props is MaterialTableComponentPropsWithRows {
183 return (props as MaterialTableComponentPropsWithRows).rows !== undefined && (props as MaterialTableComponentPropsWithRows).rows instanceof Array;
186 function isMaterialTableComponentPropsWithRequestData(props: MaterialTableComponentProps): props is MaterialTableComponentPropsWithRequestData {
187 return (props as MaterialTableComponentPropsWithRequestData).onRequestData !== undefined && (props as MaterialTableComponentPropsWithRequestData).onRequestData instanceof Function;
190 function isMaterialTableComponentPropsWithRowsAndRequestData(props: MaterialTableComponentProps): props is MaterialTableComponentPropsWithExternalState {
191 const propsWithExternalState = (props as MaterialTableComponentPropsWithExternalState)
192 return propsWithExternalState.onFilterChanged instanceof Function ||
193 propsWithExternalState.onHandleChangePage instanceof Function ||
194 propsWithExternalState.onHandleChangeRowsPerPage instanceof Function ||
195 propsWithExternalState.onToggleFilter instanceof Function ||
196 propsWithExternalState.onHandleRequestSort instanceof Function
199 class MaterialTableComponent<TData extends {} = {}> extends React.Component<MaterialTableComponentProps, MaterialTableComponentState & { contextMenuInfo: { index: number; mouseX?: number; mouseY?: number }; }> {
201 constructor(props: MaterialTableComponentProps) {
204 const page = isMaterialTableComponentPropsWithRowsAndRequestData(this.props) ? this.props.page : 0;
205 const rowsPerPage = isMaterialTableComponentPropsWithRowsAndRequestData(this.props) ? this.props.rowsPerPage || 10 : 10;
208 contextMenuInfo: { index: -1 },
209 filter: isMaterialTableComponentPropsWithRowsAndRequestData(this.props) ? this.props.filter || {} : {},
210 showFilter: isMaterialTableComponentPropsWithRowsAndRequestData(this.props) ? this.props.showFilter : false,
211 loading: isMaterialTableComponentPropsWithRowsAndRequestData(this.props) ? this.props.loading : false,
212 order: isMaterialTableComponentPropsWithRowsAndRequestData(this.props) ? this.props.order : this.props.defaultSortOrder || 'asc',
213 orderBy: isMaterialTableComponentPropsWithRowsAndRequestData(this.props) ? this.props.orderBy : this.props.defaultSortColumn || null,
214 selected: isMaterialTableComponentPropsWithRowsAndRequestData(this.props) ? this.props.selected : null,
215 rows: isMaterialTableComponentPropsWithRows(this.props) && this.props.rows.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage) || [],
216 total: isMaterialTableComponentPropsWithRows(this.props) && this.props.rows.length || 0,
221 if (isMaterialTableComponentPropsWithRequestData(this.props)) {
224 if (this.props.tableApi) {
225 this.props.tableApi.forceRefresh = () => this.update();
229 render(): JSX.Element {
230 const { classes, columns } = this.props;
231 const { rows, total: rowCount, order, orderBy, selected, rowsPerPage, page, showFilter, filter } = this.state;
232 const emptyRows = rowsPerPage - Math.min(rowsPerPage, rowCount - page * rowsPerPage);
233 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;
234 const toggleFilter = isMaterialTableComponentPropsWithRowsAndRequestData(this.props) ? this.props.onToggleFilter : () => { !this.props.disableFilter && this.setState({ showFilter: !showFilter }, this.update) }
236 <Paper className={this.props.className ? `${classes.root} ${this.props.className}` : classes.root}>
237 <TableContainer className={classes.container}>
238 <TableToolbar tableId={this.props.tableId} numSelected={selected && selected.length} title={this.props.title} customActionButtons={this.props.customActionButtons} onExportToCsv={this.exportToCsv}
239 onToggleFilter={toggleFilter} />
240 <Table aria-label={this.props.tableId ? this.props.tableId : 'tableTitle'} stickyHeader={this.props.stickyHeader || false} >
243 numSelected={selected && selected.length}
246 onSelectAllClick={this.handleSelectAllClick}
247 onRequestSort={this.onHandleRequestSort}
248 rowCount={rows.length}
249 enableSelection={this.props.enableSelection}
252 {showFilter && <EnhancedTableFilter columns={columns} filter={filter} onFilterChanged={this.onFilterChanged} enableSelection={this.props.enableSelection} /> || null}
253 {rows // may need ordering here
254 .map((entry: TData & { [RowDisabled]?: boolean, [kex: string]: any }, index) => {
255 const entryId = getId(entry);
256 const isSelected = this.isSelected(entryId);
257 const contextMenu = (this.props.createContextMenu && this.state.contextMenuInfo.index === index && this.props.createContextMenu(entry)) || null;
262 if (this.props.createContextMenu) {
269 this.handleClick(event, entry, entryId);
271 onContextMenu={event => {
272 if (this.props.createContextMenu) {
273 event.preventDefault();
274 event.stopPropagation();
275 this.setState({ contextMenuInfo: { index, mouseX: event.clientX - 2, mouseY: event.clientY - 4 } });
279 aria-checked={isSelected}
280 aria-label="table-row"
283 selected={isSelected}
284 disabled={entry[RowDisabled] || false}
286 {this.props.enableSelection
287 ? <TableCell padding="checkbox" style={{ width: "50px", color: entry[RowDisabled] || false ? "inherit" : undefined } }>
288 <Checkbox checked={isSelected} />
293 this.props.columns.map(
295 const style = col.width ? { width: col.width } : {};
297 <TableCell style={ entry[RowDisabled] || false ? { ...style, color: "inherit" } : style } aria-label={col.title? col.title.toLowerCase().replace(/\s/g, "-") : col.property.toLowerCase().replace(/\s/g, "-")} key={col.property} align={col.type === ColumnType.numeric && !col.align ? "right" : col.align} >
298 {col.type === ColumnType.custom && col.customControl
299 ? <col.customControl className={col.className} style={col.style} rowData={entry} />
300 : col.type === ColumnType.boolean
301 ? <span className={col.className} style={col.style}>{col.labels ? col.labels[entry[col.property] ? "true" : "false"] : String(entry[col.property])}</span>
302 : <span className={col.className} style={col.style}>{String(entry[col.property])}</span>
309 {<Menu open={!!contextMenu} onClose={() => this.setState({ contextMenuInfo: { index: -1 } })} anchorReference="anchorPosition" keepMounted
310 anchorPosition={this.state.contextMenuInfo.mouseY != null && this.state.contextMenuInfo.mouseX != null ? { top: this.state.contextMenuInfo.mouseY, left: this.state.contextMenuInfo.mouseX } : undefined}>
317 <TableRow style={{ height: 49 * emptyRows }}>
318 <TableCell colSpan={this.props.columns.length} />
324 <TablePagination className={classes.pagination}
325 rowsPerPageOptions={[5, 10, 20, 50]}
328 rowsPerPage={rowsPerPage}
330 aria-label="table-pagination-footer"
331 backIconButtonProps={{
332 'aria-label': 'previous-page',
334 nextIconButtonProps={{
335 'aria-label': 'next-page',
337 onChangePage={this.onHandleChangePage}
338 onChangeRowsPerPage={this.onHandleChangeRowsPerPage}
344 static getDerivedStateFromProps(props: MaterialTableComponentProps, state: MaterialTableComponentState & { _rawRows: {}[] }): MaterialTableComponentState & { _rawRows: {}[] } {
345 if (isMaterialTableComponentPropsWithRowsAndRequestData(props)) {
350 orderBy: props.orderBy,
352 filter: props.filter,
353 loading: props.loading,
354 showFilter: props.showFilter,
356 rowsPerPage: props.rowsPerPage
358 } else if (isMaterialTableComponentPropsWithRows(props) && props.asynchronus && state._rawRows !== props.rows) {
359 const newState = MaterialTableComponent.updateRows(props, state);
363 _rawRows: props.rows || []
369 private static updateRows(props: MaterialTableComponentPropsWithRows, state: MaterialTableComponentState): { rows: {}[], total: number, page: number } {
371 let data = [...props.rows as dataType[] || []];
372 const columns = props.columns;
374 const { page, rowsPerPage, order, orderBy, filter } = state;
377 if (state.showFilter) {
378 Object.keys(filter).forEach(prop => {
379 const column = columns.find(c => c.property === prop);
380 const filterExpression = filter[prop];
382 if (!column) throw new Error("Filter for not existing column found.");
384 if (filterExpression != null) {
385 data = data.filter((val) => {
386 const dataValue = val[prop];
388 if (dataValue != null) {
390 if (column.type === ColumnType.boolean) {
392 const boolDataValue = JSON.parse(String(dataValue).toLowerCase());
393 const boolFilterExpression = JSON.parse(String(filterExpression).toLowerCase());
394 return boolDataValue == boolFilterExpression;
396 } else if (column.type === ColumnType.text) {
398 const valueAsString = String(dataValue);
399 const filterExpressionAsString = String(filterExpression).trim();
400 if (filterExpressionAsString.length === 0) return true;
401 return wildcardCheck(valueAsString, filterExpressionAsString);
403 } else if (column.type === ColumnType.numeric){
405 const valueAsNumber = Number(dataValue);
406 const filterExpressionAsString = String(filterExpression).trim();
407 if (filterExpressionAsString.length === 0 || isNaN(valueAsNumber)) return true;
409 if (filterExpressionAsString.startsWith('>=')) {
410 return valueAsNumber >= Number(filterExpressionAsString.substr(2).trim());
411 } else if (filterExpressionAsString.startsWith('<=')) {
412 return valueAsNumber <= Number(filterExpressionAsString.substr(2).trim());
413 } else if (filterExpressionAsString.startsWith('>')) {
414 return valueAsNumber > Number(filterExpressionAsString.substr(1).trim());
415 } else if (filterExpressionAsString.startsWith('<')) {
416 return valueAsNumber < Number(filterExpressionAsString.substr(1).trim());
418 } else if (column.type === ColumnType.date){
419 const valueAsString = String(dataValue);
421 const convertToDate = (valueAsString: string) => {
422 // time value needs to be padded
423 const hasTimeValue = /T\d{2,2}/.test(valueAsString);
424 const indexCollon = valueAsString.indexOf(':');
425 if (hasTimeValue && (indexCollon === -1 || indexCollon >= valueAsString.length-2)) {
426 valueAsString = indexCollon === -1
427 ? valueAsString + ":00"
428 : indexCollon === valueAsString.length-1
429 ? valueAsString + "00"
430 : valueAsString += "0"
432 return new Date(Date.parse(valueAsString));
436 const valueAsDate = new Date(Date.parse(dataValue));
437 const filterExpressionAsString = String(filterExpression).trim();
439 if (filterExpressionAsString.startsWith('>=')) {
440 return valueAsDate >= convertToDate(filterExpressionAsString.substr(2).trim());
441 } else if (filterExpressionAsString.startsWith('<=')) {
442 return valueAsDate <= convertToDate(filterExpressionAsString.substr(2).trim());
443 } else if (filterExpressionAsString.startsWith('>')) {
444 return valueAsDate > convertToDate(filterExpressionAsString.substr(1).trim());
445 } else if (filterExpressionAsString.startsWith('<')) {
446 return valueAsDate < convertToDate(filterExpressionAsString.substr(1).trim());
450 if (filterExpressionAsString.length === 0) return true;
451 return wildcardCheck(valueAsString, filterExpressionAsString);
456 return (dataValue == filterExpression)
462 const rowCount = data.length;
464 if (page > 0 && rowsPerPage * page > rowCount) { //if result is smaller than the currently shown page, new search and repaginate
465 let newPage = Math.floor(rowCount / rowsPerPage);
472 data = (orderBy && order
473 ? stableSort(data, getSorting(order, orderBy))
474 : data).slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage);
494 private async update() {
495 if (isMaterialTableComponentPropsWithRequestData(this.props)) {
496 const response = await Promise.resolve(
497 this.props.onRequestData(
498 this.state.page, this.state.rowsPerPage, this.state.orderBy, this.state.order, this.state.showFilter && this.state.filter || {})
500 this.setState(response);
502 let updateResult = MaterialTableComponent.updateRows(this.props, this.state);
503 this.setState(updateResult);
507 private onFilterChanged = (property: string, filterTerm: string) => {
508 if (isMaterialTableComponentPropsWithRowsAndRequestData(this.props)) {
509 this.props.onFilterChanged(property, filterTerm);
512 if (this.props.disableFilter) return;
513 const colDefinition = this.props.columns && this.props.columns.find(col => col.property === property);
514 if (colDefinition && colDefinition.disableFilter) return;
516 const filter = { ...this.state.filter, [property]: filterTerm };
522 private onHandleRequestSort = (event: React.SyntheticEvent, property: string) => {
523 if (isMaterialTableComponentPropsWithRowsAndRequestData(this.props)) {
524 this.props.onHandleRequestSort(property);
527 if (this.props.disableSorting) return;
528 const colDefinition = this.props.columns && this.props.columns.find(col => col.property === property);
529 if (colDefinition && colDefinition.disableSorting) return;
531 const orderBy = this.state.orderBy === property && this.state.order === 'desc' ? null : property;
532 const order = this.state.orderBy === property && this.state.order === 'asc' ? 'desc' : 'asc';
539 handleSelectAllClick: () => {};
541 private onHandleChangePage = (event: any | null, page: number) => {
542 if (isMaterialTableComponentPropsWithRowsAndRequestData(this.props)) {
543 this.props.onHandleChangePage(page);
551 private onHandleChangeRowsPerPage = (event: React.ChangeEvent<HTMLTextAreaElement | HTMLInputElement>) => {
552 if (isMaterialTableComponentPropsWithRowsAndRequestData(this.props)) {
553 this.props.onHandleChangeRowsPerPage(+(event && event.target.value));
556 const rowsPerPage = +(event && event.target.value);
557 if (rowsPerPage && rowsPerPage > 0) {
564 private isSelected(id: string | number): boolean {
565 let selected = this.state.selected || [];
566 const selectedIndex = selected.indexOf(id);
567 return (selectedIndex > -1);
570 private handleClick(event: any, rowData: TData, id: string | number): void {
571 if (this.props.onHandleClick instanceof Function) {
572 this.props.onHandleClick(event, rowData);
575 if (!this.props.enableSelection) {
578 let selected = this.state.selected || [];
579 const selectedIndex = selected.indexOf(id);
580 if (selectedIndex > -1) {
582 ...selected.slice(0, selectedIndex),
583 ...selected.slice(selectedIndex + 1)
597 private exportToCsv = async () => {
599 let data: dataType[] | null = null;
600 let csv: string[] = [];
602 if (isMaterialTableComponentPropsWithRequestData(this.props)) {
603 // table with extra request handler
604 this.setState({ loading: true });
605 const result = await Promise.resolve(
606 this.props.onRequestData(0, 1000, this.state.orderBy, this.state.order, this.state.showFilter && this.state.filter || {})
609 this.setState({ loading: true });
610 } else if (isMaterialTableComponentPropsWithRowsAndRequestData(this.props)) {
611 // table with generated handlers note: exports data shown on current page
612 data = this.props.rows;
615 // table with local data
616 data = MaterialTableComponent.updateRows(this.props, this.state).rows;
619 if (data && data.length > 0) {
620 csv.push(this.props.columns.map(col => col.title || col.property).join(',') + "\r\n");
621 this.state.rows && this.state.rows.forEach((row: any) => {
622 csv.push(this.props.columns.map(col => row[col.property]).join(',') + "\r\n");
624 const properties = { type: "text/csv;charset=utf-8" }; // Specify the file's mime-type.
626 // Specify the filename using the File constructor, but ...
627 file = new File(csv, "export.csv", properties);
629 // ... fall back to the Blob constructor if that isn't supported.
630 file = new Blob(csv, properties);
634 var reader = new FileReader();
635 reader.onload = function (e) {
636 const dataUri = reader.result as any;
637 const link = document.createElement("a");
638 if (typeof link.download === 'string') {
640 link.download = "export.csv";
642 //Firefox requires the link to be in the body
643 document.body.appendChild(link);
648 //remove the link when done
649 document.body.removeChild(link);
651 window.open(dataUri);
654 reader.readAsDataURL(file);
656 // const url = URL.createObjectURL(file);
657 // window.location.replace(url);
661 export type MaterialTableCtorType<TData extends {} = {}> = new () => React.Component<Omit<MaterialTableComponentProps<TData>, 'classes'>>;
663 export const MaterialTable = withStyles(styles)(MaterialTableComponent);
664 export default MaterialTable;