diff options
Diffstat (limited to 'sdnr/wt/odlux/framework/src/components')
19 files changed, 2600 insertions, 0 deletions
diff --git a/sdnr/wt/odlux/framework/src/components/errorDisplay.tsx b/sdnr/wt/odlux/framework/src/components/errorDisplay.tsx new file mode 100644 index 000000000..4cf63e927 --- /dev/null +++ b/sdnr/wt/odlux/framework/src/components/errorDisplay.tsx @@ -0,0 +1,126 @@ +/** + * ============LICENSE_START======================================================================== + * ONAP : ccsdk feature sdnr wt odlux + * ================================================================================================= + * Copyright (C) 2019 highstreet technologies GmbH Intellectual Property. All rights reserved. + * ================================================================================================= + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + * ============LICENSE_END========================================================================== + */ +import * as React from 'react'; +import { withStyles, WithStyles, createStyles, Theme } from '@material-ui/core/styles'; + +import Modal from '@material-ui/core/Modal'; +import Button from '@material-ui/core/Button'; +import Card from '@material-ui/core/Card'; +import CardActions from '@material-ui/core/CardActions'; +import CardContent from '@material-ui/core/CardContent'; +import Typography from '@material-ui/core/Typography'; + +import { ClearErrorInfoAction, RemoveErrorInfoAction } from '../actions/errorActions'; + +import connect, { Connect } from '../flux/connect'; + +const styles = (theme: Theme) => createStyles({ + modal: { + display: "flex", + alignItems: "center", + justifyContent: "center", + }, + paper: { + width: theme.spacing(50), + backgroundColor: theme.palette.background.paper, + boxShadow: theme.shadows[5], + padding: theme.spacing(4), + }, + card: { + minWidth: 275, + }, + bullet: { + display: 'inline-block', + margin: '0 2px', + transform: 'scale(0.8)', + }, + title: { + marginBottom: 16, + fontSize: 14, + }, + pos: { + marginBottom: 12, + }, +}); + +type ErrorDisplayProps = WithStyles<typeof styles> & Connect; + +// function getModalStyle() { +// const top = 50 + rand(); +// const left = 50 + rand(); + +// return { +// top: `${ top }%`, +// left: `${ left }%`, +// transform: `translate(-${ top }%, -${ left }%)`, +// }; +// } + +/** + * Represents a compnent for formaing and displaying errors. + */ +class ErrorDisplayComponent extends React.Component<ErrorDisplayProps> { + constructor(props: ErrorDisplayProps) { + super(props); + } + + render(): JSX.Element { + const { classes, state } = this.props; + const errorInfo = state.framework.applicationState.errors.length && state.framework.applicationState.errors[state.framework.applicationState.errors.length - 1]; + return ( + <Modal className={classes.modal} + aria-labelledby="simple-modal-title" + aria-describedby="simple-modal-description" + open={state.framework.applicationState.errors && state.framework.applicationState.errors.length > 0} + onClose={() => this.props.dispatch(new ClearErrorInfoAction())} + > + {errorInfo && + <div className={classes.paper}> + <Card className={classes.card}> + <CardContent> + <Typography className={classes.title} color="textSecondary"> + {errorInfo.title != null ? errorInfo.title : "Something went wrong."} + </Typography> + <Typography variant="h5" component="h2"> + {errorInfo.error && errorInfo.error.toString()} + </Typography> + <Typography className={classes.pos} color="textSecondary"> + {errorInfo.message && errorInfo.message.toString()} + </Typography> + <Typography component="p"> + {errorInfo.info && errorInfo.info.componentStack && errorInfo.info.componentStack.split('\n').map(line => { + return [line, <br />]; + })} + {errorInfo.info && errorInfo.info.extra && errorInfo.info.extra.split('\n').map(line => { + return [line, <br />]; + })} + </Typography> + </CardContent> + <CardActions> + <Button size="small" onClick={() => this.props.dispatch(new RemoveErrorInfoAction(errorInfo))} >Close</Button> + </CardActions> + </Card> + </div> || <div></div> + } + </Modal> + ); + } +} + +export const ErrorDisplay = withStyles(styles)(connect()(ErrorDisplayComponent)); +export default ErrorDisplay;
\ No newline at end of file diff --git a/sdnr/wt/odlux/framework/src/components/logo.tsx b/sdnr/wt/odlux/framework/src/components/logo.tsx new file mode 100644 index 000000000..470eb9620 --- /dev/null +++ b/sdnr/wt/odlux/framework/src/components/logo.tsx @@ -0,0 +1,98 @@ +/** + * ============LICENSE_START======================================================================== + * ONAP : ccsdk feature sdnr wt odlux + * ================================================================================================= + * Copyright (C) 2019 highstreet technologies GmbH Intellectual Property. All rights reserved. + * ================================================================================================= + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + * ============LICENSE_END========================================================================== + */ +/****************************************************************************** + * Copyright 2018 highstreet technologies GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *****************************************************************************/ + +import * as React from 'react'; +import { withRouter, RouteComponentProps } from 'react-router-dom'; + +import { WithStyles, withStyles, createStyles, Theme } from '@material-ui/core/styles'; // infra for styling + + +import defaultLogo from '../assets/images/defaultLogo.svg'; + +const styles = (theme: Theme) => createStyles({ + headerLogo: { + backgroundImage: "url(" + (theme.design && theme.design.url || defaultLogo) + ")", + backgroundColor: theme.palette.primary.main, + backgroundRepeat: "no-repeat", + backgroundSize: "auto " + (theme.design && theme.design.logoHeight || 70) + "px", + height: theme.design && theme.design.logoHeight || 70, + width: theme.design ? theme.design.width / theme.design.height * theme.design.logoHeight : 220 + } +}); + +type LogoProps = RouteComponentProps<{ id: string }> & WithStyles<typeof styles>; +interface ILogoState { + windowWidth: number +} + +class LogoComponent extends React.Component<LogoProps, ILogoState> { + + private hideLogoWhenWindowWidthIsLower: number = 800; + + constructor(props: LogoProps) { + super(props); + this.state = { + windowWidth: 0 + }; + this.updateWindowDimensions = this.updateWindowDimensions.bind(this); + } + + componentDidMount(): void { + this.updateWindowDimensions(); + window.addEventListener('resize', this.updateWindowDimensions); + }; + componentWillUnmount(): void { + window.removeEventListener('resize', this.updateWindowDimensions); + }; + updateWindowDimensions(): void { + this.setState({ windowWidth: window.innerWidth }); + } + + render(): JSX.Element { + let div: JSX.Element = <div />; + if (this.state.windowWidth >= this.hideLogoWhenWindowWidthIsLower) { + div = <div className={this.props.classes.headerLogo} />; + } else { + console.info([ + "Logo hidden, because browser window width (", + this.state.windowWidth, + "px) is lower thershold (", + this.hideLogoWhenWindowWidthIsLower, + "px)."].join('')); + } + return div; + } +} + +export const Logo = withStyles(styles)(withRouter(LogoComponent)); +export default Logo; diff --git a/sdnr/wt/odlux/framework/src/components/material-table/columnModel.ts b/sdnr/wt/odlux/framework/src/components/material-table/columnModel.ts new file mode 100644 index 000000000..c05142084 --- /dev/null +++ b/sdnr/wt/odlux/framework/src/components/material-table/columnModel.ts @@ -0,0 +1,54 @@ +/** + * ============LICENSE_START======================================================================== + * ONAP : ccsdk feature sdnr wt odlux + * ================================================================================================= + * Copyright (C) 2019 highstreet technologies GmbH Intellectual Property. All rights reserved. + * ================================================================================================= + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + * ============LICENSE_END========================================================================== + */ +
+import * as React from 'react';
+
+export enum ColumnType {
+ text,
+ numeric,
+ boolean,
+ custom
+}
+
+type CustomControl<TData> = {
+ className?: string;
+ style?: React.CSSProperties;
+ rowData: TData;
+}
+
+export type ColumnModel<TData> = {
+ title?: string;
+ disablePadding?: boolean;
+ width?: string | number;
+ className?: string;
+ style?: React.CSSProperties;
+ align?: 'inherit' | 'left' | 'center' | 'right' | 'justify';
+ disableSorting?: boolean;
+ disableFilter?: boolean;
+} & ({
+ property: string;
+ type: ColumnType.custom;
+ customControl: React.ComponentType<CustomControl<TData>>;
+} | {
+ property: keyof TData;
+ type: ColumnType.boolean;
+ labels?: { "true": string, "false": string };
+} | {
+ property: keyof TData;
+ type?: ColumnType.numeric | ColumnType.text;
+});
\ No newline at end of file diff --git a/sdnr/wt/odlux/framework/src/components/material-table/index.tsx b/sdnr/wt/odlux/framework/src/components/material-table/index.tsx new file mode 100644 index 000000000..3e31c5e03 --- /dev/null +++ b/sdnr/wt/odlux/framework/src/components/material-table/index.tsx @@ -0,0 +1,552 @@ +/** + * ============LICENSE_START======================================================================== + * ONAP : ccsdk feature sdnr wt odlux + * ================================================================================================= + * Copyright (C) 2019 highstreet technologies GmbH Intellectual Property. All rights reserved. + * ================================================================================================= + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + * ============LICENSE_END========================================================================== + */ +import * as React from 'react'; +import { withStyles, WithStyles, createStyles, Theme } from '@material-ui/core/styles'; + +import Table from '@material-ui/core/Table'; +import TableBody from '@material-ui/core/TableBody'; +import TableCell from '@material-ui/core/TableCell'; +import TablePagination from '@material-ui/core/TablePagination'; +import TableRow from '@material-ui/core/TableRow'; +import Paper from '@material-ui/core/Paper'; +import Checkbox from '@material-ui/core/Checkbox'; + +import { TableToolbar } from './tableToolbar'; +import { EnhancedTableHead } from './tableHead'; +import { EnhancedTableFilter } from './tableFilter'; + +import { ColumnModel, ColumnType } from './columnModel'; +import { Omit } from '@material-ui/core'; +import { SvgIconProps } from '@material-ui/core/SvgIcon/SvgIcon'; +import { replaceHyphen } from '../../utilities/yangHelper'; +import { string } from 'prop-types'; +export { ColumnModel, ColumnType } from './columnModel'; + +type propType = string | number | null | undefined | (string | number)[]; +type dataType = { [prop: string]: propType }; +type resultType<TData = dataType> = { page: number, total: number, rows: TData[] }; + +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>>; + +function desc(a: dataType, b: dataType, orderBy: string) { + if ((b[orderBy] || "") < (a[orderBy] || "")) { + return -1; + } + if ((b[orderBy] || "") > (a[orderBy] || "")) { + return 1; + } + return 0; +} + +function stableSort(array: dataType[], cmp: (a: dataType, b: dataType) => number) { + const stabilizedThis = array.map((el, index) => [el, index]) as [dataType, number][]; + stabilizedThis.sort((a, b) => { + const order = cmp(a[0], b[0]); + if (order !== 0) return order; + return a[1] - b[1]; + }); + return stabilizedThis.map(el => el[0]); +} + +function getSorting(order: 'asc' | 'desc' | null, orderBy: string) { + return order === 'desc' ? (a: dataType, b: dataType) => desc(a, b, orderBy) : (a: dataType, b: dataType) => -desc(a, b, orderBy); +} + +const styles = (theme: Theme) => createStyles({ + root: { + width: '100%', + marginTop: theme.spacing(3), + }, + table: { + minWidth: 1020, + }, + tableWrapper: { + overflowX: 'auto', + }, +}); + +export type MaterialTableComponentState<TData = {}> = { + order: 'asc' | 'desc'; + orderBy: string | null; + selected: any[] | null; + rows: TData[]; + total: number; + page: number; + rowsPerPage: number; + loading: boolean; + showFilter: boolean; + filter: { [property: string]: string }; +}; + +export type TableApi = { forceRefresh?: () => Promise<void> }; + +type MaterialTableComponentBaseProps<TData> = WithStyles<typeof styles> & { + columns: ColumnModel<TData>[]; + idProperty: keyof TData | ((data: TData) => React.Key); + tableId?: string; + title?: string; + enableSelection?: boolean; + disableSorting?: boolean; + disableFilter?: boolean; + customActionButtons?: { icon: React.ComponentType<SvgIconProps>, tooltip?: string, onClick: () => void }[]; + onHandleClick?(event: React.MouseEvent<HTMLTableRowElement>, rowData: TData): void; +}; + +type MaterialTableComponentPropsWithRows<TData = {}> = MaterialTableComponentBaseProps<TData> & { rows: TData[]; asynchronus?: boolean; }; +type MaterialTableComponentPropsWithRequestData<TData = {}> = MaterialTableComponentBaseProps<TData> & { onRequestData: DataCallback; tableApi?: TableApi; }; +type MaterialTableComponentPropsWithExternalState<TData = {}> = MaterialTableComponentBaseProps<TData> & MaterialTableComponentState & { + onToggleFilter: () => void; + onFilterChanged: (property: string, filterTerm: string) => void; + onHandleChangePage: (page: number) => void; + onHandleChangeRowsPerPage: (rowsPerPage: number | null) => void; + onHandleRequestSort: (property: string) => void; +}; + +type MaterialTableComponentProps<TData = {}> = + MaterialTableComponentPropsWithRows<TData> | + MaterialTableComponentPropsWithRequestData<TData> | + MaterialTableComponentPropsWithExternalState<TData>; + +function isMaterialTableComponentPropsWithRows(props: MaterialTableComponentProps): props is MaterialTableComponentPropsWithRows { + return (props as MaterialTableComponentPropsWithRows).rows !== undefined && (props as MaterialTableComponentPropsWithRows).rows instanceof Array; +} + +function isMaterialTableComponentPropsWithRequestData(props: MaterialTableComponentProps): props is MaterialTableComponentPropsWithRequestData { + return (props as MaterialTableComponentPropsWithRequestData).onRequestData !== undefined && (props as MaterialTableComponentPropsWithRequestData).onRequestData instanceof Function; +} + +function isMaterialTableComponentPropsWithRowsAndRequestData(props: MaterialTableComponentProps): props is MaterialTableComponentPropsWithExternalState { + const propsWithExternalState = (props as MaterialTableComponentPropsWithExternalState) + return propsWithExternalState.onFilterChanged instanceof Function || + propsWithExternalState.onHandleChangePage instanceof Function || + propsWithExternalState.onHandleChangeRowsPerPage instanceof Function || + propsWithExternalState.onToggleFilter instanceof Function || + propsWithExternalState.onHandleRequestSort instanceof Function +} + +class MaterialTableComponent<TData extends {} = {}> extends React.Component<MaterialTableComponentProps, MaterialTableComponentState> { + + constructor(props: MaterialTableComponentProps) { + super(props); + + const page = isMaterialTableComponentPropsWithRowsAndRequestData(this.props) ? this.props.page : 0; + const rowsPerPage = isMaterialTableComponentPropsWithRowsAndRequestData(this.props) ? this.props.rowsPerPage || 10 : 10; + + this.state = { + filter: isMaterialTableComponentPropsWithRowsAndRequestData(this.props) ? this.props.filter || {} : {}, + showFilter: isMaterialTableComponentPropsWithRowsAndRequestData(this.props) ? this.props.showFilter : false, + loading: isMaterialTableComponentPropsWithRowsAndRequestData(this.props) ? this.props.loading : false, + order: isMaterialTableComponentPropsWithRowsAndRequestData(this.props) ? this.props.order : 'asc', + orderBy: isMaterialTableComponentPropsWithRowsAndRequestData(this.props) ? this.props.orderBy : null, + selected: isMaterialTableComponentPropsWithRowsAndRequestData(this.props) ? this.props.selected : null, + rows: isMaterialTableComponentPropsWithRows(this.props) && this.props.rows.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage) || [], + total: isMaterialTableComponentPropsWithRows(this.props) && this.props.rows.length || 0, + page, + rowsPerPage, + }; + + if (isMaterialTableComponentPropsWithRequestData(this.props)) { + this.update(); + + if (this.props.tableApi) { + this.props.tableApi.forceRefresh = () => this.update(); + } + } + } + render(): JSX.Element { + const { classes, columns } = this.props; + const { rows, total: rowCount, order, orderBy, selected, rowsPerPage, page, showFilter, filter } = this.state; + const emptyRows = rowsPerPage - Math.min(rowsPerPage, rowCount - page * rowsPerPage); + 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; + const toggleFilter = isMaterialTableComponentPropsWithRowsAndRequestData(this.props) ? this.props.onToggleFilter : () => { !this.props.disableFilter && this.setState({ showFilter: !showFilter }, this.update) } + return ( + <Paper className={classes.root}> + <TableToolbar tableId={this.props.tableId} numSelected={selected && selected.length} title={this.props.title} customActionButtons={this.props.customActionButtons} onExportToCsv={this.exportToCsv} + onToggleFilter={toggleFilter} /> + <div className={classes.tableWrapper}> + <Table className={classes.table} aria-labelledby="tableTitle"> + <EnhancedTableHead + columns={columns} + numSelected={selected && selected.length} + order={order} + orderBy={orderBy} + onSelectAllClick={this.handleSelectAllClick} + onRequestSort={this.onHandleRequestSort} + rowCount={rows.length} + enableSelection={this.props.enableSelection} + /> + <TableBody> + {showFilter && <EnhancedTableFilter columns={columns} filter={filter} onFilterChanged={this.onFilterChanged} enableSelection={this.props.enableSelection} /> || null} + {rows // may need ordering here + .map((entry: TData & { [key: string]: any }) => { + const entryId = getId(entry); + const isSelected = this.isSelected(entryId); + return ( + <TableRow + hover + onClick={event => this.handleClick(event, entry, entryId)} + role="checkbox" + aria-checked={isSelected} + tabIndex={-1} + key={entryId} + selected={isSelected} + > + {this.props.enableSelection + ? <TableCell padding="checkbox" style={{ width: "50px" }}> + <Checkbox checked={isSelected} /> + </TableCell> + : null + } + { + this.props.columns.map( + col => { + const style = col.width ? { width: col.width } : {}; + return ( + <TableCell key={col.property} align={col.type === ColumnType.numeric && !col.align ? "right" : col.align} style={style}> + {col.type === ColumnType.custom && col.customControl + ? <col.customControl className={col.className} style={col.style} rowData={entry} /> + : col.type === ColumnType.boolean + ? <span className={col.className} style={col.style}>{col.labels ? col.labels[entry[col.property] ? "true" : "false"] : String(entry[col.property])}</span> + : <span className={col.className} style={col.style}>{String(entry[col.property])}</span> + } + </TableCell> + ); + } + ) + } + </TableRow> + ); + })} + {emptyRows > 0 && ( + <TableRow style={{ height: 49 * emptyRows }}> + <TableCell colSpan={this.props.columns.length} /> + </TableRow> + )} + </TableBody> + </Table> + </div> + <TablePagination + rowsPerPageOptions={[5, 10, 20, 50]} + component="div" + count={rowCount} + rowsPerPage={rowsPerPage} + page={page} + backIconButtonProps={{ + 'aria-label': 'Previous Page', + }} + nextIconButtonProps={{ + 'aria-label': 'Next Page', + }} + onChangePage={this.onHandleChangePage} + onChangeRowsPerPage={this.onHandleChangeRowsPerPage} + /> + </Paper> + ); + } + + static getDerivedStateFromProps(props: MaterialTableComponentProps, state: MaterialTableComponentState & { _rawRows: {}[] }): MaterialTableComponentState & { _rawRows: {}[] } { + if (isMaterialTableComponentPropsWithRowsAndRequestData(props)) { + return { + ...state, + rows: props.rows, + total: props.total, + orderBy: props.orderBy, + order: props.order, + filter: props.filter, + loading: props.loading, + showFilter: props.showFilter, + page: props.page, + rowsPerPage: props.rowsPerPage + } + } else if (isMaterialTableComponentPropsWithRows(props) && props.asynchronus && state._rawRows !== props.rows) { + const newState = MaterialTableComponent.updateRows(props, state); + return { + ...state, + ...newState, + _rawRows: props.rows || [] + }; + } + return state; + } + + private static updateRows(props: MaterialTableComponentPropsWithRows, state: MaterialTableComponentState): { rows: {}[], total: number, page: number } { + + const { page, rowsPerPage, order, orderBy, filter } = state; + + try { + let data: dataType[] = props.rows || []; + let filtered = false; + if (state.showFilter) { + Object.keys(filter).forEach(prop => { + const exp = filter[prop]; + filtered = filtered || exp !== undefined; + data = exp !== undefined ? data.filter((val) => { + const value = val[prop]; + + if (value) { + + if (typeof exp === 'boolean') { + return value == exp; + + } else if (typeof exp === 'string') { + + const valueAsString = value.toString(); + if (exp.length === 0) return value; + + const regex = new RegExp("\\*", "g"); + const regex2 = new RegExp("\\?", "g"); + + const countStar = (exp.match(regex) || []).length; + const countQuestionmarks = (exp.match(regex2) || []).length; + + if (countStar > 0 || countQuestionmarks > 0) { + let editableExpression = exp; + + if (!exp.startsWith('*')) { + editableExpression = '^' + exp; + } + + if (!exp.endsWith('*')) { + editableExpression = editableExpression + '$'; + } + + const expressionAsRegex = editableExpression.replace(/\*/g, ".*").replace(/\?/g, "."); + + return valueAsString.match(new RegExp(expressionAsRegex, "g")); + } + else if (exp.includes('>=')) { + return Number(valueAsString) >= Number(exp.replace('>=', '')); + } else if (exp.includes('<=')) { + return Number(valueAsString) <= Number(exp.replace('<=', '')); + } else + if (exp.includes('>')) { + return Number(valueAsString) > Number(exp.replace('>', '')); + } else if (exp.includes('<')) { + return Number(valueAsString) < Number(exp.replace('<', '')); + } + } + } + + return (value == exp) + }) : data; + }); + } + + const rowCount = data.length; + + if (page > 0 && rowsPerPage * page > rowCount) { //if result is smaller than the currently shown page, new search and repaginate + let newPage = Math.floor(rowCount / rowsPerPage); + return { + rows: data, + total: rowCount, + page: newPage + }; + } else { + data = (orderBy && order + ? stableSort(data, getSorting(order, orderBy)) + : data).slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage); + + return { + rows: data, + total: rowCount, + page: page + }; + } + + + } catch (e) { + console.error(e); + return { + rows: [], + total: 0, + page: page + } + } + } + + private async update() { + if (isMaterialTableComponentPropsWithRequestData(this.props)) { + const response = await Promise.resolve( + this.props.onRequestData( + this.state.page, this.state.rowsPerPage, this.state.orderBy, this.state.order, this.state.showFilter && this.state.filter || {}) + ); + this.setState(response); + } else { + let updateResult = MaterialTableComponent.updateRows(this.props, this.state); + this.setState(updateResult); + } + } + + private onFilterChanged = (property: string, filterTerm: string) => { + if (isMaterialTableComponentPropsWithRowsAndRequestData(this.props)) { + this.props.onFilterChanged(property, filterTerm); + return; + } + if (this.props.disableFilter) return; + const colDefinition = this.props.columns && this.props.columns.find(col => col.property === property); + if (colDefinition && colDefinition.disableFilter) return; + + const filter = { ...this.state.filter, [property]: filterTerm }; + this.setState({ + filter + }, this.update); + }; + + private onHandleRequestSort = (event: React.SyntheticEvent, property: string) => { + if (isMaterialTableComponentPropsWithRowsAndRequestData(this.props)) { + this.props.onHandleRequestSort(property); + return; + } + if (this.props.disableSorting) return; + const colDefinition = this.props.columns && this.props.columns.find(col => col.property === property); + if (colDefinition && colDefinition.disableSorting) return; + + const orderBy = this.state.orderBy === property && this.state.order === 'desc' ? null : property; + const order = this.state.orderBy === property && this.state.order === 'asc' ? 'desc' : 'asc'; + this.setState({ + order, + orderBy + }, this.update); + }; + + handleSelectAllClick: () => {}; + + private onHandleChangePage = (event: any | null, page: number) => { + if (isMaterialTableComponentPropsWithRowsAndRequestData(this.props)) { + this.props.onHandleChangePage(page); + return; + } + this.setState({ + page + }, this.update); + }; + + private onHandleChangeRowsPerPage = (event: React.ChangeEvent<HTMLTextAreaElement | HTMLInputElement>) => { + if (isMaterialTableComponentPropsWithRowsAndRequestData(this.props)) { + this.props.onHandleChangeRowsPerPage(+(event && event.target.value)); + return; + } + const rowsPerPage = +(event && event.target.value); + if (rowsPerPage && rowsPerPage > 0) { + this.setState({ + rowsPerPage + }, this.update); + } + }; + + private isSelected(id: string | number): boolean { + let selected = this.state.selected || []; + const selectedIndex = selected.indexOf(id); + return (selectedIndex > -1); + } + + private handleClick(event: any, rowData: TData, id: string | number): void { + if (this.props.onHandleClick instanceof Function) { + this.props.onHandleClick(event, rowData); + return; + } + if (!this.props.enableSelection) { + return; + } + let selected = this.state.selected || []; + const selectedIndex = selected.indexOf(id); + if (selectedIndex > -1) { + selected = [ + ...selected.slice(0, selectedIndex), + ...selected.slice(selectedIndex + 1) + ]; + } else { + selected = [ + ...selected, + id + ]; + } + this.setState({ + selected + }); + } + + + private exportToCsv = async () => { + let file; + let data: dataType[] | null = null; + let csv: string[] = []; + + if (isMaterialTableComponentPropsWithRequestData(this.props)) { + // table with extra request handler + this.setState({ loading: true }); + const result = await Promise.resolve( + this.props.onRequestData(0, 1000, this.state.orderBy, this.state.order, this.state.showFilter && this.state.filter || {}) + ); + data = result.rows; + this.setState({ loading: true }); + } else if (isMaterialTableComponentPropsWithRowsAndRequestData(this.props)) { + // table with generated handlers note: exports data shown on current page + data = this.props.rows; + } + else { + // table with local data + data = MaterialTableComponent.updateRows(this.props, this.state).rows; + } + + if (data && data.length > 0) { + csv.push(this.props.columns.map(col => col.title || col.property).join(',') + "\r\n"); + this.state.rows && this.state.rows.forEach((row: any) => { + csv.push(this.props.columns.map(col => row[col.property]).join(',') + "\r\n"); + }); + const properties = { type: "text/csv;charset=utf-8" }; // Specify the file's mime-type. + try { + // Specify the filename using the File constructor, but ... + file = new File(csv, "export.csv", properties); + } catch (e) { + // ... fall back to the Blob constructor if that isn't supported. + file = new Blob(csv, properties); + } + } + if (!file) return; + var reader = new FileReader(); + reader.onload = function (e) { + const dataUri = reader.result as any; + const link = document.createElement("a"); + if (typeof link.download === 'string') { + link.href = dataUri; + link.download = "export.csv"; + + //Firefox requires the link to be in the body + document.body.appendChild(link); + + //simulate click + link.click(); + + //remove the link when done + document.body.removeChild(link); + } else { + window.open(dataUri); + } + } + reader.readAsDataURL(file); + + // const url = URL.createObjectURL(file); + // window.location.replace(url); + } +} + +export type MaterialTableCtorType<TData extends {} = {}> = new () => React.Component<Omit<MaterialTableComponentProps<TData>, 'classes'>>; + +export const MaterialTable = withStyles(styles)(MaterialTableComponent); +export default MaterialTable;
\ No newline at end of file diff --git a/sdnr/wt/odlux/framework/src/components/material-table/tableFilter.tsx b/sdnr/wt/odlux/framework/src/components/material-table/tableFilter.tsx new file mode 100644 index 000000000..737ea85f9 --- /dev/null +++ b/sdnr/wt/odlux/framework/src/components/material-table/tableFilter.tsx @@ -0,0 +1,88 @@ +/** + * ============LICENSE_START======================================================================== + * ONAP : ccsdk feature sdnr wt odlux + * ================================================================================================= + * Copyright (C) 2019 highstreet technologies GmbH Intellectual Property. All rights reserved. + * ================================================================================================= + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + * ============LICENSE_END========================================================================== + */ + +import * as React from 'react'; +import { ColumnModel, ColumnType } from './columnModel'; +import { withStyles, WithStyles, createStyles, Theme } from '@material-ui/core/styles'; + + +import TableCell from '@material-ui/core/TableCell'; +import TableRow from '@material-ui/core/TableRow'; +import Input from '@material-ui/core/Input'; +import { Select, FormControl, InputLabel, MenuItem } from '@material-ui/core'; + + +const styles = (theme: Theme) => createStyles({ + container: { + display: 'flex', + flexWrap: 'wrap', + }, + input: { + margin: theme.spacing(1), + }, +}); + +interface IEnhancedTableFilterComponentProps extends WithStyles<typeof styles> { + onFilterChanged: (property: string, filterTerm: string) => void; + filter: { [property: string]: string }; + columns: ColumnModel<{}>[]; + enableSelection?: boolean; +} + +class EnhancedTableFilterComponent extends React.Component<IEnhancedTableFilterComponentProps> { + createFilterHandler = (property: string) => (event: React.ChangeEvent<HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement>) => { + this.props.onFilterChanged && this.props.onFilterChanged(property, event.target.value); + }; + + render() { + const { columns, filter, classes } = this.props; + return ( + <TableRow> + {this.props.enableSelection + ? <TableCell padding="checkbox" style={{ width: "50px" }}> + </TableCell> + : null + } + {columns.map(col => { + const style = col.width ? { width: col.width } : {}; + return ( + <TableCell + key={col.property} + padding={col.disablePadding ? 'none' : 'default'} + style={style} + > + {col.disableFilter || (col.type === ColumnType.custom) + ? null + : (col.type === ColumnType.boolean) + ? <Select className={classes.input} value={filter[col.property] !== undefined ? filter[col.property] : ''} onChange={this.createFilterHandler(col.property)} inputProps={{ name: `${col.property}-bool`, id: `${col.property}-bool` }} > + <MenuItem value={undefined}> + <em>None</em> + </MenuItem> + <MenuItem value={true as any as string}>{col.labels ? col.labels["true"] : "true"}</MenuItem> + <MenuItem value={false as any as string}>{col.labels ? col.labels["false"] : "false"}</MenuItem> + </Select> + : <Input className={classes.input} inputProps={{ 'aria-label': 'Filter' }} value={filter[col.property] || ''} onChange={this.createFilterHandler(col.property)} />} + </TableCell> + ); + }, this)} + </TableRow> + ); + } +} + +export const EnhancedTableFilter = withStyles(styles)(EnhancedTableFilterComponent);
\ No newline at end of file diff --git a/sdnr/wt/odlux/framework/src/components/material-table/tableHead.tsx b/sdnr/wt/odlux/framework/src/components/material-table/tableHead.tsx new file mode 100644 index 000000000..428f4cf3f --- /dev/null +++ b/sdnr/wt/odlux/framework/src/components/material-table/tableHead.tsx @@ -0,0 +1,101 @@ +/** + * ============LICENSE_START======================================================================== + * ONAP : ccsdk feature sdnr wt odlux + * ================================================================================================= + * Copyright (C) 2019 highstreet technologies GmbH Intellectual Property. All rights reserved. + * ================================================================================================= + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + * ============LICENSE_END========================================================================== + */ + +import * as React from 'react'; +import { ColumnModel, ColumnType } from './columnModel'; +import { withStyles, WithStyles, createStyles, Theme } from '@material-ui/core/styles'; + +import TableSortLabel from '@material-ui/core/TableSortLabel'; +import TableCell from '@material-ui/core/TableCell'; +import TableHead from '@material-ui/core/TableHead'; +import TableRow from '@material-ui/core/TableRow'; +import Checkbox from '@material-ui/core/Checkbox'; +import Tooltip from '@material-ui/core/Tooltip'; + +interface IEnhancedTableHeadComponentProps { + numSelected: number | null; + onRequestSort: (event: React.SyntheticEvent, property: string) => void; + onSelectAllClick: () => void; + order: 'asc' | 'desc'; + orderBy: string | null; + rowCount: number; + columns: ColumnModel<{}>[]; + enableSelection?: boolean; +} + +class EnhancedTableHeadComponent extends React.Component<IEnhancedTableHeadComponentProps> { + createSortHandler = (property: string) => (event: React.SyntheticEvent) => { + this.props.onRequestSort(event, property); + }; + + render() { + const { onSelectAllClick, order, orderBy, numSelected, rowCount, columns } = this.props; + + return ( + <TableHead> + <TableRow> + { this.props.enableSelection + ? <TableCell padding="checkbox" style={ { width: "50px" } }> + <Checkbox + indeterminate={ numSelected && numSelected > 0 && numSelected < rowCount || undefined } + checked={ numSelected === rowCount } + onChange={ onSelectAllClick } + /> + </TableCell> + : null + } + { columns.map(col => { + const style = col.width ? { width: col.width } : {}; + return ( + <TableCell + key={ col.property } + align={ col.type === ColumnType.numeric ? 'right' : 'left' } + padding={ col.disablePadding ? 'none' : 'default' } + sortDirection={ orderBy === (col.property) ? order : false } + style={ style } + > + { col.disableSorting || (col.type === ColumnType.custom) + ? <TableSortLabel + active={ false } + direction={ undefined } + > + { col.title || col.property } + </TableSortLabel> + : <Tooltip + title="Sort" + placement={ col.type === ColumnType.numeric ? 'bottom-end' : 'bottom-start' } + enterDelay={ 300 } + > + <TableSortLabel + active={ orderBy === col.property } + direction={ order || undefined } + onClick={ this.createSortHandler(col.property) } + > + { col.title || col.property } + </TableSortLabel> + </Tooltip> } + </TableCell> + ); + }, this) } + </TableRow> + </TableHead> + ); + } +} + +export const EnhancedTableHead = EnhancedTableHeadComponent;
\ No newline at end of file diff --git a/sdnr/wt/odlux/framework/src/components/material-table/tableToolbar.tsx b/sdnr/wt/odlux/framework/src/components/material-table/tableToolbar.tsx new file mode 100644 index 000000000..a4080b51b --- /dev/null +++ b/sdnr/wt/odlux/framework/src/components/material-table/tableToolbar.tsx @@ -0,0 +1,149 @@ +/** + * ============LICENSE_START======================================================================== + * ONAP : ccsdk feature sdnr wt odlux + * ================================================================================================= + * Copyright (C) 2019 highstreet technologies GmbH Intellectual Property. All rights reserved. + * ================================================================================================= + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + * ============LICENSE_END========================================================================== + */ +import * as React from 'react'; +import { withStyles, WithStyles, createStyles, Theme } from '@material-ui/core/styles'; + +import IconButton from '@material-ui/core/IconButton'; +import Tooltip from '@material-ui/core/Tooltip'; +import Toolbar from '@material-ui/core/Toolbar'; +import Typography from '@material-ui/core/Typography'; +import DeleteIcon from '@material-ui/icons/Delete'; +import MoreIcon from '@material-ui/icons/MoreVert'; +import FilterListIcon from '@material-ui/icons/FilterList'; +import MenuItem from '@material-ui/core/MenuItem'; +import Menu from '@material-ui/core/Menu'; +import { lighten } from '@material-ui/core/styles/colorManipulator'; +import { SvgIconProps } from '@material-ui/core/SvgIcon/SvgIcon'; + +const styles = (theme: Theme) => createStyles({ + root: { + paddingRight: theme.spacing(1), + }, + highlight: + theme.palette.type === 'light' + ? { + color: theme.palette.secondary.main, + backgroundColor: lighten(theme.palette.secondary.light, 0.85), + } + : { + color: theme.palette.text.primary, + backgroundColor: theme.palette.secondary.dark, + }, + spacer: { + flex: '1 1 100%', + }, + actions: { + color: theme.palette.text.secondary, + display: "flex", + flex: "auto", + flexDirection: "row" + }, + title: { + flex: '0 0 auto', + }, + menuButton: { + marginLeft: -12, + marginRight: 20, + }, +}); + +interface ITableToolbarComponentProps extends WithStyles<typeof styles> { + numSelected: number | null; + title?: string; + tableId?: string; + customActionButtons?: { icon: React.ComponentType<SvgIconProps>, tooltip?: string, onClick: () => void }[]; + onToggleFilter: () => void; + onExportToCsv: () => void; +} + +class TableToolbarComponent extends React.Component<ITableToolbarComponentProps, { anchorEl: EventTarget & HTMLElement | null }> { + constructor(props: ITableToolbarComponentProps) { + super(props); + + this.state = { + anchorEl: null + }; + } + + private handleMenu = (event: React.MouseEvent<HTMLElement>) => { + this.setState({ anchorEl: event.currentTarget }); + }; + + private handleClose = () => { + this.setState({ anchorEl: null }); + }; + render() { + const { numSelected, classes } = this.props; + const open = !!this.state.anchorEl; + const buttonPrefix = this.props.tableId !== undefined ? this.props.tableId + '-' : ''; + return ( + <Toolbar className={`${classes.root} ${numSelected && numSelected > 0 ? classes.highlight : ''} `} > + <div className={classes.title}> + {numSelected && numSelected > 0 ? ( + <Typography color="inherit" variant="subtitle1"> + {numSelected} selected + </Typography> + ) : ( + <Typography variant="h5" id="tableTitle"> + {this.props.title || null} + </Typography> + )} + </div> + <div className={classes.spacer} /> + <div className={classes.actions}> + {this.props.customActionButtons + ? this.props.customActionButtons.map((action, ind) => ( + <Tooltip key={`custom-action-${ind}`} title={action.tooltip}> + <IconButton aria-label={buttonPrefix + `custom-action-${ind}`} onClick={() => action.onClick()}> + <action.icon /> + </IconButton> + </Tooltip> + )) + : null} + {numSelected && numSelected > 0 ? ( + <Tooltip title="Delete"> + <IconButton aria-label={buttonPrefix + "delete"}> + <DeleteIcon /> + </IconButton> + </Tooltip> + ) : ( + <Tooltip title="Filter list"> + <IconButton aria-label={buttonPrefix + "filter-list"} onClick={() => { this.props.onToggleFilter && this.props.onToggleFilter() }}> + <FilterListIcon /> + </IconButton> + </Tooltip> + )} + <Tooltip title="Actions"> + <IconButton color="inherit" + aria-owns={open ? 'menu-appbar' : undefined} + aria-haspopup="true" + onClick={this.handleMenu} > + <MoreIcon /> + </IconButton> + </Tooltip> + <Menu id="menu-appbar" anchorEl={this.state.anchorEl} anchorOrigin={{ vertical: 'top', horizontal: 'right' }} + transformOrigin={{ vertical: 'top', horizontal: 'right' }} open={open} onClose={this.handleClose} > + <MenuItem onClick={this.props.onExportToCsv}>Export as CSV</MenuItem> + </Menu> + </div> + </Toolbar> + ); + } +}; + +export const TableToolbar = withStyles(styles)(TableToolbarComponent);
\ No newline at end of file diff --git a/sdnr/wt/odlux/framework/src/components/material-table/utilities.ts b/sdnr/wt/odlux/framework/src/components/material-table/utilities.ts new file mode 100644 index 000000000..6e8902c07 --- /dev/null +++ b/sdnr/wt/odlux/framework/src/components/material-table/utilities.ts @@ -0,0 +1,263 @@ +/** + * ============LICENSE_START======================================================================== + * ONAP : ccsdk feature sdnr wt odlux + * ================================================================================================= + * Copyright (C) 2019 highstreet technologies GmbH Intellectual Property. All rights reserved. + * ================================================================================================= + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + * ============LICENSE_END========================================================================== + */ +import { Action, IActionHandler } from '../../flux/action'; +import { Dispatch } from '../../flux/store'; + +import { AddErrorInfoAction } from '../../actions/errorActions'; +import { IApplicationStoreState } from '../../store/applicationStore'; + +import { DataCallback } from "."; +export interface IExternalTableState<TData> { + order: 'asc' | 'desc'; + orderBy: string | null; + selected: any[] | null; + rows: TData[]; + total: number; + page: number; + rowsPerPage: number; + loading: boolean; + showFilter: boolean; + filter: { [property: string]: string }; + preFilter: { [property: string]: string }; +} + +/** Create an actionHandler and actions for external table states. */ +export function createExternal<TData>(callback: DataCallback<TData>, selectState: (appState: IApplicationStoreState) => IExternalTableState<TData>) { + + //#region Actions + abstract class TableAction extends Action { } + + + class RequestSortAction extends TableAction { + constructor(public orderBy: string) { + super(); + } + } + + class SetSelectedAction extends TableAction { + constructor(public selected: TData[] | null) { + super(); + } + } + + class SetPageAction extends TableAction { + constructor(public page: number) { + super(); + } + } + + class SetRowsPerPageAction extends TableAction { + constructor(public rowsPerPage: number) { + super(); + } + } + + class SetPreFilterChangedAction extends TableAction { + constructor(public preFilter: { [key: string]: string }) { + super(); + } + } + + class SetFilterChangedAction extends TableAction { + constructor(public filter: { [key: string]: string }) { + super(); + } + } + + class SetShowFilterAction extends TableAction { + constructor(public show: boolean) { + super(); + } + } + + class RefreshAction extends TableAction { + constructor() { + super(); + } + } + + class SetResultAction extends TableAction { + constructor(public result: { page: number, total: number, rows: TData[] }) { + super(); + } + } + + // #endregion + + //#region Action Handler + const externalTableStateInit: IExternalTableState<TData> = { + order: 'asc', + orderBy: null, + selected: null, + rows: [], + total: 0, + page: 0, + rowsPerPage: 10, + loading: false, + showFilter: false, + filter: {}, + preFilter: {} + }; + + const externalTableStateActionHandler: IActionHandler<IExternalTableState<TData>> = (state = externalTableStateInit, action) => { + if (!(action instanceof TableAction)) return state; + if (action instanceof RefreshAction) { + state = { + ...state, + loading: true + } + } else if (action instanceof SetResultAction) { + state = { + ...state, + loading: false, + rows: action.result.rows, + total: action.result.total, + page: action.result.page, + } + } else if (action instanceof RequestSortAction) { + state = { + ...state, + loading: true, + orderBy: state.orderBy === action.orderBy && state.order === 'desc' ? null : action.orderBy, + order: state.orderBy === action.orderBy && state.order === 'asc' ? 'desc' : 'asc', + } + } else if (action instanceof SetShowFilterAction) { + state = { + ...state, + loading: true, + showFilter: action.show + } + } else if (action instanceof SetPreFilterChangedAction) { + state = { + ...state, + loading: true, + preFilter: action.preFilter + } + } else if (action instanceof SetFilterChangedAction) { + state = { + ...state, + loading: true, + filter: action.filter + } + } else if (action instanceof SetPageAction) { + state = { + ...state, + loading: true, + page: action.page + } + } else if (action instanceof SetRowsPerPageAction) { + state = { + ...state, + loading: true, + rowsPerPage: action.rowsPerPage + } + } + return state; + } + + //const createTableAction(tableAction) + + //#endregion + const reloadAction = (dispatch: Dispatch, getAppState: () => IApplicationStoreState) => { + dispatch(new RefreshAction()); + const ownState = selectState(getAppState()); + const filter = { ...ownState.preFilter, ...(ownState.showFilter && ownState.filter || {}) }; + Promise.resolve(callback(ownState.page, ownState.rowsPerPage, ownState.orderBy, ownState.order, filter)).then(result => { + + if (ownState.page > 0 && ownState.rowsPerPage * ownState.page > result.total) { //if result is smaller than the currently shown page, new search and repaginate + + let newPage = Math.floor(result.total / ownState.rowsPerPage); + + Promise.resolve(callback(newPage, ownState.rowsPerPage, ownState.orderBy, ownState.order, filter)).then(result1 => { + dispatch(new SetResultAction(result1)); + }); + + + } else { + dispatch(new SetResultAction(result)); + } + + + }).catch(error => new AddErrorInfoAction(error)); + }; + + const createPreActions = (dispatch: Dispatch, skipRefresh: boolean = false) => { + return { + onPreFilterChanged: (preFilter: { [key: string]: string }) => { + dispatch(new SetPreFilterChangedAction(preFilter)); + (!skipRefresh) && dispatch(reloadAction); + } + }; + } + + const createActions = (dispatch: Dispatch, skipRefresh: boolean = false) => { + return { + onRefresh: () => { + dispatch(reloadAction); + }, + onHandleRequestSort: (orderBy: string) => { + dispatch((dispatch: Dispatch) => { + dispatch(new RequestSortAction(orderBy)); + (!skipRefresh) && dispatch(reloadAction); + }); + }, + onToggleFilter: () => { + dispatch((dispatch: Dispatch, getAppState: () => IApplicationStoreState) => { + const { showFilter } = selectState(getAppState()); + dispatch(new SetShowFilterAction(!showFilter)); + (!skipRefresh) && dispatch(reloadAction); + }); + }, + onFilterChanged: (property: string, filterTerm: string) => { + dispatch((dispatch: Dispatch, getAppState: () => IApplicationStoreState) => { + let { filter } = selectState(getAppState()); + filter = { ...filter, [property]: filterTerm }; + dispatch(new SetFilterChangedAction(filter)); + (!skipRefresh) && dispatch(reloadAction); + }); + }, + onHandleChangePage: (page: number) => { + dispatch((dispatch: Dispatch) => { + dispatch(new SetPageAction(page)); + (!skipRefresh) && dispatch(reloadAction); + }); + }, + onHandleChangeRowsPerPage: (rowsPerPage: number | null) => { + dispatch((dispatch: Dispatch) => { + dispatch(new SetRowsPerPageAction(rowsPerPage || 10)); + (!skipRefresh) && dispatch(reloadAction); + }); + } + // selected: + }; + }; + + const createProperties = (state: IApplicationStoreState) => { + return { + ...selectState(state) + } + } + + return { + reloadAction: reloadAction, + createActions: createActions, + createProperties: createProperties, + createPreActions: createPreActions, + actionHandler: externalTableStateActionHandler + } +}
\ No newline at end of file diff --git a/sdnr/wt/odlux/framework/src/components/material-ui/index.ts b/sdnr/wt/odlux/framework/src/components/material-ui/index.ts new file mode 100644 index 000000000..e0e3fc943 --- /dev/null +++ b/sdnr/wt/odlux/framework/src/components/material-ui/index.ts @@ -0,0 +1,22 @@ +/** + * ============LICENSE_START======================================================================== + * ONAP : ccsdk feature sdnr wt odlux + * ================================================================================================= + * Copyright (C) 2019 highstreet technologies GmbH Intellectual Property. All rights reserved. + * ================================================================================================= + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + * ============LICENSE_END========================================================================== + */ +export { ListItemLink } from './listItemLink'; +export { Panel } from './panel'; +export { ToggleButton, ToggleButtonClassKey } from './toggleButton'; +export { TreeView, ITreeItem, TreeViewCtorType} from './treeView'; +export { Loader } from './loader'; diff --git a/sdnr/wt/odlux/framework/src/components/material-ui/listItemLink.tsx b/sdnr/wt/odlux/framework/src/components/material-ui/listItemLink.tsx new file mode 100644 index 000000000..1a7e58f77 --- /dev/null +++ b/sdnr/wt/odlux/framework/src/components/material-ui/listItemLink.tsx @@ -0,0 +1,67 @@ +/** + * ============LICENSE_START======================================================================== + * ONAP : ccsdk feature sdnr wt odlux + * ================================================================================================= + * Copyright (C) 2019 highstreet technologies GmbH Intellectual Property. All rights reserved. + * ================================================================================================= + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + * ============LICENSE_END========================================================================== + */ +import * as React from 'react';
+import { NavLink, Link, Route } from 'react-router-dom';
+
+import ListItem from '@material-ui/core/ListItem';
+import ListItemIcon from '@material-ui/core/ListItemIcon';
+import ListItemText from '@material-ui/core/ListItemText';
+
+import { withStyles, WithStyles, createStyles, Theme } from '@material-ui/core/styles';
+
+const styles = (theme: Theme) => createStyles({
+ active: {
+ backgroundColor: theme.palette.action.selected
+ }
+});
+
+export interface IListItemLinkProps extends WithStyles<typeof styles> {
+ icon: JSX.Element | null;
+ primary: string | React.ComponentType;
+ secondary?: React.ComponentType;
+ to: string;
+ exact?: boolean;
+}
+
+export const ListItemLink = withStyles(styles)((props: IListItemLinkProps) => {
+ const { icon, primary: Primary, secondary: Secondary, classes, to, exact = false } = props;
+ const renderLink = (itemProps: any): JSX.Element => (<NavLink exact={ exact } to={ to } activeClassName={ classes.active } { ...itemProps } />);
+
+ return (
+ <>
+ <ListItem button component={ renderLink }>
+ { icon
+ ? <ListItemIcon>{ icon }</ListItemIcon>
+ : null
+ }
+ { typeof Primary === 'string'
+ ? <ListItemText primary={ Primary } style={{ padding: 0 }} />
+ : <Primary />
+ }
+ </ListItem>
+ { Secondary
+ ? <Route exact={ exact } path={ to } component={ Secondary } />
+ : null
+ }
+ </>
+ );
+ }
+);
+
+export default ListItemLink;
+
diff --git a/sdnr/wt/odlux/framework/src/components/material-ui/loader.tsx b/sdnr/wt/odlux/framework/src/components/material-ui/loader.tsx new file mode 100644 index 000000000..5ab2fd415 --- /dev/null +++ b/sdnr/wt/odlux/framework/src/components/material-ui/loader.tsx @@ -0,0 +1,45 @@ +/** + * ============LICENSE_START======================================================================== + * ONAP : ccsdk feature sdnr wt odlux + * ================================================================================================= + * Copyright (C) 2019 highstreet technologies GmbH Intellectual Property. All rights reserved. + * ================================================================================================= + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + * ============LICENSE_END========================================================================== + */ + + +import * as React from "react"; + +import { WithStyles, withStyles, createStyles, Theme } from '@material-ui/core/styles'; + +const styles = (theme: Theme) => createStyles({ + "@keyframes spin": { + "0%": { transform: "rotate(0deg)" }, + "100%": { transform: "rotate(360deg)" }, + }, + loader: { + border: `16px solid ${theme.palette.grey.A200}`, + borderTop: `16px solid ${theme.palette.secondary.main}`, + borderRadius: "50%", + width: "120px", + height: "120px", + animation: "$spin 2s linear infinite", + } +}); + +const LoaderComponent: React.FC<WithStyles<typeof styles>> = (props) => { + return ( + <div className={props.classes.loader} /> + ); +}; + +export const Loader = withStyles(styles)(LoaderComponent); diff --git a/sdnr/wt/odlux/framework/src/components/material-ui/panel.tsx b/sdnr/wt/odlux/framework/src/components/material-ui/panel.tsx new file mode 100644 index 000000000..378d48592 --- /dev/null +++ b/sdnr/wt/odlux/framework/src/components/material-ui/panel.tsx @@ -0,0 +1,73 @@ +/**
+ * ============LICENSE_START========================================================================
+ * ONAP : ccsdk feature sdnr wt odlux
+ * =================================================================================================
+ * Copyright (C) 2019 highstreet technologies GmbH Intellectual Property. All rights reserved.
+ * =================================================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ * ============LICENSE_END==========================================================================
+ */
+import * as React from 'react';
+
+import { withStyles, Theme, WithStyles, createStyles } from '@material-ui/core/styles';
+
+import { ExpansionPanel, ExpansionPanelSummary, ExpansionPanelDetails, Typography, ExpansionPanelActions } from '@material-ui/core';
+
+import ExpandMoreIcon from '@material-ui/icons/ExpandMore';
+import { SvgIconProps } from '@material-ui/core/SvgIcon';
+
+const styles = (theme: Theme) => createStyles({
+ accordion: {
+ // background: theme.palette.secondary.dark,
+ // color: theme.palette.primary.contrastText
+ },
+ detail: {
+ // background: theme.palette.background.paper,
+ // color: theme.palette.text.primary,
+ position: "relative",
+ display: 'flex',
+ flexDirection: 'column'
+ },
+ text: {
+ // color: theme.palette.common.white,
+ // fontSize: "1rem"
+ },
+});
+
+type PanalProps = WithStyles<typeof styles> & {
+ activePanel: string | null,
+ panelId: string,
+ title: string,
+ customActionButtons?: JSX.Element[];
+ onToggle: (panelId: string | null) => void;
+}
+
+const PanelComponent: React.SFC<PanalProps> = (props) => {
+ const { classes, activePanel, onToggle } = props;
+ return (
+ <ExpansionPanel className={classes.accordion} expanded={activePanel === props.panelId} onChange={() => onToggle(props.panelId)} >
+ <ExpansionPanelSummary expandIcon={<ExpandMoreIcon />}>
+ <Typography className={classes.text} >{props.title}</Typography>
+ </ExpansionPanelSummary>
+ <ExpansionPanelDetails className={classes.detail}>
+ {props.children}
+ </ExpansionPanelDetails>
+ {props.customActionButtons
+ ? <ExpansionPanelActions>
+ {props.customActionButtons}
+ </ExpansionPanelActions>
+ : null}
+ </ExpansionPanel>
+ );
+};
+
+export const Panel = withStyles(styles)(PanelComponent);
+export default Panel;
\ No newline at end of file diff --git a/sdnr/wt/odlux/framework/src/components/material-ui/snackDisplay.tsx b/sdnr/wt/odlux/framework/src/components/material-ui/snackDisplay.tsx new file mode 100644 index 000000000..437784ce5 --- /dev/null +++ b/sdnr/wt/odlux/framework/src/components/material-ui/snackDisplay.tsx @@ -0,0 +1,74 @@ +/** + * ============LICENSE_START======================================================================== + * ONAP : ccsdk feature sdnr wt odlux + * ================================================================================================= + * Copyright (C) 2019 highstreet technologies GmbH Intellectual Property. All rights reserved. + * ================================================================================================= + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + * ============LICENSE_END========================================================================== + */ +import * as React from 'react'; + +import { IApplicationStoreState } from '../../store/applicationStore'; +import { Connect, connect, IDispatcher } from '../../flux/connect'; +import { RemoveSnackbarNotification } from '../../actions/snackbarActions'; + +import { WithSnackbarProps, withSnackbar } from 'notistack'; + +const mapProps = (state: IApplicationStoreState) => ({ + notifications: state.framework.applicationState.snackBars +}); + +const mapDispatch = (dispatcher: IDispatcher) => ({ + removeSnackbar: (key: number) => { + dispatcher.dispatch(new RemoveSnackbarNotification(key)); + } +}); + +type DisplaySnackbarsComponentProps = Connect<typeof mapProps, typeof mapDispatch> & WithSnackbarProps; + +class DisplaySnackbarsComponent extends React.Component<DisplaySnackbarsComponentProps> { + private displayed: number[] = []; + + private storeDisplayed = (id: number) => { + this.displayed = [...this.displayed, id]; + }; + + public shouldComponentUpdate({ notifications: newSnacks = [] }: DisplaySnackbarsComponentProps) { + + const { notifications: currentSnacks } = this.props; + let notExists = false; + for (let i = 0; i < newSnacks.length; i++) { + if (notExists) continue; + notExists = notExists || !currentSnacks.filter(({ key }) => newSnacks[i].key === key).length; + } + return notExists; + } + + componentDidUpdate() { + const { notifications = [] } = this.props; + + notifications.forEach(notification => { + if (this.displayed.includes(notification.key)) return; + const options = notification.options || {}; + this.props.enqueueSnackbar(notification.message, options); + this.storeDisplayed(notification.key); + this.props.removeSnackbar(notification.key); + }); + } + + render() { + return null; + } +} + +const DisplayStackbars = withSnackbar(connect(mapProps, mapDispatch)(DisplaySnackbarsComponent)); +export default DisplayStackbars;
\ No newline at end of file diff --git a/sdnr/wt/odlux/framework/src/components/material-ui/toggleButton.tsx b/sdnr/wt/odlux/framework/src/components/material-ui/toggleButton.tsx new file mode 100644 index 000000000..1a29d6970 --- /dev/null +++ b/sdnr/wt/odlux/framework/src/components/material-ui/toggleButton.tsx @@ -0,0 +1,179 @@ +/**
+ * ============LICENSE_START========================================================================
+ * ONAP : ccsdk feature sdnr wt odlux
+ * =================================================================================================
+ * Copyright (C) 2019 highstreet technologies GmbH Intellectual Property. All rights reserved.
+ * =================================================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ * ============LICENSE_END==========================================================================
+ */
+
+import * as React from 'react';
+import classNames from 'classnames';
+import { withStyles, WithStyles, Theme, createStyles } from '@material-ui/core/styles';
+import { fade } from '@material-ui/core/styles/colorManipulator';
+import ButtonBase from '@material-ui/core/ButtonBase';
+
+
+export const styles = (theme: Theme) => createStyles({
+ /* Styles applied to the root element. */
+ root: {
+ ...theme.typography.button,
+ height: 32,
+ minWidth: 48,
+ margin: 0,
+ padding: `${theme.spacing(1 - 4)}px ${theme.spacing(1.5)}px`,
+ borderRadius: 2,
+ willChange: 'opacity',
+ color: fade(theme.palette.action.active, 0.38),
+ '&:hover': {
+ textDecoration: 'none',
+ // Reset on mouse devices
+ backgroundColor: fade(theme.palette.text.primary, 0.12),
+ '@media (hover: none)': {
+ backgroundColor: 'transparent',
+ },
+ '&$disabled': {
+ backgroundColor: 'transparent',
+ },
+ },
+ '&:not(:first-child)': {
+ borderTopLeftRadius: 0,
+ borderBottomLeftRadius: 0,
+ },
+ '&:not(:last-child)': {
+ borderTopRightRadius: 0,
+ borderBottomRightRadius: 0,
+ },
+ },
+ /* Styles applied to the root element if `disabled={true}`. */
+ disabled: {
+ color: fade(theme.palette.action.disabled, 0.12),
+ },
+ /* Styles applied to the root element if `selected={true}`. */
+ selected: {
+ color: theme.palette.action.active,
+ '&:after': {
+ content: '""',
+ display: 'block',
+ position: 'absolute',
+ overflow: 'hidden',
+ borderRadius: 'inherit',
+ width: '100%',
+ height: '100%',
+ left: 0,
+ top: 0,
+ pointerEvents: 'none',
+ zIndex: 0,
+ backgroundColor: 'currentColor',
+ opacity: 0.38,
+ },
+ '& + &:before': {
+ content: '""',
+ display: 'block',
+ position: 'absolute',
+ overflow: 'hidden',
+ width: 1,
+ height: '100%',
+ left: 0,
+ top: 0,
+ pointerEvents: 'none',
+ zIndex: 0,
+ backgroundColor: 'currentColor',
+ opacity: 0.12,
+ },
+ },
+ /* Styles applied to the `label` wrapper element. */
+ label: {
+ width: '100%',
+ display: 'inherit',
+ alignItems: 'inherit',
+ justifyContent: 'inherit',
+ },
+});
+
+export type ToggleButtonClassKey = 'disabled' | 'root' | 'label' | 'selected';
+
+interface IToggleButtonProps extends WithStyles<typeof styles> {
+ className?: string;
+ component?: React.ReactType<IToggleButtonProps>;
+ disabled?: boolean;
+ disableFocusRipple?: boolean;
+ disableRipple?: boolean;
+ selected?: boolean;
+ type?: string;
+ value?: any;
+ onClick?: (event: React.FormEvent<HTMLElement>, value?: any) => void;
+ onChange?: (event: React.FormEvent<HTMLElement>, value?: any) => void;
+}
+
+class ToggleButtonComponent extends React.Component<IToggleButtonProps> {
+ handleChange = (event: React.FormEvent<HTMLElement>) => {
+ const { onChange, onClick, value } = this.props;
+
+ event.stopPropagation();
+ if (onClick) {
+ onClick(event, value);
+ if (event.isDefaultPrevented()) {
+ return;
+ }
+ }
+
+ if (onChange) {
+ onChange(event, value);
+ }
+ event.preventDefault();
+ };
+
+ render() {
+ const {
+ children,
+ className: classNameProp,
+ classes,
+ disableFocusRipple,
+ disabled,
+ selected,
+ ...other
+ } = this.props;
+
+ const className = classNames(
+ classes.root,
+ {
+ [classes.disabled]: disabled,
+ [classes.selected]: selected,
+ },
+ classNameProp,
+ );
+
+ return (
+ <ButtonBase
+ className={className}
+ disabled={disabled}
+ focusRipple={!disableFocusRipple}
+ onClick={this.handleChange}
+ href="#"
+ {...other}
+ >
+ <span className={classes.label}>{children}</span>
+ </ButtonBase>
+ );
+ }
+ public static defaultProps = {
+ disabled: false,
+ disableFocusRipple: false,
+ disableRipple: false,
+ };
+
+ public static muiName = 'ToggleButton';
+}
+
+export const ToggleButton = withStyles(styles, { name: 'MuiToggleButton' })(ToggleButtonComponent);
+export default ToggleButton;
\ No newline at end of file diff --git a/sdnr/wt/odlux/framework/src/components/material-ui/toggleButtonGroup.tsx b/sdnr/wt/odlux/framework/src/components/material-ui/toggleButtonGroup.tsx new file mode 100644 index 000000000..6460e8a3f --- /dev/null +++ b/sdnr/wt/odlux/framework/src/components/material-ui/toggleButtonGroup.tsx @@ -0,0 +1,36 @@ +/** + * ============LICENSE_START======================================================================== + * ONAP : ccsdk feature sdnr wt odlux + * ================================================================================================= + * Copyright (C) 2019 highstreet technologies GmbH Intellectual Property. All rights reserved. + * ================================================================================================= + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + * ============LICENSE_END========================================================================== + */ +import * as React from 'react'; +import classNames from 'classnames'; +import { withStyles, WithStyles, Theme, createStyles } from '@material-ui/core/styles'; + +export const styles = (theme: Theme) => createStyles({ + /* Styles applied to the root element. */ + root: { + transition: theme.transitions.create('background,box-shadow'), + background: 'transparent', + borderRadius: 2, + overflow: 'hidden', + }, + /* Styles applied to the root element if `selected={true}` or `selected="auto" and `value` set. */ + selected: { + background: theme.palette.background.paper, + boxShadow: theme.shadows[2], + }, +}); + diff --git a/sdnr/wt/odlux/framework/src/components/material-ui/treeView.tsx b/sdnr/wt/odlux/framework/src/components/material-ui/treeView.tsx new file mode 100644 index 000000000..e4eb3a794 --- /dev/null +++ b/sdnr/wt/odlux/framework/src/components/material-ui/treeView.tsx @@ -0,0 +1,268 @@ +/** + * ============LICENSE_START======================================================================== + * ONAP : ccsdk feature sdnr wt odlux + * ================================================================================================= + * Copyright (C) 2019 highstreet technologies GmbH Intellectual Property. All rights reserved. + * ================================================================================================= + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + * ============LICENSE_END========================================================================== + */ +import * as React from 'react'; + +import { SvgIconProps } from '@material-ui/core/SvgIcon'; +import { List, ListItem, TextField, ListItemText, ListItemIcon, WithTheme, withTheme, Omit } from '@material-ui/core'; + +import FileIcon from '@material-ui/icons/InsertDriveFile'; +import CloseIcon from '@material-ui/icons/ExpandLess'; +import OpenIcon from '@material-ui/icons/ExpandMore'; +import FolderIcon from '@material-ui/icons/Folder'; + +export interface ITreeItem { + disabled?: boolean; + icon?: React.ComponentType<SvgIconProps>; +} + +type TreeViewComponentState<TData extends ITreeItem = ITreeItem> = { + /** All indices of all expanded Items */ + expandedItems: TData[]; + /** The index of the active iten or undefined if no item is active. */ + activeItem: undefined | TData; + /** The search term or undefined if search is corrently not active. */ + searchTerm: undefined | string; +} + +type TreeViewComponentBaseProps<TData extends ITreeItem = ITreeItem> = WithTheme & { + items: TData[]; + contentProperty: keyof Omit<TData, keyof ITreeItem>; + childrenProperty: keyof Omit<TData, keyof ITreeItem>; + useFolderIcons?: boolean; + enableSearchBar?: boolean; + autoExpandFolder?: boolean; + style?: React.CSSProperties; + itemHeight?: number; + depthOffset?: number; +} + +type TreeViewComponentWithInternalStateProps<TData extends ITreeItem = ITreeItem> = TreeViewComponentBaseProps<TData> & { + onItemClick?: (item: TData) => void; + onFolderClick?: (item: TData) => void; +} + +type TreeViewComponentWithExternalStateProps<TData extends ITreeItem = ITreeItem> = TreeViewComponentBaseProps<TData> & TreeViewComponentState<TData> & { + onSearch: (searchTerm: string) => void; + onItemClick: (item: TData) => void; + onFolderClick: (item: TData) => void; +} + +type TreeViewComponentProps<TData extends ITreeItem = ITreeItem> = + TreeViewComponentWithInternalStateProps<TData> | + TreeViewComponentWithExternalStateProps<TData>; + +function isTreeViewComponentWithExternalStateProps<TData extends ITreeItem = ITreeItem>(props: TreeViewComponentProps<TData>): props is TreeViewComponentWithExternalStateProps<TData> { + const propsWithExternalState = (props as TreeViewComponentWithExternalStateProps<TData>) + return ( + propsWithExternalState.onSearch instanceof Function || + propsWithExternalState.expandedItems !== undefined || + propsWithExternalState.activeItem !== undefined || + propsWithExternalState.searchTerm !== undefined + ); +} + +class TreeViewComponent<TData extends ITreeItem> extends React.Component<TreeViewComponentProps<TData>, TreeViewComponentState> { + + /** + * Initializes a new instance. + */ + constructor(props: TreeViewComponentProps<TData>) { + super(props); + + this.state = { + expandedItems: [], + activeItem: undefined, + searchTerm: undefined + }; + } + + render(): JSX.Element { + this.itemIndex = 0; + const { searchTerm } = this.state; + const { children, items, enableSearchBar } = this.props; + const styles = { + root: { + padding: 0, + paddingBottom: 8, + paddingTop: children ? 0 : 8, + ...this.props.style + }, + search: { + padding: `0px ${this.props.theme.spacing(1)}px` + } + }; + return ( + <div style={styles.root}> + {children} + {enableSearchBar && <TextField label={"Search"} fullWidth={true} style={styles.search} value={searchTerm} onChange={this.onChangeSearchText} /> || null} + <List> + {this.renderItems(items, searchTerm && searchTerm.toLowerCase())} + </List> + </div> + ); + } + + private itemIndex: number = 0; + private renderItems = (items: TData[], searchTerm: string | undefined, depth: number = 1) => { + return items.reduce((acc, item) => { + + const children = this.props.childrenProperty && ((item as any)[this.props.childrenProperty] as TData[]); + const childrenJsx = children && this.renderItems(children, searchTerm, depth + 1); + + const expanded = searchTerm + ? children && childrenJsx.length > 0 + : !children + ? false + : this.state.expandedItems.indexOf(item) > -1; + const isFolder = children !== undefined; + + const itemJsx = this.renderItem(item, searchTerm, depth, isFolder, expanded); + itemJsx && acc.push(itemJsx); + + if (isFolder && expanded) { + acc.push(...childrenJsx); + } + return acc; + + }, [] as JSX.Element[]); + } + private renderItem = (item: TData, searchTerm: string | undefined, depth: number, isFolder: boolean, expanded: boolean): JSX.Element | null => { + const styles = { + item: { + paddingLeft: (((this.props.depthOffset || 0) + depth) * this.props.theme.spacing(3)), + backgroundColor: this.state.activeItem === item ? this.props.theme.palette.action.selected : undefined, + height: this.props.itemHeight || undefined, + cursor: item.disabled ? 'not-allowed' : 'pointer', + color: item.disabled ? this.props.theme.palette.text.disabled : this.props.theme.palette.text.primary, + overflow: 'hidden', + transform: 'translateZ(0)', + } + }; + + const text = (item as any)[this.props.contentProperty] as string || ''; // need to keep track of search + const matchIndex = searchTerm ? text.toLowerCase().indexOf(searchTerm) : -1; + const searchTermLength = searchTerm && searchTerm.length || 0; + + const handleClickCreator = (isIcon: boolean) => (event: React.SyntheticEvent) => { + if (item.disabled) return; + event.preventDefault(); + event.stopPropagation(); + if (isFolder && (this.props.autoExpandFolder || isIcon)) { + this.props.onFolderClick ? this.props.onFolderClick(item) : this.onFolderClick(item); + } else { + this.props.onItemClick ? this.props.onItemClick(item) : this.onItemClick(item); + } + }; + + return ((searchTerm && (matchIndex > -1 || expanded) || !searchTerm) + ? ( + <ListItem key={`tree-list-${this.itemIndex++}`} style={styles.item} onClick={handleClickCreator(false)} button > + + { // display the left icon + (this.props.useFolderIcons && <ListItemIcon>{isFolder ? <FolderIcon /> : <FileIcon />}</ListItemIcon>) || + (item.icon && (<ListItemIcon><item.icon /></ListItemIcon>))} + + + { // highlight search result + matchIndex > -1 + ? (<span> + {text.substring(0, matchIndex)} + <span + style={{ + display: 'inline-block', + backgroundColor: 'rgba(255,235,59,0.5)', + padding: '3px', + }} + > + {text.substring(matchIndex, matchIndex + searchTermLength)} + </span> + {text.substring(matchIndex + searchTermLength)} + </span>) + : (<ListItemText primary={text} />) + } + + { // display the right icon, depending on the state + !isFolder ? null : expanded ? (<OpenIcon onClick={handleClickCreator(true)} />) : (<CloseIcon onClick={handleClickCreator(true)} />)} + </ListItem> + ) + : null + ); + } + + private onFolderClick = (item: TData) => { + // toggle items with children + if (this.state.searchTerm) return; + const indexOfItemToToggle = this.state.expandedItems.indexOf(item); + if (indexOfItemToToggle === -1) { + this.setState({ + expandedItems: [...this.state.expandedItems, item], + }); + } else { + this.setState({ + expandedItems: [ + ...this.state.expandedItems.slice(0, indexOfItemToToggle), + ...this.state.expandedItems.slice(indexOfItemToToggle + 1), + ] + }); + } + }; + + private onItemClick = (item: TData) => { + // activate items without children + this.setState({ + activeItem: item, + }); + }; + + private onChangeSearchText = (event: React.ChangeEvent<HTMLInputElement>) => { + event.preventDefault(); + event.stopPropagation(); + + if (isTreeViewComponentWithExternalStateProps(this.props)) { + this.props.onSearch(event.target.value) + } else { + this.setState({ + searchTerm: event.target.value + }); + } + }; + + static getDerivedStateFromProps(props: TreeViewComponentProps, state: TreeViewComponentState): TreeViewComponentState { + if (isTreeViewComponentWithExternalStateProps(props)) { + return { + ...state, + expandedItems: props.expandedItems || [], + activeItem: props.activeItem, + searchTerm: props.searchTerm + }; + } + return state; + } + + public static defaultProps = { + useFolderIcons: false, + enableSearchBar: false, + autoExpandFolder: false, + depthOffset: 0 + } +} + +export type TreeViewCtorType<TData extends ITreeItem = ITreeItem> = new () => React.Component<Omit<TreeViewComponentProps<TData>, 'theme'>>; + +export const TreeView = withTheme(TreeViewComponent); +export default TreeView;
\ No newline at end of file diff --git a/sdnr/wt/odlux/framework/src/components/navigationMenu.tsx b/sdnr/wt/odlux/framework/src/components/navigationMenu.tsx new file mode 100644 index 000000000..00d43d99c --- /dev/null +++ b/sdnr/wt/odlux/framework/src/components/navigationMenu.tsx @@ -0,0 +1,142 @@ +/**
+ * ============LICENSE_START========================================================================
+ * ONAP : ccsdk feature sdnr wt odlux
+ * =================================================================================================
+ * Copyright (C) 2019 highstreet technologies GmbH Intellectual Property. All rights reserved.
+ * =================================================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ * ============LICENSE_END==========================================================================
+ */
+import * as React from 'react';
+import { withStyles, WithStyles, createStyles, Theme } from '@material-ui/core/styles';
+
+import { faHome, faAddressBook } from '@fortawesome/free-solid-svg-icons';
+
+import Drawer from '@material-ui/core/Drawer';
+import List from '@material-ui/core/List';
+
+import Divider from '@material-ui/core/Divider';
+
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+
+import ListItemLink from '../components/material-ui/listItemLink';
+
+import connect, { Connect, IDispatcher } from '../flux/connect';
+import { MenuAction } from '../actions/menuAction';
+import * as classNames from 'classnames';
+
+const drawerWidth = 240;
+
+const styles = (theme: Theme) => createStyles({
+ drawerPaper: {
+ position: 'relative',
+ width: drawerWidth,
+ },
+ toolbar: theme.mixins.toolbar,
+ drawerOpen: {
+ width: drawerWidth,
+ transition: theme.transitions.create('width', {
+ easing: theme.transitions.easing.sharp,
+ duration: theme.transitions.duration.enteringScreen,
+ }),
+ },
+ drawerClose: {
+ transition: theme.transitions.create('width', {
+ easing: theme.transitions.easing.sharp,
+ duration: theme.transitions.duration.leavingScreen,
+ }),
+ overflowX: 'hidden',
+ width: theme.spacing(7) + 1,
+ [theme.breakpoints.up('sm')]: {
+ width: theme.spacing(9) + 1,
+ },
+ }
+});
+
+const tabletWidthBreakpoint = 768;
+
+export const NavigationMenu = withStyles(styles)(connect()(({ classes, state, dispatch }: WithStyles<typeof styles> & Connect & Connect) => {
+ const { user } = state.framework.authenticationState
+ const isOpen = state.framework.applicationState.isMenuOpen
+ const closedByUser = state.framework.applicationState.isMenuClosedByUser
+
+ const [responsive, setResponsive] = React.useState(false);
+
+ React.useEffect(() => {
+
+ function handleResize() {
+ if (user && user.isValid) {
+ if (window.innerWidth < tabletWidthBreakpoint && !responsive) {
+ setResponsive(true);
+ if (!closedByUser) {
+ console.log("responsive menu collapsed")
+ dispatch(new MenuAction(false));
+ }
+
+ } else if (window.innerWidth > tabletWidthBreakpoint && responsive) {
+ setResponsive(false);
+ if (!closedByUser) {
+ console.log("responsive menu restored")
+ dispatch(new MenuAction(true));
+ }
+
+ }
+ }
+ }
+ window.addEventListener("resize", handleResize);
+
+
+ return () => {
+ window.removeEventListener("resize", handleResize);
+ }
+ })
+
+ return (
+ <Drawer
+ variant="permanent"
+ className={
+ classNames({
+ [classes.drawerOpen]: isOpen,
+ [classes.drawerClose]: !isOpen
+ })
+ }
+ classes={{
+ paper: classes.drawerPaper,
+ }}
+ >
+ {user && user.isValid && <>
+ <div className={classes.toolbar} />
+ { /* https://fiffty.github.io/react-treeview-mui/ */}
+ <List component="nav">
+ <ListItemLink exact to="/" primary="Home" icon={<FontAwesomeIcon icon={faHome} />} />
+ <Divider />
+ {
+ state.framework.applicationRegistraion && Object.keys(state.framework.applicationRegistraion).map(key => {
+ const reg = state.framework.applicationRegistraion[key];
+ return reg && (
+ <ListItemLink
+ key={reg.name}
+ to={reg.path || `/${reg.name}`}
+ primary={reg.menuEntry || reg.name}
+ secondary={reg.subMenuEntry}
+ icon={reg.icon && <FontAwesomeIcon icon={reg.icon} /> || null} />
+ ) || null;
+ }) || null
+ }
+ <Divider />
+ <ListItemLink to="/about" primary="About" icon={<FontAwesomeIcon icon={faAddressBook} />} />
+ </List>
+ </> || null
+ }
+ </Drawer>)
+}));
+
+export default NavigationMenu;
\ No newline at end of file diff --git a/sdnr/wt/odlux/framework/src/components/routing/appFrame.tsx b/sdnr/wt/odlux/framework/src/components/routing/appFrame.tsx new file mode 100644 index 000000000..e6af2eb5d --- /dev/null +++ b/sdnr/wt/odlux/framework/src/components/routing/appFrame.tsx @@ -0,0 +1,55 @@ +/** + * ============LICENSE_START======================================================================== + * ONAP : ccsdk feature sdnr wt odlux + * ================================================================================================= + * Copyright (C) 2019 highstreet technologies GmbH Intellectual Property. All rights reserved. + * ================================================================================================= + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + * ============LICENSE_END========================================================================== + */ +import * as React from 'react';
+
+import connect, { Connect } from '../../flux/connect';
+
+import { SetTitleAction } from '../../actions/titleActions';
+import { AddErrorInfoAction } from '../../actions/errorActions';
+
+import { IconType } from '../../models/iconDefinition';
+
+export interface IAppFrameProps {
+ title: string;
+ icon?: IconType;
+ appId?: string
+}
+
+/**
+ * Represents a component to wich will embed each single app providing the
+ * functionality to update the title and implement an exeprion border.
+ */
+export class AppFrame extends React.Component<IAppFrameProps & Connect> {
+
+ public render(): JSX.Element {
+ return (
+ <div style={{ flex: "1", overflow: "auto", display: "flex", flexDirection: "column" }}>
+ { this.props.children }
+ </div>
+ )
+ }
+
+ public componentDidMount() {
+ this.props.dispatch(new SetTitleAction(this.props.title, this.props.icon, this.props.appId));
+ }
+ public componentDidCatch(error: Error | null, info: object) {
+ this.props.dispatch(new AddErrorInfoAction({ error, info }));
+ }
+}
+
+export default connect()(AppFrame);
\ No newline at end of file diff --git a/sdnr/wt/odlux/framework/src/components/titleBar.tsx b/sdnr/wt/odlux/framework/src/components/titleBar.tsx new file mode 100644 index 000000000..7168ff496 --- /dev/null +++ b/sdnr/wt/odlux/framework/src/components/titleBar.tsx @@ -0,0 +1,208 @@ +/**
+ * ============LICENSE_START========================================================================
+ * ONAP : ccsdk feature sdnr wt odlux
+ * =================================================================================================
+ * Copyright (C) 2019 highstreet technologies GmbH Intellectual Property. All rights reserved.
+ * =================================================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ * ============LICENSE_END==========================================================================
+ */
+import * as React from 'react';
+import { withRouter, RouteComponentProps } from 'react-router-dom';
+
+import { withStyles, WithStyles, createStyles, Theme } from '@material-ui/core/styles';
+import AppBar from '@material-ui/core/AppBar';
+import Toolbar from '@material-ui/core/Toolbar';
+import Typography from '@material-ui/core/Typography';
+import Button from '@material-ui/core/Button';
+import IconButton from '@material-ui/core/IconButton';
+import Block from '@material-ui/icons/Block';
+import Adjust from '@material-ui/icons/Adjust';
+import MenuIcon from '@material-ui/icons/Menu';
+import AccountCircle from '@material-ui/icons/AccountCircle';
+import MenuItem from '@material-ui/core/MenuItem';
+import Menu from '@material-ui/core/Menu';
+
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { faBan } from '@fortawesome/free-solid-svg-icons';
+import { faDotCircle } from '@fortawesome/free-solid-svg-icons';
+
+import { UpdateAuthentication } from '../actions/authentication';
+import { ReplaceAction } from '../actions/navigationActions';
+
+import connect, { Connect, IDispatcher } from '../flux/connect';
+import Logo from './logo';
+import { MenuAction, MenuClosedByUser } from '../actions/menuAction';
+
+const styles = (theme: Theme) => createStyles({
+ appBar: {
+ zIndex: theme.zIndex.drawer + 1,
+ },
+ grow: {
+ flexGrow: 1,
+ },
+ menuButton: {
+ marginLeft: -12,
+ marginRight: 20,
+ },
+ icon: {
+ marginLeft: 16,
+ marginRight: 8
+ },
+ connected: {
+ color: "green"
+ },
+ notConnected: {
+ color: "red"
+ },
+ notificationInfo: {
+ marginLeft: 5
+ }
+});
+
+const mapDispatch = (dispatcher: IDispatcher) => {
+ return {
+ logout: () => {
+ dispatcher.dispatch(new UpdateAuthentication(null));
+ dispatcher.dispatch(new ReplaceAction("/login"));
+ },
+ toggleMainMenu: (value: boolean, value2: boolean) => {
+ dispatcher.dispatch(new MenuAction(value));
+ dispatcher.dispatch(new MenuClosedByUser(value2))
+ }
+ }
+};
+
+type TitleBarProps = RouteComponentProps<{}> & WithStyles<typeof styles> & Connect<undefined, typeof mapDispatch>
+
+class TitleBarComponent extends React.Component<TitleBarProps, { anchorEl: HTMLElement | null }> {
+
+ constructor(props: TitleBarProps) {
+ super(props);
+ this.state = {
+ anchorEl: null
+ }
+
+ }
+ render(): JSX.Element {
+ const { classes, state, history, location } = this.props;
+ const open = !!this.state.anchorEl;
+ let toolbarElements: Array<JSX.Element>;
+ toolbarElements = [];
+
+ // create notificationInfo element
+ const notificationInfo = state.framework.applicationState.isWebsocketAvailable != undefined ?
+ (state.framework.applicationState.isWebsocketAvailable ?
+ <Typography variant="body1" className={classes.notificationInfo}>Notifications <FontAwesomeIcon className={classes.connected} icon={faDotCircle} /> |</Typography> : <Typography variant="body1" className={classes.notificationInfo}>Notifications <FontAwesomeIcon className={classes.notConnected} icon={faBan} /> |</Typography>)
+ : <Typography variant="body1" className={classes.notificationInfo}>Notifications N/A |</Typography>;
+
+
+ // add notificationInfo element before help
+ if (state.framework.applicationRegistraion) {
+ let isNotificationInfoAdded = false;
+ Object.keys(state.framework.applicationRegistraion).map(key => {
+ const reg = state.framework.applicationRegistraion[key];
+ if (reg && reg.statusBarElement) {
+ if (key === "help") {
+ isNotificationInfoAdded = true;
+ toolbarElements.push(notificationInfo);
+ }
+ toolbarElements.push(<reg.statusBarElement key={key} />);
+ }
+ });
+
+ // add notificationInfo in case help wasn't found
+ if (!isNotificationInfoAdded) {
+ toolbarElements.push(notificationInfo);
+ }
+ }
+
+ return (
+ <AppBar position="absolute" className={classes.appBar}>
+ <Toolbar>
+ <IconButton className={classes.menuButton} color="inherit" aria-label="Menu" onClick={this.toggleMainMenu}>
+ <MenuIcon />
+ </IconButton>
+ <Logo />
+ <Typography variant="h6" color="inherit" >
+ {state.framework.applicationState.icon
+ ? (<FontAwesomeIcon className={classes.icon} icon={state.framework.applicationState.icon} />)
+ : null}
+ {state.framework.applicationState.title}
+ </Typography>
+ <div className={classes.grow}></div>
+ {
+ // render toolbar
+ toolbarElements.map((item) => {
+ return item
+ })
+ }
+
+ {state.framework.authenticationState.user
+ ? (<div>
+ <Button aria-label="current user menu button"
+ aria-owns={open ? 'menu-appbar' : undefined}
+ aria-haspopup="true"
+ onClick={this.openMenu}
+ color="inherit"
+ >
+ <AccountCircle />
+ {state.framework.authenticationState.user.user}
+ </Button>
+ <Menu
+ id="menu-appbar"
+ anchorEl={this.state.anchorEl}
+ anchorOrigin={{
+ vertical: 'top',
+ horizontal: 'right',
+ }}
+ transformOrigin={{
+ vertical: 'top',
+ horizontal: 'right',
+ }}
+ open={open}
+ onClose={this.closeMenu}
+ >
+ {/* <MenuItem onClick={ this.closeMenu }>Profile</MenuItem> */}
+ <MenuItem onClick={() => {
+ this.props.logout();
+ this.closeMenu();
+ }}>Logout</MenuItem>
+ </Menu>
+ </div>)
+ : (<Button onClick={() => { history.push('/login') }} color="inherit" disabled={location.pathname == "/login"}>Login</Button>)}
+ </Toolbar>
+ </AppBar>
+ );
+ };
+
+ private toggleMainMenu = (event: React.MouseEvent<HTMLElement>) => {
+ console.log(this.props);
+ if (this.props.state.framework.authenticationState.user && this.props.state.framework.authenticationState.user.isValid) {
+ const isMainMenuOpen = this.props.state.framework.applicationState.isMenuOpen
+ const isClosedByUser = this.props.state.framework.applicationState.isMenuClosedByUser
+ this.props.toggleMainMenu(!isMainMenuOpen, !isClosedByUser);
+ }
+ }
+
+ private openMenu = (event: React.MouseEvent<HTMLElement>) => {
+ this.setState({ anchorEl: event.currentTarget });
+ };
+
+ private closeMenu = () => {
+ this.setState({ anchorEl: null });
+ };
+}
+
+//todo: ggf. https://github.com/acdlite/recompose verwenden zur Vereinfachung
+
+export const TitleBar = withStyles(styles)(withRouter(connect(undefined, mapDispatch)(TitleBarComponent)));
+export default TitleBar;
\ No newline at end of file |