diff options
author | Ravi Pendurty <ravi.pendurty@highstreet-technologies.com> | 2023-12-07 22:45:28 +0530 |
---|---|---|
committer | Ravi Pendurty <ravi.pendurty@highstreet-technologies.com> | 2023-12-07 22:46:39 +0530 |
commit | dfd91573b7567e1dab482f17111ab8f809553d99 (patch) | |
tree | 8368580d1b1add9cfef5e8354ccf1080f27109b0 /sdnr/wt-odlux/odlux/framework/src/components | |
parent | bf8d701f85d02a140a1290d288adc7f437c1cc90 (diff) |
Create wt-odlux directory
Include odlux apps, helpserver and readthedocs
Issue-ID: CCSDK-3970
Change-Id: I1aee1327e7da12e8f658185b9a985a5204ad6065
Signed-off-by: Ravi Pendurty <ravi.pendurty@highstreet-technologies.com>
Diffstat (limited to 'sdnr/wt-odlux/odlux/framework/src/components')
23 files changed, 3728 insertions, 0 deletions
diff --git a/sdnr/wt-odlux/odlux/framework/src/components/errorDisplay.tsx b/sdnr/wt-odlux/odlux/framework/src/components/errorDisplay.tsx new file mode 100644 index 000000000..d41d82687 --- /dev/null +++ b/sdnr/wt-odlux/odlux/framework/src/components/errorDisplay.tsx @@ -0,0 +1,131 @@ +/** + * ============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 React from 'react'; +import { Theme } from '@mui/material/styles'; + +import { WithStyles } from '@mui/styles'; +import withStyles from '@mui/styles/withStyles'; +import createStyles from '@mui/styles/createStyles'; + +import Modal from '@mui/material/Modal'; +import Button from '@mui/material/Button'; +import Card from '@mui/material/Card'; +import CardActions from '@mui/material/CardActions'; +import CardContent from '@mui/material/CardContent'; +import Typography from '@mui/material/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 component for formatting 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 color="inherit" 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/odlux/framework/src/components/icons/menuIcon.tsx b/sdnr/wt-odlux/odlux/framework/src/components/icons/menuIcon.tsx new file mode 100644 index 000000000..0d7d734c9 --- /dev/null +++ b/sdnr/wt-odlux/odlux/framework/src/components/icons/menuIcon.tsx @@ -0,0 +1,29 @@ +import React from 'react'; + +type MenuIconPropsBase = { + className?: string; + size?: number | string; +}; + +type MenuIconPropsWithColor = MenuIconPropsBase & { + color: string; +}; + +type MenuIconProps = MenuIconPropsBase | MenuIconPropsWithColor; + +const MenuIcon = (props: MenuIconProps) => { + const { className, size = '30px' } = props; + const color = 'color' in props ? props.color : '#36A9E1'; + + return ( + <svg className={className} width={size} height={size} viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg" xmlnsXlink="http://www.w3.org/1999/xlink"> + <path fill={color} d="M4,10h24c1.104,0,2-0.896,2-2s-0.896-2-2-2H4C2.896,6,2,6.896,2,8S2.896,10,4,10z" /> + <path fill={color} d="M28,14H4c-1.104,0-2,0.896-2,2 s0.896,2,2,2h24c1.104,0,2-0.896,2-2S29.104,14,28,14z" /> + <path fill={color} d="M28,22H4c-1.104,0-2,0.896-2,2s0.896,2,2,2h24c1.104,0,2-0.896,2-2 S29.104,22,28,22z" /> + </svg> + ); +}; + +MenuIcon.defaultName = 'MenuIcon'; + +export default MenuIcon;
\ No newline at end of file diff --git a/sdnr/wt-odlux/odlux/framework/src/components/logo.tsx b/sdnr/wt-odlux/odlux/framework/src/components/logo.tsx new file mode 100644 index 000000000..f2bb1f575 --- /dev/null +++ b/sdnr/wt-odlux/odlux/framework/src/components/logo.tsx @@ -0,0 +1,103 @@ +/** + * ============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 { Theme } from '@mui/material/styles'; // infra for styling + + +import { WithStyles } from '@mui/styles'; +import withStyles from '@mui/styles/withStyles'; +import createStyles from '@mui/styles/createStyles'; + + +const defaultLogo = require('../assets/icons/ht.Connect.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 threshold (", + this.hideLogoWhenWindowWidthIsLower, + "px)."].join('')); + } + return div; + } +} + +export const Logo = withStyles(styles)(withRouter(LogoComponent)); +export default Logo; diff --git a/sdnr/wt-odlux/odlux/framework/src/components/material-table/columnModel.ts b/sdnr/wt-odlux/odlux/framework/src/components/material-table/columnModel.ts new file mode 100644 index 000000000..3ed313497 --- /dev/null +++ b/sdnr/wt-odlux/odlux/framework/src/components/material-table/columnModel.ts @@ -0,0 +1,56 @@ +/** + * ============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, + date, + custom +} + +type CustomControl<TData> = { + className?: string; + style?: React.CSSProperties; + rowData: TData; +} + +export type ColumnModel<TData> = { + title?: string; + disablePadding?: boolean; + width?: string | number ; + className?: string; + hide?: boolean; + 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 | ColumnType.date; +});
\ No newline at end of file diff --git a/sdnr/wt-odlux/odlux/framework/src/components/material-table/index.tsx b/sdnr/wt-odlux/odlux/framework/src/components/material-table/index.tsx new file mode 100644 index 000000000..c1a5005d4 --- /dev/null +++ b/sdnr/wt-odlux/odlux/framework/src/components/material-table/index.tsx @@ -0,0 +1,707 @@ +/** + * ============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 { Theme } from '@mui/material/styles'; + +import { WithStyles } from '@mui/styles'; +import withStyles from '@mui/styles/withStyles'; +import createStyles from '@mui/styles/createStyles'; + +import Table from '@mui/material/Table'; +import TableBody from '@mui/material/TableBody'; +import TableCell from '@mui/material/TableCell'; +import TableContainer from '@mui/material/TableContainer'; +import TablePagination from '@mui/material/TablePagination'; +import TableRow from '@mui/material/TableRow'; +import Paper from '@mui/material/Paper'; +import Checkbox from '@mui/material/Checkbox'; + +import { TableToolbar } from './tableToolbar'; +import { EnhancedTableHead } from './tableHead'; +import { EnhancedTableFilter } from './tableFilter'; + +import { ColumnModel, ColumnType } from './columnModel'; +import { Menu, Typography } from '@mui/material'; +import { DistributiveOmit } from '@mui/types'; + +import makeStyles from '@mui/styles/makeStyles'; + +import { SvgIconProps } from '@mui/material/SvgIcon'; + +import { DividerTypeMap } from '@mui/material/Divider'; +import { MenuItemProps } from '@mui/material/MenuItem'; +import { flexbox } from '@mui/system'; +import { RowDisabled } from './utilities'; +import { toAriaLabel } from '../../utilities/yangHelper'; +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 regExpEscape(s: string) { + return s.replace(/[|\\{}()[\]^$+*?.]/g, '\\$&'); +} + +function wildcardCheck(input: string, pattern: string) { + if (!pattern) return true; + const regex = new RegExp( + (!pattern.startsWith('*') ? '^' : '') + + pattern.split(/\*+/).map(p => p.split(/\?+/).map(regExpEscape).join('.')).join('.*') + + (!pattern.endsWith('*') ? '$' : '') + ); + return input.match(regex) !== null && input.match(regex)!.length >= 1; +} + +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%', + overflow: "hidden", + marginTop: theme.spacing(3), + position: "relative", + boxSizing: "border-box", + display: "flex", + flexDirection: "column", + }, + container: { + flex: "1 1 100%" + }, + pagination: { + overflow: "hidden", + minHeight: "52px" + } +}); + +const useTableRowExtStyles = makeStyles((theme: Theme) => createStyles({ + disabled: { + color: "rgba(180, 180, 180, 0.7)", + }, +})); + +type GetStatelessComponentProps<T> = T extends (props: infer P & { children?: React.ReactNode }) => any ? P : any; +type TableRowExtProps = GetStatelessComponentProps<typeof TableRow> & { disabled: boolean }; +const TableRowExt : React.FC<TableRowExtProps> = (props) => { + const [disabled, setDisabled] = React.useState(true); + const classes = useTableRowExtStyles(); + + const onMouseDown = (ev: React.MouseEvent<HTMLElement>) => { + if (ev.button ===1){ + setDisabled(!disabled); + ev.preventDefault(); + ev.stopPropagation(); + } else if (props.disabled && disabled) { + ev.preventDefault(); + ev.stopPropagation(); + } + }; + + return ( + <TableRow {...{...props, color: props.disabled && disabled ? '#a0a0a0' : undefined , className: props.disabled && disabled ? classes.disabled : '', onMouseDown, onContextMenu: props.disabled && disabled ? onMouseDown : props.onContextMenu } } /> + ); +}; + +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; + hiddenColumns: string[]; + filter: { [property: string]: string }; +}; + +export type TableApi = { forceRefresh?: () => Promise<void> }; + +type MaterialTableComponentBaseProps<TData> = WithStyles<typeof styles> & { + className?: string; + columns: ColumnModel<TData>[]; + idProperty: keyof TData | ((data: TData) => React.Key); + + //Note: used to save settings as well. Must be unique across apps. Null tableIds will not get saved to the settings + tableId: string | null; + isPopup?: boolean; + title?: string; + stickyHeader?: boolean; + allowHtmlHeader?: boolean; + defaultSortOrder?: 'asc' | 'desc'; + defaultSortColumn?: keyof TData; + enableSelection?: boolean; + disableSorting?: boolean; + disableFilter?: boolean; + customActionButtons?: { icon: React.ComponentType<SvgIconProps>, tooltip?: string, ariaLabel: string, onClick: () => void, disabled?: boolean }[]; + onHandleClick?(event: React.MouseEvent<HTMLTableRowElement>, rowData: TData): void; + createContextMenu?: (row: TData) => React.ReactElement<MenuItemProps | DividerTypeMap<{}, "hr">, React.ComponentType<MenuItemProps | DividerTypeMap<{}, "hr">>>[]; +}; + +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; + onHideColumns : (columnNames: string[]) => void + onShowColumns: (columnNames: 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.onHideColumns instanceof Function || + propsWithExternalState.onHandleRequestSort instanceof Function +} + +// get settings in here! + + +class MaterialTableComponent<TData extends {} = {}> extends React.Component<MaterialTableComponentProps, MaterialTableComponentState & { contextMenuInfo: { index: number; mouseX?: number; mouseY?: number }; }> { + + 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 = { + contextMenuInfo: { index: -1 }, + 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 : this.props.defaultSortOrder || 'asc', + orderBy: isMaterialTableComponentPropsWithRowsAndRequestData(this.props) ? this.props.orderBy : this.props.defaultSortColumn || 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, + hiddenColumns: isMaterialTableComponentPropsWithRowsAndRequestData(this.props) && this.props.hiddenColumns || [], + page, + rowsPerPage, + }; + + if (isMaterialTableComponentPropsWithRequestData(this.props)) { + this.update(); + + if (this.props.tableApi) { + this.props.tableApi.forceRefresh = () => this.update(); + } + } + } + render(): JSX.Element { + const { classes, columns, allowHtmlHeader } = 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) } + + const hideColumns = isMaterialTableComponentPropsWithRowsAndRequestData(this.props) ? this.props.onHideColumns : (data: string[]) => { const newArray = [...new Set([...this.state.hiddenColumns, ...data])]; this.setState({hiddenColumns:newArray}); } + const showColumns = isMaterialTableComponentPropsWithRowsAndRequestData(this.props) ? this.props.onShowColumns : (data: string[]) => { const newArray = this.state.hiddenColumns.filter(el=> !data.includes(el)); this.setState({hiddenColumns:newArray}); } + + const allColumnsHidden = this.props.columns.length === this.state.hiddenColumns.length; + return ( + <Paper className={this.props.className ? `${classes.root} ${this.props.className}` : classes.root}> + <TableContainer className={classes.container}> + <TableToolbar tableId={this.props.tableId} numSelected={selected && selected.length} title={this.props.title} customActionButtons={this.props.customActionButtons} onExportToCsv={this.exportToCsv} + onToggleFilter={toggleFilter} + columns={columns} + onHideColumns={hideColumns} + onShowColumns={showColumns} /> + <Table padding="normal" aria-label={this.props.tableId ? this.props.tableId : 'tableTitle'} stickyHeader={this.props.stickyHeader || false} > + <EnhancedTableHead + allowHtmlHeader={allowHtmlHeader || false} + columns={columns} + numSelected={selected && selected.length} + order={order} + orderBy={orderBy} + onSelectAllClick={this.handleSelectAllClick} + onRequestSort={this.onHandleRequestSort} + rowCount={rows.length} + enableSelection={this.props.enableSelection} + hiddenColumns={this.state.hiddenColumns} + /> + <TableBody> + {showFilter && <EnhancedTableFilter columns={columns} hiddenColumns={this.state.hiddenColumns} filter={filter} onFilterChanged={this.onFilterChanged} enableSelection={this.props.enableSelection} /> || null} + + {allColumnsHidden ? <Typography variant="body1" textAlign="center">All columns of this table are hidden.</Typography> : + + rows // may need ordering here + .map((entry: TData & { [RowDisabled]?: boolean, [kex: string]: any }, index) => { + const entryId = getId(entry); + const contextMenu = (this.props.createContextMenu && this.state.contextMenuInfo.index === index && this.props.createContextMenu(entry)) || null; + const isSelected = this.isSelected(entryId) || this.state.contextMenuInfo.index === index; + return ( + <TableRowExt + hover + onClick={event => { + if (this.props.createContextMenu) { + this.setState({ + contextMenuInfo: { + index: -1 + } + }); + } + this.handleClick(event, entry, entryId); + }} + onContextMenu={event => { + if (this.props.createContextMenu) { + event.preventDefault(); + event.stopPropagation(); + this.setState({ contextMenuInfo: { index, mouseX: event.clientX - 2, mouseY: event.clientY - 4 } }); + } + }} + role="checkbox" + aria-checked={isSelected} + aria-label="table-row" + tabIndex={-1} + key={entryId} + selected={isSelected} + disabled={entry[RowDisabled] || false} + > + {this.props.enableSelection + ? <TableCell padding="checkbox" style={{ width: "50px", color: entry[RowDisabled] || false ? "inherit" : undefined } }> + <Checkbox color='secondary' checked={isSelected} /> + </TableCell> + : null + } + { + + this.props.columns.map( + col => { + const style = col.width ? { width: col.width } : {}; + const tableCell = ( + + <TableCell style={ entry[RowDisabled] || false ? { ...style, color: "inherit" } : style } aria-label={col.title? toAriaLabel(col.title) : toAriaLabel(col.property)} key={col.property} align={col.type === ColumnType.numeric && !col.align ? "right" : col.align} > + {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> + ); + + //show column if... + const showColumn = !this.state.hiddenColumns.includes(col.property); + return showColumn && tableCell + } + ) + } + {<Menu open={!!contextMenu} onClose={() => this.setState({ contextMenuInfo: { index: -1 } })} anchorReference="anchorPosition" keepMounted + anchorPosition={this.state.contextMenuInfo.mouseY != null && this.state.contextMenuInfo.mouseX != null ? { top: this.state.contextMenuInfo.mouseY, left: this.state.contextMenuInfo.mouseX } : undefined}> + {contextMenu} + </Menu> || null} + </TableRowExt> + ); + })} + {emptyRows > 0 && ( + <TableRow style={{ height: 49 * emptyRows }}> + <TableCell colSpan={this.props.columns.length} /> + </TableRow> + )} + </TableBody> + </Table> + </TableContainer> + <TablePagination className={classes.pagination} + rowsPerPageOptions={[5, 10, 20, 50]} + component="div" + count={rowCount} + rowsPerPage={rowsPerPage} + page={page} + aria-label={this.props.isPopup ? "popup-table-pagination-footer" : "table-pagination-footer" } + backIconButtonProps={{ + 'aria-label': this.props.isPopup ? 'popup-previous-page' : 'previous-page', + }} + nextIconButtonProps={{ + 'aria-label': this.props.isPopup ? 'popup-next-page': 'next-page', + }} + onPageChange={this.onHandleChangePage} + onRowsPerPageChange={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, + hiddenColumns: props.hiddenColumns, + 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 } { + + let data = [...(props.rows as dataType[] || [])]; + const columns = props.columns; + + const { page, rowsPerPage, order, orderBy, filter } = state; + + try { + if (state.showFilter) { + Object.keys(filter).forEach(prop => { + const column = columns.find(c => c.property === prop); + const filterExpression = filter[prop]; + + if (!column) throw new Error("Filter for not existing column found."); + + if (filterExpression != null) { + data = data.filter((val) => { + const dataValue = val[prop]; + + if (dataValue != null) { + + if (column.type === ColumnType.boolean) { + + const boolDataValue = JSON.parse(String(dataValue).toLowerCase()); + const boolFilterExpression = JSON.parse(String(filterExpression).toLowerCase()); + return boolDataValue == boolFilterExpression; + + } else if (column.type === ColumnType.text) { + + const valueAsString = String(dataValue); + const filterExpressionAsString = String(filterExpression).trim(); + if (filterExpressionAsString.length === 0) return true; + return wildcardCheck(valueAsString, filterExpressionAsString); + + } else if (column.type === ColumnType.numeric){ + + const valueAsNumber = Number(dataValue); + const filterExpressionAsString = String(filterExpression).trim(); + if (filterExpressionAsString.length === 0 || isNaN(valueAsNumber)) return true; + + if (filterExpressionAsString.startsWith('>=')) { + return valueAsNumber >= Number(filterExpressionAsString.substring(2).trim()); + } else if (filterExpressionAsString.startsWith('<=')) { + return valueAsNumber <= Number(filterExpressionAsString.substring(2).trim()); + } else if (filterExpressionAsString.startsWith('>')) { + return valueAsNumber > Number(filterExpressionAsString.substring(1).trim()); + } else if (filterExpressionAsString.startsWith('<')) { + return valueAsNumber < Number(filterExpressionAsString.substring(1).trim()); + } + } else if (column.type === ColumnType.date){ + const valueAsString = String(dataValue); + + const convertToDate = (valueAsString: string) => { + // time value needs to be padded + const hasTimeValue = /T\d{2,2}/.test(valueAsString); + const indexCollon = valueAsString.indexOf(':'); + if (hasTimeValue && (indexCollon === -1 || indexCollon >= valueAsString.length-2)) { + valueAsString = indexCollon === -1 + ? valueAsString + ":00" + : indexCollon === valueAsString.length-1 + ? valueAsString + "00" + : valueAsString += "0" + } + return new Date(Date.parse(valueAsString)); + }; + + // @ts-ignore + const valueAsDate = new Date(Date.parse(dataValue)); + const filterExpressionAsString = String(filterExpression).trim(); + + if (filterExpressionAsString.startsWith('>=')) { + return valueAsDate >= convertToDate(filterExpressionAsString.substring(2).trim()); + } else if (filterExpressionAsString.startsWith('<=')) { + return valueAsDate <= convertToDate(filterExpressionAsString.substring(2).trim()); + } else if (filterExpressionAsString.startsWith('>')) { + return valueAsDate > convertToDate(filterExpressionAsString.substring(1).trim()); + } else if (filterExpressionAsString.startsWith('<')) { + return valueAsDate < convertToDate(filterExpressionAsString.substring(1).trim()); + } + + + if (filterExpressionAsString.length === 0) return true; + return wildcardCheck(valueAsString, filterExpressionAsString); + + } + } + + return (dataValue == filterExpression) + }); + }; + }); + } + + 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<DistributiveOmit<MaterialTableComponentProps<TData>, 'classes'>>; + +export const MaterialTable = withStyles(styles)(MaterialTableComponent); +export default MaterialTable;
\ No newline at end of file diff --git a/sdnr/wt-odlux/odlux/framework/src/components/material-table/showColumnDialog.tsx b/sdnr/wt-odlux/odlux/framework/src/components/material-table/showColumnDialog.tsx new file mode 100644 index 000000000..ab0d465e7 --- /dev/null +++ b/sdnr/wt-odlux/odlux/framework/src/components/material-table/showColumnDialog.tsx @@ -0,0 +1,188 @@ +/** + * ============LICENSE_START======================================================================== + * ONAP : ccsdk feature sdnr wt odlux + * ================================================================================================= + * Copyright (C) 2022 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 React from 'react'; +import { Button, FormControlLabel, Popover, Switch, Typography } from '@mui/material'; +import { connect, Connect, IDispatcher } from '../../flux/connect'; + +import { ColumnModel } from './columnModel'; +import { IApplicationStoreState } from '../../store/applicationStore'; +import { TableSettingsColumn } from '../../models/settings'; +import { updateTableSettings } from '../../actions/settingsAction'; + +const mapStateToProps = (state: IApplicationStoreState) => ({ + settings: state.framework.applicationState.settings, + settingsDoneLoading: state.framework.applicationState.settings.isInitialLoadDone +}); + +const mapDispatchToProps = (dispatcher: IDispatcher) => ({ + saveToSettings: (tableName: string, columns: TableSettingsColumn[]) => dispatcher.dispatch(updateTableSettings(tableName, columns)) +}) + +type DialogProps = { + columns: ColumnModel<{}>[], + settingsName: string | null, + anchorEl: HTMLElement | null; + hideColumns: (columnNames: string[]) => void + showColumns: (columnNames: string[]) => void + onClose(): void + +} & Connect<typeof mapStateToProps, typeof mapDispatchToProps>; + + //TODO: figure out why everything gets triggered twice... + +const ShowColumnsDialog: React.FunctionComponent<DialogProps> = (props) => { + + const savedSettings = props.settingsName && props.settings.tables[props.settingsName]; + + const [checkedColumns, setCheckedColumns] = React.useState<{ property: string, display: boolean, title: string | undefined }[]>([]); + + const open = Boolean(props.anchorEl); + const allColumnNames = props.columns.map(e => e.property); + + React.useEffect(() => { + + createHideShowSelection(); + + }, []); + + React.useEffect(() => { + + createHideShowSelection(); + + }, [props.settings.isInitialLoadDone]); + + + const createHideShowSelection = () => { + let columns = props.columns.map(e => { return { property: e.property, display: !Boolean(e.hide), title: e.title } }); + + + if (savedSettings) { + + if (columns.length !== savedSettings.columns.length) { + console.error("saved column length does not match current column length. Maybe a settings entry got wrongly overridden?") + } + + //overwrite column data with settings + savedSettings?.columns.forEach(el => { + let foundIndex = columns.findIndex(e => e.property == el.property); + if (columns[foundIndex] !== undefined) + columns[foundIndex].display = el.displayed; + }); + + } else { + console.warn("No settingsName set, changes will not be saved.") + } + + setCheckedColumns(columns); + + const hideColumns = columns.filter(el => !el.display).map(e => e.property); + props.hideColumns(hideColumns); + } + + + const handleChange = (propertyName: string, checked: boolean) => { + if (!checked) { + props.hideColumns([propertyName]); + } else { + props.showColumns([propertyName]) + + } + + let updatedList = checkedColumns.map(item => { + if (item.property == propertyName) { + return { ...item, display: checked }; + } + return item; + }); + + setCheckedColumns(updatedList); + }; + + const onHideAll = () => { + + switchCheckedColumns(false); + props.hideColumns(allColumnNames); + } + + const onShowAll = () => { + + switchCheckedColumns(true); + props.showColumns(allColumnNames); + } + + const onClose = () => { + + const tableColumns: TableSettingsColumn[] = checkedColumns.map(el => { + return { + property: el.property, + displayed: el.display + } + }); + + if (props.settingsName) { + props.saveToSettings(props.settingsName, tableColumns); + } + props.onClose(); + + } + + const switchCheckedColumns = (changeToValue: boolean) => { + let updatedList = checkedColumns.map(item => { + return { ...item, display: changeToValue }; + }); + + setCheckedColumns(updatedList); + + } + + return (<Popover open={open} onClose={onClose} + anchorEl={props.anchorEl} + anchorOrigin={{ + vertical: 'top', + horizontal: 'left', + }} > + <div> + <Typography fontWeight={600} style={{ margin: 10 }} >Hide / Show Columns</Typography> + </div> + <div style={{ display: "flex", flexDirection: "column", margin: 10 }}> + { + checkedColumns?.map((el, i) => { + + return <> + + <FormControlLabel + value="end" + key={"hide-show-column-"+i} + aria-label={"hide-or-show-column-button"} + control={<Switch color="secondary" checked={el.display} onChange={e => handleChange(el.property, e.target.checked)} />} + label={el.title || el.property} + labelPlacement="end" + /> + </> + }) + } + <div style={{ display: "flex", flexDirection: "row", justifyContent: "space-between" }}> + <Button color="secondary" aria-label="hide-all-columns-button" onClick={(e) => onHideAll()}>Hide all</Button> + <Button color="secondary" aria-label="show-all-columns-button" onClick={(e) => onShowAll()}>Show all</Button> + </div> + </div> + </Popover>) +} + +export default connect(mapStateToProps, mapDispatchToProps)(ShowColumnsDialog); diff --git a/sdnr/wt-odlux/odlux/framework/src/components/material-table/tableFilter.tsx b/sdnr/wt-odlux/odlux/framework/src/components/material-table/tableFilter.tsx new file mode 100644 index 000000000..1b9136844 --- /dev/null +++ b/sdnr/wt-odlux/odlux/framework/src/components/material-table/tableFilter.tsx @@ -0,0 +1,113 @@ +/** + * ============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 { Theme } from '@mui/material/styles'; + + +import { WithStyles } from '@mui/styles'; +import withStyles from '@mui/styles/withStyles'; +import createStyles from '@mui/styles/createStyles'; + + +import TableCell from '@mui/material/TableCell'; +import TableRow from '@mui/material/TableRow'; +import Input from '@mui/material/Input'; +import { Select, FormControl, InputLabel, MenuItem, SelectChangeEvent } from '@mui/material'; +import { toAriaLabel } from '../../utilities/yangHelper'; + + +const styles = (theme: Theme) => createStyles({ + container: { + display: 'flex', + flexWrap: 'wrap', + }, + input: { + margin: theme.spacing(1), + }, + numberInput: { + float: "right" + } +}); + +interface IEnhancedTableFilterComponentProps extends WithStyles<typeof styles> { + onFilterChanged: (property: string, filterTerm: string) => void; + filter: { [property: string]: string }; + columns: ColumnModel<{}>[]; + hiddenColumns: string[]; + enableSelection?: boolean; +} + +class EnhancedTableFilterComponent extends React.Component<IEnhancedTableFilterComponentProps> { + createSelectFilterHandler = (property: string) => (event: SelectChangeEvent<HTMLSelectElement | string>) => { + this.props.onFilterChanged && this.props.onFilterChanged(property, event.target.value as string); + }; + createInputFilterHandler = (property: string) => (event: React.ChangeEvent<HTMLTextAreaElement | HTMLInputElement>) => { + this.props.onFilterChanged && this.props.onFilterChanged(property, event.currentTarget.value); + }; + + + render() { + const { columns, filter, classes } = this.props; + return ( + <TableRow> + {this.props.enableSelection + ? <TableCell padding="checkbox" style={{ width: "50px" }}> + </TableCell> + : null + } + {columns.map((col, ind) => { + const style = col.width ? { width: col.width } : {}; + const tableCell = ( + <TableCell + className={col.type === ColumnType.numeric ? classes.numberInput : ''} + key={col.property} + padding={col.disablePadding ? 'none' : 'normal'} + style={style} + > + {col.disableFilter || (col.type === ColumnType.custom) + ? null + : (col.type === ColumnType.boolean) + ? <Select variant="standard" className={classes.input} aria-label={col.title ? toAriaLabel(col.title as string) + '-filter' : `${ind + 1}-filter`} + value={filter[col.property] !== undefined ? filter[col.property] : ''} + onChange={this.createSelectFilterHandler(col.property)} + inputProps={{ name: `${col.property}-bool`, id: `${col.property}-bool` }} > + <MenuItem value={undefined} aria-label="none-value" > + <em>None</em> + </MenuItem> + <MenuItem aria-label="true-value" value={true as any as string}>{col.labels ? col.labels["true"] : "true"}</MenuItem> + <MenuItem aria-label="false-value" value={false as any as string}>{col.labels ? col.labels["false"] : "false"}</MenuItem> + </Select> + : <Input className={classes.input} + inputProps={{ 'aria-label': col.title ? toAriaLabel(col.title as string) + '-filter' : `${ind + 1}-filter` }} + value={filter[col.property] || ''} + onChange={this.createInputFilterHandler(col.property)} />} + </TableCell> + ); + + const showColumn = !this.props.hiddenColumns.includes(col.property); + + return showColumn && tableCell; + }, this)} + </TableRow> + ); + } +} + +export const EnhancedTableFilter = withStyles(styles)(EnhancedTableFilterComponent);
\ No newline at end of file diff --git a/sdnr/wt-odlux/odlux/framework/src/components/material-table/tableHead.tsx b/sdnr/wt-odlux/odlux/framework/src/components/material-table/tableHead.tsx new file mode 100644 index 000000000..d6f7b7def --- /dev/null +++ b/sdnr/wt-odlux/odlux/framework/src/components/material-table/tableHead.tsx @@ -0,0 +1,127 @@ +/** + * ============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 { Theme } from '@mui/material/styles'; + +import { WithStyles } from '@mui/styles'; +import withStyles from '@mui/styles/withStyles'; +import createStyles from '@mui/styles/createStyles'; + +import TableSortLabel from '@mui/material/TableSortLabel'; +import TableCell from '@mui/material/TableCell'; +import TableHead from '@mui/material/TableHead'; +import TableRow from '@mui/material/TableRow'; +import Checkbox from '@mui/material/Checkbox'; +import Tooltip from '@mui/material/Tooltip'; + +const styles = (theme: Theme) => createStyles({ + header: { + backgroundColor: "#fafafa", + position: "sticky", + top: 0 + } +}); + + +type styles_header = WithStyles<typeof styles>; + +interface IEnhancedTableHeadComponentProps extends styles_header { + numSelected: number | null; + onRequestSort: (event: React.SyntheticEvent, property: string) => void; + onSelectAllClick: () => void; + order: 'asc' | 'desc'; + orderBy: string | null; + rowCount: number; + columns: ColumnModel<{}>[]; + hiddenColumns: string[]; + enableSelection?: boolean; + allowHtmlHeader?: 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; + const {classes} = this.props; + + return ( + <TableHead> + <TableRow> + { this.props.enableSelection + ? <TableCell padding="checkbox" style={ { width: "50px" } } className= {classes.header} > + <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 } : {}; + const tableCell = ( + <TableCell className= {classes.header} + key={ col.property } + align={ col.type === ColumnType.numeric ? 'right' : 'left' } + padding={ col.disablePadding ? 'none' : 'normal' } + 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 disableInteractive + 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) } + > + { + this.props.allowHtmlHeader ? <div className="content" dangerouslySetInnerHTML={{__html: col.title || col.property}}></div> + : (col.title || col.property ) + } + </TableSortLabel> + </Tooltip> } + </TableCell> + ); + + //show column if... + const showColumn = !this.props.hiddenColumns.includes(col.property); + + return showColumn && tableCell; + }, this) } + </TableRow> + </TableHead> + ); + } +} + +export const EnhancedTableHead = withStyles(styles)(EnhancedTableHeadComponent);
\ No newline at end of file diff --git a/sdnr/wt-odlux/odlux/framework/src/components/material-table/tableToolbar.tsx b/sdnr/wt-odlux/odlux/framework/src/components/material-table/tableToolbar.tsx new file mode 100644 index 000000000..143b802a4 --- /dev/null +++ b/sdnr/wt-odlux/odlux/framework/src/components/material-table/tableToolbar.tsx @@ -0,0 +1,191 @@ +/** + * ============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 { Theme, lighten } from '@mui/material/styles'; + +import { WithStyles } from '@mui/styles'; +import withStyles from '@mui/styles/withStyles'; +import createStyles from '@mui/styles/createStyles'; + +import IconButton from '@mui/material/IconButton'; +import Tooltip from '@mui/material/Tooltip'; +import Toolbar from '@mui/material/Toolbar'; +import Typography from '@mui/material/Typography'; +import DeleteIcon from '@mui/icons-material/Delete'; +import MoreIcon from '@mui/icons-material/MoreVert'; +import FilterListIcon from '@mui/icons-material/FilterList'; +import MenuItem from '@mui/material/MenuItem'; +import Menu from '@mui/material/Menu'; +import { SvgIconProps } from '@mui/material/SvgIcon'; +import { Button } from '@mui/material'; +import { ColumnModel } from './columnModel'; +import ShowColumnsDialog from './showColumnDialog' + +const styles = (theme: Theme) => createStyles({ + root: { + paddingRight: theme.spacing(1), + }, + highlight: + theme.palette.mode === '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 | null; + customActionButtons?: { icon: React.ComponentType<SvgIconProps>, tooltip?: string, ariaLabel: string, onClick: () => void, disabled?: boolean }[]; + columns: ColumnModel<{}>[]; + onHideColumns: (columnNames: string[]) => void + onShowColumns: (columnNames: string[]) => void + onToggleFilter: () => void; + onExportToCsv: () => void; +} + +class TableToolbarComponent extends React.Component<ITableToolbarComponentProps, { anchorEl: EventTarget & HTMLElement | null, anchorElDialog: HTMLElement | null }> { + + + constructor(props: ITableToolbarComponentProps) { + super(props); + + this.state = { + anchorEl: null, + anchorElDialog: null + }; + } + + private handleMenu = (event: React.MouseEvent<HTMLElement>) => { + this.setState({ anchorEl: event.currentTarget }); + }; + + private handleClose = () => { + this.setState({ anchorEl: null }); + }; + + private showColumnsDialog = (event: React.MouseEvent<HTMLElement>) =>{ + this.setState({ anchorElDialog: this.state.anchorEl }); + } + + private onCloseDialog = () =>{ + this.setState({ anchorElDialog: null }); + + } + + render() { + const { numSelected, classes } = this.props; + const open = !!this.state.anchorEl; + const buttonPrefix = this.props.tableId !== null ? this.props.tableId : 'table'; + 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 disableInteractive key={`custom-action-${ind}`} title={action.tooltip || ''}> + <IconButton + disabled={action.disabled} + aria-label={`${buttonPrefix}-${action.ariaLabel}-button`} + onClick={() => action.onClick()} + size="large"> + <action.icon /> + </IconButton> + </Tooltip> + )) + : null} + {numSelected && numSelected > 0 ? ( + <Tooltip disableInteractive title="Delete"> + <IconButton aria-label={`${buttonPrefix}-delete-button`} size="large"> + <DeleteIcon /> + </IconButton> + </Tooltip> + ) : ( + <Tooltip disableInteractive title="Filter list"> + <IconButton + aria-label={`${buttonPrefix}-filter-list-button`} + onClick={() => { this.props.onToggleFilter && this.props.onToggleFilter() }} + size="large"> + <FilterListIcon /> + </IconButton> + </Tooltip> + )} + <Tooltip disableInteractive title="Actions"> + <IconButton + color="inherit" + aria-label={`${buttonPrefix}-additional-actions-button`} + aria-owns={open ? 'menu-appbar' : undefined} + aria-haspopup="true" + onClick={this.handleMenu} + size="large"> + <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 aria-label="export-table-as-csv" onClick={(e) =>{ this.props.onExportToCsv(); this.handleClose()}}>Export as CSV</MenuItem> + <MenuItem aria-label="hide-show-table-columns" onClick={(e) =>{ this.showColumnsDialog(e); this.handleClose()}}>Hide/show columns</MenuItem> + </Menu> + </div> + </Toolbar> + <ShowColumnsDialog + anchorEl={this.state.anchorElDialog} + onClose={this.onCloseDialog} + settingsName={this.props.tableId} + columns={this.props.columns} + hideColumns={this.props.onHideColumns} + showColumns={this.props.onShowColumns} /> + </> + ); + } +} + +export const TableToolbar = withStyles(styles)(TableToolbarComponent);
\ No newline at end of file diff --git a/sdnr/wt-odlux/odlux/framework/src/components/material-table/utilities.ts b/sdnr/wt-odlux/odlux/framework/src/components/material-table/utilities.ts new file mode 100644 index 000000000..e2fda7647 --- /dev/null +++ b/sdnr/wt-odlux/odlux/framework/src/components/material-table/utilities.ts @@ -0,0 +1,357 @@ +/** + * ============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'; + +export const RowDisabled = Symbol("RowDisabled"); +import { DataCallback } from "."; + +export interface IExternalTableState<TData> { + order: 'asc' | 'desc'; + orderBy: string | null; + selected: any[] | null; + hiddenColumns: string[] + rows: (TData & { [RowDisabled]?: boolean })[]; + total: number; + page: number; + rowsPerPage: number; + loading: boolean; + showFilter: boolean; + filter: { [property: string]: string }; + preFilter: { [property: string]: string }; +} + +export type ExternalMethodes<TData> = { + reloadAction: (dispatch: Dispatch, getAppState: () => IApplicationStoreState) => Promise<void | AddErrorInfoAction>; + createActions: (dispatch: Dispatch, skipRefresh?: boolean) => { + onRefresh: () => void; + onHandleRequestSort: (orderBy: string) => void; + onHandleExplicitRequestSort: (property: string, sortOrder: "asc" | "desc") => void; + onToggleFilter: (refresh?: boolean | undefined) => void; + onFilterChanged: (property: string, filterTerm: string) => void; + onHandleChangePage: (page: number) => void; + onHandleChangeRowsPerPage: (rowsPerPage: number | null) => void; + onHideColumns: (columnName: string[]) => void; + onShowColumns: (columnName: string[]) => void; + onClearFilters: () => void; + }, + createPreActions: (dispatch: Dispatch, skipRefresh?: boolean) => { + onPreFilterChanged: (preFilter: { + [key: string]: string; + }) => void; + }; + createProperties: (state: IApplicationStoreState) => IExternalTableState<TData>; + actionHandler: IActionHandler<IExternalTableState<TData>, Action>; +} + + +/** Create an actionHandler and actions for external table states. */ +export function createExternal<TData>(callback: DataCallback<TData>, selectState: (appState: IApplicationStoreState) => IExternalTableState<TData>) : ExternalMethodes<TData> ; +export function createExternal<TData>(callback: DataCallback<TData>, selectState: (appState: IApplicationStoreState) => IExternalTableState<TData>, disableRow: (data: TData) => boolean) : ExternalMethodes<TData>; +export function createExternal<TData>(callback: DataCallback<TData>, selectState: (appState: IApplicationStoreState) => IExternalTableState<TData>, disableRow?: (data: TData) => boolean) : ExternalMethodes<TData> { + + //#region Actions + abstract class TableAction extends Action { } + + + class RequestSortAction extends TableAction { + constructor(public orderBy: string) { + super(); + } + } + + class RequestExplicitSortAction extends TableAction { + constructor(public propertyName: string, public sortOrder: "asc" | "desc") { + 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(); + } + } + + class HideColumnsAction extends TableAction{ + constructor(public property: string[]){ + super(); + } + } + + class ShowColumnsAction extends TableAction{ + constructor(public property: string[]){ + super(); + } + } + + // #endregion + + //#region Action Handler + const externalTableStateInit: IExternalTableState<TData> = { + order: 'asc', + orderBy: null, + selected: null, + hiddenColumns:[], + 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: disableRow + ? action.result.rows.map((row: TData) => ({...row, [RowDisabled]: disableRow(row) })) + : 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 RequestExplicitSortAction) { + state = { + ...state, + loading: true, + orderBy: action.propertyName, + order: action.sortOrder + } + } + 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 + } + } + else if (action instanceof HideColumnsAction){ + + //merge arrays, remove duplicates + const newArray = [...new Set([...state.hiddenColumns, ...action.property])] + state = {...state, hiddenColumns: newArray}; + } + else if(action instanceof ShowColumnsAction){ + + const newArray = state.hiddenColumns.filter(el=> !action.property.includes(el)); + state = {...state, hiddenColumns: newArray}; + } + + 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 || {}) }; + return 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 => dispatch(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); + }); + }, + onHandleExplicitRequestSort: (property: string, sortOrder: "asc" | "desc") => { + dispatch((dispatch: Dispatch) => { + dispatch(new RequestExplicitSortAction(property, sortOrder)); + (!skipRefresh) && dispatch(reloadAction); + }); + }, + onToggleFilter: (refresh?: boolean) => { + dispatch((dispatch: Dispatch, getAppState: () => IApplicationStoreState) => { + const { showFilter } = selectState(getAppState()); + dispatch(new SetShowFilterAction(!showFilter)); + if (!skipRefresh && (refresh === undefined || refresh)) + 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); + }); + }, + onHideColumns: (columnName: string[]) =>{ + dispatch((dispatch: Dispatch) => { + dispatch(new HideColumnsAction(columnName)); + }) + }, + onShowColumns: (columnName: string[]) =>{ + dispatch((dispatch: Dispatch) => { + dispatch(new ShowColumnsAction(columnName)); + }) + }, + onClearFilters: () => { + dispatch((dispatch: Dispatch) => { + let filter = { }; + dispatch(new SetFilterChangedAction(filter)); + }); + }, + // selected: + }; + }; + + const createProperties = (state: IApplicationStoreState) => { + return { + ...selectState(state) + } + } + + return { + reloadAction: reloadAction, + createActions: createActions, + createProperties: createProperties, + createPreActions: createPreActions, + actionHandler: externalTableStateActionHandler, + } +} + diff --git a/sdnr/wt-odlux/odlux/framework/src/components/material-ui/index.ts b/sdnr/wt-odlux/odlux/framework/src/components/material-ui/index.ts new file mode 100644 index 000000000..096e4439d --- /dev/null +++ b/sdnr/wt-odlux/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, TreeItem, TreeViewCtorType} from './treeView'; +export { Loader } from './loader'; diff --git a/sdnr/wt-odlux/odlux/framework/src/components/material-ui/listItemLink.tsx b/sdnr/wt-odlux/odlux/framework/src/components/material-ui/listItemLink.tsx new file mode 100644 index 000000000..626cb8978 --- /dev/null +++ b/sdnr/wt-odlux/odlux/framework/src/components/material-ui/listItemLink.tsx @@ -0,0 +1,83 @@ +/** + * ============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 '@mui/material/ListItem'; +import ListItemIcon from '@mui/material/ListItemIcon'; +import ListItemText from '@mui/material/ListItemText'; + +import { Theme } from '@mui/material/styles'; +import { WithStyles } from '@mui/styles'; +import withStyles from '@mui/styles/withStyles'; +import createStyles from '@mui/styles/createStyles'; +import { toAriaLabel } from '../../utilities/yangHelper'; +import { IconType } from '../../models/iconDefinition'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; + +const styles = (theme: Theme) => createStyles({ + active: { + backgroundColor: theme.palette.action.selected + } +}); + +export interface IListItemLinkProps extends WithStyles<typeof styles> { + icon: IconType | null; + primary: string | React.ComponentType; + secondary?: React.ComponentType; + to: string; + exact?: boolean; + external?: boolean; +} + +export const ListItemLink = withStyles(styles)((props: IListItemLinkProps) => { + const { icon, primary: Primary, secondary: Secondary, classes, to, exact = false, external=false } = props; + const renderLink = (itemProps: any): JSX.Element => ( + props.external ? <a target="_blank" href={to} { ...itemProps }></a> : + <NavLink exact={ exact } to={ to } activeClassName={ classes.active } { ...itemProps } />); + + const customIconHeight = 22; + const ariaLabel = typeof Primary === 'string' ? toAriaLabel("link-to-"+Primary) : toAriaLabel("link-to-"+Primary.displayName); + + //create menu icon, either using an faIcon or a link to a custom svg icon + //moved to one place for easier usage + const listItemIcon = icon && ( typeof icon === 'string' ? <img height={customIconHeight} src={icon} /> : <FontAwesomeIcon icon={icon} /> ); + + return ( + <> + <ListItem button component={ renderLink } aria-label={ariaLabel}> + { icon + ? <ListItemIcon>{ listItemIcon }</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/odlux/framework/src/components/material-ui/loader.tsx b/sdnr/wt-odlux/odlux/framework/src/components/material-ui/loader.tsx new file mode 100644 index 000000000..bd523e1f4 --- /dev/null +++ b/sdnr/wt-odlux/odlux/framework/src/components/material-ui/loader.tsx @@ -0,0 +1,49 @@ +/** + * ============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 { Theme } from '@mui/material/styles'; + +import { WithStyles } from '@mui/styles'; +import withStyles from '@mui/styles/withStyles'; +import createStyles from '@mui/styles/createStyles'; + +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/odlux/framework/src/components/material-ui/panel.tsx b/sdnr/wt-odlux/odlux/framework/src/components/material-ui/panel.tsx new file mode 100644 index 000000000..6d192d2f0 --- /dev/null +++ b/sdnr/wt-odlux/odlux/framework/src/components/material-ui/panel.tsx @@ -0,0 +1,76 @@ +/** + * ============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 { Theme } from '@mui/material/styles'; + +import { WithStyles } from '@mui/styles'; +import withStyles from '@mui/styles/withStyles'; +import createStyles from '@mui/styles/createStyles'; + +import { Accordion, AccordionSummary, AccordionDetails, Typography, AccordionActions } from '@mui/material'; + +import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; +import { SvgIconProps } from '@mui/material/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", + 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 ( + <Accordion className={classes.accordion} expanded={activePanel === props.panelId} onChange={() => onToggle(props.panelId)} > + <AccordionSummary expandIcon={<ExpandMoreIcon />}> + <Typography className={classes.text} >{props.title}</Typography> + </AccordionSummary> + <AccordionDetails className={classes.detail}> + {props.children} + </AccordionDetails> + {props.customActionButtons + ? <AccordionActions> + {props.customActionButtons} + </AccordionActions> + : null} + </Accordion> + ); +}; + +export const Panel = withStyles(styles)(PanelComponent); +export default Panel;
\ No newline at end of file diff --git a/sdnr/wt-odlux/odlux/framework/src/components/material-ui/snackDisplay.tsx b/sdnr/wt-odlux/odlux/framework/src/components/material-ui/snackDisplay.tsx new file mode 100644 index 000000000..437784ce5 --- /dev/null +++ b/sdnr/wt-odlux/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/odlux/framework/src/components/material-ui/toggleButton.tsx b/sdnr/wt-odlux/odlux/framework/src/components/material-ui/toggleButton.tsx new file mode 100644 index 000000000..54f14a7e0 --- /dev/null +++ b/sdnr/wt-odlux/odlux/framework/src/components/material-ui/toggleButton.tsx @@ -0,0 +1,181 @@ +/** + * ============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 { Theme, alpha } from '@mui/material/styles'; +import { WithStyles } from '@mui/styles'; +import withStyles from '@mui/styles/withStyles'; +import createStyles from '@mui/styles/createStyles'; +import ButtonBase from '@mui/material/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)} ${theme.spacing(1.5)}`, + borderRadius: 2, + willChange: 'opacity', + color: alpha(theme.palette.action.active, 0.38), + '&:hover': { + textDecoration: 'none', + // Reset on mouse devices + backgroundColor: alpha(theme.palette.text.primary, 0.12), + '@media (hover: none)': { + backgroundColor: 'transparent', + }, + '&.Mui-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: alpha(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/odlux/framework/src/components/material-ui/toggleButtonGroup.tsx b/sdnr/wt-odlux/odlux/framework/src/components/material-ui/toggleButtonGroup.tsx new file mode 100644 index 000000000..bdabe0d56 --- /dev/null +++ b/sdnr/wt-odlux/odlux/framework/src/components/material-ui/toggleButtonGroup.tsx @@ -0,0 +1,40 @@ +/** + * ============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 { Theme } from '@mui/material/styles'; + +import { WithStyles } from '@mui/styles'; +import withStyles from '@mui/styles/withStyles'; +import createStyles from '@mui/styles/createStyles'; + +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/odlux/framework/src/components/material-ui/treeView.tsx b/sdnr/wt-odlux/odlux/framework/src/components/material-ui/treeView.tsx new file mode 100644 index 000000000..5c23909c4 --- /dev/null +++ b/sdnr/wt-odlux/odlux/framework/src/components/material-ui/treeView.tsx @@ -0,0 +1,380 @@ +/** + * ============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 { Theme } from '@mui/material/styles'; + +import { makeStyles, WithStyles, WithTheme } from '@mui/styles'; +import withStyles from '@mui/styles/withStyles'; +import createStyles from '@mui/styles/createStyles'; + +import { List, ListItem, TextField, ListItemText, ListItemIcon, Typography } from '@mui/material'; +import { DistributiveOmit } from '@mui/types'; + +import withTheme from '@mui/styles/withTheme'; + +import { SvgIconProps } from '@mui/material/SvgIcon'; +import FileIcon from '@mui/icons-material/InsertDriveFile'; +import CloseIcon from '@mui/icons-material/ExpandLess'; +import OpenIcon from '@mui/icons-material/ExpandMore'; +import FolderIcon from '@mui/icons-material/Folder'; + +declare module '@mui/styles/defaultTheme' { + // eslint-disable-next-line @typescript-eslint/no-empty-interface (remove this line if you don't have the rule enabled) + interface DefaultTheme extends Theme {} +} + +const styles = (theme: Theme) => createStyles({ + root: { + padding: 0, + paddingBottom: 8, + paddingTop: 8, + }, + search: { + padding: `0px ${theme.spacing(1)}` + } +}); + +export enum SearchMode { + OnKeyDown = 1, + OnEnter =2 +} + +export type TreeItem<TData = { }> = { + disabled?: boolean; + icon?: React.ComponentType<SvgIconProps>; + iconClass?: string; + content: string; + contentClass?: string; + children?: TreeItem<TData>[]; + value?: TData; +} + +export type ExternalTreeItem<TData = {}> = TreeItem<TData> & { + isMatch?: boolean; +} + + +type TreeViewComponentState<TData = { }> = { + /** All indices of all expanded Items */ + expandedItems: ExternalTreeItem<TData>[]; + /** The index of the active iten or undefined if no item is active. */ + activeItem?: ExternalTreeItem<TData>; + /** The search term or undefined if search is currently not active. */ + searchTerm?: string; + searchTermValue?: string; +} + +type TreeViewComponentBaseProps<TData = {}> = WithTheme & WithStyles<typeof styles> & { + className?: string; + items: TreeItem<TData>[]; + useFolderIcons?: boolean; + enableSearchBar?: boolean; + autoExpandFolder?: boolean; + style?: React.CSSProperties; + itemHeight?: number; + depthOffset?: number; + searchMode?: SearchMode; + +} + +type TreeViewComponentWithInternalStateProps<TData = { }> = TreeViewComponentBaseProps<TData> & { + initialSearchTerm? : string; + onItemClick?: (item: TreeItem<TData>) => void; + onFolderClick?: (item: TreeItem<TData>) => void; +} + +type TreeViewComponentWithExternalSearchProps<TData = {}> = TreeViewComponentBaseProps<TData> & { + items: ExternalTreeItem<TData>[]; + initialSearchTerm? : string; + searchTerm: string; + onSearch: (searchTerm: string) => void; + onItemClick?: (item: TreeItem<TData>) => void; + onFolderClick?: (item: TreeItem<TData>) => void; +} + +type TreeViewComponentWithExternalStateProps<TData = {}> = TreeViewComponentBaseProps<TData> & TreeViewComponentState<TData> & { + items: ExternalTreeItem<TData>[]; + initialSearchTerm? : string; + searchTerm: string; + onSearch: (searchTerm: string) => void; + onItemClick: (item: TreeItem<TData>) => void; + onFolderClick: (item: TreeItem<TData>) => void; +} + +type TreeViewComponentProps<TData = { }> = + | TreeViewComponentWithInternalStateProps<TData> + | TreeViewComponentWithExternalSearchProps<TData> + | TreeViewComponentWithExternalStateProps<TData>; + +function isTreeViewComponentWithExternalSearchProps(props: TreeViewComponentProps): props is TreeViewComponentWithExternalSearchProps { + const propsWithExternalState = (props as TreeViewComponentWithExternalStateProps) + return ( + propsWithExternalState.onSearch instanceof Function && + propsWithExternalState.onFolderClick === undefined && + propsWithExternalState.expandedItems === undefined && + propsWithExternalState.searchTerm !== undefined + ); +} + +function isTreeViewComponentWithExternalStateProps(props: TreeViewComponentProps): props is TreeViewComponentWithExternalStateProps { + const propsWithExternalState = (props as TreeViewComponentWithExternalStateProps) + return ( + propsWithExternalState.onSearch instanceof Function && + propsWithExternalState.onFolderClick instanceof Function && + propsWithExternalState.expandedItems !== undefined && + propsWithExternalState.searchTerm !== undefined + ); +} + +class TreeViewComponent<TData = { }> extends React.Component<TreeViewComponentProps<TData>, TreeViewComponentState<TData>> { + + /** + * Initializes a new instance. + */ + constructor(props: TreeViewComponentProps<TData>) { + super(props); + + this.state = { + expandedItems: [], + activeItem: undefined, + searchTerm: undefined, + searchTermValue: props.initialSearchTerm + }; + } + + render(): JSX.Element { + this.itemIndex = 0; + const { searchTerm , searchTermValue} = this.state; + const { children, items, enableSearchBar } = this.props; + + return ( + <div className={this.props.className ? `${this.props.classes.root} ${this.props.className}` : this.props.classes.root} style={this.props.style}> + {children} + {enableSearchBar && <TextField variant="standard" label={"Search"} inputProps={{'aria-label': 'treeview-searchfield'}} fullWidth={true} className={this.props.classes.search} value={searchTermValue} onKeyDown={this.onSearchKeyDown} onChange={this.onChangeSearchText} /> || null} + {enableSearchBar && (searchTerm === undefined || searchTerm.length===0 )&& <Typography style={{marginTop:'10px'}}>Please search for an inventory identifier or use *.</Typography>} + <List> + {this.renderItems(items, searchTerm && searchTerm.toLowerCase())} + </List> + </div> + ); + } + + private itemIndex: number = 0; + private renderItems = (items: TreeItem<TData>[], searchTerm: string | undefined, depth: number = 1, forceRender: boolean = true) => { + + return items.reduce((acc, item) => { + + const children = item.children; // this.props.childrenProperty && ((item as any)[this.props.childrenProperty] as TData[]); + const childrenJsx = children && this.renderItems(children, searchTerm, depth + 1, this.state.expandedItems.indexOf(item) > -1); + + const expanded = !isTreeViewComponentWithExternalStateProps(this.props) && searchTerm + ? childrenJsx && childrenJsx.length > 0 + : !children + ? false + : this.state.expandedItems.indexOf(item) > -1; + const isFolder = children !== undefined; + + const itemJsx = this.renderItem(item, searchTerm, depth, isFolder, expanded || false, forceRender); + itemJsx && acc.push(itemJsx); + + if (isFolder && expanded && childrenJsx) { + acc.push(...childrenJsx); + } + return acc; + + }, [] as JSX.Element[]); + } + private renderItem = (item: ExternalTreeItem<TData> , searchTerm: string | undefined, depth: number, isFolder: boolean, expanded: boolean, forceRender: boolean): JSX.Element | null => { + const styles = { + item: { + paddingLeft: (((this.props.depthOffset || 0) + depth) * Number(this.props.theme.spacing(3).replace("px", ''))), + 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.content || ''; // need to keep track of search + const search_array = searchTerm?.split("*"); + const index = search_array?.findIndex(function (_str: String) { + return _str.length > 0; + }) || 0; + const firstSearchSubString = search_array ? search_array[index] : ""; + const matchIndex = firstSearchSubString ? text.toLowerCase().indexOf(firstSearchSubString) : -1; + + const hasStarInSearch = search_array ? search_array.length > 1 : false; + const isSearchStringWithStar = hasStarInSearch && firstSearchSubString?.length > 0 || false; + + const searchTermLength = firstSearchSubString && firstSearchSubString.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 || (!isTreeViewComponentWithExternalStateProps(this.props) && item.isMatch || depth === 1)) || !searchTerm || forceRender) + ? ( + <ListItem key={`tree-list-${this.itemIndex++}`} aria-label="tree-view-item" style={styles.item} onClick={handleClickCreator(false)} button > + + { // display the left icon + (this.props.useFolderIcons && <ListItemIcon>{isFolder ? <FolderIcon /> : <FileIcon />}</ListItemIcon>) || + (item.icon && (<ListItemIcon className={ item.iconClass }><item.icon /></ListItemIcon>))} + + + { // highlight search result + isSearchStringWithStar && matchIndex > -1 + ? <ListItemText className={item.contentClass} primary={( + <span + style={{ + display: 'inline-block', + backgroundColor: 'rgba(255,235,59,0.5)', + padding: '3px', + }} + > + {text} + </span>)} /> + : matchIndex > -1 + ? <ListItemText className={item.contentClass} primary={(<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 className={item.contentClass} primary={( + <span style={item.isMatch ? { + display: 'inline-block', + padding: '3px', + } : undefined}> + {text} </span> + )} /> + } + + { // display the right icon, depending on the state + !isFolder ? null : expanded ? (<OpenIcon onClick={handleClickCreator(true)} />) : (<CloseIcon onClick={handleClickCreator(true)} />)} + </ListItem> + ) + : null + ); + } + + private onFolderClick = (item: TreeItem<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: TreeItem<TData>) => { + // activate items without children + this.setState({ + activeItem: item, + }); + }; + + private onSearchKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => { + const enterMode = this.props.searchMode === SearchMode.OnEnter; + + if (enterMode && event.keyCode === 13) { + event.preventDefault(); + event.stopPropagation(); + + enterMode && this.setState({ + searchTerm: this.state.searchTermValue + }); + + if (isTreeViewComponentWithExternalSearchProps(this.props) || isTreeViewComponentWithExternalStateProps(this.props)) { + this.props.onSearch(this.state.searchTermValue || ""); + } + } + } + + private onChangeSearchText = (event: React.ChangeEvent<HTMLInputElement>) => { + event.preventDefault(); + event.stopPropagation(); + + const keyDownMode = (!this.props.searchMode || this.props.searchMode === SearchMode.OnKeyDown); + + this.setState(keyDownMode + ? { + searchTerm: event.target.value, + searchTermValue: event.target.value, + } as any : { + searchTermValue: event.target.value, + }) as any; + + if ((isTreeViewComponentWithExternalSearchProps(this.props) || isTreeViewComponentWithExternalStateProps(this.props)) && keyDownMode) { + this.props.onSearch(event.target.value); + } + }; + + static getDerivedStateFromProps(props: TreeViewComponentProps, state: TreeViewComponentState): TreeViewComponentState { + if (isTreeViewComponentWithExternalStateProps(props)) { + return { + ...state, + expandedItems: props.expandedItems || [], + activeItem: props.activeItem, + searchTerm: props.searchTerm + }; + } else if (isTreeViewComponentWithExternalSearchProps(props)) { + return { + ...state, + searchTerm: props.searchTerm, + }; + } + return state; + } + + public static defaultProps = { + useFolderIcons: false, + enableSearchBar: false, + autoExpandFolder: false, + depthOffset: 0 + } +} + +export type TreeViewCtorType<TData = { }> = new () => React.Component<DistributiveOmit<TreeViewComponentProps<TData>, 'theme'|'classes'>>; + +export const TreeView = withTheme(withStyles(styles)(TreeViewComponent)); +export default TreeView;
\ No newline at end of file diff --git a/sdnr/wt-odlux/odlux/framework/src/components/navigationMenu.tsx b/sdnr/wt-odlux/odlux/framework/src/components/navigationMenu.tsx new file mode 100644 index 000000000..bcc191a89 --- /dev/null +++ b/sdnr/wt-odlux/odlux/framework/src/components/navigationMenu.tsx @@ -0,0 +1,218 @@ +/** + * ============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 React from 'react'; +import { Theme } from '@mui/material/styles'; +import classNames from 'classnames'; + +import { WithStyles } from '@mui/styles'; +import withStyles from '@mui/styles/withStyles'; +import createStyles from '@mui/styles/createStyles'; + +import Drawer from '@mui/material/Drawer'; +import List from '@mui/material/List'; + +import Divider from '@mui/material/Divider'; + +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { faProjectDiagram } from '@fortawesome/free-solid-svg-icons'; + +import ListItemLink from '../components/material-ui/listItemLink'; + +import { connect, Connect } from '../flux/connect'; +import { MenuAction } from '../actions/menuAction'; +import { transportPCEUrl } from '../app'; + +const aboutIcon = require('../assets/icons/About.svg'); +const homeIcon = require('../assets/icons/Home.svg'); +const loginIcon = require('../assets/icons/User.svg'); +const settingsIcon = require('../assets/icons/Tools.svg'); + +const drawerWidth = 240; + +const extraLinks = (window as any)._odluxExtraLinks as [string, string][]; + +const styles = (theme: Theme) => createStyles({ + drawerPaper: { + position: 'relative', + width: drawerWidth, + }, + toolbar: theme.mixins.toolbar as any, + + 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), + [theme.breakpoints.up('sm')]: { + width: theme.spacing(9), + }, + }, + drawer: { + + }, + menu: { + flex: "1 0 0%", + }, + optLinks: { + borderTop: "2px solid #cfcfcf", + display: "flex", + flexDirection: "row", + flexWrap: "wrap", + justifyContent: "space-around" + }, + link: { + margin: theme.spacing(1)+1, + fontSize: theme.typography.fontSize-2, + }, +}); + +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 transportUrl = state.framework.applicationState.transportpceUrl; + + const [responsive, setResponsive] = React.useState(false); + + //collapse menu on mount if necessary + React.useEffect(()=>{ + + if(isOpen && window.innerWidth <= tabletWidthBreakpoint){ + + setResponsive(true); + dispatch(new MenuAction(false)); + } + + },[]); + + React.useEffect(() => { + + function handleResize() { + if (user && user.isValid) { + if (window.innerWidth < tabletWidthBreakpoint && !responsive) { + setResponsive(true); + if (!closedByUser) { + dispatch(new MenuAction(false)); + } + + } else if (window.innerWidth > tabletWidthBreakpoint && responsive) { + setResponsive(false); + if (!closedByUser) { + dispatch(new MenuAction(true)); + } + + } + } + } + window.addEventListener("resize", handleResize); + + + return () => { + window.removeEventListener("resize", handleResize); + } + }) + + React.useEffect(()=>{ + // trigger a resize if menu changed in case elements have to re-arrange + window.dispatchEvent(new Event('menu-resized')); + }, [isOpen]) + + let menuItems = state.framework.applicationRegistration && Object.keys(state.framework.applicationRegistration).map(key => { + const reg = state.framework.applicationRegistration[key]; + return reg && ( + <ListItemLink + key={reg.name} + to={reg.path || `/${reg.name}`} + primary={reg.menuEntry || reg.name} + secondary={reg.subMenuEntry} + icon={reg.icon || null} /> + ) || null; + }) || null; + + if(transportUrl.length>0){ + + const transportPCELink = <ListItemLink + key={"transportPCE"} + to={transportUrl} + primary={"TransportPCE"} + icon={faProjectDiagram} + external />; + + const linkFound = menuItems.find(obj => obj.key === "microwave"); + + if (linkFound) { + const index = menuItems.indexOf(linkFound); + menuItems.splice(index + 1, 0, transportPCELink); + } else { + menuItems.push(transportPCELink); + } + } + + + return ( + <Drawer + variant="permanent" + className={ + classNames(classes.drawer, { + [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 className={classes.menu} component="nav"> + <ListItemLink exact to="/" primary="Home" icon={homeIcon} /> + <Divider /> + { + menuItems + } + <Divider /> + <ListItemLink to="/about" primary="About" icon={aboutIcon} /> + {(false && process.env.NODE_ENV === "development") + ? <> + <Divider /> + <ListItemLink to="/test" primary="Test" icon={settingsIcon} /> + </> + : null + } + </List> + {isOpen && extraLinks && <div className={classes.optLinks}> + {extraLinks.map(linkInfo => (<a className={classes.link} href={linkInfo[1]}>{linkInfo[0]}</a>))} + </div> || null} + </> || null + } + </Drawer>) +})); + +export default NavigationMenu;
\ No newline at end of file diff --git a/sdnr/wt-odlux/odlux/framework/src/components/objectDump/index.tsx b/sdnr/wt-odlux/odlux/framework/src/components/objectDump/index.tsx new file mode 100644 index 000000000..10a0547be --- /dev/null +++ b/sdnr/wt-odlux/odlux/framework/src/components/objectDump/index.tsx @@ -0,0 +1,205 @@ +/** + * ============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 makeStyles from '@mui/styles/makeStyles'; + +export const getTypeName = (obj: any): string => { + if (obj == null) { + return obj === undefined ? "Undefined" : "Null"; + } + return Object.prototype.toString.call(obj).slice(8, -1); +}; + +const isObjectLike = (obj: any) => { + return typeof obj === "object" && obj !== null; +}; + +const isBoolean = (obj: any) => { + return obj === true || obj === false || + (isObjectLike(obj) && getTypeName(obj) === "Boolean"); +}; + +const isNumber = (obj: any) => { + return typeof obj === "number" || + (isObjectLike(obj) && getTypeName(obj) === "Number"); +}; + +const isString = (obj: any) => { + return typeof obj === "string" || + (isObjectLike(obj) && getTypeName(obj) === "String"); +}; + +const isNull = (obj: any) => { + return obj === null; +}; + +const isDate = (obj: any): boolean => { + return isObjectLike(obj) && (obj instanceof Date); +}; + +const useSimpleTableStyles = makeStyles({ + root: { + }, + table: { + fontFamily: "verdana, arial, helvetica, sans-serif", + borderSpacing: "3px", + borderCollapse: "separate", + marginLeft: "30px" + }, + label: { + cursor: "pointer", + }, + th: { + textAlign: "left", + color: "white", + padding: "5px", + backgroundColor: "#cccccc", + + }, + td: { + verticalAlign: "top", + padding: "0.5rem 1rem", + border: "2px solid #DDD" + }, + object: { + }, + objectTh: { + backgroundColor: "#cccccc", + }, + objectTd: { + padding: "0.5rem 1rem", + border: "2px solid #DDD" + }, + chevron: { + '&:before': { + borderStyle: 'solid', + borderWidth: '0.25em 0.25em 0 0', + content: '\'\'', + display: 'inline-block', + height: '0.45em', + left: '0.15em', + position: 'relative', + top: '0.15em', + transform: 'rotate(-45deg)', + transition: 'all 0.3s', + verticalAlign: 'top', + width: '0.45em', + } + + }, + right: { + '&:before': { + left: '0', + transform: 'rotate(45deg)', + } + }, + bottom: { + '&:before': { + left: '0', + transform: 'rotate(135deg)', + } + }, +}); + + +type SimpleTableProps = { + classNameTh?: string; + label?: JSX.Element | string | null; + cols?: number; + expand?: boolean; + ariaLabel?: string; +} + +const SimpleTable: React.FC<SimpleTableProps> = (props) => { + const { label = '', cols = 2, expand = true, classNameTh, children } = props; + const [isExpanded, setIsExpanded] = React.useState(expand); + + const classes = useSimpleTableStyles(); + + React.useEffect(() => { + setIsExpanded(expand); + }, [expand]); + + const handleClick = () => { + setIsExpanded(!isExpanded); + }; + + return ( + <table className={`${classes.root} ${classes.table}`} aria-label={props.ariaLabel? props.ariaLabel+'-table' : 'table'}> + {label && (<thead> + <tr aria-label={props.ariaLabel? props.ariaLabel+'-title-row' : 'title row'}> + <th className={`${classes.th} ${classes.label} ${classNameTh ? classNameTh : ''}`} colSpan={cols} onClick={handleClick}> + <span className={`${classes.chevron} ${isExpanded ? classes.bottom : classes.right }`}></span> { label } + </th> + </tr> + </thead>) || null + } + {isExpanded && <tbody >{children}</tbody> || null} + </table> + ); +}; + + +type ObjectRendererProps = { + className?: string; + label?: JSX.Element | string | null; + expand?: boolean; + object: { [key: string]: any }; + ariaLabel?: string; +}; + +const ObjectRenderer: React.FC<ObjectRendererProps> = (props) => { + const { object, className, label = 'Object', expand = true } = props; + const classes = useSimpleTableStyles(); + + return ( + <SimpleTable ariaLabel={props.ariaLabel} classNameTh={classes.objectTh} label={getTypeName(object) || label} expand={expand}> + { + Object.keys(object).map(key => { + return ( + <tr key={String(key)} aria-label={props.ariaLabel? props.ariaLabel+'-row': 'row'}> + <td className={`${classes.td} ${classes.objectTd}`} aria-label="object-title">{String(key)} </td> + <td className={`${classes.td}`} aria-label="object-details">{renderObject(object[key], "sub-element")}</td> + </tr> + ); + }) + } + </SimpleTable> + ); +}; + + +type ArrayRendererProps = { + label?: JSX.Element | string | null; + extraRenderer?: { [label: string]: React.ComponentType<{ label?: JSX.Element | string | null; object: any; }> }; + description?: string; + object: any; +}; + +const ArrayRenderer: React.FC<ArrayRendererProps> = (props) => { + + return null; +}; + +export const renderObject = (object: any, ariaLabel?: string): JSX.Element | string => { + if (isString(object) || isNumber(object) || isBoolean(object)) { + return String(object); + } + return <ObjectRenderer object={object} ariaLabel={ariaLabel} />; +}; diff --git a/sdnr/wt-odlux/odlux/framework/src/components/routing/appFrame.tsx b/sdnr/wt-odlux/odlux/framework/src/components/routing/appFrame.tsx new file mode 100644 index 000000000..aa22f17f4 --- /dev/null +++ b/sdnr/wt-odlux/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 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: "hidden", 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/odlux/framework/src/components/settings/general.tsx b/sdnr/wt-odlux/odlux/framework/src/components/settings/general.tsx new file mode 100644 index 000000000..ffd516ba1 --- /dev/null +++ b/sdnr/wt-odlux/odlux/framework/src/components/settings/general.tsx @@ -0,0 +1,110 @@ +/** + * ============LICENSE_START======================================================================== + * ONAP : ccsdk feature sdnr wt odlux + * ================================================================================================= + * Copyright (C) 2021 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 React from 'react'; +import { Button, FormControlLabel, Switch, Typography } from '@mui/material'; +import makeStyles from '@mui/styles/makeStyles'; +import { SettingsComponentProps } from '../../models/settings'; +import { connect, Connect, IDispatcher } from '../../flux/connect'; +import { IApplicationStoreState } from '../../store/applicationStore'; +import { getGeneralSettingsAction, SetGeneralSettingsAction, updateGeneralSettingsAction } from '../../actions/settingsAction'; +import { sendMessage, SettingsMessage } from '../../services/broadcastService'; + + +type props = Connect<typeof mapProps, typeof mapDispatch> & SettingsComponentProps; + +const mapProps = (state: IApplicationStoreState) => ({ + settings: state.framework.applicationState.settings, + user: state.framework.authenticationState.user?.user + +}); + +const mapDispatch = (dispatcher: IDispatcher) => ({ + + updateSettings :(activateNotifications: boolean) => dispatcher.dispatch(updateGeneralSettingsAction(activateNotifications)), + getSettings: () =>dispatcher.dispatch(getGeneralSettingsAction()), + }); + +const styles = makeStyles({ + sectionMargin: { + marginTop: "30px", + marginBottom: "15px" + }, + elementMargin: { + marginLeft: "10px" + }, + buttonPosition:{ + position: "absolute", + right: "32%" + } + }); + +const General : React.FunctionComponent<props> = (props) =>{ + +const classes = styles(); + +const [areWebsocketsEnabled, setWebsocketsEnabled] = React.useState(props.settings.general.areNotificationsEnabled || false); + +React.useEffect(()=>{ + props.getSettings(); +},[]); + +React.useEffect(()=>{ + if(props.settings.general.areNotificationsEnabled!==null) + setWebsocketsEnabled(props.settings.general.areNotificationsEnabled) +},[props.settings]); + +const onWebsocketsChange = (event: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>, newValue: boolean) =>{ + setWebsocketsEnabled(newValue); + } + +const onSave = (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) =>{ + + e.preventDefault(); + const message: SettingsMessage = {key: 'general', enableNotifications: areWebsocketsEnabled, user: props.user!}; + sendMessage(message, "odlux_settings"); + props.updateSettings(areWebsocketsEnabled); + props.onClose(); +} + +const onCancel = (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) =>{ + e.preventDefault(); + props.onClose(); + +} + + + return <div> + <Typography className={classes.sectionMargin} variant="body1" style={{ fontWeight: "bold" }} gutterBottom> + Enable Notifications + </Typography> + <FormControlLabel style={{ padding:5}} + value="end" + control={<Switch color="secondary" aria-label="enable-notifications-button" aria-checked={areWebsocketsEnabled} checked={areWebsocketsEnabled} onChange={onWebsocketsChange} />} + label="Enable Notifications" + labelPlacement="end" + /> + <div className={classes.buttonPosition}> + <Button aria-label="cancel-button" className={classes.elementMargin} variant="contained" color="primary" onClick={onCancel}>Cancel</Button> + <Button aria-label="save-button" className={classes.elementMargin} variant="contained" color="secondary" onClick={onSave}>Save</Button> + </div> + </div> +} + +export const GeneralUserSettings = connect(mapProps, mapDispatch)(General); +export default GeneralUserSettings;
\ No newline at end of file diff --git a/sdnr/wt-odlux/odlux/framework/src/components/titleBar.tsx b/sdnr/wt-odlux/odlux/framework/src/components/titleBar.tsx new file mode 100644 index 000000000..40c0fc736 --- /dev/null +++ b/sdnr/wt-odlux/odlux/framework/src/components/titleBar.tsx @@ -0,0 +1,233 @@ +/** + * ============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 React from 'react'; +import { withRouter, RouteComponentProps } from 'react-router-dom'; + +import { Theme } from '@mui/material/styles'; +import { WithStyles } from '@mui/styles'; +import withStyles from '@mui/styles/withStyles'; +import createStyles from '@mui/styles/createStyles'; +import AppBar from '@mui/material/AppBar'; +import Toolbar from '@mui/material/Toolbar'; +import Typography from '@mui/material/Typography'; +import Button from '@mui/material/Button'; +import IconButton from '@mui/material/IconButton'; +import AccountCircle from '@mui/icons-material/AccountCircle'; +import MenuItem from '@mui/material/MenuItem'; +import Menu from '@mui/material/Menu'; + +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { faBan } from '@fortawesome/free-solid-svg-icons'; +import { faDotCircle } from '@fortawesome/free-solid-svg-icons'; + +import { logoutUser } from '../actions/authentication'; +import { PushAction, ReplaceAction } from '../actions/navigationActions'; + +import { connect, Connect, IDispatcher } from '../flux/connect'; +import { MenuAction, MenuClosedByUser } from '../actions/menuAction'; + +import MenuIcon from './icons/menuIcon'; +import Logo from './logo'; + +const styles = (theme: Theme) => createStyles({ + appBar: { + zIndex: theme.zIndex.drawer + 1, + }, + grow: { + flexGrow: 1, + }, + menuButton: { + marginLeft: -12, + marginRight: 20, + }, + icon: { + marginLeft: 16, + marginRight: 8, + marginBottom: -2, + }, + connected: { + color: "green" + }, + notConnected: { + color: "red" + }, + notificationInfo: { + marginLeft: 5 + } +}); + +const mapDispatch = (dispatcher: IDispatcher) => { + return { + logout: () => { + dispatcher.dispatch(logoutUser()); + dispatcher.dispatch(new ReplaceAction("/login")); + }, + openSettings : () =>{ + dispatcher.dispatch(new PushAction("/settings")); + }, + 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 aria-label="notifications-are-active" variant="body1" className={classes.notificationInfo}>Notifications <FontAwesomeIcon className={classes.connected} icon={faDotCircle} /> |</Typography> : <Typography aria-label="notifications-are-inactive" variant="body1" className={classes.notificationInfo}>Notifications <FontAwesomeIcon className={classes.notConnected} icon={faBan} /> |</Typography>) + : <Typography variant="body1" aria-label="notifications-are-not-available" className={classes.notificationInfo}>Notifications N/A |</Typography>; + + + // add notificationInfo element before help + if (state.framework.applicationRegistration) { + let isNotificationInfoAdded = false; + Object.keys(state.framework.applicationRegistration).map(key => { + const reg = state.framework.applicationRegistration[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); + } + } + + const stateIcon = state.framework.applicationState.icon; + const customIconHeight = 22; + const icon = !stateIcon + ? null + : (typeof stateIcon === 'string' + ? <img className={classes.icon} height={customIconHeight} src={stateIcon} /> + : <FontAwesomeIcon className={classes.icon} icon={stateIcon} />) + + + return ( + <AppBar enableColorOnDark position="absolute" className={classes.appBar}> + <Toolbar> + <IconButton + className={classes.menuButton} + color="inherit" + aria-label="Menu" + onClick={this.toggleMainMenu} + size="large"> + <MenuIcon /> + </IconButton> + <Logo /> + <Typography variant="h6" color="inherit" > + {icon} + {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 + aria-label="settings-button" + onClick={ () =>{ + this.props.openSettings(); + this.closeMenu(); }}>Settings</MenuItem> + <MenuItem + aria-label="logout-button" + 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 |