From 152cb381ea2c915c762416092337ce1d8589d1c6 Mon Sep 17 00:00:00 2001 From: Aijana Schumann Date: Mon, 6 Dec 2021 15:09:15 +0100 Subject: 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 Change-Id: I21137756b204287e25766a9646bf2faf7bad9d35 --- sdnr/wt/odlux/framework/pom.xml | 3 +- .../odlux/framework/src/actions/authentication.ts | 54 ++++++ .../odlux/framework/src/actions/settingsAction.ts | 64 +++++++ .../odlux/framework/src/actions/websocketAction.ts | 20 ++- sdnr/wt/odlux/framework/src/app.tsx | 14 +- .../src/components/material-table/index.tsx | 13 +- .../src/components/material-table/tableFilter.tsx | 5 +- .../src/components/material-ui/listItemLink.tsx | 3 +- .../framework/src/components/navigationMenu.tsx | 11 ++ .../framework/src/components/settings/general.tsx | 109 ++++++++++++ .../wt/odlux/framework/src/components/titleBar.tsx | 18 +- .../src/handlers/applicationStateHandler.ts | 19 ++- .../src/handlers/authenticationHandler.ts | 6 +- sdnr/wt/odlux/framework/src/index.dev.html | 1 + .../odlux/framework/src/middleware/navigation.ts | 13 +- .../odlux/framework/src/models/applicationInfo.ts | 3 + .../odlux/framework/src/models/authentication.ts | 28 +++- sdnr/wt/odlux/framework/src/models/settings.ts | 27 +++ .../odlux/framework/src/services/applicationApi.ts | 15 ++ .../src/services/authenticationService.ts | 10 +- .../framework/src/services/broadcastService.ts | 110 ++++++++++++ sdnr/wt/odlux/framework/src/services/index.ts | 9 +- .../framework/src/services/notificationService.ts | 37 ++++- .../wt/odlux/framework/src/services/restService.ts | 1 + .../framework/src/services/settingsService.ts | 41 +++++ .../framework/src/services/userSessionService.ts | 80 +++++++++ .../wt/odlux/framework/src/utilities/yangHelper.ts | 5 + sdnr/wt/odlux/framework/src/views/frame.tsx | 10 +- sdnr/wt/odlux/framework/src/views/home.tsx | 185 +++++++++++++++++---- sdnr/wt/odlux/framework/src/views/login.tsx | 79 +++++---- sdnr/wt/odlux/framework/src/views/settings.tsx | 126 ++++++++++++++ sdnr/wt/odlux/framework/webpack.config.js | 30 ++-- 32 files changed, 1021 insertions(+), 128 deletions(-) create mode 100644 sdnr/wt/odlux/framework/src/actions/settingsAction.ts create mode 100644 sdnr/wt/odlux/framework/src/components/settings/general.tsx create mode 100644 sdnr/wt/odlux/framework/src/models/settings.ts create mode 100644 sdnr/wt/odlux/framework/src/services/broadcastService.ts create mode 100644 sdnr/wt/odlux/framework/src/services/settingsService.ts create mode 100644 sdnr/wt/odlux/framework/src/services/userSessionService.ts create mode 100644 sdnr/wt/odlux/framework/src/views/settings.tsx (limited to 'sdnr/wt/odlux/framework') 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======================================================= ~ --> + 4.0.0 @@ -45,7 +46,7 @@ ${maven.build.timestamp} ONAP Frankfurt (Neon, mdsal ${odl.mdsal.version}) - 116.8c2f6b7(21/08/05) + 137.be0dfd7(21/12/03) ONAP SDN-R | ONF Wireless for ${distversion} - Build: ${buildtime} ${buildno} ${project.version} 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(); + + 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 = WithStyles & { columns: ColumnModel[]; idProperty: keyof TData | ((data: TData) => React.Key); tableId?: string; + isPopup?: boolean; title?: string; stickyHeader?: boolean; defaultSortOrder?: 'asc' | 'desc'; @@ -294,7 +297,7 @@ class MaterialTableComponent extends React.Component { const style = col.width ? { width: col.width } : {}; return ( - + {col.type === ColumnType.custom && col.customControl ? : col.type === ColumnType.boolean @@ -327,12 +330,12 @@ class MaterialTableComponent extends React.Component createStyles({ @@ -73,14 +74,14 @@ class EnhancedTableFilterComponent extends React.Component + ? - : } + : } ); }, 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 ? : ); - 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 ( <> 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 & 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) =>{ + +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, newValue: boolean) =>{ + setWebsocketsEnabled(newValue); + } + +const onSave = (e: React.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) =>{ + e.preventDefault(); + props.onClose(); + +} + + + return
+ + Enable Notifications + + } + label="Enable Notifications" + labelPlacement="end" + /> +
+ + +
+
+} + +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 {/* Profile */} - { + { + this.props.openSettings(); + this.closeMenu(); }}>Settings + { this.props.logout(); this.closeMenu(); }}>Logout 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 = (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 = 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(); }); 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; /** 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 = 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) => { + 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) => { + 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; -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 => { 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 => { 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(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(partialPath?: string){ + let path = settingsPath; + if(partialPath){ + path+=partialPath + } + + const result = requestRest(path, {method: "GET"}) + return result; + } + + export function putSettings(partialPath: string, data: string){ + + const result = requestRest(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 = (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; class FrameComponent extends React.Component{ @@ -89,6 +92,11 @@ class FrameComponent extends React.Component{ )} /> + ( + + + + )} /> {process.env.NODE_ENV === "development" ? ( 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 { 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 { }] }; + /** 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 { '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 {

Welcome to ODLUX

- {this.checkConnectionStatus() ? - + {this.checkElementsAreLoaded() ? + this.checkConnectionStatus() && connectionTotalCount != 0 ? + + : : { }
- {this.checkAlarmStatus() ? - + {this.checkAlarmsAreLoaded() ? + this.checkAlarmStatus() && alarmTotalCount != 0 ? + + : : { /** 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 { render(): JSX.Element { const { classes } = this.props; + const areProvidersAvailable = this.props.externalLoginProviders && this.props.externalLoginProviders.length > 0; return ( <> @@ -148,6 +163,32 @@ class LoginComponent extends React.Component { Sign in
+ + + {areProvidersAvailable && + <> + { + this.props.externalLoginProviders!.map((provider, index) => ( + )) + } + +
+ + OR + +
+ + } + Username { onChange={event => { this.setState({ scope: event.target.value }) }} /> - } - label="Remember me" - /> - { this.props.externalLoginProviders && this.props.externalLoginProviders.length > 0 - ? - [ - , - { this.setExternalProviderAnchor(null); }} - > - { - this.props.externalLoginProviders.map((provider) => ( - { window.location = provider.loginUrl as any; } }>{ provider.title} - )) - } - - ] - : null - } + + {this.state.message && {this.state.message}} 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; + +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) => { + + 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: }) + + + //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: }; + 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
+
+ + + { + settingsArray.map((el, index) => { + return ( + <> + { onSelectElement(e, index) }} aria-label={toAriaLabel(el?.name+"-settings")}> + + + + ) + }) + } + + + +
+
+
+ { + settingsArray[selectedIndex]?.element + } +
+
+
+} + + +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 -- cgit 1.2.3-korg