diff options
author | Aijana Schumann <aijana.schumann@highstreet-technologies.com> | 2021-12-06 15:09:15 +0100 |
---|---|---|
committer | Aijana Schumann <aijana.schumann@highstreet-technologies.com> | 2021-12-06 15:12:24 +0100 |
commit | 152cb381ea2c915c762416092337ce1d8589d1c6 (patch) | |
tree | 63b71c8343f9292281f5d7f5eac14342fec06402 /sdnr/wt/odlux/framework | |
parent | 8ea94e1210671b941f84abfe16e248cfa086fe49 (diff) |
Update ODLUX
Update login view, add logout after user session ends, add user settings, several bugfixes
Issue-ID: CCSDK-3540
Signed-off-by: Aijana Schumann <aijana.schumann@highstreet-technologies.com>
Change-Id: I21137756b204287e25766a9646bf2faf7bad9d35
Diffstat (limited to 'sdnr/wt/odlux/framework')
32 files changed, 1021 insertions, 128 deletions
diff --git a/sdnr/wt/odlux/framework/pom.xml b/sdnr/wt/odlux/framework/pom.xml index 0de1bcd68..7bc35df37 100644 --- a/sdnr/wt/odlux/framework/pom.xml +++ b/sdnr/wt/odlux/framework/pom.xml @@ -19,6 +19,7 @@ ~ ============LICENSE_END======================================================= ~ --> + <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> @@ -45,7 +46,7 @@ <properties> <buildtime>${maven.build.timestamp}</buildtime> <distversion>ONAP Frankfurt (Neon, mdsal ${odl.mdsal.version})</distversion> - <buildno>116.8c2f6b7(21/08/05)</buildno> + <buildno>137.be0dfd7(21/12/03)</buildno> <odlux.version>ONAP SDN-R | ONF Wireless for ${distversion} - Build: ${buildtime} ${buildno} ${project.version}</odlux.version> </properties> diff --git a/sdnr/wt/odlux/framework/src/actions/authentication.ts b/sdnr/wt/odlux/framework/src/actions/authentication.ts index de8093573..20a248dc1 100644 --- a/sdnr/wt/odlux/framework/src/actions/authentication.ts +++ b/sdnr/wt/odlux/framework/src/actions/authentication.ts @@ -15,8 +15,12 @@ * the License. * ============LICENSE_END========================================================================== */ +import { Dispatch } from '../flux/store'; import { Action } from '../flux/action'; import { AuthPolicy, User } from '../models/authentication'; +import { GeneralSettings } from '../models/settings'; +import { SetGeneralSettingsAction, setGeneralSettingsAction } from './settingsAction'; +import { endWebsocketSession } from '../services/notificationService'; export class UpdateUser extends Action { @@ -30,4 +34,54 @@ export class UpdatePolicies extends Action { constructor (public authPolicies?: AuthPolicy[]) { super(); } +} + + +export const loginUserAction = (user?: User) => (dispatcher: Dispatch) =>{ + + dispatcher(new UpdateUser(user)); + loadUserSettings(user, dispatcher); + + +} + +export const logoutUser = () => (dispatcher: Dispatch) =>{ + + dispatcher(new UpdateUser(undefined)); + dispatcher(new SetGeneralSettingsAction(null)); + endWebsocketSession(); +} + +const loadUserSettings = (user: User | undefined, dispatcher: Dispatch) =>{ + + + //fetch used, because state change for user login is not done when frameworks restRequest call is started (and is accordingly undefined -> /userdata call yields 401, unauthorized) and triggering an action from inside the handler / login event is impossible + //no timeout used, because it's bad practise to add a timeout to hopefully avoid a race condition + //hence, fetch used to simply use supplied user data for getting settings + + if(user && user.isValid){ + + fetch("/userdata", { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + 'Authorization': `${user.tokenType} ${user.token}` + } + }).then((res: Response)=>{ + if(res.status==200){ + return res.json(); + }else{ + return null; + } + }).then((result:GeneralSettings)=>{ + if(result?.general){ + //will start websocket session if applicable + dispatcher(setGeneralSettingsAction(result.general.areNotificationsEnabled!)); + + }else{ + dispatcher(setGeneralSettingsAction(false)); + } + }) + } }
\ No newline at end of file diff --git a/sdnr/wt/odlux/framework/src/actions/settingsAction.ts b/sdnr/wt/odlux/framework/src/actions/settingsAction.ts new file mode 100644 index 000000000..ffcdfc26a --- /dev/null +++ b/sdnr/wt/odlux/framework/src/actions/settingsAction.ts @@ -0,0 +1,64 @@ +/** + * ============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 { Dispatch } from "../flux/store"; +import { Action } from "../flux/action"; +import { GeneralSettings } from "../models/settings"; +import { getSettings, putSettings } from "../services/settingsService"; +import { startWebsocketSession, suspendWebsocketSession } from "../services/notificationService"; + + +export class SetGeneralSettingsAction extends Action{ + /** + * + */ + constructor(public areNoticationsActive: boolean|null) { + super(); + + } +} + +export const setGeneralSettingsAction = (value: boolean) => (dispatcher: Dispatch) =>{ + + dispatcher(new SetGeneralSettingsAction(value)); + + if(value){ + startWebsocketSession(); + }else{ + suspendWebsocketSession(); + } +} + + +export const updateGeneralSettingsAction = (activateNotifications: boolean) => async (dispatcher: Dispatch) =>{ + + const value: GeneralSettings = {general:{areNotificationsEnabled: activateNotifications}}; + const result = await putSettings("/general", JSON.stringify(value.general)); + dispatcher(setGeneralSettingsAction(activateNotifications)); + +} + +export const getGeneralSettingsAction = () => async (dispatcher: Dispatch) => { + + const result = await getSettings<GeneralSettings>(); + + if(result && result.general){ + dispatcher(new SetGeneralSettingsAction(result.general.areNotificationsEnabled!)) + } + +}
\ No newline at end of file diff --git a/sdnr/wt/odlux/framework/src/actions/websocketAction.ts b/sdnr/wt/odlux/framework/src/actions/websocketAction.ts index 8512d59d5..0b45f7ac7 100644 --- a/sdnr/wt/odlux/framework/src/actions/websocketAction.ts +++ b/sdnr/wt/odlux/framework/src/actions/websocketAction.ts @@ -1,8 +1,26 @@ +/** + * ============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 { Action } from "../flux/action"; export class SetWebsocketAction extends Action { - constructor(public isConnected: boolean) { + constructor(public isConnected: boolean|null) { super(); } }
\ No newline at end of file diff --git a/sdnr/wt/odlux/framework/src/app.tsx b/sdnr/wt/odlux/framework/src/app.tsx index ada78b90f..a73b7529b 100644 --- a/sdnr/wt/odlux/framework/src/app.tsx +++ b/sdnr/wt/odlux/framework/src/app.tsx @@ -26,7 +26,7 @@ import { Frame } from './views/frame'; import { User } from './models/authentication';
import { AddErrorInfoAction } from './actions/errorActions';
-import { UpdateUser } from './actions/authentication';
+import { loginUserAction } from './actions/authentication';
import { applicationStoreCreator } from './store/applicationStore';
import { ApplicationStoreProvider } from './flux/connect';
@@ -34,11 +34,12 @@ import { ApplicationStoreProvider } from './flux/connect'; import { startHistoryListener } from './middleware/navigation';
import { startRestService } from './services/restService';
-import { startForceLogoutService } from './services/forceLogoutService';
+import { startUserSessionService } from './services/userSessionService';
import { startNotificationService } from './services/notificationService';
import theme from './design/default';
import '!style-loader!css-loader!./app.css';
+import { startBroadcastChannel } from './services/broadcastService';
declare module '@material-ui/core/styles/createMuiTheme' {
@@ -64,12 +65,15 @@ export { configureApplication } from "./handlers/applicationStateHandler"; export const transportPCEUrl = "transportPCEUrl";
export const runApplication = () => {
-
+
const initialToken = localStorage.getItem("userToken");
const applicationStore = applicationStoreCreator();
+ startBroadcastChannel(applicationStore);
+ startUserSessionService(applicationStore);
+
if (initialToken) {
- applicationStore.dispatch(new UpdateUser(User.fromString(initialToken) || undefined));
+ applicationStore.dispatch(loginUserAction(User.fromString(initialToken) || undefined));
}
window.onerror = function (msg: string, url: string, line: number, col: number, error: Error) {
@@ -86,10 +90,10 @@ export const runApplication = () => { // Internet Explorer) will be suppressed.
return suppressErrorAlert;
};
+
startRestService(applicationStore);
startHistoryListener(applicationStore);
- startForceLogoutService(applicationStore);
startNotificationService(applicationStore);
const App = (): JSX.Element => (
diff --git a/sdnr/wt/odlux/framework/src/components/material-table/index.tsx b/sdnr/wt/odlux/framework/src/components/material-table/index.tsx index 9155f38ec..cb675218f 100644 --- a/sdnr/wt/odlux/framework/src/components/material-table/index.tsx +++ b/sdnr/wt/odlux/framework/src/components/material-table/index.tsx @@ -40,6 +40,7 @@ import { DividerTypeMap } from '@material-ui/core/Divider'; import { MenuItemProps } from '@material-ui/core/MenuItem'; import { flexbox } from '@material-ui/system'; import { RowDisabled } from './utilities'; +import { toAriaLabel } from '../../utilities/yangHelper'; export { ColumnModel, ColumnType } from './columnModel'; type propType = string | number | null | undefined | (string | number)[]; @@ -100,7 +101,8 @@ const styles = (theme: Theme) => createStyles({ flex: "1 1 100%" }, pagination: { - overflow: "hidden" + overflow: "hidden", + minHeight: "52px" } }); @@ -152,6 +154,7 @@ type MaterialTableComponentBaseProps<TData> = WithStyles<typeof styles> & { columns: ColumnModel<TData>[]; idProperty: keyof TData | ((data: TData) => React.Key); tableId?: string; + isPopup?: boolean; title?: string; stickyHeader?: boolean; defaultSortOrder?: 'asc' | 'desc'; @@ -294,7 +297,7 @@ class MaterialTableComponent<TData extends {} = {}> extends React.Component<Mate col => { const style = col.width ? { width: col.width } : {}; return ( - <TableCell style={ entry[RowDisabled] || false ? { ...style, color: "inherit" } : style } aria-label={col.title? col.title.toLowerCase().replace(/\s/g, "-") : col.property.toLowerCase().replace(/\s/g, "-")} key={col.property} align={col.type === ColumnType.numeric && !col.align ? "right" : col.align} > + <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 @@ -327,12 +330,12 @@ class MaterialTableComponent<TData extends {} = {}> extends React.Component<Mate count={rowCount} rowsPerPage={rowsPerPage} page={page} - aria-label="table-pagination-footer" + aria-label={"table-pagination-footer" } backIconButtonProps={{ - 'aria-label': 'previous-page', + 'aria-label': this.props.isPopup ? 'popup-previous-page' : 'previous-page', }} nextIconButtonProps={{ - 'aria-label': 'next-page', + 'aria-label': this.props.isPopup ? 'popup-next-page': 'next-page', }} onChangePage={this.onHandleChangePage} onChangeRowsPerPage={this.onHandleChangeRowsPerPage} diff --git a/sdnr/wt/odlux/framework/src/components/material-table/tableFilter.tsx b/sdnr/wt/odlux/framework/src/components/material-table/tableFilter.tsx index 5aefac445..e4cc5ab7c 100644 --- a/sdnr/wt/odlux/framework/src/components/material-table/tableFilter.tsx +++ b/sdnr/wt/odlux/framework/src/components/material-table/tableFilter.tsx @@ -25,6 +25,7 @@ import TableCell from '@material-ui/core/TableCell'; import TableRow from '@material-ui/core/TableRow'; import Input from '@material-ui/core/Input'; import { Select, FormControl, InputLabel, MenuItem } from '@material-ui/core'; +import { toAriaLabel } from '../../utilities/yangHelper'; const styles = (theme: Theme) => createStyles({ @@ -73,14 +74,14 @@ class EnhancedTableFilterComponent extends React.Component<IEnhancedTableFilterC {col.disableFilter || (col.type === ColumnType.custom) ? null : (col.type === ColumnType.boolean) - ? <Select className={classes.input} aria-label={col.title ? (col.title as string).toLowerCase().replace(/\s/g, "-") + '-filter' : `${ind + 1}-filter`} value={filter[col.property] !== undefined ? filter[col.property] : ''} onChange={this.createFilterHandler(col.property)} inputProps={{ name: `${col.property}-bool`, id: `${col.property}-bool` }} > + ? <Select 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.createFilterHandler(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 ? (col.title as string).toLowerCase().replace(/\s/g, "-") + '-filter' : `${ind + 1}-filter` }} value={filter[col.property] || ''} onChange={this.createFilterHandler(col.property)} />} + : <Input className={classes.input} inputProps={{ 'aria-label': col.title ? toAriaLabel(col.title as string)+ '-filter' : `${ind + 1}-filter` }} value={filter[col.property] || ''} onChange={this.createFilterHandler(col.property)} />} </TableCell> ); }, this)} diff --git a/sdnr/wt/odlux/framework/src/components/material-ui/listItemLink.tsx b/sdnr/wt/odlux/framework/src/components/material-ui/listItemLink.tsx index 8828ac3fc..49e7be514 100644 --- a/sdnr/wt/odlux/framework/src/components/material-ui/listItemLink.tsx +++ b/sdnr/wt/odlux/framework/src/components/material-ui/listItemLink.tsx @@ -23,6 +23,7 @@ import ListItemIcon from '@material-ui/core/ListItemIcon'; import ListItemText from '@material-ui/core/ListItemText';
import { withStyles, WithStyles, createStyles, Theme } from '@material-ui/core/styles';
+import { toAriaLabel } from '../../utilities/yangHelper';
const styles = (theme: Theme) => createStyles({
active: {
@@ -45,7 +46,7 @@ export const ListItemLink = withStyles(styles)((props: IListItemLinkProps) => { props.external ? <a target="_blank" href={to} { ...itemProps }></a> :
<NavLink exact={ exact } to={ to } activeClassName={ classes.active } { ...itemProps } />);
- const ariaLabel = typeof Primary === 'string' ? "link-to-"+Primary.toLowerCase().replace(/\s/g, "-") : "link-to-"+Primary.displayName?.toLowerCase();
+ const ariaLabel = typeof Primary === 'string' ? toAriaLabel("link-to-"+Primary) : toAriaLabel("link-to-"+Primary.displayName);
return (
<>
<ListItem button component={ renderLink } aria-label={ariaLabel}>
diff --git a/sdnr/wt/odlux/framework/src/components/navigationMenu.tsx b/sdnr/wt/odlux/framework/src/components/navigationMenu.tsx index b65eb29e2..b50d68081 100644 --- a/sdnr/wt/odlux/framework/src/components/navigationMenu.tsx +++ b/sdnr/wt/odlux/framework/src/components/navigationMenu.tsx @@ -94,6 +94,17 @@ export const NavigationMenu = withStyles(styles)(connect()(({ classes, state, di 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() {
diff --git a/sdnr/wt/odlux/framework/src/components/settings/general.tsx b/sdnr/wt/odlux/framework/src/components/settings/general.tsx new file mode 100644 index 000000000..ca1849049 --- /dev/null +++ b/sdnr/wt/odlux/framework/src/components/settings/general.tsx @@ -0,0 +1,109 @@ +/** + * ============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 { Button, FormControlLabel, makeStyles, Switch, Typography } from '@material-ui/core'; +import { SettingsComponentProps } from '../../models/settings'; +import * as React from 'react'; +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" checked={areWebsocketsEnabled} onChange={onWebsocketsChange} />} + label="Enable Notifications" + labelPlacement="end" + /> + <div className={classes.buttonPosition}> + <Button className={classes.elementMargin} variant="contained" color="primary" onClick={onCancel}>Cancel</Button> + <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/framework/src/components/titleBar.tsx b/sdnr/wt/odlux/framework/src/components/titleBar.tsx index 62db1de40..5d916e8c8 100644 --- a/sdnr/wt/odlux/framework/src/components/titleBar.tsx +++ b/sdnr/wt/odlux/framework/src/components/titleBar.tsx @@ -35,8 +35,8 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faBan } from '@fortawesome/free-solid-svg-icons';
import { faDotCircle } from '@fortawesome/free-solid-svg-icons';
-import { UpdateUser } from '../actions/authentication';
-import { ReplaceAction } from '../actions/navigationActions';
+import { logoutUser } from '../actions/authentication';
+import { PushAction, ReplaceAction } from '../actions/navigationActions';
import connect, { Connect, IDispatcher } from '../flux/connect';
import Logo from './logo';
@@ -71,9 +71,12 @@ const styles = (theme: Theme) => createStyles({ const mapDispatch = (dispatcher: IDispatcher) => {
return {
logout: () => {
- dispatcher.dispatch(new UpdateUser(undefined));
+ 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))
@@ -172,7 +175,14 @@ class TitleBarComponent extends React.Component<TitleBarProps, { anchorEl: HTMLE onClose={this.closeMenu}
>
{/* <MenuItem onClick={ this.closeMenu }>Profile</MenuItem> */}
- <MenuItem onClick={() => {
+ <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>
diff --git a/sdnr/wt/odlux/framework/src/handlers/applicationStateHandler.ts b/sdnr/wt/odlux/framework/src/handlers/applicationStateHandler.ts index 61e1334e7..16c6ed5d3 100644 --- a/sdnr/wt/odlux/framework/src/handlers/applicationStateHandler.ts +++ b/sdnr/wt/odlux/framework/src/handlers/applicationStateHandler.ts @@ -31,6 +31,9 @@ import { ExternalLoginProvider } from '../models/externalLoginProvider'; import { ApplicationConfig } from '../models/applicationConfig'; import { IConnectAppStoreState } from '../../../apps/connectApp/src/handlers/connectAppRootHandler'; import { IFaultAppStoreState } from '../../../apps/faultApp/src/handlers/faultAppRootHandler'; +import { GeneralSettings } from '../models/settings'; +import { SetGeneralSettingsAction } from '../actions/settingsAction'; +import { startWebsocketSession, suspendWebsocketSession } from '../services/notificationService'; declare module '../store/applicationStore' { @@ -47,11 +50,12 @@ export interface IApplicationState { isMenuClosedByUser: boolean; errors: ErrorInfo[]; snackBars: SnackbarItem[]; - isWebsocketAvailable: boolean | undefined; + isWebsocketAvailable: boolean | null; externalLoginProviders: ExternalLoginProvider[] | null; authentication: "basic"|"oauth", // basic enablePolicy: boolean, // false - transportpceUrl : string + transportpceUrl : string, + settings: GeneralSettings } const applicationStateInit: IApplicationState = { @@ -60,11 +64,12 @@ const applicationStateInit: IApplicationState = { snackBars: [], isMenuOpen: true, isMenuClosedByUser: false, - isWebsocketAvailable: undefined, + isWebsocketAvailable: null, externalLoginProviders: null, authentication: "basic", enablePolicy: false, - transportpceUrl: "" + transportpceUrl: "", + settings:{ general: { areNotificationsEnabled: null }} }; export const configureApplication = (config: ApplicationConfig) => { @@ -141,6 +146,12 @@ export const applicationStateHandler: IActionHandler<IApplicationState> = (state ...state, externalLoginProviders: action.externalLoginProvders, } + }else if(action instanceof SetGeneralSettingsAction){ + + state = { + ...state, + settings:{general:{areNotificationsEnabled: action.areNoticationsActive}} + } } return state; }; diff --git a/sdnr/wt/odlux/framework/src/handlers/authenticationHandler.ts b/sdnr/wt/odlux/framework/src/handlers/authenticationHandler.ts index 5217bd414..1bcb43528 100644 --- a/sdnr/wt/odlux/framework/src/handlers/authenticationHandler.ts +++ b/sdnr/wt/odlux/framework/src/handlers/authenticationHandler.ts @@ -22,6 +22,8 @@ import { AuthPolicy, User } from '../models/authentication'; import { onLogin, onLogout } from '../services/applicationApi'; import { startWebsocketSession, endWebsocketSession } from '../services/notificationService'; +import { startUserSession, endUserSession } from '../services/userSessionService'; +import { getSettings } from '../services/settingsService'; export interface IAuthenticationState { user?: User; @@ -38,11 +40,11 @@ export const authenticationStateHandler: IActionHandler<IAuthenticationState> = if (user) { localStorage.setItem("userToken", user.toString()); - startWebsocketSession(); + startUserSession(user); onLogin(); } else { localStorage.removeItem("userToken"); - endWebsocketSession(); + endUserSession(); onLogout(); } diff --git a/sdnr/wt/odlux/framework/src/index.dev.html b/sdnr/wt/odlux/framework/src/index.dev.html index 6c956386b..4dc353c44 100644 --- a/sdnr/wt/odlux/framework/src/index.dev.html +++ b/sdnr/wt/odlux/framework/src/index.dev.html @@ -26,6 +26,7 @@ faultApp.register(); // inventoryApp.register(); // helpApp.register(); + app("./app.tsx").configureApplication({ authentication:"oauth", enablePolicy: false, transportpceUrl:"http://test.de"}); app("./app.tsx").runApplication(); }); </script> diff --git a/sdnr/wt/odlux/framework/src/middleware/navigation.ts b/sdnr/wt/odlux/framework/src/middleware/navigation.ts index c5ab788f3..94350ab5d 100644 --- a/sdnr/wt/odlux/framework/src/middleware/navigation.ts +++ b/sdnr/wt/odlux/framework/src/middleware/navigation.ts @@ -24,7 +24,7 @@ import { LocationChanged, NavigateToApplication } from "../actions/navigationAct import { PushAction, ReplaceAction, GoAction, GoBackAction, GoForwardeAction } from '../actions/navigationActions'; import { applicationManager } from "../services/applicationManager"; -import { UpdateUser } from "../actions/authentication"; +import { loginUserAction, logoutUser } from "../actions/authentication"; import { ApplicationStore } from "../store/applicationStore"; import { Dispatch } from '../flux/store'; @@ -59,12 +59,17 @@ const routerMiddlewareCreator = (history: History) => () => (next: Dispatch): Di const token = tokenStr && jwt.decode(tokenStr); if (tokenStr && token) { // @ts-ignore - const user = new User({ username: token["name"], access_token: tokenStr, token_type: "Bearer", expires: (new Date().valueOf()) + ( (+token['exp']) * 1000) }) || undefined; - return next(new UpdateUser(user)) as any; + const user = new User({ username: token["name"], access_token: tokenStr, token_type: "Bearer", expires: token['exp'], issued: token['iat'] }) || undefined; + return next(loginUserAction(user)) as any; } } if (!action.pathname.startsWith("/login") && applicationStore && (!applicationStore.state.framework.authenticationState.user || !applicationStore.state.framework.authenticationState.user.isValid)) { history.replace(`/login?returnTo=${action.pathname}`); - } else { + return next(logoutUser()) as any; + + }else if (action.pathname.startsWith("/login") && applicationStore && (applicationStore.state.framework.authenticationState.user && applicationStore.state.framework.authenticationState.user.isValid)) { + history.replace(`/`); + } + else { return next(action); } } else { diff --git a/sdnr/wt/odlux/framework/src/models/applicationInfo.ts b/sdnr/wt/odlux/framework/src/models/applicationInfo.ts index 0b33777dc..ff07b7d7b 100644 --- a/sdnr/wt/odlux/framework/src/models/applicationInfo.ts +++ b/sdnr/wt/odlux/framework/src/models/applicationInfo.ts @@ -20,6 +20,7 @@ import { IconType } from './iconDefinition'; import { IActionHandler } from '../flux/action'; import { Middleware } from '../flux/middleware'; +import { SettingsComponentProps } from './settings'; /** Represents the information needed about an application to integrate. */ export class ApplicationInfo { @@ -47,6 +48,8 @@ export class ApplicationInfo { statusBarElement?: React.ComponentType; /** Optional: A component to be shown in the dashboardview. If undefiened the name will be used. */ dashbaordElement?: React.ComponentType; + /** Optional: A component shown in the settings view */ + settingsElement?: React.ComponentType<SettingsComponentProps>; /** Optional: The pasth for this application. If undefined the name will be use as path. */ path?: string; } diff --git a/sdnr/wt/odlux/framework/src/models/authentication.ts b/sdnr/wt/odlux/framework/src/models/authentication.ts index b6840a0ce..f56538184 100644 --- a/sdnr/wt/odlux/framework/src/models/authentication.ts +++ b/sdnr/wt/odlux/framework/src/models/authentication.ts @@ -20,7 +20,19 @@ export type AuthToken = { username: string; access_token: string; token_type: string; + /*** + * datetime the token should expire in unix timestamp + * + * must be in seconds + */ expires: number; + /*** + * time the token was issued in unix timestamp + * + * must be in seconds + * + */ + issued: number; } export type AuthPolicy = { @@ -52,8 +64,22 @@ export class User { return this._bearerToken && this._bearerToken.token_type; } + /*** + * Time the user should be logged out, in unix timestamp in seconds + */ + public get logoutAt(): number{ + return this._bearerToken && this._bearerToken.expires; + } + + /*** + * Time the user logged in, in unix timestamp in seconds + */ + public get loginAt(): number{ + return this._bearerToken && this._bearerToken.issued; + } + public get isValid(): boolean { - return (this._bearerToken && (new Date().valueOf()) < this._bearerToken.expires) || false; + return (this._bearerToken && (new Date().valueOf()) < this._bearerToken.expires*1000) || false; } public toString() { diff --git a/sdnr/wt/odlux/framework/src/models/settings.ts b/sdnr/wt/odlux/framework/src/models/settings.ts new file mode 100644 index 000000000..6d01a34e5 --- /dev/null +++ b/sdnr/wt/odlux/framework/src/models/settings.ts @@ -0,0 +1,27 @@ +/** + * ============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========================================================================== + */ + +export type GeneralSettings = { + general:{ + areNotificationsEnabled: boolean | null + } +}; + +export type SettingsComponentProps = { + onClose(): void +};
\ No newline at end of file diff --git a/sdnr/wt/odlux/framework/src/services/applicationApi.ts b/sdnr/wt/odlux/framework/src/services/applicationApi.ts index ff9ef0663..36523f9eb 100644 --- a/sdnr/wt/odlux/framework/src/services/applicationApi.ts +++ b/sdnr/wt/odlux/framework/src/services/applicationApi.ts @@ -15,8 +15,13 @@ * the License. * ============LICENSE_END========================================================================== */ +import { GeneralSettings } from '../models/settings'; +import { setGeneralSettingsAction, SetGeneralSettingsAction } from '../actions/settingsAction'; import { Event } from '../common/event'; import { ApplicationStore } from '../store/applicationStore'; +import { AuthMessage, getBroadcastChannel, sendMessage } from './broadcastService'; +import { endWebsocketSession } from './notificationService'; +import { getSettings } from './settingsService'; let resolveApplicationStoreInitialized: (store: ApplicationStore) => void; let applicationStore: ApplicationStore | null = null; @@ -24,13 +29,23 @@ const applicationStoreInitialized: Promise<ApplicationStore> = new Promise((reso const loginEvent = new Event(); const logoutEvent = new Event(); +let channel : BroadcastChannel | undefined; +const authChannelName = "odlux_auth"; export const onLogin = () => { + + const message : AuthMessage = {key: 'login', data: {}} + sendMessage(message, authChannelName); loginEvent.invoke(); + } export const onLogout = () => { + document.cookie = "JSESSIONID=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;"; + + const message : AuthMessage = {key: 'logout', data: {}} + sendMessage(message, authChannelName); logoutEvent.invoke(); } diff --git a/sdnr/wt/odlux/framework/src/services/authenticationService.ts b/sdnr/wt/odlux/framework/src/services/authenticationService.ts index 4e7d109d9..a7691bf6f 100644 --- a/sdnr/wt/odlux/framework/src/services/authenticationService.ts +++ b/sdnr/wt/odlux/framework/src/services/authenticationService.ts @@ -24,6 +24,7 @@ type AuthTokenResponse = { access_token: string; token_type: string; expires_at: number; + issued_at: number; } class AuthenticationService { @@ -50,11 +51,14 @@ class AuthenticationService { scope: scope }) }, false); + + return result && { username: email, access_token: result.access_token, token_type: result.token_type, - expires: (result.expires_at * 1000) + expires: result.expires_at, + issued: result.issued_at } || null; } @@ -65,12 +69,14 @@ class AuthenticationService { 'Authorization': "Basic " + btoa(email + ":" + password) }, }, false); + if (result) { return { username: email, access_token: btoa(email + ":" + password), token_type: "Basic", - expires: (new Date()).valueOf() + 2678400000 // 31 days + expires: (new Date()).valueOf() / 1000 + 86400, // 1 day + issued: (new Date()).valueOf() / 1000 } } return null; diff --git a/sdnr/wt/odlux/framework/src/services/broadcastService.ts b/sdnr/wt/odlux/framework/src/services/broadcastService.ts new file mode 100644 index 000000000..85ae3e65c --- /dev/null +++ b/sdnr/wt/odlux/framework/src/services/broadcastService.ts @@ -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 { setGeneralSettingsAction } from "../actions/settingsAction"; +import { loginUserAction, logoutUser } from "../actions/authentication"; +import { ReplaceAction } from "../actions/navigationActions"; +import { User } from "../models/authentication"; +import { ApplicationStore } from "../store/applicationStore"; + +type Broadcaster = {channel: BroadcastChannel, key: String}; + +type AuthTypes = 'login' | 'logout'; +export type AuthMessage={key: AuthTypes, data: any}; + +type SettingsType = 'general'; +export type SettingsMessage={key: SettingsType, enableNotifications: boolean, user: string}; + +let channels: Broadcaster[] = []; +let store : ApplicationStore | null = null; + +export const subscribe = (channel: BroadcastChannel, channelName: string) => { + channels.push({channel: channel, key: channelName}); +} + +export const startBroadcastChannel = (applicationStore: ApplicationStore)=>{ + store=applicationStore; + + //might decide to use one general broadcast channel with more keys in the future + createAuthBroadcastChannel(); + createSettingsBroadcastChannel(); +} + +const createSettingsBroadcastChannel = () =>{ + + const name = "odlux_settings"; + const bc: BroadcastChannel = new BroadcastChannel(name); + channels.push({ channel: bc, key: name }); + + bc.onmessage = (eventMessage: MessageEvent<SettingsMessage>) => { + console.log(eventMessage) + + if (eventMessage.data.key === 'general') { + + if (store?.state.framework.authenticationState.user) { + const data = eventMessage.data; + if(store.state.framework.authenticationState.user.user === data.user){ + store?.dispatch(setGeneralSettingsAction(data.enableNotifications)); + } + } + } + } + +} + +const createAuthBroadcastChannel = () => { + const name = "odlux_auth"; + const bc: BroadcastChannel = new BroadcastChannel(name); + channels.push({ channel: bc, key: name }); + + bc.onmessage = (eventMessage: MessageEvent<AuthMessage>) => { + console.log(eventMessage) + + if (eventMessage.data.key === 'login') { + if (!store?.state.framework.authenticationState.user) { + const initialToken = localStorage.getItem("userToken"); + if (initialToken) { + store?.dispatch(loginUserAction(User.fromString(initialToken))); + store?.dispatch(new ReplaceAction("/")); + } + } + } + else if (eventMessage.data.key === 'logout') { + + if (store?.state.framework.authenticationState.user) { + store?.dispatch(logoutUser()); + store?.dispatch(new ReplaceAction("/login")); + } + } + } +} + +export const getBroadcastChannel = (channelName: string) =>{ + const foundChannel = channels.find(s =>s.key===channelName); + return foundChannel?.channel; +} + + +export const sendMessage = (data: any, channel: string) =>{ + + const foundChannel = channels.find(s =>s.key===channel); + if(foundChannel){ + foundChannel.channel.postMessage(data); + } + + } diff --git a/sdnr/wt/odlux/framework/src/services/index.ts b/sdnr/wt/odlux/framework/src/services/index.ts index c6071e7b8..19b451345 100644 --- a/sdnr/wt/odlux/framework/src/services/index.ts +++ b/sdnr/wt/odlux/framework/src/services/index.ts @@ -15,7 +15,8 @@ * the License. * ============LICENSE_END========================================================================== */ -export { applicationManager } from './applicationManager';
-export { subscribe, unsubscribe } from './notificationService';
-export { requestRest } from './restService';
-
+export { applicationManager } from './applicationManager'; +export { subscribe, unsubscribe } from './notificationService'; +export { requestRest } from './restService'; +export { putSettings, getSettings} from './settingsService'; + diff --git a/sdnr/wt/odlux/framework/src/services/notificationService.ts b/sdnr/wt/odlux/framework/src/services/notificationService.ts index 99e697e9a..b2880b9de 100644 --- a/sdnr/wt/odlux/framework/src/services/notificationService.ts +++ b/sdnr/wt/odlux/framework/src/services/notificationService.ts @@ -21,10 +21,12 @@ import { SetWebsocketAction } from '../actions/websocketAction'; const socketUrl = [location.protocol === 'https:' ? 'wss://' : 'ws://', location.hostname, ':', location.port, '/websocket'].join(''); const subscriptions: { [scope: string]: SubscriptionCallback[] } = {}; let socketReady: Promise<WebSocket>; -let userLoggedOut = false; let wasWebsocketConnectionEstablished: undefined | boolean; let applicationStore: ApplicationStore | null; +let areWebsocketsStoppedViaSettings = false; + + export interface IFormatedMessage { "event-time": string, "data": { @@ -166,10 +168,11 @@ const connect = (): Promise<WebSocket> => { notificationSocket.onclose = function (event) { console.log("socket connection closed"); - if (applicationStore) { - applicationStore.dispatch(new SetWebsocketAction(false)); - } - if (!userLoggedOut) { + dispatchSocketClose(); + + const isUserLoggedIn = applicationStore?.state.framework.authenticationState.user && applicationStore?.state.framework.authenticationState.user?.isValid; + + if (isUserLoggedIn && !areWebsocketsStoppedViaSettings) { socketReady = connect(); } }; @@ -179,17 +182,37 @@ const connect = (): Promise<WebSocket> => { export const startWebsocketSession = () => { socketReady = connect(); - userLoggedOut = false; + areWebsocketsStoppedViaSettings = false; +} + +export const suspendWebsocketSession = () =>{ + areWebsocketsStoppedViaSettings = true; + closeSocket(); } export const endWebsocketSession = () => { + closeSocket(); +} + +const closeSocket = () =>{ + if (socketReady) { socketReady.then(websocket => { websocket.close(); - userLoggedOut = true; }); + }else{ + dispatchSocketClose(); } +} + +const dispatchSocketClose = () =>{ + const isUserLoggedIn = applicationStore?.state.framework.authenticationState.user && applicationStore?.state.framework.authenticationState.user?.isValid; + if(isUserLoggedIn){ + applicationStore?.dispatch(new SetWebsocketAction(false)); + }else{ + applicationStore?.dispatch(new SetWebsocketAction(null)); + } } diff --git a/sdnr/wt/odlux/framework/src/services/restService.ts b/sdnr/wt/odlux/framework/src/services/restService.ts index c7b122449..b21e3ec75 100644 --- a/sdnr/wt/odlux/framework/src/services/restService.ts +++ b/sdnr/wt/odlux/framework/src/services/restService.ts @@ -105,6 +105,7 @@ export async function requestRestExt<TData>(path: string = '', init: RequestInit if (!isAbsUrl && authenticate && applicationStore) { const { state: { framework: { authenticationState: { user } } } } = applicationStore; // do not request if the user is not valid + if (!user || !user.isValid) { return { ...result, diff --git a/sdnr/wt/odlux/framework/src/services/settingsService.ts b/sdnr/wt/odlux/framework/src/services/settingsService.ts new file mode 100644 index 000000000..6633a794d --- /dev/null +++ b/sdnr/wt/odlux/framework/src/services/settingsService.ts @@ -0,0 +1,41 @@ +/** + * ============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 { requestRest } from "./restService"; + + + const settingsPath ="/userdata"; + + + export function getSettings<TData>(partialPath?: string){ + let path = settingsPath; + if(partialPath){ + path+=partialPath + } + + const result = requestRest<TData>(path, {method: "GET"}) + return result; + } + + export function putSettings<TData>(partialPath: string, data: string){ + + const result = requestRest<TData>(settingsPath+partialPath, {method: "PUT", body: data}) + return result; + } + + diff --git a/sdnr/wt/odlux/framework/src/services/userSessionService.ts b/sdnr/wt/odlux/framework/src/services/userSessionService.ts new file mode 100644 index 000000000..0d5936a7e --- /dev/null +++ b/sdnr/wt/odlux/framework/src/services/userSessionService.ts @@ -0,0 +1,80 @@ +/** + * ============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 { ApplicationStore } from "../store/applicationStore"; +import { logoutUser } from "../actions/authentication"; +import { ReplaceAction } from "../actions/navigationActions"; +import { AuthMessage, getBroadcastChannel } from "./broadcastService"; +import { User } from "../models/authentication"; + +let currentUser: User | null; +let applicationStore: ApplicationStore | null = null; +let timer : NodeJS.Timeout | null = null; + +export const startUserSessionService = (store: ApplicationStore) =>{ + applicationStore=store; +} + +export const startUserSession = (user: User) => { + console.log("user session started...") + + const currentTime = new Date(); + //get time differnce between login time and now (eg after user refreshes page) + const timeDiffernce =(currentTime.valueOf()/1000 - user.loginAt); + + currentUser = user; + + if (process.env.NODE_ENV === "development") { + //console.warn("logout timer not started in development mode"); + + const expiresIn = (user.logoutAt - user.loginAt) - timeDiffernce; + console.log("user should be logged out in: "+expiresIn/60 +"minutes") + createForceLogoutInterval(expiresIn); + } else { + const expiresIn = (user.logoutAt - user.loginAt) - timeDiffernce; + console.log("user should be logged out in: "+expiresIn/60 +"minutes") + createForceLogoutInterval(expiresIn); + } +}; + +const createForceLogoutInterval = (intervalInSec: number) => { + console.log("logout timer running..."); + + if(timer!==null){ + console.error("an old session was available"); + clearTimeout(timer); + } + + timer = setTimeout(function () { + if (currentUser && applicationStore) { + + applicationStore.dispatch(logoutUser()); + applicationStore.dispatch(new ReplaceAction("/login")); + + } + + }, intervalInSec * 1000) +} + +export const endUserSession = ()=>{ + + if(timer!==null){ + clearTimeout(timer); + timer=null; + } +}
\ No newline at end of file diff --git a/sdnr/wt/odlux/framework/src/utilities/yangHelper.ts b/sdnr/wt/odlux/framework/src/utilities/yangHelper.ts index 127f3e07d..7e77c055c 100644 --- a/sdnr/wt/odlux/framework/src/utilities/yangHelper.ts +++ b/sdnr/wt/odlux/framework/src/utilities/yangHelper.ts @@ -20,6 +20,11 @@ export const replaceHyphen = (name: string) => name.replace(/-([a-z])/g, (g) => (g[1].toUpperCase())); export const replaceUpperCase = (name: string) => name.replace(/([a-z][A-Z])/g, (g) => g[0] + '-' + g[1].toLowerCase()); +/*** + * Replaces whitespace with '-' and cast everything to lowercase + */ +export const toAriaLabel = (value: string) => value.replace(/\s/g, "-").toLowerCase(); + export const convertPropertyNames = <T extends { [prop: string]: any }>(obj: T, conv: (name: string) => string): T => { return Object.keys(obj).reduce<{ [prop: string]: any }>((acc, cur) => { acc[conv(cur)] = typeof obj[cur] === "object" ? convertPropertyNames(obj[cur], conv) : obj[cur]; diff --git a/sdnr/wt/odlux/framework/src/views/frame.tsx b/sdnr/wt/odlux/framework/src/views/frame.tsx index b4cc43e0b..1c78dd297 100644 --- a/sdnr/wt/odlux/framework/src/views/frame.tsx +++ b/sdnr/wt/odlux/framework/src/views/frame.tsx @@ -19,7 +19,7 @@ import * as React from 'react'; import { HashRouter as Router, Route, Redirect, Switch } from 'react-router-dom';
import { withStyles, WithStyles, createStyles, Theme } from '@material-ui/core/styles';
-import { faHome, faAddressBook, faSignInAlt } from '@fortawesome/free-solid-svg-icons';
+import { faHome, faAddressBook, faSignInAlt, faCog } from '@fortawesome/free-solid-svg-icons';
import { SnackbarProvider } from 'notistack';
import { ConfirmProvider } from 'material-ui-confirm';
@@ -34,6 +34,7 @@ import Home from '../views/home'; import Login from '../views/login';
import About from '../views/about';
import Test from '../views/test';
+import UserSettings from '../views/settings';
import applicationService from '../services/applicationManager';
@@ -58,6 +59,8 @@ const styles = (theme: Theme) => createStyles({ toolbar: theme.mixins.toolbar as any
});
+
+
type FrameProps = WithStyles<typeof styles>;
class FrameComponent extends React.Component<FrameProps>{
@@ -89,6 +92,11 @@ class FrameComponent extends React.Component<FrameProps>{ <About />
</AppFrame>
)} />
+ <Route path="/settings" component={() => (
+ <AppFrame title={"Settings"} icon={faCog} >
+ <UserSettings />
+ </AppFrame>
+ )} />
{process.env.NODE_ENV === "development" ? <Route path="/test" component={() => (
<AppFrame title={"Test"} icon={faAddressBook} >
<Test />
diff --git a/sdnr/wt/odlux/framework/src/views/home.tsx b/sdnr/wt/odlux/framework/src/views/home.tsx index 0e1d487e3..176de02ab 100644 --- a/sdnr/wt/odlux/framework/src/views/home.tsx +++ b/sdnr/wt/odlux/framework/src/views/home.tsx @@ -34,6 +34,16 @@ const styles = (theme: Theme) => createStyles({ const scrollbar = { overflow: "auto", paddingRight: "20px" } +let connectionStatusinitialLoad = true; +let connectionStatusinitialStateChanged = false; +let connectionStatusDataLoad: number[] = [0, 0, 0, 0]; +let connectionTotalCount = 0; + +let alarmStatusinitialLoad = true; +let alarmStatusinitialStateChanged = false; +let alarmStatusDataLoad: number[] = [0, 0, 0, 0]; +let alarmTotalCount = 0; + const mapProps = (state: IApplicationStoreState) => ({ connectionStatusCount: state.connect.connectionStatusCount, alarmStatus: state.fault.faultStatus @@ -55,16 +65,34 @@ class Home extends React.Component<HomeComponentProps> { render(): JSX.Element { const { classes } = this.props; + if (!this.props.connectionStatusCount.isLoadingConnectionStatusChart) { + connectionStatusDataLoad = [ + this.props.connectionStatusCount.Connected, + this.props.connectionStatusCount.Connecting, + this.props.connectionStatusCount.Disconnected, + this.props.connectionStatusCount.UnableToConnect + ]; + connectionTotalCount = this.props.connectionStatusCount.Connected + this.props.connectionStatusCount.Connecting + + this.props.connectionStatusCount.Disconnected + this.props.connectionStatusCount.UnableToConnect; + + } + + if (!this.props.alarmStatus.isLoadingAlarmStatusChart) { + alarmStatusDataLoad = [ + this.props.alarmStatus.critical, + this.props.alarmStatus.major, + this.props.alarmStatus.minor, + this.props.alarmStatus.warning + ]; + alarmTotalCount = this.props.alarmStatus.critical + this.props.alarmStatus.major + + this.props.alarmStatus.minor + this.props.alarmStatus.warning; + } + /** Available Network Connection Status chart data */ const connectionStatusData = { labels: ['Connected', 'Connecting', 'Disconnected', 'UnableToConnect'], datasets: [{ - data: [ - this.props.connectionStatusCount.Connected, - this.props.connectionStatusCount.Connecting, - this.props.connectionStatusCount.Disconnected, - this.props.connectionStatusCount.UnableToConnect - ], + data: connectionStatusDataLoad, backgroundColor: [ 'rgb(0, 153, 51)', 'rgb(255, 102, 0)', @@ -86,6 +114,28 @@ class Home extends React.Component<HomeComponentProps> { }] }; + /** Loading Connection Status chart */ + const connectionStatusisLoading = { + labels: ['Loading chart...'], + datasets: [{ + data: [1], + backgroundColor: [ + 'rgb(255, 255, 255)' + ] + }] + }; + + /** Loading Alarm Status chart */ + const alarmStatusisLoading = { + labels: ['Loading chart...'], + datasets: [{ + data: [1], + backgroundColor: [ + 'rgb(255, 255, 255)' + ] + }] + }; + /** Connection status options */ let labels: String[] = ['Connected', 'Connecting', 'Disconnected', 'UnableToConnect']; const connectionStatusOptions = { @@ -153,12 +203,7 @@ class Home extends React.Component<HomeComponentProps> { 'Warning' ], datasets: [{ - data: [ - this.props.alarmStatus.critical, - this.props.alarmStatus.major, - this.props.alarmStatus.minor, - this.props.alarmStatus.warning - ], + data: alarmStatusDataLoad, backgroundColor: [ 'rgb(240, 25, 10)', 'rgb(240, 133, 10)', @@ -241,17 +286,25 @@ class Home extends React.Component<HomeComponentProps> { <div style={scrollbar} > <h1>Welcome to ODLUX</h1> <div className={classes.pageWidthSettings}> - {this.checkConnectionStatus() ? - <Doughnut - data={connectionStatusData} - type={Doughnut} - width={500} - height={500} - options={connectionStatusOptions} - plugins={connectionStatusPlugins} - /> + {this.checkElementsAreLoaded() ? + this.checkConnectionStatus() && connectionTotalCount != 0 ? + <Doughnut + data={connectionStatusData} + type={Doughnut} + width={500} + height={500} + options={connectionStatusOptions} + plugins={connectionStatusPlugins} + /> + : <Doughnut + data={connectionStatusUnavailableData} + type={Doughnut} + width={500} + height={500} + options={connectionStatusUnavailableOptions} + plugins={connectionStatusPlugins} /> : <Doughnut - data={connectionStatusUnavailableData} + data={connectionStatusisLoading} type={Doughnut} width={500} height={500} @@ -261,17 +314,26 @@ class Home extends React.Component<HomeComponentProps> { } </div> <div className={classes.pageWidthSettings}> - {this.checkAlarmStatus() ? - <Doughnut - data={alarmStatusData} - type={Doughnut} - width={500} - height={500} - options={alarmStatusOptions} - plugins={alarmStatusPlugins} - /> + {this.checkAlarmsAreLoaded() ? + this.checkAlarmStatus() && alarmTotalCount != 0 ? + <Doughnut + data={alarmStatusData} + type={Doughnut} + width={500} + height={500} + options={alarmStatusOptions} + plugins={alarmStatusPlugins} + /> + : <Doughnut + data={alarmStatusUnavailableData} + type={Doughnut} + width={500} + height={500} + options={alarmStatusUnavailableOptions} + plugins={alarmStatusPlugins} + /> : <Doughnut - data={alarmStatusUnavailableData} + data={alarmStatusisLoading} type={Doughnut} width={500} height={500} @@ -288,23 +350,74 @@ class Home extends React.Component<HomeComponentProps> { /** Check if connection status data available */ public checkConnectionStatus = () => { let statusCount = this.props.connectionStatusCount; - if (statusCount.Connected == 0 && statusCount.Connecting == 0 && statusCount.Disconnected == 0 && statusCount.UnableToConnect == 0) { - return false; + if (statusCount.isLoadingConnectionStatusChart) { + return true; } - else + if (statusCount.Connected == 0 && statusCount.Connecting == 0 && statusCount.Disconnected == 0 + && statusCount.UnableToConnect == 0) { + return false; + } else { return true; + } + } + + /** Check if connection status chart data is loaded */ + public checkElementsAreLoaded = () => { + let isLoadingCheck = this.props.connectionStatusCount; + if (connectionStatusinitialLoad && !isLoadingCheck.isLoadingConnectionStatusChart) { + if (this.checkConnectionStatus()) { + connectionStatusinitialLoad = false; + return true; + } + return false; + } else if (connectionStatusinitialLoad && isLoadingCheck.isLoadingConnectionStatusChart) { + connectionStatusinitialLoad = false; + connectionStatusinitialStateChanged = true; + return !isLoadingCheck.isLoadingConnectionStatusChart; + } else if (connectionStatusinitialStateChanged) { + if (!isLoadingCheck.isLoadingConnectionStatusChart) { + connectionStatusinitialStateChanged = false; + } + return !isLoadingCheck.isLoadingConnectionStatusChart; + } + return true; } /** Check if alarms data available */ public checkAlarmStatus = () => { let alarmCount = this.props.alarmStatus; + if (alarmCount.isLoadingAlarmStatusChart) { + return true; + } if (alarmCount.critical == 0 && alarmCount.major == 0 && alarmCount.minor == 0 && alarmCount.warning == 0) { return false; } - else + else { return true; + } } + /** Check if alarm status chart data is loaded */ + public checkAlarmsAreLoaded = () => { + let isLoadingCheck = this.props.alarmStatus; + if (alarmStatusinitialLoad && !isLoadingCheck.isLoadingAlarmStatusChart) { + if (this.checkAlarmStatus()) { + alarmStatusinitialLoad = false; + return true; + } + return false; + } else if (alarmStatusinitialLoad && isLoadingCheck.isLoadingAlarmStatusChart) { + alarmStatusinitialLoad = false; + alarmStatusinitialStateChanged = true; + return !isLoadingCheck.isLoadingAlarmStatusChart; + } else if (alarmStatusinitialStateChanged) { + if (!isLoadingCheck.isLoadingAlarmStatusChart) { + alarmStatusinitialStateChanged = false; + } + return !isLoadingCheck.isLoadingAlarmStatusChart; + } + return true; + } } export default withStyles(styles)(withRouter(connect(mapProps, mapDispatch)(Home)));
\ No newline at end of file diff --git a/sdnr/wt/odlux/framework/src/views/login.tsx b/sdnr/wt/odlux/framework/src/views/login.tsx index be1fb801f..53219facd 100644 --- a/sdnr/wt/odlux/framework/src/views/login.tsx +++ b/sdnr/wt/odlux/framework/src/views/login.tsx @@ -36,7 +36,7 @@ import connect, { Connect, IDispatcher } from '../flux/connect'; import authenticationService from '../services/authenticationService'; import { updateExternalLoginProviderAsyncActionCreator } from '../actions/loginProvider'; -import { UpdatePolicies, UpdateUser } from '../actions/authentication'; +import { loginUserAction, UpdatePolicies } from '../actions/authentication'; import { IApplicationStoreState } from '../store/applicationStore'; import { AuthPolicy, AuthToken, User } from '../models/authentication'; @@ -73,6 +73,20 @@ const styles = (theme: Theme) => createStyles({ submit: { marginTop: theme.spacing(3), }, + lineContainer:{ + width: '100%', + height: 10, + borderBottom: '1px solid grey', + textAlign: 'center', + marginTop:15, + marginBottom:5 + }, + thirdPartyDivider:{ + fontSize: 15, + backgroundColor: 'white', + padding: '0 10px', + color: 'grey' + } }); const mapProps = (state: IApplicationStoreState) => ({ @@ -85,7 +99,7 @@ const mapDispatch = (dispatcher: IDispatcher) => ({ updateExternalProviders: () => dispatcher.dispatch(updateExternalLoginProviderAsyncActionCreator()), updateAuthentication: (token: AuthToken | null) => { const user = token && new User(token) || undefined; - dispatcher.dispatch(new UpdateUser(user)); + dispatcher.dispatch(loginUserAction(user)); }, updatePolicies: (policies?: AuthPolicy[]) => { return dispatcher.dispatch(new UpdatePolicies(policies)); @@ -138,6 +152,7 @@ class LoginComponent extends React.Component<LoginProps, ILoginState> { render(): JSX.Element { const { classes } = this.props; + const areProvidersAvailable = this.props.externalLoginProviders && this.props.externalLoginProviders.length > 0; return ( <> <CssBaseline /> @@ -148,6 +163,32 @@ class LoginComponent extends React.Component<LoginProps, ILoginState> { </Avatar> <Typography variant="caption">Sign in</Typography> <form className={classes.form}> + + + {areProvidersAvailable && + <> + { + this.props.externalLoginProviders!.map((provider, index) => ( + <Button + aria-controls="externalLogin" + aria-label={"external-login-identity-provider-" + (index + 1)} + aria-haspopup="true" + fullWidth + variant="contained" + color="primary" + className={classes.submit} onClick={() => { window.location = provider.loginUrl as any; }}> + {provider.title} + </Button>)) + } + + <div className={classes.lineContainer}> + <span className={classes.thirdPartyDivider}> + OR + </span> + </div> + </> + } + <FormControl margin="normal" required fullWidth> <InputLabel htmlFor="username">Username</InputLabel> <Input id="username" name="username" autoComplete="username" autoFocus @@ -178,10 +219,6 @@ class LoginComponent extends React.Component<LoginProps, ILoginState> { onChange={event => { this.setState({ scope: event.target.value }) }} /> </FormControl> - <FormControlLabel - control={<Checkbox value="remember" color="secondary" />} - label="Remember me" - /> <Button aria-label="login-button" type="submit" @@ -193,34 +230,8 @@ class LoginComponent extends React.Component<LoginProps, ILoginState> { onClick={this.onSignIn} > Sign in - </Button> - { this.props.externalLoginProviders && this.props.externalLoginProviders.length > 0 - ? - [ - <Button - aria-controls="externalLogin" - aria-haspopup="true" - fullWidth - variant="contained" - color="primary" - className={classes.submit} onClick={(ev) => { this.setExternalProviderAnchor(ev.currentTarget); }}> - Use external Login - </Button>, - <Menu - anchorEl={this.state.externalProviderAnchor} - keepMounted - open={Boolean(this.state.externalProviderAnchor)} - onClose={() => { this.setExternalProviderAnchor(null); }} - > - { - this.props.externalLoginProviders.map((provider) => ( - <MenuItem key={provider.id} onClick={() => { window.location = provider.loginUrl as any; } }>{ provider.title} </MenuItem> - )) - } - </Menu> - ] - : null - } + </Button> + </form> {this.state.message && <Alert severity="error">{this.state.message}</Alert>} </Paper> diff --git a/sdnr/wt/odlux/framework/src/views/settings.tsx b/sdnr/wt/odlux/framework/src/views/settings.tsx new file mode 100644 index 000000000..f1a8ab35a --- /dev/null +++ b/sdnr/wt/odlux/framework/src/views/settings.tsx @@ -0,0 +1,126 @@ +/** + * ============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 * as React from 'react'; +import { IApplicationStoreState } from "../store/applicationStore"; +import connect, { Connect, IDispatcher } from "../flux/connect"; + +import applicationService from '../services/applicationManager'; +import { makeStyles } from '@material-ui/styles'; +import { Divider, List, ListItem, ListItemText, Paper } from '@material-ui/core'; + +import { GeneralUserSettings } from '../components/settings/general' +import { GoBackAction } from '../actions/navigationActions'; +import { toAriaLabel } from '../utilities/yangHelper'; + +type props = Connect<typeof mapProps, typeof mapDispatch>; + +type SettingsEntry = { name: string, element: JSX.Element } + + +const mapProps = (state: IApplicationStoreState) => ({ + +}); + +const mapDispatch = (dispatcher: IDispatcher) => ({ + goBack: () => dispatcher.dispatch(new GoBackAction()) +}); + +const styles = makeStyles({ + sectionMargin: { + marginTop: "30px", + marginBottom: "15px" + }, + elementMargin: { + + marginLeft: "10px" + }, + menu: { + flex: "1 0 0%", + } +}); + +const UserSettings: React.FunctionComponent<props> = (props) => { + + const classes = styles(); + const registrations = applicationService.applications; + + const [selectedIndex, setSelectedIndex] = React.useState(0); + + const navigateBack = () => { + props.goBack(); + } + + let settingsArray: SettingsEntry[] = []; + + //add all framework specific settings + settingsArray.push({name:"General", element: <GeneralUserSettings onClose={navigateBack} />}) + + + //get app settings + let settingsElements : (SettingsEntry) [] = Object.keys(registrations).map(p => { + const application = registrations[p]; + + if (application.settingsElement) { + const value: SettingsEntry = { name: application.menuEntry?.toString()!, element: <application.settingsElement onClose={navigateBack} /> }; + return value; + + } else { + return null; + } + }).filter((x): x is SettingsEntry => x !== null); + + + settingsArray.push(...settingsElements); + + const onSelectElement = (e: any, newValue: number) => { + e.preventDefault(); + setSelectedIndex(newValue); + } + + return <div style={{ display: "flex", flexDirection: "row", height: "100%" }}> + <div style={{ display: "flex", flexDirection: "column", height: "100%", width: "15%" }}> + <Paper variant="outlined" style={{ height: "70%" }}> + <List className={classes.menu} component="nav"> + { + settingsArray.map((el, index) => { + return ( + <> + <ListItem selected={selectedIndex === index} button onClick={e => { onSelectElement(e, index) }} aria-label={toAriaLabel(el?.name+"-settings")}> + <ListItemText primary={el?.name} style={{ padding: 0 }} /> + </ListItem> + <Divider /> + </>) + }) + } + </List> + </Paper> + + </div> + <div style={{ height: "100%", width: "80%", marginLeft: 15 }}> + <div style={{ height: "100%" }}> + { + settingsArray[selectedIndex]?.element + } + </div> + </div> + </div> +} + + +export default connect(mapProps, mapDispatch)(UserSettings); diff --git a/sdnr/wt/odlux/framework/webpack.config.js b/sdnr/wt/odlux/framework/webpack.config.js index cef310136..95b5f5ed7 100644 --- a/sdnr/wt/odlux/framework/webpack.config.js +++ b/sdnr/wt/odlux/framework/webpack.config.js @@ -82,7 +82,8 @@ module.exports = (env) => { use: [{
loader: "babel-loader"
}]
- }, {
+ },
+ {
//don't minify images
test: /\.(png|gif|jpg|svg)$/,
use: [{
@@ -92,7 +93,8 @@ module.exports = (env) => { name: './images/[name].[ext]'
}
}]
- }]
+ }
+ ]
},
optimization: {
@@ -202,55 +204,55 @@ module.exports = (env) => { proxy: {
"/about": {
// target: "http://10.20.6.29:48181",
- target: "http://localhost:18181",
+ target: "http://sdnr:8181",
secure: false
},
"/yang-schema/": {
- target: "http://localhost:18181",
+ target: "http://sdnr:8181",
secure: false
},
"/oauth/": {
// target: "https://10.20.35.188:30205",
- target: "http://localhost:18181",
+ target: "http://sdnr:8181",
secure: false
},
"/oauth2/": {
// target: "https://10.20.35.188:30205",
- target: "http://localhost:18181",
+ target: "http://sdnr:8181",
secure: false
},
"/database/": {
- target: "http://localhost:18181",
+ target: "http://sdnr:8181",
secure: false
},
"/restconf/": {
- target: "http://localhost:18181",
+ target: "http://sdnr:8181",
secure: false
},
"/rests/": {
- target: "http://localhost:18181",
+ target: "http://sdnr:8181",
secure: false
},
"/help/": {
- target: "http://localhost:18181",
+ target: "http://sdnr:8181",
secure: false
},
"/about/": {
- target: "http://localhost:18181",
+ target: "http://sdnr:8181",
secure: false
},
"/tree/": {
- target: "http://localhost:18181",
+ target: "http://sdnr:8181",
secure: false
},
"/websocket": {
- target: "http://localhost:18181",
+ target: "http://sdnr:8181",
ws: true,
changeOrigin: true,
secure: false
},
"/apidoc": {
- target: "http://localhost:18181",
+ target: "http://sdnr:8181",
ws: true,
changeOrigin: true,
secure: false
|