From 3d202a04b99f0e61b6ccf8b7a5610e1a15ca58e7 Mon Sep 17 00:00:00 2001 From: Herbert Eiselt Date: Mon, 11 Feb 2019 14:54:12 +0100 Subject: Add sdnr wt odlux Add complete sdnr wireless transport app odlux core and apps Change-Id: I5dcbfb8f3b790e3bda7c8df67bd69d81958f65e5 Issue-ID: SDNC-576 Signed-off-by: Herbert Eiselt --- .../framework/src/components/material-ui/index.ts | 3 + .../src/components/material-ui/listItemLink.tsx | 50 ++++ .../framework/src/components/material-ui/panel.tsx | 56 +++++ .../src/components/material-ui/snackDisplay.tsx | 57 +++++ .../src/components/material-ui/toggleButton.tsx | 159 +++++++++++++ .../components/material-ui/toggleButtonGroup.tsx | 19 ++ .../src/components/material-ui/treeView.tsx | 251 +++++++++++++++++++++ 7 files changed, 595 insertions(+) create mode 100644 sdnr/wt/odlux/framework/src/components/material-ui/index.ts create mode 100644 sdnr/wt/odlux/framework/src/components/material-ui/listItemLink.tsx create mode 100644 sdnr/wt/odlux/framework/src/components/material-ui/panel.tsx create mode 100644 sdnr/wt/odlux/framework/src/components/material-ui/snackDisplay.tsx create mode 100644 sdnr/wt/odlux/framework/src/components/material-ui/toggleButton.tsx create mode 100644 sdnr/wt/odlux/framework/src/components/material-ui/toggleButtonGroup.tsx create mode 100644 sdnr/wt/odlux/framework/src/components/material-ui/treeView.tsx (limited to 'sdnr/wt/odlux/framework/src/components/material-ui') diff --git a/sdnr/wt/odlux/framework/src/components/material-ui/index.ts b/sdnr/wt/odlux/framework/src/components/material-ui/index.ts new file mode 100644 index 000000000..890312ce2 --- /dev/null +++ b/sdnr/wt/odlux/framework/src/components/material-ui/index.ts @@ -0,0 +1,3 @@ +export { ListItemLink } from './listItemLink'; +export { Panel } from './panel'; +export { ToggleButton, ToggleButtonClassKey } from './toggleButton'; diff --git a/sdnr/wt/odlux/framework/src/components/material-ui/listItemLink.tsx b/sdnr/wt/odlux/framework/src/components/material-ui/listItemLink.tsx new file mode 100644 index 000000000..6ace59534 --- /dev/null +++ b/sdnr/wt/odlux/framework/src/components/material-ui/listItemLink.tsx @@ -0,0 +1,50 @@ +import * as React from 'react'; +import { NavLink, Link, Route } from 'react-router-dom'; + +import ListItem from '@material-ui/core/ListItem'; +import ListItemIcon from '@material-ui/core/ListItemIcon'; +import ListItemText from '@material-ui/core/ListItemText'; + +import { withStyles, WithStyles, createStyles, Theme } from '@material-ui/core/styles'; + +const styles = (theme: Theme) => createStyles({ + active: { + backgroundColor: theme.palette.action.selected + } +}); + +export interface IListItemLinkProps extends WithStyles { + icon: JSX.Element | null; + primary: string | React.ComponentType; + secondary?: React.ComponentType; + to: string; + exact?: boolean; +} + +export const ListItemLink = withStyles(styles)((props: IListItemLinkProps) => { + const { icon, primary: Primary, secondary: Secondary, classes, to, exact = false } = props; + const renderLink = (itemProps: any): JSX.Element => (); + + return ( + <> + + { icon + ? { icon } + : null + } + { typeof Primary === 'string' + ? + : + } + + { Secondary + ? + : null + } + + ); + } +); + +export default ListItemLink; + diff --git a/sdnr/wt/odlux/framework/src/components/material-ui/panel.tsx b/sdnr/wt/odlux/framework/src/components/material-ui/panel.tsx new file mode 100644 index 000000000..0b64666c0 --- /dev/null +++ b/sdnr/wt/odlux/framework/src/components/material-ui/panel.tsx @@ -0,0 +1,56 @@ +import * as React from 'react'; + +import { withStyles, Theme, WithStyles, createStyles } from '@material-ui/core/styles'; + +import { ExpansionPanel, ExpansionPanelSummary, ExpansionPanelDetails, Typography, ExpansionPanelActions } from '@material-ui/core'; + +import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; +import { SvgIconProps } from '@material-ui/core/SvgIcon'; + +const styles = (theme: Theme) => createStyles({ + accordion: { + // background: theme.palette.secondary.dark, + // color: theme.palette.primary.contrastText + }, + detail: { + // background: theme.palette.background.paper, + // color: theme.palette.text.primary, + position: "relative", + display: 'flex', + flexDirection: 'column' + }, + text: { + // color: theme.palette.common.white, + // fontSize: "1rem" + }, +}); + +type PanalProps = WithStyles & { + activePanel: string | null, + panelId: string, + title: string, + customActionButtons?: JSX.Element[]; + onToggle: (panelId: string | null) => void; +} + +const PanelComponent: React.SFC = (props) => { + const { classes, activePanel, onToggle } = props; + return ( + onToggle(props.panelId) } > + }> + { props.title } + + + { props.children } + + { props.customActionButtons + ? + { props.customActionButtons } + + : null } + + ); +}; + +export const Panel = withStyles(styles)(PanelComponent); +export default Panel; \ No newline at end of file diff --git a/sdnr/wt/odlux/framework/src/components/material-ui/snackDisplay.tsx b/sdnr/wt/odlux/framework/src/components/material-ui/snackDisplay.tsx new file mode 100644 index 000000000..c02bf93e9 --- /dev/null +++ b/sdnr/wt/odlux/framework/src/components/material-ui/snackDisplay.tsx @@ -0,0 +1,57 @@ +import * as React from 'react'; + +import { IApplicationStoreState } from '../../store/applicationStore'; +import { Connect, connect, IDispatcher } from '../../flux/connect'; +import { RemoveSnackbarNotification } from '../../actions/snackbarActions'; + +import { InjectedNotistackProps, 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 & InjectedNotistackProps; + +class DisplaySnackbarsComponent extends React.Component { + private displayed: number[] = []; + + private storeDisplayed = (id: number) => { + this.displayed = [...this.displayed, id]; + }; + + public shouldComponentUpdate({ notifications: newSnacks = [] }: DisplaySnackbarsComponentProps) { + + const { notifications: currentSnacks } = this.props; + let notExists = false; + for (let i = 0; i < newSnacks.length; i++) { + if (notExists) continue; + notExists = notExists || !currentSnacks.filter(({ key }) => newSnacks[i].key === key).length; + } + return notExists; + } + + componentDidUpdate() { + const { notifications = [] } = this.props; + + notifications.forEach(notification => { + if (this.displayed.includes(notification.key)) return; + const options = notification.options || {}; + this.props.enqueueSnackbar(notification.message, options); + this.storeDisplayed(notification.key); + this.props.removeSnackbar(notification.key); + }); + } + + render() { + return null; + } +} + +const DisplayStackbars = withSnackbar(connect(mapProps, mapDispatch)(DisplaySnackbarsComponent)); +export default DisplayStackbars; \ No newline at end of file diff --git a/sdnr/wt/odlux/framework/src/components/material-ui/toggleButton.tsx b/sdnr/wt/odlux/framework/src/components/material-ui/toggleButton.tsx new file mode 100644 index 000000000..522ff12c8 --- /dev/null +++ b/sdnr/wt/odlux/framework/src/components/material-ui/toggleButton.tsx @@ -0,0 +1,159 @@ + +import * as React from 'react'; +import classNames from 'classnames'; +import { withStyles, WithStyles, Theme, createStyles } from '@material-ui/core/styles'; +import { fade } from '@material-ui/core/styles/colorManipulator'; +import ButtonBase from '@material-ui/core/ButtonBase'; + + +export const styles = (theme: Theme) => createStyles({ + /* Styles applied to the root element. */ + root: { + ...theme.typography.button, + height: 32, + minWidth: 48, + margin: 0, + padding: `${theme.spacing.unit - 4}px ${theme.spacing.unit * 1.5}px`, + borderRadius: 2, + willChange: 'opacity', + color: fade(theme.palette.action.active, 0.38), + '&:hover': { + textDecoration: 'none', + // Reset on mouse devices + backgroundColor: fade(theme.palette.text.primary, 0.12), + '@media (hover: none)': { + backgroundColor: 'transparent', + }, + '&$disabled': { + backgroundColor: 'transparent', + }, + }, + '&:not(:first-child)': { + borderTopLeftRadius: 0, + borderBottomLeftRadius: 0, + }, + '&:not(:last-child)': { + borderTopRightRadius: 0, + borderBottomRightRadius: 0, + }, + }, + /* Styles applied to the root element if `disabled={true}`. */ + disabled: { + color: fade(theme.palette.action.disabled, 0.12), + }, + /* Styles applied to the root element if `selected={true}`. */ + selected: { + color: theme.palette.action.active, + '&:after': { + content: '""', + display: 'block', + position: 'absolute', + overflow: 'hidden', + borderRadius: 'inherit', + width: '100%', + height: '100%', + left: 0, + top: 0, + pointerEvents: 'none', + zIndex: 0, + backgroundColor: 'currentColor', + opacity: 0.38, + }, + '& + &:before': { + content: '""', + display: 'block', + position: 'absolute', + overflow: 'hidden', + width: 1, + height: '100%', + left: 0, + top: 0, + pointerEvents: 'none', + zIndex: 0, + backgroundColor: 'currentColor', + opacity: 0.12, + }, + }, + /* Styles applied to the `label` wrapper element. */ + label: { + width: '100%', + display: 'inherit', + alignItems: 'inherit', + justifyContent: 'inherit', + }, +}); + +export type ToggleButtonClassKey = 'disabled' | 'root' | 'label' | 'selected'; + +interface IToggleButtonProps extends WithStyles { + className?: string; + component?: React.ReactType; + disabled?: boolean; + disableFocusRipple?: boolean; + disableRipple?: boolean; + selected?: boolean; + type?: string; + value?: any; + onClick?: (event: React.FormEvent, value?: any) => void; + onChange?: (event: React.FormEvent, value?: any) => void; +} + +class ToggleButtonComponent extends React.Component { + handleChange = (event: React.FormEvent) => { + const { onChange, onClick, value } = this.props; + + if (onClick) { + onClick(event, value); + if (event.isDefaultPrevented()) { + return; + } + } + + if (onChange) { + onChange(event, value); + } + }; + + 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 ( + + {children} + + ); + } + public static defaultProps = { + disabled: false, + disableFocusRipple: false, + disableRipple: false, + }; + + public static muiName = 'ToggleButton'; +} + +export const ToggleButton = withStyles(styles, { name: 'MuiToggleButton' })(ToggleButtonComponent); +export default ToggleButton; \ No newline at end of file diff --git a/sdnr/wt/odlux/framework/src/components/material-ui/toggleButtonGroup.tsx b/sdnr/wt/odlux/framework/src/components/material-ui/toggleButtonGroup.tsx new file mode 100644 index 000000000..8ab7c2b91 --- /dev/null +++ b/sdnr/wt/odlux/framework/src/components/material-ui/toggleButtonGroup.tsx @@ -0,0 +1,19 @@ +import * as React from 'react'; +import classNames from 'classnames'; +import { withStyles, WithStyles, Theme, createStyles } from '@material-ui/core/styles'; + +export const styles = (theme: Theme) => createStyles({ + /* Styles applied to the root element. */ + root: { + transition: theme.transitions.create('background,box-shadow'), + background: 'transparent', + borderRadius: 2, + overflow: 'hidden', + }, + /* Styles applied to the root element if `selected={true}` or `selected="auto" and `value` set. */ + selected: { + background: theme.palette.background.paper, + boxShadow: theme.shadows[2], + }, +}); + diff --git a/sdnr/wt/odlux/framework/src/components/material-ui/treeView.tsx b/sdnr/wt/odlux/framework/src/components/material-ui/treeView.tsx new file mode 100644 index 000000000..8bcdc8bc6 --- /dev/null +++ b/sdnr/wt/odlux/framework/src/components/material-ui/treeView.tsx @@ -0,0 +1,251 @@ +import * as React from 'react'; + +import { SvgIconProps } from '@material-ui/core/SvgIcon'; +import { List, ListItem, TextField, ListItemText, ListItemIcon, WithTheme, withTheme, Omit } from '@material-ui/core'; + +import FileIcon from '@material-ui/icons/InsertDriveFile'; +import CloseIcon from '@material-ui/icons/ExpandLess'; +import OpenIcon from '@material-ui/icons/ExpandMore'; +import FolderIcon from '@material-ui/icons/Folder'; + +export interface ITreeItem { + disabled?: boolean; + icon?: React.ComponentType; +} + +type TreeViewComponentState = { + /** All indices of all expanded Items */ + expandedItems: TData[]; + /** The index of the active iten or undefined if no item is active. */ + activeItem: undefined | TData; + /** The search term or undefined if search is corrently not active. */ + searchTerm: undefined | string; +} + +type TreeViewComponentBaseProps = WithTheme & { + items: TData[]; + contentProperty: keyof Omit; + childrenProperty: keyof Omit; + useFolderIcons?: boolean; + enableSearchBar?: boolean; + autoExpandFolder?: boolean; + style?: React.CSSProperties; + itemHeight?: number; + depthOffset?: number; +} + +type TreeViewComponentWithInternalStateProps = TreeViewComponentBaseProps & { + onItemClick?: (item: TData) => void; + onFolderClick?: (item: TData) => void; +} + +type TreeViewComponentWithExternalStateProps = TreeViewComponentBaseProps & TreeViewComponentState & { + onSearch: (searchTerm: string) => void; + onItemClick: (item: TData) => void; + onFolderClick: (item: TData) => void; +} + +type TreeViewComponentProps = + TreeViewComponentWithInternalStateProps | + TreeViewComponentWithExternalStateProps; + +function isTreeViewComponentWithExternalStateProps(props: TreeViewComponentProps): props is TreeViewComponentWithExternalStateProps { + const propsWithExternalState = (props as TreeViewComponentWithExternalStateProps) + return ( + propsWithExternalState.onSearch instanceof Function || + propsWithExternalState.expandedItems !== undefined || + propsWithExternalState.activeItem !== undefined || + propsWithExternalState.searchTerm !== undefined + ); +} + +class TreeViewComponent extends React.Component, TreeViewComponentState> { + + /** + * Initializes a new instance. + */ + constructor(props: TreeViewComponentProps) { + super(props); + + this.state = { + expandedItems: [], + activeItem: undefined, + searchTerm: undefined + }; + } + + render(): JSX.Element { + this.itemIndex = 0; + const { searchTerm } = this.state; + const { children, items, enableSearchBar } = this.props; + const styles = { + root: { + padding: 0, + paddingBottom: 8, + paddingTop: children ? 0 : 8, + ...this.props.style + }, + search: { + padding: `0px ${ this.props.theme.spacing.unit }px` + } + }; + return ( +
+ { children } + { enableSearchBar && || null } + + { this.renderItems(items, searchTerm && searchTerm.toLowerCase()) } + +
+ ); + } + + private itemIndex: number = 0; + private renderItems = (items: TData[], searchTerm: string | undefined, depth: number = 1) => { + return items.reduce((acc, item) => { + + const children = this.props.childrenProperty && ((item as any)[this.props.childrenProperty] as TData[]); + const childrenJsx = children && this.renderItems(children, searchTerm, depth + 1); + + const expanded = searchTerm + ? children && childrenJsx.length > 0 + : !children + ? false + : this.state.expandedItems.indexOf(item) > -1; + const isFolder = children !== undefined; + + const itemJsx = this.renderItem(item, searchTerm, depth, isFolder, expanded); + itemJsx && acc.push(itemJsx); + + if (isFolder && expanded) { + acc.push(...childrenJsx); + } + return acc; + + }, [] as JSX.Element[]); + } + private renderItem = (item: TData, searchTerm: string | undefined, depth: number, isFolder: boolean, expanded: boolean): JSX.Element | null => { + const styles = { + item: { + paddingLeft: (((this.props.depthOffset || 0) + depth) * this.props.theme.spacing.unit * 3), + backgroundColor: this.state.activeItem === item ? this.props.theme.palette.action.selected : undefined, + height: this.props.itemHeight || undefined, + cursor: item.disabled ? 'not-allowed' : 'pointer', + color: item.disabled ? this.props.theme.palette.text.disabled : this.props.theme.palette.text.primary, + overflow: 'hidden', + transform: 'translateZ(0)', + } + }; + + const text = (item as any)[this.props.contentProperty] as string || ''; // need to keep track of search + const matchIndex = searchTerm ? text.toLowerCase().indexOf(searchTerm) : -1; + const searchTermLength = searchTerm && searchTerm.length || 0; + + const handleClickCreator = (isIcon: boolean) => (event: React.SyntheticEvent) => { + if (item.disabled) return; + event.preventDefault(); + event.stopPropagation(); + if (isFolder && (this.props.autoExpandFolder || isIcon)) { + this.props.onFolderClick ? this.props.onFolderClick(item) : this.onFolderClick(item); + } else { + this.props.onItemClick ? this.props.onItemClick(item) : this.onItemClick(item); + } + }; + + return ((searchTerm && (matchIndex > -1 || expanded) || !searchTerm) + ? ( + + + { // display the left icon + (this.props.useFolderIcons && { isFolder ? : }) || + (item.icon && ()) } + + + { // highlight search result + matchIndex > -1 + ? ( + { text.substring(0, matchIndex) } + + { text.substring(matchIndex, matchIndex + searchTermLength) } + + { text.substring(matchIndex + searchTermLength) } + ) + : () + } + + { // display the right icon, depending on the state + !isFolder ? null : expanded ? () : () } + + ) + : null + ); + } + + private onFolderClick = (item: TData) => { + // toggle items with children + if (this.state.searchTerm) return; + const indexOfItemToToggle = this.state.expandedItems.indexOf(item); + if (indexOfItemToToggle === -1) { + this.setState({ + expandedItems: [...this.state.expandedItems, item], + }); + } else { + this.setState({ + expandedItems: [ + ...this.state.expandedItems.slice(0, indexOfItemToToggle), + ...this.state.expandedItems.slice(indexOfItemToToggle + 1), + ] + }); + } + }; + + private onItemClick = (item: TData) => { + // activate items without children + this.setState({ + activeItem: item, + }); + }; + + private onChangeSearchText = (event: React.ChangeEvent) => { + event.preventDefault(); + event.stopPropagation(); + + if (isTreeViewComponentWithExternalStateProps(this.props)) { + this.props.onSearch(event.target.value) + } else { + this.setState({ + searchTerm: event.target.value + }); + } + }; + + static getDerivedStateFromProps(props: TreeViewComponentProps, state: TreeViewComponentState): TreeViewComponentState { + if (isTreeViewComponentWithExternalStateProps(props)) { + return { + ...state, + expandedItems: props.expandedItems || [], + activeItem: props.activeItem, + searchTerm: props.searchTerm + }; + } + return state; + } + + public static defaultProps = { + useFolderIcons: false, + enableSearchBar: false, + autoExpandFolder: false, + depthOffset: 0 + } +} + +export type TreeViewCtorType = new () => React.Component, 'theme'>>; + +export const TreeView = withTheme()(TreeViewComponent); +export default TreeView; \ No newline at end of file -- cgit 1.2.3-korg