diff --git a/lib/db/api.js b/lib/db/api.js index ff0a472c..2d80a9e6 100644 --- a/lib/db/api.js +++ b/lib/db/api.js @@ -1036,6 +1036,19 @@ dbapi.loadUser = function(email) { return db.run(r.table('users').get(email)) } +dbapi.updateUsersAlertMessage = function(alertMessage) { + return dbapi.getRootGroup().then(function(group) { + return db.run(r.table('users').get(group.owner.email).update({settings: { + alertMessage: + r.branch( + r.row.hasFields({settings: 'alertMessage'}) + , r.row('settings')('alertMessage').merge(alertMessage) + , alertMessage + ) + }}, {returnChanges: true})) + }) +} + dbapi.updateUserSettings = function(email, changes) { return db.run(r.table('users').get(email).update({ settings: changes diff --git a/lib/units/api/controllers/users.js b/lib/units/api/controllers/users.js index 7820710d..d9a11441 100644 --- a/lib/units/api/controllers/users.js +++ b/lib/units/api/controllers/users.js @@ -1,5 +1,5 @@ /** -* Copyright © 2019 code initially contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0 +* Copyright © 2019-2024 code initially contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0 **/ const dbapi = require('../../../db/api') @@ -11,6 +11,7 @@ const wire = require('../../../wire') const wireutil = require('../../../wire/util') const userapi = require('./user') + /* --------------------------------- PRIVATE FUNCTIONS --------------------------------------- */ function userApiWrapper(fn, req, res) { @@ -142,6 +143,92 @@ function getUserInfo(req, email) { }) } +function updateUsersAlertMessage(req, res) { + const lock = {} + + return dbapi.lockUser(req.user.email).then(function(stats) { + if (!stats.replaced) { + return apiutil.lightComputeStats(res, stats) + } + lock.user = stats.changes[0].new_val + + return dbapi.updateUsersAlertMessage(req.body).then(function(stats) { + if (stats.unchanged) { + dbapi.loadUser(req.user.email).then(function(user) { + apiutil.respond(res, 200, 'Unchanged (users alert message)', + {alertMessage: user.settings.alertMessage}) + }) + } + else { + apiutil.respond(res, 200, 'Updated (users alert message)', + {alertMessage: stats.changes[0].new_val.settings.alertMessage}) + } + }) + .catch(function(err) { + apiutil.internalError(res, 'Failed to update users alert message: ', err.stack) + }) + }) + .catch(function(err) { + if (err !== 'busy') { + throw err + } + }) + .finally(function() { + lockutil.unlockUser(lock) + }) +} + +function getUsersAlertMessage(req, res) { + const fields = req.swagger.params.fields.value + + dbapi.getRootGroup().then(function(group) { + return dbapi.loadUser(group.owner.email).then(function(user) { + if (typeof user.settings.alertMessage === 'undefined') { + const lock = {} + + return dbapi.lockUser(req.user.email).then(function(stats) { + if (!stats.replaced) { + return apiutil.lightComputeStats(res, stats) + } + lock.user = stats.changes[0].new_val + const alertMessage = { + activation: 'False' + , data: '*** this site is currently under maintenance, please wait ***' + , level: 'Critical' + } + + return dbapi.updateUsersAlertMessage(alertMessage).then(function(stats) { + if (!stats.errors) { + return stats.changes[0].new_val.settings.alertMessage + } + throw new Error('Failed to initialize users alert message') + }) + }) + .finally(function() { + lockutil.unlockUser(lock) + }) + } + return user.settings.alertMessage + }) + .then(function(alertMessage) { + if (fields) { + return _.pick(alertMessage, fields.split(',')) + } + else { + return alertMessage + } + }) + .then(function(alertMessage) { + apiutil.respond(res, 200, 'Users Alert Message', {alertMessage: alertMessage}) + }) + }) + .catch(function(err) { + if (err !== 'busy') { + apiutil.internalError(res, 'Failed to get users alert message: ', err.stack) + } + }) +} + function updateUserGroupsQuotas(req, res) { const email = req.swagger.params.email.value const duration = @@ -379,6 +466,8 @@ module.exports = { updateUserGroupsQuotas: updateUserGroupsQuotas , updateDefaultUserGroupsQuotas: updateDefaultUserGroupsQuotas , getUsers: getUsers + , getUsersAlertMessage: getUsersAlertMessage + , updateUsersAlertMessage: updateUsersAlertMessage , getUserByEmail: getUserByEmail , getUserInfo: getUserInfo , createUser: createUser diff --git a/lib/units/api/swagger/api_v1.yaml b/lib/units/api/swagger/api_v1.yaml index 4cc50f9c..7dc2b8b8 100644 --- a/lib/units/api/swagger/api_v1.yaml +++ b/lib/units/api/swagger/api_v1.yaml @@ -1,10 +1,10 @@ ## -# Copyright © 2019 contains code contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0 +# Copyright © 2019-2024 contains code contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0 ## swagger: "2.0" info: - version: "2.4.0" + version: "2.4.1" title: Smartphone Test Farm description: Control and manages real Smartphone devices from browser and restful apis license: @@ -728,6 +728,62 @@ paths: $ref: "#/definitions/UnexpectedErrorResponse" security: - accessTokenAuth: [] + /users/alertMessage: + x-swagger-router-controller: users + get: + summary: Gets the users alert message + description: The Users Alert Message endpoint returns the current alert message launched by the administrator user + operationId: getUsersAlertMessage + tags: + - users + parameters: + - name: fields + in: query + description: Fields query parameter takes a comma seperated list of fields. Only listed field will be return in response + required: false + type: string + responses: + "200": + description: Current Users Alert Message + schema: + $ref: "#/definitions/AlertMessageResponse" + default: + description: > + Unexpected Error: + * 401: Unauthorized => bad credentials + * 500: Internal Server Error + schema: + $ref: "#/definitions/UnexpectedErrorResponse" + security: + - accessTokenAuth: [] + put: + summary: Updates the users alert message + description: Updates the users alert message + operationId: updateUsersAlertMessage + tags: + - admin + parameters: + - name: alertMessage + in: body + description: Alert message properties + required: true + schema: + $ref: "#/definitions/AlertMessagePayload" + responses: + "200": + description: Updated Users Alert Message + schema: + $ref: "#/definitions/AlertMessageResponse" + default: + description: > + Unexpected Error: + * 400: Bad Request + * 401: Unauthorized => bad credentials + * 500: Internal Server Error + schema: + $ref: "#/definitions/UnexpectedErrorResponse" + security: + - accessTokenAuth: [] /users/groupsQuotas: x-swagger-router-controller: users put: @@ -2270,6 +2326,49 @@ definitions: type: string user: type: object + AlertMessage: + type: object + properties: + activation: + description: Enable or disablee the alert message + type: string + data: + description: Alert message text to display + type: string + level: + description: Alert message level + type: string + AlertMessageResponse: + required: + - success + - description + - alertMessage + properties: + success: + type: boolean + description: + type: string + alertMessage: + $ref: '#/definitions/AlertMessage' + AlertMessagePayload: + description: Payload object for updating the alert message + properties: + activation: + description: Enable or disablee the alert message + type: string + enum: + - 'True' + - 'False' + data: + description: Alert message text to display + type: string + level: + description: Alert message level + type: string + enum: + - Information + - Warning + - Critical Token: type: object properties: diff --git a/lib/units/groups-engine/watchers/users.js b/lib/units/groups-engine/watchers/users.js index ce23553c..0bc48825 100644 --- a/lib/units/groups-engine/watchers/users.js +++ b/lib/units/groups-engine/watchers/users.js @@ -1,5 +1,5 @@ /** -* Copyright © 2019 code initially contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0 +* Copyright © 2019-2024 code initially contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0 **/ const timeutil = require('../../../util/timeutil') @@ -33,8 +33,9 @@ module.exports = function(pushdev) { 'email' , 'name' , 'privilege' - , {groups: ['quotas', 'subscribed'] - }) + , {groups: ['quotas', 'subscribed']} + , {settings: ['alertMessage']} + ) .changes(), function(err, cursor) { if (err) { throw err @@ -77,6 +78,11 @@ module.exports = function(pushdev) { !_.isEqual(data.new_val.groups.subscribed, data.old_val.groups.subscribed)) { targets.push('settings') } + else if (!_.isEqual( + data.new_val.settings.alertMessage + , data.old_val.settings.alertMessage)) { + targets.push('menu') + } if (targets.length) { sendUserChange( data.new_val diff --git a/lib/wire/wire.proto b/lib/wire/wire.proto index 1e45526a..c7b8d984 100644 --- a/lib/wire/wire.proto +++ b/lib/wire/wire.proto @@ -125,11 +125,21 @@ message UserGroupsField { repeated string subscribed = 2; } +message AlertMessageField { + required string activation = 1; + required string data = 2; + required string level = 3; +} + +message UserSettingsField { + optional AlertMessageField alertMessage = 1; +} message UserField { required string email = 1; required string name = 2; required string privilege = 3; required UserGroupsField groups = 4; + required UserSettingsField settings = 5; } message UserChangeMessage { diff --git a/res/app/components/stf/users/users-service.js b/res/app/components/stf/users/users-service.js index ef6df566..93117248 100644 --- a/res/app/components/stf/users/users-service.js +++ b/res/app/components/stf/users/users-service.js @@ -1,5 +1,5 @@ /** -* Copyright © 2019 code initially contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0 +* Copyright © 2019-2024 code initially contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0 **/ const oboe = require('oboe') @@ -28,6 +28,10 @@ module.exports = function UsersServiceFactory( }) } + UsersService.getUsersAlertMessage = function() { + return $http.get('/api/v1/users/alertMessage') + } + UsersService.getUsers = function(fields) { return $http.get('/api/v1/users?fields=' + fields) } @@ -92,5 +96,10 @@ module.exports = function UsersServiceFactory( $rootScope.$apply() }) + socket.on('user.menu.users.updated', function(user) { + $rootScope.$broadcast('user.menu.users.updated', user) + $rootScope.$apply() + }) + return UsersService } diff --git a/res/app/menu/index.js b/res/app/menu/index.js index 88ab1128..257e404d 100644 --- a/res/app/menu/index.js +++ b/res/app/menu/index.js @@ -1,5 +1,5 @@ /** -* Copyright © 2019 contains code contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0 +* Copyright © 2019-2024 contains code contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0 **/ require('./menu.css') @@ -7,6 +7,8 @@ require('angular-cookies') module.exports = angular.module('stf.menu', [ 'ngCookies', + require('stf/users').name, + require('stf/app-state').name, require('stf/socket').name, require('stf/util/common').name, require('stf/nav-menu').name, diff --git a/res/app/menu/menu-controller.js b/res/app/menu/menu-controller.js index 62ccd7e0..3e0a191b 100644 --- a/res/app/menu/menu-controller.js +++ b/res/app/menu/menu-controller.js @@ -1,10 +1,12 @@ /** -* Copyright © 2019-2023 contains code contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0 +* Copyright © 2019-2024 contains code contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0 **/ module.exports = function MenuCtrl( $scope , $rootScope +, UsersService +, AppState , SettingsService , $location , $http @@ -48,4 +50,41 @@ module.exports = function MenuCtrl( socket.disconnect() }, 100) } + + $scope.alertMessage = { + activation: 'False' + , data: '' + , level: '' + } + + if (AppState.user.privilege === 'admin') { + $scope.alertMessage = SettingsService.get('alertMessage') + } + else { + UsersService.getUsersAlertMessage().then(function(response) { + $scope.alertMessage = response.data.alertMessage + }) + } + + $scope.isAlertMessageActive = function() { + return $scope.alertMessage.activation === 'True' + } + + $scope.isInformationAlert = function() { + return $scope.alertMessage.level === 'Information' + } + + $scope.isWarningAlert = function() { + return $scope.alertMessage.level === 'Warning' + } + + $scope.isCriticalAlert = function() { + return $scope.alertMessage.level === 'Critical' + } + + $scope.$on('user.menu.users.updated', function(event, message) { + if (message.user.privilege === 'admin') { + $scope.alertMessage = message.user.settings.alertMessage + } + }) } diff --git a/res/app/menu/menu.css b/res/app/menu/menu.css index 539700a0..694ce00a 100644 --- a/res/app/menu/menu.css +++ b/res/app/menu/menu.css @@ -1,3 +1,7 @@ +/* + Copyright © 2019-2024 contains code contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0 +*/ + .stf-menu .stf-logo { background: url(../../common/logo/exports/STF-512.png) no-repeat 0 0; width: 32px; @@ -65,3 +69,44 @@ height: 44px !important; min-height: 44px !important; } + +.stf-menu .information-level { + background-color: #5bc0df; +} + +.stf-menu .warning-level { + background-color: #f0ad4f; +} + +.stf-menu .critical-level { + background-color: #ff4b2b; +} + +.stf-menu .maintenance-banner { + color: #ffffff; + padding: 10px; + position: relative; + text-align: center; + overflow: hidden; + height: 44px; +} + +.stf-menu .maintenance-banner p { + font-size: 18px; + animation: slideBanner 15s linear infinite; + white-space: nowrap; + position: relative; + top: 50%; + transform: translate(0%,-50%); + line-height: 44px; +} + +@keyframes slideBanner { + 0% { + left: 100%; + } + 100% { + left: -100%; + } +} + diff --git a/res/app/menu/menu.pug b/res/app/menu/menu.pug index 49a241a0..0de1fbe1 100644 --- a/res/app/menu/menu.pug +++ b/res/app/menu/menu.pug @@ -1,5 +1,5 @@ // - Copyright © 2019 contains code contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0 + Copyright © 2019-2024 contains code contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0 // .navbar.stf-menu(ng-controller='MenuCtrl') @@ -19,6 +19,10 @@ a(ng-href='/#!/settings') span.fa.fa-gears span(ng-if='!$root.basicMode', translate) Settings + ul.nav.stf-nav(ng-if='isAlertMessageActive()', nav-menu='current').unselectable.col-xs-5 + li.col-xs-12 + .maintenance-banner(ng-class="{'information-level': isInformationAlert(), 'warning-level': isWarningAlert(), 'critical-level': isCriticalAlert()}") + p(translate) {{ alertMessage.data }} ul.nav.stf-nav.stf-feedback.pull-right(ng-cloak, nav-menu='current').unselectable li.stf-nav-web-native-button(ng-if='!$root.basicMode && isControlRoute') .btn-group diff --git a/res/app/settings/general/alert-message/alert-message-controller.js b/res/app/settings/general/alert-message/alert-message-controller.js new file mode 100644 index 00000000..c8cf7429 --- /dev/null +++ b/res/app/settings/general/alert-message/alert-message-controller.js @@ -0,0 +1,41 @@ +/** +* Copyright © 2024 code initially contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0 +**/ + +module.exports = function AlertMessageCtrl( + $scope +, SettingsService +) { + + $scope.defaultAlertMessage = { + data: '*** This site is currently under maintenance, please wait ***' + , activation: 'False' + , level: 'Critical' + } + + SettingsService.bind($scope, { + target: 'alertMessage' + , source: 'alertMessage' + , defaultValue: $scope.defaultAlertMessage + }) + + $scope.alertMessageActivationOptions = ['True', 'False'] + $scope.alertMessageLevelOptions = ['Information', 'Warning', 'Critical'] + + $scope.$watch( + function() { + return SettingsService.get('alertMessage') + } + , function(newvalue) { + if (typeof newvalue === 'undefined') { + SettingsService.set('alertMessage', $scope.defaultAlertMessage) + } + } + ) + + $scope.$on('user.menu.users.updated', function(event, message) { + if (message.user.privilege === 'admin') { + $scope.alertMessage = message.user.settings.alertMessage + } + }) +} diff --git a/res/app/settings/general/alert-message/alert-message.pug b/res/app/settings/general/alert-message/alert-message.pug new file mode 100644 index 00000000..d4e01f37 --- /dev/null +++ b/res/app/settings/general/alert-message/alert-message.pug @@ -0,0 +1,34 @@ +// + Copyright © 2024 code initially contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0 +// + +.widget-container.fluid-height(ng-controller='AlertMessageCtrl') + .heading + i.fa.fa-exclamation-triangle + span(translate) Alert Message + .widget-content.padded + .form-horizontal + .form-group.general-item + .input-group + .input-group-addon.input-sm + i.fa.fa-exclamation-triangle( + uib-tooltip="{{'Define your own alert message' | translate}}" tooltip-placement='auto top-right' tooltip-popup-delay='500') + input.form-control.input-sm(size='20' type='text' placeholder='' ng-model='alertMessage.data') + .form-group.general-item + label.general-label( + translate + uib-tooltip="{{'Alert message activation' | translate}}" + tooltip-placement='auto top-right' + tooltip-popup-delay='500') Activation + select( + ng-model='alertMessage.activation' + ng-options='option for option in alertMessageActivationOptions') + .form-group.general-item + label.general-label( + translate + uib-tooltip="{{'Alert message level' | translate}}" + tooltip-placement='auto top-right' + tooltip-popup-delay='500') Level + select( + ng-model='alertMessage.level' + ng-options='option for option in alertMessageLevelOptions') diff --git a/res/app/settings/general/alert-message/index.js b/res/app/settings/general/alert-message/index.js new file mode 100644 index 00000000..902f82b7 --- /dev/null +++ b/res/app/settings/general/alert-message/index.js @@ -0,0 +1,14 @@ +/** +* Copyright © 2024 code initially contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0 +**/ + +module.exports = angular.module('stf.settings.general.alert-message', [ + require('stf/settings').name +]) + .run(['$templateCache', function($templateCache) { + $templateCache.put( + 'settings/general/alert-message/alert-message.pug' + , require('./alert-message.pug') + ) + }]) + .controller('AlertMessageCtrl', require('./alert-message-controller')) diff --git a/res/app/settings/general/general-controller.js b/res/app/settings/general/general-controller.js index aae72a36..0e1fcb0d 100644 --- a/res/app/settings/general/general-controller.js +++ b/res/app/settings/general/general-controller.js @@ -1,3 +1,9 @@ -module.exports = function GeneralCtrl() { +/** +* Copyright © 2024 contains code contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0 +**/ +module.exports = function GeneralCtrl($scope, AppState) { + $scope.isAdmin = function() { + return AppState.user.privilege === 'admin' + } } diff --git a/res/app/settings/general/general.css b/res/app/settings/general/general.css index 59dd8ad4..1cf54577 100644 --- a/res/app/settings/general/general.css +++ b/res/app/settings/general/general.css @@ -1,3 +1,16 @@ +/* + Copyright © 2024 contains code contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0 +*/ + .stf-general { -} \ No newline at end of file +} + +.stf-general .general-item { + margin: 0px 10px 15px 0px; +} + +.stf-general .general-label { + font-weight: bold; + margin-right: 10px; +} diff --git a/res/app/settings/general/general.pug b/res/app/settings/general/general.pug index 0fa05bdb..847b6d05 100644 --- a/res/app/settings/general/general.pug +++ b/res/app/settings/general/general.pug @@ -1,8 +1,9 @@ // - Copyright © 2019 contains code contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0 + Copyright © 2019-2024 contains code contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0 // -.row +.div(ng-controller='GeneralCtrl') + .row.stf-general .col-md-3 div(ng-include='"settings/general/local/local-settings.pug"') .col-md-3 @@ -11,3 +12,6 @@ div(ng-include='"settings/general/date-format/date-format.pug"') .col-md-3 div(ng-include='"settings/general/email-address-separator/email-address-separator.pug"') + .row.stf-general(ng-if='isAdmin()') + .col-md-12 + div(ng-include='"settings/general/alert-message/alert-message.pug"') diff --git a/res/app/settings/general/index.js b/res/app/settings/general/index.js index 5033197a..768c5fd2 100644 --- a/res/app/settings/general/index.js +++ b/res/app/settings/general/index.js @@ -1,5 +1,5 @@ /** -* Copyright © 2019 contains code contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0 +* Copyright © 2019-2024 contains code contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0 **/ require('./general.css') @@ -8,7 +8,9 @@ module.exports = angular.module('stf.settings.general', [ require('./language').name, require('./local').name, require('./email-address-separator').name, - require('./date-format').name + require('./date-format').name, + require('stf/app-state').name, + require('./alert-message').name ]) .run(['$templateCache', function($templateCache) { $templateCache.put(