aboutsummaryrefslogtreecommitdiffstats
path: root/sdnr/wt/odlux/framework/src
diff options
context:
space:
mode:
authorAijana Schumann <aijana.schumann@highstreet-technologies.com>2021-12-06 15:09:15 +0100
committerAijana Schumann <aijana.schumann@highstreet-technologies.com>2021-12-06 15:12:24 +0100
commit152cb381ea2c915c762416092337ce1d8589d1c6 (patch)
tree63b71c8343f9292281f5d7f5eac14342fec06402 /sdnr/wt/odlux/framework/src
parent8ea94e1210671b941f84abfe16e248cfa086fe49 (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/src')
-rw-r--r--sdnr/wt/odlux/framework/src/actions/authentication.ts54
-rw-r--r--sdnr/wt/odlux/framework/src/actions/settingsAction.ts64
-rw-r--r--sdnr/wt/odlux/framework/src/actions/websocketAction.ts20
-rw-r--r--sdnr/wt/odlux/framework/src/app.tsx14
-rw-r--r--sdnr/wt/odlux/framework/src/components/material-table/index.tsx13
-rw-r--r--sdnr/wt/odlux/framework/src/components/material-table/tableFilter.tsx5
-rw-r--r--sdnr/wt/odlux/framework/src/components/material-ui/listItemLink.tsx3
-rw-r--r--sdnr/wt/odlux/framework/src/components/navigationMenu.tsx11
-rw-r--r--sdnr/wt/odlux/framework/src/components/settings/general.tsx109
-rw-r--r--sdnr/wt/odlux/framework/src/components/titleBar.tsx18
-rw-r--r--sdnr/wt/odlux/framework/src/handlers/applicationStateHandler.ts19
-rw-r--r--sdnr/wt/odlux/framework/src/handlers/authenticationHandler.ts6
-rw-r--r--sdnr/wt/odlux/framework/src/index.dev.html1
-rw-r--r--sdnr/wt/odlux/framework/src/middleware/navigation.ts13
-rw-r--r--sdnr/wt/odlux/framework/src/models/applicationInfo.ts3
-rw-r--r--sdnr/wt/odlux/framework/src/models/authentication.ts28
-rw-r--r--sdnr/wt/odlux/framework/src/models/settings.ts27
-rw-r--r--sdnr/wt/odlux/framework/src/services/applicationApi.ts15
-rw-r--r--sdnr/wt/odlux/framework/src/services/authenticationService.ts10
-rw-r--r--sdnr/wt/odlux/framework/src/services/broadcastService.ts110
-rw-r--r--sdnr/wt/odlux/framework/src/services/index.ts9
-rw-r--r--sdnr/wt/odlux/framework/src/services/notificationService.ts37
-rw-r--r--sdnr/wt/odlux/framework/src/services/restService.ts1
-rw-r--r--sdnr/wt/odlux/framework/src/services/settingsService.ts41
-rw-r--r--sdnr/wt/odlux/framework/src/services/userSessionService.ts80
-rw-r--r--sdnr/wt/odlux/framework/src/utilities/yangHelper.ts5
-rw-r--r--sdnr/wt/odlux/framework/src/views/frame.tsx10
-rw-r--r--sdnr/wt/odlux/framework/src/views/home.tsx185
-rw-r--r--sdnr/wt/odlux/framework/src/views/login.tsx79
-rw-r--r--sdnr/wt/odlux/framework/src/views/settings.tsx126
30 files changed, 1003 insertions, 113 deletions
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);