diff --git a/LICENSE b/LICENSE index ff328e27..84a05165 100644 --- a/LICENSE +++ b/LICENSE @@ -1,5 +1,6 @@ Copyright © 2013 CyberAgent, Inc. Copyright © 2016 The OpenSTF Project +Copyright © 2019 Orange SA Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/TESTING.md b/TESTING.md index 3b0fcac5..acb57e05 100644 --- a/TESTING.md +++ b/TESTING.md @@ -5,20 +5,48 @@ ## E2E Frontend -### On first run +## On first run - `gulp webdriver-update` -### Chrome Local STF -- Connect a device -- Run stf -- `gulp protractor` -### Multiple Browsers Local STF with a specific suite + +## Protractor&Jasmine - Local STF tests + + +--- +#### Preconditions +Test configuration point to Google Chrome browser. Test works on Google Chrome v.77.0.3865.75 together with chromedriver with ver. 77.0.3865.40. + +--- + +- Connect a device or start android emulator +- Run RethinkDb + ``` + rethinkdb + ``` +- Run stf + ``` + ./bin/stf local + ``` + Wait till STF will be fully functional and devices will be discovered +- Run tests + ``` + gulp protractor + ``` + +--- +#### Info +Test results can be found in: + test-results/reports-protractor/dashboardReport-protractor/index.html + +--- + +## Multiple Browsers Local STF with a specific suite - Connect a device - Run stf - `gulp protractor --multi --suite devices` -### Chrome Remote STF +## Chrome Remote STF - `export STF_URL='http://stf-url/#!/'` - `export STF_USERNAME='user'` - `export STF_PASSWORD='pass'` diff --git a/lib/cli/api/index.js b/lib/cli/api/index.js index 6654f953..78ef9468 100644 --- a/lib/cli/api/index.js +++ b/lib/cli/api/index.js @@ -1,3 +1,7 @@ +/** +* Copyright © 2019 contains code contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0 +**/ + module.exports.command = 'api' module.exports.describe = 'Start an API unit.' @@ -18,6 +22,18 @@ module.exports.builder = function(yargs) { , array: true , demand: true }) + .option('connect-push-dev', { + alias: 'pd' + , describe: 'Device-side ZeroMQ PULL endpoint to connect to.' + , array: true + , demand: true + }) + .option('connect-sub-dev', { + alias: 'sd' + , describe: 'Device-side ZeroMQ PUB endpoint to connect to.' + , array: true + , demand: true + }) .option('port', { alias: 'p' , describe: 'The port to bind to.' @@ -53,6 +69,8 @@ module.exports.handler = function(argv) { , endpoints: { push: argv.connectPush , sub: argv.connectSub + , pushdev: argv.connectPushDev + , subdev: argv.connectSubDev } }) } diff --git a/lib/cli/doctor/index.js b/lib/cli/doctor/index.js index 249e3da7..7e8540a7 100644 --- a/lib/cli/doctor/index.js +++ b/lib/cli/doctor/index.js @@ -30,16 +30,13 @@ module.exports.handler = function() { var proc = cp.spawn(command, args, options) var stdout = [] - proc.stdout.on('readable', function() { - var chunk - while ((chunk = proc.stdout.read())) { - stdout.push(chunk) - } + proc.stdout.on('data', function(data) { + stdout.push(data) }) proc.on('error', reject) - proc.on('exit', function(code, signal) { + proc.on('close', function(code, signal) { if (signal) { reject(new CheckError('Exited with signal %s', signal)) } diff --git a/lib/cli/generate-fake-group/index.js b/lib/cli/generate-fake-group/index.js new file mode 100644 index 00000000..aaecfbf3 --- /dev/null +++ b/lib/cli/generate-fake-group/index.js @@ -0,0 +1,39 @@ +/** +* Copyright © 2019 code initially contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0 +**/ + +module.exports.command = 'generate-fake-group' + +module.exports.builder = function(yargs) { + return yargs + .strict() + .option('n', { + alias: 'number' + , describe: 'How many groups to create.' + , type: 'number' + , default: 1 + }) +} + +module.exports.handler = function(argv) { + var logger = require('../../util/logger') + var log = logger.createLogger('cli:generate-fake-group') + var fake = require('../../util/fakegroup') + var n = argv.number + + function next() { + return fake.generate().then(function(email) { + log.info('Created fake group "%s"', email) + return --n ? next() : null + }) + } + + return next() + .then(function() { + process.exit(0) + }) + .catch(function(err) { + log.fatal('Fake group creation had an error:', err.stack) + process.exit(1) + }) +} diff --git a/lib/cli/generate-fake-user/index.js b/lib/cli/generate-fake-user/index.js new file mode 100644 index 00000000..581c31d1 --- /dev/null +++ b/lib/cli/generate-fake-user/index.js @@ -0,0 +1,39 @@ +/** +* Copyright © 2019 code initially contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0 +**/ + +module.exports.command = 'generate-fake-user' + +module.exports.builder = function(yargs) { + return yargs + .strict() + .option('n', { + alias: 'number' + , describe: 'How many users to create.' + , type: 'number' + , default: 1 + }) +} + +module.exports.handler = function(argv) { + var logger = require('../../util/logger') + var log = logger.createLogger('cli:generate-fake-user') + var fake = require('../../util/fakeuser') + var n = argv.number + + function next() { + return fake.generate().then(function(email) { + log.info('Created fake user "%s"', email) + return --n ? next() : null + }) + } + + return next() + .then(function() { + process.exit(0) + }) + .catch(function(err) { + log.fatal('Fake user creation had an error:', err.stack) + process.exit(1) + }) +} diff --git a/lib/cli/groups-engine/index.js b/lib/cli/groups-engine/index.js new file mode 100644 index 00000000..83841218 --- /dev/null +++ b/lib/cli/groups-engine/index.js @@ -0,0 +1,51 @@ +/** +* Copyright © 2019 code initially contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0 +**/ + +module.exports.command = 'groups-engine' + +module.exports.describe = 'Start the groups engine unit.' + +module.exports.builder = function(yargs) { + return yargs + .env('STF_GROUPS_ENGINE') + .strict() + .option('connect-push', { + alias: 'c' + , describe: 'App-side ZeroMQ PULL endpoint to connect to.' + , array: true + , demand: true + }) + .option('connect-sub', { + alias: 'u' + , describe: 'App-side ZeroMQ PUB endpoint to connect to.' + , array: true + , demand: true + }) + .option('connect-push-dev', { + alias: 'pd' + , describe: 'Device-side ZeroMQ PULL endpoint to connect to.' + , array: true + , demand: true + }) + .option('connect-sub-dev', { + alias: 'sd' + , describe: 'Device-side ZeroMQ PUB endpoint to connect to.' + , array: true + , demand: true + }) + .epilog('Each option can be be overwritten with an environment variable ' + + 'by converting the option to uppercase, replacing dashes with ' + + 'underscores and prefixing it with `STF_GROUPS_ENGINE_` .)') +} + +module.exports.handler = function(argv) { + return require('../../units/groups-engine')({ + endpoints: { + push: argv.connectPush + , sub: argv.connectSub + , pushdev: argv.connectPushDev + , subdev: argv.connectSubDev + } + }) +} diff --git a/lib/cli/index.js b/lib/cli/index.js index 48b57e03..3f7c9ee0 100644 --- a/lib/cli/index.js +++ b/lib/cli/index.js @@ -1,3 +1,7 @@ +/** +* Copyright © 2019 contains code contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0 +**/ + var yargs = require('yargs') var Promise = require('bluebird') @@ -12,9 +16,12 @@ var _argv = yargs.usage('Usage: $0 [options]') .command(require('./auth-oauth2')) .command(require('./auth-openid')) .command(require('./auth-saml2')) + .command(require('./groups-engine')) .command(require('./device')) .command(require('./doctor')) .command(require('./generate-fake-device')) + .command(require('./generate-fake-user')) + .command(require('./generate-fake-group')) .command(require('./local')) .command(require('./log-rethinkdb')) .command(require('./migrate')) diff --git a/lib/cli/local/index.js b/lib/cli/local/index.js index 11fcfb27..4e1b0b97 100644 --- a/lib/cli/local/index.js +++ b/lib/cli/local/index.js @@ -1,3 +1,7 @@ +/** +* Copyright © 2019 contains code contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0 +**/ + module.exports.command = 'local [serial..]' module.exports.describe = 'Start a complete local development environment.' @@ -337,6 +341,17 @@ module.exports.handler = function(argv) { , '--secret', argv.authSecret , '--connect-push', argv.bindAppPull , '--connect-sub', argv.bindAppPub + , '--connect-push-dev', argv.bindDevPull + , '--connect-sub-dev', argv.bindDevPub + ]) + + // groups engine + , procutil.fork(path.resolve(__dirname, '..'), [ + 'groups-engine' + , '--connect-push', argv.bindAppPull + , '--connect-sub', argv.bindAppPub + , '--connect-push-dev', argv.bindDevPull + , '--connect-sub-dev', argv.bindDevPub ]) // websocket diff --git a/lib/cli/migrate/index.js b/lib/cli/migrate/index.js index f5954c0d..ce507289 100644 --- a/lib/cli/migrate/index.js +++ b/lib/cli/migrate/index.js @@ -1,3 +1,7 @@ +/** +* Copyright © 2019 contains code contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0 +**/ + module.exports.command = 'migrate' module.exports.describe = 'Migrates the database to the latest version.' @@ -10,13 +14,44 @@ module.exports.handler = function() { var logger = require('../../util/logger') var log = logger.createLogger('cli:migrate') var db = require('../../db') + var dbapi = require('../../db/api') + const apiutil = require('../../util/apiutil') + const Promise = require('bluebird') return db.setup() .then(function() { - process.exit(0) + return new Promise(function(resolve, reject) { + setTimeout(function() { + return dbapi.getGroupByIndex(apiutil.ROOT, 'privilege').then(function(group) { + if (!group) { + const env = { + STF_ROOT_GROUP_NAME: 'Common' + , STF_ADMIN_NAME: 'administrator' + , STF_ADMIN_EMAIL: 'administrator@fakedomain.com' + } + for (const i in env) { + if (process.env[i]) { + env[i] = process.env[i] + } + } + return dbapi.createBootStrap(env) + } + return group + }) + .then(function() { + resolve(true) + }) + .catch(function(err) { + reject(err) + }) + }, 1000) + }) }) .catch(function(err) { log.fatal('Migration had an error:', err.stack) process.exit(1) }) + .finally(function() { + process.exit(0) + }) } diff --git a/lib/db/api.js b/lib/db/api.js index 5d4a0618..c1b07beb 100644 --- a/lib/db/api.js +++ b/lib/db/api.js @@ -1,3 +1,7 @@ +/** +* Copyright © 2019 contains code contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0 +**/ + var r = require('rethinkdb') var util = require('util') @@ -6,6 +10,11 @@ var wireutil = require('../wire/util') var dbapi = Object.create(null) +const uuid = require('uuid') +const apiutil = require('../util/apiutil') +const Promise = require('bluebird') +const _ = require('lodash') + dbapi.DuplicateSecondaryIndexError = function DuplicateSecondaryIndexError() { Error.call(this) this.name = 'DuplicateSecondaryIndexError' @@ -18,6 +27,869 @@ dbapi.close = function(options) { return db.close(options) } +dbapi.unlockBookingObjects = function() { + return Promise.all([ + db.run(r.table('users').update({groups: {lock: false}})) + , db.run(r.table('devices').update({group: {lock: false}})) + , db.run(r.table('groups').update({lock: {admin: false, user: false}})) + ]) +} + +dbapi.createBootStrap = function(env) { + const now = Date.now() + + return dbapi.createGroup({ + name: env.STF_ROOT_GROUP_NAME + , owner: { + email: env.STF_ADMIN_EMAIL + , name: env.STF_ADMIN_NAME + } + , users: [env.STF_ADMIN_EMAIL] + , privilege: apiutil.ROOT + , class: apiutil.STANDARD + , repetitions: 0 + , duration: 0 + , isActive: true + , state: apiutil.READY + , dates: [{ + start: new Date(now) + , stop: new Date(now + apiutil.ONE_YEAR) + }] + , envUserGroupsNumber: apiutil.MAX_USER_GROUPS_NUMBER + , envUserGroupsDuration: apiutil.MAX_USER_GROUPS_DURATION + , envUserGroupsRepetitions: apiutil.MAX_USER_GROUPS_REPETITIONS + }) + .then(function(group) { + return dbapi.saveUserAfterLogin({ + name: group.owner.name + , email: group.owner.email + , ip: '127.0.0.1' + }) + .then(function() { + return dbapi.reserveUserGroupInstance(group.owner.email) + }) + }) +} + +dbapi.deleteDevice = function(serial) { + return db.run(r.table('devices').get(serial).delete()) +} + +dbapi.deleteUser = function(email) { + return db.run(r.table('users').get(email).delete()) +} + +dbapi.getReadyGroupsOrderByIndex = function(index) { + return db + .run(r.table('groups') + .orderBy({index: index}) + .filter(function(group) { + return group('state').ne(apiutil.PENDING) + })) + .then(function(cursor) { + return cursor.toArray() + }) +} + +dbapi.getGroupsByIndex = function(value, index) { + return db.run(r.table('groups').getAll(value, {index: index})) + .then(function(cursor) { + return cursor.toArray() + }) +} + + +dbapi.getGroupByIndex = function(value, index) { + return dbapi.getGroupsByIndex(value, index) + .then(function(array) { + return array[0] + }) +} + +dbapi.getGroupsByUser = function(email) { + return db + .run(r.table('groups') + .filter(function(group) { + return group('users').contains(email) + })) + .then(function(cursor) { + return cursor.toArray() + }) +} + +dbapi.getGroup = function(id) { + return db.run(r.table('groups').get(id)) +} + +dbapi.getGroups = function() { + return db.run(r.table('groups')) + .then(function(cursor) { + return cursor.toArray() + }) +} + +dbapi.getUsers = function() { + return db.run(r.table('users')) + .then(function(cursor) { + return cursor.toArray() + }) +} + +dbapi.getEmails = function() { + return db.run(r.table('users').filter(function(user) { + return user('privilege').ne(apiutil.ADMIN) + }) + .getField('email')) + .then(function(cursor) { + return cursor.toArray() + }) +} + +dbapi.addGroupUser = function(id, email) { + return Promise.all([ + db.run(r.table('groups') + .get(id) + .update({users: r.row('users').setInsert(email)})) + , db.run(r.table('users') + .get(email) + .update({groups: {subscribed: r.row('groups')('subscribed').setInsert(id)}})) + ]) + .then(function(statss) { + return statss[0].unchanged ? 'unchanged' : 'added' + }) +} + +dbapi.removeGroupUser = function(id, email) { + return Promise.all([ + db.run(r.table('groups') + .get(id) + .update({users: r.row('users').setDifference([email])})) + , db.run(r.table('users') + .get(email) + .update({groups: {subscribed: r.row('groups')('subscribed').setDifference([id])}})) + ]) + .then(function() { + return 'deleted' + }) +} + +dbapi.lockBookableDevice = function(groups, serial) { + function wrappedlockBookableDevice() { + return db.run(r.table('devices').get(serial).update({group: {lock: + r.branch( + r.row('group')('lock') + .eq(false) + .and(r.row('group')('class') + .ne(apiutil.STANDARD)) + .and(r.expr(groups) + .setIntersection([r.row('group')('origin')]) + .isEmpty() + .not()) + , true + , r.row('group')('lock')) + }}, {returnChanges: true})) + .then(function(stats) { + return apiutil.lockDeviceResult(stats, dbapi.loadBookableDevice, groups, serial) + }) + } + + return apiutil.setIntervalWrapper( + wrappedlockBookableDevice + , 10 + , Math.random() * 500 + 50) +} + +dbapi.lockDeviceByOrigin = function(groups, serial) { + function wrappedlockDeviceByOrigin() { + return db.run(r.table('devices').get(serial).update({group: {lock: + r.branch( + r.row('group')('lock') + .eq(false) + .and(r.expr(groups) + .setIntersection([r.row('group')('origin')]) + .isEmpty() + .not()) + , true + , r.row('group')('lock')) + }}, {returnChanges: true})) + .then(function(stats) { + return apiutil.lockDeviceResult(stats, dbapi.loadDeviceByOrigin, groups, serial) + }) + } + + return apiutil.setIntervalWrapper( + wrappedlockDeviceByOrigin + , 10 + , Math.random() * 500 + 50) +} + +dbapi.addOriginGroupDevice = function(group, serial) { + return db + .run(r.table('groups') + .get(group.id) + .update({devices: r.row('devices').setInsert(serial)})) + .then(function() { + return dbapi.getGroup(group.id) + }) +} + +dbapi.removeOriginGroupDevice = function(group, serial) { + return db + .run(r.table('groups') + .get(group.id) + .update({devices: r.row('devices').setDifference([serial])})) + .then(function() { + return dbapi.getGroup(group.id) + }) +} + +dbapi.addGroupDevices = function(group, serials) { + const duration = apiutil.computeDuration(group, serials.length) + + return dbapi.updateUserGroupDuration(group.owner.email, group.duration, duration) + .then(function(stats) { + if (stats.replaced) { + return dbapi.updateGroup( + group.id + , { + duration: duration + , devices: _.union(group.devices, serials) + }) + } + return Promise.reject('quota is reached') + }) +} + +dbapi.removeGroupDevices = function(group, serials) { + const duration = apiutil.computeDuration(group, -serials.length) + + return dbapi.updateUserGroupDuration(group.owner.email, group.duration, duration) + .then(function() { + return dbapi.updateGroup( + group.id + , { + duration: duration + , devices: _.difference(group.devices, serials) + }) + }) +} + +function setLockOnDevice(serial, state) { + return db.run(r.table('devices').get(serial).update({group: {lock: + r.branch( + r.row('group')('lock').eq(!state) + , state + , r.row('group')('lock')) + }})) +} + +dbapi.lockDevice = function(serial) { + return setLockOnDevice(serial, true) +} + +dbapi.unlockDevice = function(serial) { + return setLockOnDevice(serial, false) +} + +function setLockOnUser(email, state) { + return db.run(r.table('users').get(email).update({groups: {lock: + r.branch( + r.row('groups')('lock').eq(!state) + , state + , r.row('groups')('lock')) + }}, {returnChanges: true})) +} + +dbapi.lockUser = function(email) { + function wrappedlockUser() { + return setLockOnUser(email, true) + .then(function(stats) { + return apiutil.lockResult(stats) + }) + } + + return apiutil.setIntervalWrapper( + wrappedlockUser + , 10 + , Math.random() * 500 + 50) +} + +dbapi.unlockUser = function(email) { + return setLockOnUser(email, false) +} + +dbapi.lockGroupByOwner = function(email, id) { + function wrappedlockGroupByOwner() { + return dbapi.getRootGroup().then(function(group) { + return db.run(r.table('groups').get(id).update({lock: {user: + r.branch( + r.row('lock')('admin') + .eq(false) + .and(r.row('lock')('user').eq(false)) + .and(r.row('owner')('email') + .eq(email) + .or(r.expr(email) + .eq(group.owner.email))) + , true + , r.row('lock')('user')) + }}, {returnChanges: true})) + }) + .then(function(stats) { + const result = apiutil.lockResult(stats) + + if (!result.status) { + return dbapi.getGroupAsOwnerOrAdmin(email, id).then(function(group) { + if (!group) { + result.data.locked = false + result.status = true + } + return result + }) + } + return result + }) + } + + return apiutil.setIntervalWrapper( + wrappedlockGroupByOwner + , 10 + , Math.random() * 500 + 50) +} + +dbapi.lockGroup = function(id) { + function wrappedlockGroup() { + return db.run(r.table('groups').get(id).update({lock: {user: + r.branch( + r.row('lock')('admin') + .eq(false) + .and(r.row('lock')('user') + .eq(false)) + , true + , r.row('lock')('user')) + }})) + .then(function(stats) { + return apiutil.lockResult(stats) + }) + } + + return apiutil.setIntervalWrapper( + wrappedlockGroup + , 10 + , Math.random() * 500 + 50) +} + +dbapi.unlockGroup = function(id) { + return db.run(r.table('groups').get(id).update({lock: {user: false}})) +} + +dbapi.adminLockGroup = function(id, lock) { + function wrappedAdminLockGroup() { + return db + .run(r.table('groups') + .get(id) + .update({lock: {user: true, admin: true}}, {returnChanges: true})) + .then(function(stats) { + const result = {} + + if (stats.replaced) { + result.status = + stats.changes[0].new_val.lock.admin && !stats.changes[0].old_val.lock.user + if (result.status) { + result.data = true + lock.group = stats.changes[0].new_val + } + } + else if (stats.skipped) { + result.status = true + } + return result + }) + } + + return apiutil.setIntervalWrapper( + wrappedAdminLockGroup + , 10 + , Math.random() * 500 + 50) +} + +dbapi.adminUnlockGroup = function(lock) { + if (lock.group) { + return db + .run(r.table('groups') + .get(lock.group.id) + .update({lock: {user: false, admin: false}})) + } + return true +} + +dbapi.getRootGroup = function() { + return dbapi.getGroupByIndex(apiutil.ROOT, 'privilege').then(function(group) { + if (!group) { + throw new Error('Root group not found') + } + return group + }) +} + +dbapi.getUserGroup = function(email, id) { + return db.run(r.table('groups').getAll(id).filter(function(group) { + return group('users').contains(email) + })) + .then(function(cursor) { + return cursor.toArray() + }) + .then(function(groups) { + return groups[0] + }) +} + +dbapi.getUserGroups = function(email) { + return db + .run(r.table('groups') + .filter(function(group) { + return group('users').contains(email) + })) + .then(function(cursor) { + return cursor.toArray() + }) +} + +dbapi.getOnlyUserGroups = function(email) { + return db + .run(r.table('groups') + .filter(function(group) { + return group('owner')('email') + .ne(email) + .and(group('users').contains(email)) + })) + .then(function(cursor) { + return cursor.toArray() + }) +} + +dbapi.getTransientGroups = function() { + return db + .run(r.table('groups') + .filter(function(group) { + return group('class') + .ne(apiutil.BOOKABLE) + .and(group('class').ne(apiutil.STANDARD)) + })) + .then(function(cursor) { + return cursor.toArray() + }) +} + +dbapi.getDeviceTransientGroups = function(serial) { + return db + .run(r.table('groups') + .filter(function(group) { + return group('class') + .ne(apiutil.BOOKABLE) + .and(group('class').ne(apiutil.STANDARD)) + .and(group('devices').contains(serial)) + })) + .then(function(cursor) { + return cursor.toArray() + }) +} + +dbapi.isDeviceBooked = function(serial) { + return dbapi.getDeviceTransientGroups(serial) + .then(function(groups) { + return groups.length > 0 + }) +} + +dbapi.isRemoveGroupUserAllowed = function(email, targetGroup) { + if (targetGroup.class !== apiutil.BOOKABLE) { + return Promise.resolve(true) + } + return db.run( + r.table('groups') + .getAll(email, {index: 'owner'}) + .filter(function(group) { + return group('class') + .ne(apiutil.BOOKABLE) + .and(group('class').ne(apiutil.STANDARD)) + .and(r.expr(targetGroup.devices) + .setIntersection(group('devices')) + .isEmpty() + .not()) + })) + .then(function(cursor) { + return cursor.toArray() + }) + .then(function(groups) { + return groups.length === 0 + }) +} + +dbapi.isUpdateDeviceOriginGroupAllowed = function(serial, targetGroup) { + return dbapi.getDeviceTransientGroups(serial) + .then(function(groups) { + if (groups.length) { + if (targetGroup.class === apiutil.STANDARD) { + return false + } + for (const group of groups) { + if (targetGroup.users.indexOf(group.owner.email) < 0) { + return false + } + } + } + return true + }) +} + +dbapi.getDeviceGroups = function(serial) { + return db + .run(r.table('groups') + .filter(function(group) { + return group('devices').contains(serial) + })) + .then(function(cursor) { + return cursor.toArray() + }) +} + +dbapi.getGroupAsOwnerOrAdmin = function(email, id) { + return dbapi.getGroup(id).then(function(group) { + if (group) { + if (email === group.owner.email) { + return group + } + return dbapi.loadUser(email).then(function(user) { + if (user && user.privilege === apiutil.ADMIN) { + return group + } + return false + }) + } + return false + }) +} + +dbapi.getOwnerGroups = function(email) { + return dbapi.getRootGroup().then(function(group) { + if (email === group.owner.email) { + return dbapi.getGroups() + } + return dbapi.getGroupsByIndex(email, 'owner') + }) +} + +dbapi.createGroup = function(data) { + const id = util.format('%s', uuid.v4()).replace(/-/g, '') + + return db.run(r.table('groups').insert( + Object.assign(data, { + id: id + , users: _.union(data.users, [data.owner.email]) + , devices: [] + , createdAt: r.now() + , lock: { + user: false + , admin: false + } + , ticket: null + }))) + .then(function() { + return dbapi.getGroup(id) + }) +} + +dbapi.createUserGroup = function(data) { + return dbapi.reserveUserGroupInstance(data.owner.email).then(function(stats) { + if (stats.replaced) { + return dbapi.getRootGroup().then(function(rootGroup) { + data.users = [rootGroup.owner.email] + return dbapi.createGroup(data).then(function(group) { + return Promise.all([ + dbapi.addGroupUser(group.id, group.owner.email) + , dbapi.addGroupUser(group.id, rootGroup.owner.email) + ]) + .then(function() { + return dbapi.getGroup(group.id) + }) + }) + }) + } + return false + }) +} + +dbapi.updateGroup = function(id, data) { + return db.run(r.table('groups').get(id).update(data)) + .then(function() { + return dbapi.getGroup(id) + }) +} + +dbapi.reserveUserGroupInstance = function(email) { + return db.run(r.table('users').get(email) + .update({groups: {quotas: {consumed: {number: + r.branch( + r.row('groups')('quotas')('consumed')('number') + .add(1) + .le(r.row('groups')('quotas')('allocated')('number')) + , r.row('groups')('quotas')('consumed')('number') + .add(1) + , r.row('groups')('quotas')('consumed')('number')) + }}}}) + ) +} + +dbapi.releaseUserGroupInstance = function(email) { + return db.run(r.table('users').get(email) + .update({groups: {quotas: {consumed: {number: + r.branch( + r.row('groups')('quotas')('consumed')('number').ge(1) + , r.row('groups')('quotas')('consumed')('number').sub(1) + , r.row('groups')('quotas')('consumed')('number')) + }}}}) + ) +} + +dbapi.updateUserGroupDuration = function(email, oldDuration, newDuration) { + return db.run(r.table('users').get(email) + .update({groups: {quotas: {consumed: {duration: + r.branch( + r.row('groups')('quotas')('consumed')('duration') + .sub(oldDuration).add(newDuration) + .le(r.row('groups')('quotas')('allocated')('duration')) + , r.row('groups')('quotas')('consumed')('duration') + .sub(oldDuration).add(newDuration) + , r.row('groups')('quotas')('consumed')('duration')) + }}}}) + ) +} + +dbapi.updateUserGroupsQuotas = function(email, duration, number, repetitions) { + return db + .run(r.table('users').get(email) + .update({groups: {quotas: {allocated: { + duration: + r.branch( + r.expr(duration) + .ne(null) + .and(r.row('groups')('quotas')('consumed')('duration') + .le(duration)) + .and(r.expr(number) + .eq(null) + .or(r.row('groups')('quotas')('consumed')('number') + .le(number))) + , duration + , r.row('groups')('quotas')('allocated')('duration')) + , number: + r.branch( + r.expr(number) + .ne(null) + .and(r.row('groups')('quotas')('consumed')('number') + .le(number)) + .and(r.expr(duration) + .eq(null) + .or(r.row('groups')('quotas')('consumed')('duration') + .le(duration))) + , number + , r.row('groups')('quotas')('allocated')('number')) + } + , repetitions: + r.branch( + r.expr(repetitions).ne(null) + , repetitions + , r.row('groups')('quotas')('repetitions')) + }}}, {returnChanges: true})) +} + +dbapi.updateDefaultUserGroupsQuotas = function(email, duration, number, repetitions) { + return db.run(r.table('users').get(email) + .update({groups: {quotas: { + defaultGroupsDuration: + r.branch( + r.expr(duration).ne(null) + , duration + , r.row('groups')('quotas')('defaultGroupsDuration')) + , defaultGroupsNumber: + r.branch( + r.expr(number).ne(null) + , number + , r.row('groups')('quotas')('defaultGroupsNumber')) + , defaultGroupsRepetitions: + r.branch( + r.expr(repetitions).ne(null) + , repetitions + , r.row('groups')('quotas')('defaultGroupsRepetitions')) + }}}, {returnChanges: true})) +} + +dbapi.updateDeviceCurrentGroupFromOrigin = function(serial) { + return db.run(r.table('devices').get(serial)).then(function(device) { + return db.run(r.table('groups').get(device.group.origin)).then(function(group) { + return db.run(r.table('devices').get(serial).update({group: { + id: r.row('group')('origin') + , name: r.row('group')('originName') + , owner: group.owner + , lifeTime: group.dates[0] + , class: group.class + , repetitions: group.repetitions + }})) + }) + }) +} + +dbapi.askUpdateDeviceOriginGroup = function(serial, group, signature) { + return db.run(r.table('groups').get(group.id) + .update({ticket: { + serial: serial + , signature: signature + }}) + ) +} + +dbapi.updateDeviceOriginGroup = function(serial, group) { + return db.run(r.table('devices').get(serial) + .update({group: { + origin: group.id + , originName: group.name + , id: r.branch( + r.row('group')('id').eq(r.row('group')('origin')) + , group.id + , r.row('group')('id')) + , name: r.branch( + r.row('group')('id').eq(r.row('group')('origin')) + , group.name + , r.row('group')('name')) + , owner: r.branch( + r.row('group')('id').eq(r.row('group')('origin')) + , group.owner + , r.row('group')('owner')) + , lifeTime: r.branch( + r.row('group')('id').eq(r.row('group')('origin')) + , group.dates[0] + , r.row('group')('lifeTime')) + , class: r.branch( + r.row('group')('id').eq(r.row('group')('origin')) + , group.class + , r.row('group')('class')) + , repetitions: r.branch( + r.row('group')('id').eq(r.row('group')('origin')) + , group.repetitions + , r.row('group')('repetitions')) + }}) + ) + .then(function() { + return db.run(r.table('devices').get(serial)) + }) +} + +dbapi.updateDeviceCurrentGroup = function(serial, group) { + return db.run(r.table('devices').get(serial) + .update({group: { + id: group.id + , name: group.name + , owner: group.owner + , lifeTime: group.dates[0] + , class: group.class + , repetitions: group.repetitions + }}) + ) +} + +dbapi.updateUserGroup = function(group, data) { + return dbapi.updateUserGroupDuration(group.owner.email, group.duration, data.duration) + .then(function(stats) { + if (stats.replaced || stats.unchanged && group.duration === data.duration) { + return dbapi.updateGroup(group.id, data) + } + return false + }) +} + +dbapi.deleteGroup = function(id) { + return db.run(r.table('groups').get(id).delete()) +} + +dbapi.deleteUserGroup = function(id) { + function deleteUserGroup(group) { + return dbapi.deleteGroup(group.id) + .then(function() { + return Promise.map(group.users, function(email) { + return dbapi.removeGroupUser(group.id, email) + }) + }) + .then(function() { + return dbapi.releaseUserGroupInstance(group.owner.email) + }) + .then(function() { + return dbapi.updateUserGroupDuration(group.owner.email, group.duration, 0) + }) + .then(function() { + return 'deleted' + }) + } + + return dbapi.getGroup(id).then(function(group) { + if (group.privilege !== apiutil.ROOT) { + return deleteUserGroup(group) + } + return 'forbidden' + }) +} + +dbapi.createUser = function(email, name, ip) { + return dbapi.getRootGroup().then(function(group) { + return dbapi.loadUser(group.owner.email).then(function(adminUser) { + return db.run(r.table('users').insert({ + email: email + , name: name + , ip: ip + , group: wireutil.makePrivateChannel() + , lastLoggedInAt: r.now() + , createdAt: r.now() + , forwards: [] + , settings: {} + , privilege: adminUser ? apiutil.USER : apiutil.ADMIN + , groups: { + subscribed: [] + , lock: false + , quotas: { + allocated: { + number: adminUser ? + adminUser.groups.quotas.defaultGroupsNumber : + group.envUserGroupsNumber + , duration: adminUser ? + adminUser.groups.quotas.defaultGroupsDuration : + group.envUserGroupsDuration + } + , consumed: { + number: 0 + , duration: 0 + } + , defaultGroupsNumber: adminUser ? 0 : group.envUserGroupsNumber + , defaultGroupsDuration: adminUser ? 0 : group.envUserGroupsDuration + , defaultGroupsRepetitions: adminUser ? 0 : group.envUserGroupsRepetitions + , repetitions: adminUser ? + adminUser.groups.quotas.defaultGroupsRepetitions : + group.envUserGroupsRepetitions + } + } + }, {returnChanges: true})) + .then(function(stats) { + if (stats.inserted) { + return dbapi.addGroupUser(group.id, email).then(function() { + return dbapi.loadUser(email).then(function(user) { + stats.changes[0].new_val = user + return stats + }) + }) + } + return stats + }) + }) + }) +} + dbapi.saveUserAfterLogin = function(user) { return db.run(r.table('users').get(user.email).update({ name: user.name @@ -26,16 +898,7 @@ dbapi.saveUserAfterLogin = function(user) { })) .then(function(stats) { if (stats.skipped) { - return db.run(r.table('users').insert({ - email: user.email - , name: user.name - , ip: user.ip - , group: wireutil.makePrivateChannel() - , lastLoggedInAt: r.now() - , createdAt: r.now() - , forwards: [] - , settings: {} - })) + return dbapi.createUser(user.email, user.name, user.ip) } return stats }) @@ -122,9 +985,15 @@ dbapi.lookupUserByVncAuthResponse = function(response, serial) { } dbapi.loadUserDevices = function(email) { - return db.run(r.table('devices').getAll(email, { - index: 'owner' - })) + return db.run(r.table('users').get(email).getField('groups')) + .then(function(groups) { + return db.run(r.table('devices').filter(function(device) { + return r.expr(groups.subscribed) + .contains(device('group')('id')) + .and(device('owner')('email').eq(email)) + .and(device('present').eq(true)) + })) + }) } dbapi.saveDeviceLog = function(serial, entry) { @@ -143,7 +1012,7 @@ dbapi.saveDeviceLog = function(serial, entry) { dbapi.saveDeviceInitialState = function(serial, device) { var data = { - present: false + present: true , presenceChangedAt: r.now() , provider: device.provider , owner: null @@ -156,15 +1025,32 @@ dbapi.saveDeviceInitialState = function(serial, device) { , usage: null , logs_enabled: false } - return db.run(r.table('devices').get(serial).update(data)) - .then(function(stats) { - if (stats.skipped) { + return db.run(r.table('devices').get(serial).update(data)).then(function(stats) { + if (stats.skipped) { + return dbapi.getRootGroup().then(function(group) { data.serial = serial data.createdAt = r.now() - return db.run(r.table('devices').insert(data)) - } - return stats - }) + data.group = { + id: group.id + , name: group.name + , lifeTime: group.dates[0] + , owner: group.owner + , origin: group.id + , class: group.class + , repetitions: group.repetitions + , originName: group.name + , lock: false + } + return db.run(r.table('devices').insert(data)).then(function() { + dbapi.addOriginGroupDevice(group, serial) + }) + }) + } + return true + }) + .then(function() { + return db.run(r.table('devices').get(serial)) + }) } dbapi.setDeviceConnectUrl = function(serial, url) { @@ -326,11 +1212,48 @@ dbapi.saveDeviceIdentity = function(serial, identity) { , product: identity.product , cpuPlatform: identity.cpuPlatform , openGLESVersion: identity.openGLESVersion + , marketName: identity.marketName })) } -dbapi.loadDevices = function() { - return db.run(r.table('devices')) +dbapi.loadDevices = function(groups) { + return db.run(r.table('devices').filter(function(device) { + return r.expr(groups).contains(device('group')('id')) + })) + .then(function(cursor) { + return cursor.toArray() + }) +} + +dbapi.loadDevicesByOrigin = function(groups) { + return db.run(r.table('devices').filter(function(device) { + return r.expr(groups).contains(device('group')('origin')) + })) + .then(function(cursor) { + return cursor.toArray() + }) +} + +dbapi.loadBookableDevices = function(groups) { + return db.run(r.table('devices').filter(function(device) { + return r.expr(groups) + .contains(device('group')('origin')) + .and(device('group')('class').ne(apiutil.STANDARD)) + })) + .then(function(cursor) { + return cursor.toArray() + }) +} + +dbapi.loadStandardDevices = function(groups) { + return db.run(r.table('devices').filter(function(device) { + return r.expr(groups) + .contains(device('group')('origin')) + .and(device('group')('class').eq(apiutil.STANDARD)) + })) + .then(function(cursor) { + return cursor.toArray() + }) } dbapi.loadPresentDevices = function() { @@ -339,17 +1262,49 @@ dbapi.loadPresentDevices = function() { })) } -dbapi.loadDevice = function(serial) { +dbapi.loadDeviceBySerial = function(serial) { return db.run(r.table('devices').get(serial)) } +dbapi.loadDevice = function(groups, serial) { + return db.run(r.table('devices').getAll(serial).filter(function(device) { + return r.expr(groups).contains(device('group')('id')) + })) +} + +dbapi.loadBookableDevice = function(groups, serial) { + return db.run(r.table('devices').getAll(serial).filter(function(device) { + return r.expr(groups) + .contains(device('group')('origin')) + .and(device('group')('class').ne(apiutil.STANDARD)) + })) + .then(function(cursor) { + return cursor.toArray() + }) +} + +dbapi.loadDeviceByOrigin = function(groups, serial) { + return db.run(r.table('devices').getAll(serial).filter(function(device) { + return r.expr(groups).contains(device('group')('origin')) + })) + .then(function(cursor) { + return cursor.toArray() + }) +} + dbapi.saveUserAccessToken = function(email, token) { return db.run(r.table('accessTokens').insert({ email: email , id: token.id , title: token.title , jwt: token.jwt - })) + }, {returnChanges: true})) +} + +dbapi.removeUserAccessTokens = function(email) { + return db.run(r.table('accessTokens').getAll(email, { + index: 'email' + }).delete()) } dbapi.removeUserAccessToken = function(email, title) { @@ -358,6 +1313,10 @@ dbapi.removeUserAccessToken = function(email, title) { }).filter({title: title}).delete()) } +dbapi.removeAccessToken = function(id) { + return db.run(r.table('accessTokens').get(id).delete()) +} + dbapi.loadAccessTokens = function(email) { return db.run(r.table('accessTokens').getAll(email, { index: 'email' diff --git a/lib/db/tables.js b/lib/db/tables.js index bf46b179..f3c104e5 100644 --- a/lib/db/tables.js +++ b/lib/db/tables.js @@ -1,3 +1,7 @@ +/** +* Copyright © 2019 contains code contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0 +**/ + var r = require('rethinkdb') module.exports = { @@ -50,9 +54,30 @@ module.exports = { return device('provider')('channel') } } + , group: { + indexFunction: function(device) { + return device('group')('id') + } + } } } , logs: { primaryKey: 'id' } +, groups: { + primaryKey: 'id' + , indexes: { + privilege: null + , owner: { + indexFunction: function(group) { + return group('owner')('email') + } + } + , startTime: { + indexFunction: function(group) { + return group('dates').nth(0)('start') + } + } + } + } } diff --git a/lib/units/api/controllers/devices.js b/lib/units/api/controllers/devices.js index ba236b4f..7b423a16 100644 --- a/lib/units/api/controllers/devices.js +++ b/lib/units/api/controllers/devices.js @@ -1,79 +1,527 @@ +/** +* Copyright © 2019 contains code contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0 +**/ + var _ = require('lodash') var Promise = require('bluebird') var dbapi = require('../../../db/api') var logger = require('../../../util/logger') -var datautil = require('../../../util/datautil') - var log = logger.createLogger('api:controllers:devices') -module.exports = { - getDevices: getDevices -, getDeviceBySerial: getDeviceBySerial +const apiutil = require('../../../util/apiutil') +const lockutil = require('../../../util/lockutil') +const util = require('util') +const uuid = require('uuid') +const wire = require('../../../wire') +const wireutil = require('../../../wire/util') +const wirerouter = require('../../../wire/router') + +/* ------------------------------------ PRIVATE FUNCTIONS ------------------------------- */ + +function filterGenericDevices(req, res, devices) { + apiutil.respond(res, 200, 'Devices Information', { + devices: devices.map(function(device) { + return apiutil.filterDevice(req, device) + }) + }) } -function getDevices(req, res) { - var fields = req.swagger.params.fields.value +function getGenericDevices(req, res, loadDevices) { + loadDevices(req.user.groups.subscribed).then(function(devices) { + filterGenericDevices(req, res, devices) + }) + .catch(function(err) { + apiutil.internalError(res, 'Failed to load device list: ', err.stack) + }) +} - dbapi.loadDevices() - .then(function(cursor) { - return Promise.promisify(cursor.toArray, cursor)() - .then(function(list) { - var deviceList = [] - - list.forEach(function(device) { - datautil.normalize(device, req.user) - var responseDevice = device - - if (fields) { - responseDevice = _.pick(device, fields.split(',')) - } - deviceList.push(responseDevice) - }) - - res.json({ - success: true - , devices: deviceList - }) - }) +function getDeviceFilteredGroups(serial, fields, bookingOnly) { + return dbapi.getDeviceGroups(serial).then(function(groups) { + return Promise.map(groups, function(group) { + return !bookingOnly || !apiutil.isOriginGroup(group.class) ? + group : + 'filtered' }) - .catch(function(err) { - log.error('Failed to load device list: ', err.stack) - res.status(500).json({ - success: false + .then(function(groups) { + return _.without(groups, 'filtered').map(function(group) { + if (fields) { + return _.pick(apiutil.publishGroup(group), fields.split(',')) + } + return apiutil.publishGroup(group) }) }) + }) +} + +function extractStandardizableDevices(devices) { + return dbapi.getTransientGroups().then(function(groups) { + return Promise.map(devices, function(device) { + return Promise.map(groups, function(group) { + if (group.devices.indexOf(device.serial) > -1) { + return Promise.reject('booked') + } + return true + }) + .then(function() { + return device + }) + .catch(function(err) { + if (err !== 'booked') { + throw err + } + return err + }) + }) + .then(function(devices) { + return _.without(devices, 'booked') + }) + }) +} + +function getStandardizableDevices(req, res) { + dbapi.loadDevicesByOrigin(req.user.groups.subscribed).then(function(devices) { + extractStandardizableDevices(devices).then(function(devices) { + filterGenericDevices(req, res, devices) + }) + }) + .catch(function(err) { + apiutil.internalError(res, 'Failed to load device list: ', err.stack) + }) +} + +function removeDevice(serial, req, res) { + const presentState = req.swagger.params.present.value + const bookingState = req.swagger.params.booked.value + const notesState = req.swagger.params.annotated.value + const controllingState = req.swagger.params.controlled.value + const anyPresentState = typeof presentState === 'undefined' + const anyBookingState = typeof bookingState === 'undefined' + const anyNotesState = typeof notesState === 'undefined' + const anyControllingState = typeof controllingState === 'undefined' + const lock = {} + + function deleteGroupDevice(email, id) { + const lock = {} + + return dbapi.lockGroupByOwner(email, id).then(function(stats) { + if (!stats.replaced) { + return apiutil.lightComputeStats(res, stats) + } + const group = lock.group = stats.changes[0].new_val + + if (group.devices.indexOf(serial) > -1) { + return apiutil.isOriginGroup(group.class) ? + dbapi.removeOriginGroupDevice(group, serial) : + dbapi.removeGroupDevices(group, [serial]) + } + return group + }) + .finally(function() { + lockutil.unlockGroup(lock) + }) + } + + function deleteDeviceInDatabase() { + function wrappedDeleteDeviceInDatabase() { + const result = { + status: false + , data: 'not deleted' + } + + return dbapi.loadDeviceBySerial(serial).then(function(device) { + if (device && device.group.id === device.group.origin) { + return deleteGroupDevice(device.group.owner.email, device.group.id) + .then(function(group) { + if (group !== 'not found') { + return dbapi.deleteDevice(serial).then(function() { + result.status = true + result.data = 'deleted' + }) + } + return false + }) + } + return false + }) + .then(function() { + return result + }) + } + return apiutil.setIntervalWrapper( + wrappedDeleteDeviceInDatabase + , 10 + , Math.random() * 500 + 50) + } + + return dbapi.lockDeviceByOrigin(req.user.groups.subscribed, serial).then(function(stats) { + if (!stats.replaced) { + return apiutil.lightComputeStats(res, stats) + } + const device = lock.device = stats.changes[0].new_val + + if (!anyPresentState && device.present !== presentState || + !anyControllingState && (device.owner === null) === controllingState || + !anyNotesState && + (typeof device.notes !== 'undefined' && device.notes !== '') !== notesState || + !anyBookingState && (device.group.id !== device.group.origin && !bookingState || + device.group.class === apiutil.STANDARD && bookingState)) { + return 'unchanged' + } + if (device.group.class === apiutil.STANDARD) { + return deleteDeviceInDatabase() + } + return dbapi.getDeviceTransientGroups(serial).then(function(groups) { + if (groups.length && !anyBookingState && !bookingState) { + return 'unchanged' + } + return Promise.each(groups, function(group) { + return deleteGroupDevice(group.owner.email, group.id) + }) + .then(function() { + if (!groups.length && !anyBookingState && bookingState) { + return 'unchanged' + } + return deleteDeviceInDatabase() + }) + }) + }) + .finally(function() { + lockutil.unlockDevice(lock) + }) +} + +/* ------------------------------------ PUBLIC FUNCTIONS ------------------------------- */ + +function getDevices(req, res) { + const target = req.swagger.params.target.value + + switch(target) { + case apiutil.BOOKABLE: + getGenericDevices(req, res, dbapi.loadBookableDevices) + break + case apiutil.ORIGIN: + getGenericDevices(req, res, dbapi.loadDevicesByOrigin) + break + case apiutil.STANDARD: + getGenericDevices(req, res, dbapi.loadStandardDevices) + break + case apiutil.STANDARDIZABLE: + getStandardizableDevices(req, res) + break + default: + getGenericDevices(req, res, dbapi.loadDevices) + } } function getDeviceBySerial(req, res) { var serial = req.swagger.params.serial.value var fields = req.swagger.params.fields.value - dbapi.loadDevice(serial) - .then(function(device) { - if (!device) { - return res.status(404).json({ - success: false - , description: 'Device not found' + dbapi.loadDevice(req.user.groups.subscribed, serial) + .then(function(cursor) { + cursor.next(function(err, device) { + if (err) { + return res.status(404).json({ + success: false + , description: 'Device not found' + }) + } + let responseDevice = apiutil.publishDevice(device, req.user) + + if (fields) { + responseDevice = _.pick(device, fields.split(',')) + } + res.json({ + success: true + , device: responseDevice }) - } - - datautil.normalize(device, req.user) - var responseDevice = device - - if (fields) { - responseDevice = _.pick(device, fields.split(',')) - } - - res.json({ - success: true - , device: responseDevice }) }) .catch(function(err) { - log.error('Failed to load device "%s": ', req.params.serial, err.stack) + log.error('Failed to load device "%s": ', serial, err.stack) res.status(500).json({ success: false }) }) } + +function getDeviceGroups(req, res) { + const serial = req.swagger.params.serial.value + const fields = req.swagger.params.fields.value + + dbapi.loadDevice(req.user.groups.subscribed, serial).then(function(cursor) { + return cursor.toArray() + }) + .then(function(devices) { + if (!devices.length) { + apiutil.respond(res, 404, 'Not Found (device)') + } + else { + getDeviceFilteredGroups(serial, fields, false) + .then(function(groups) { + return apiutil.respond(res, 200, 'Groups Information', {groups: groups}) + }) + } + }) + .catch(function(err) { + apiutil.internalError(res, 'Failed to get device groups: ', err.stack) + }) +} + +function getDeviceBookings(req, res) { + const serial = req.swagger.params.serial.value + const fields = req.swagger.params.fields.value + + dbapi.loadDevice(req.user.groups.subscribed, serial).then(function(cursor) { + return cursor.toArray() + }) + .then(function(devices) { + if (!devices.length) { + apiutil.respond(res, 404, 'Not Found (device)') + } + else { + getDeviceFilteredGroups(serial, fields, true) + .then(function(bookings) { + apiutil.respond(res, 200, 'Bookings Information', {bookings: bookings}) + }) + } + }) + .catch(function(err) { + apiutil.internalError(res, 'Failed to get device bookings: ', err.stack) + }) +} + +function addOriginGroupDevices(req, res) { + const serials = apiutil.getBodyParameter(req.body, 'serials') + const fields = apiutil.getQueryParameter(req.swagger.params.fields) + const target = apiutil.getQueryParameter(req.swagger.params.redirected) ? 'device' : 'devices' + const lock = {} + + function askUpdateDeviceOriginGroup(group, serial) { + return new Promise(function(resolve, reject) { + const signature = util.format('%s', uuid.v4()).replace(/-/g, '') + let messageListener + const responseTimer = setTimeout(function() { + req.options.channelRouter.removeListener(wireutil.global, messageListener) + apiutil.respond(res, 504, 'Gateway Time-out') + reject('timeout') + }, 5000) + + messageListener = wirerouter() + .on(wire.DeviceOriginGroupMessage, function(channel, message) { + if (message.signature === signature) { + clearTimeout(responseTimer) + req.options.channelRouter.removeListener(wireutil.global, messageListener) + dbapi.loadDeviceBySerial(serial).then(function(device) { + if (fields) { + resolve(_.pick(apiutil.publishDevice(device, req.user), fields.split(','))) + } + else { + resolve(apiutil.publishDevice(device, req.user)) + } + }) + } + }) + .handler() + + req.options.channelRouter.on(wireutil.global, messageListener) + return dbapi.askUpdateDeviceOriginGroup(serial, group, signature) + }) + } + + function updateDeviceOriginGroup(group, serial) { + const lock = {} + + return dbapi.lockDeviceByOrigin(req.user.groups.subscribed, serial).then(function(stats) { + if (!stats.replaced) { + return apiutil.lightComputeStats(res, stats) + } + lock.device = stats.changes[0].new_val + + return dbapi.isUpdateDeviceOriginGroupAllowed(serial, group) + .then(function(updatingAllowed) { + if (!updatingAllowed) { + apiutil.respond(res, 403, 'Forbidden (device is currently booked)') + return Promise.reject('booked') + } + return askUpdateDeviceOriginGroup(group, serial) + }) + }) + .finally(function() { + lockutil.unlockDevice(lock) + }) + } + + function updateDevicesOriginGroup(group, serials) { + let results = [] + + return Promise.each(serials, function(serial) { + return updateDeviceOriginGroup(group, serial).then(function(result) { + results.push(result) + }) + }) + .then(function() { + const result = target === 'device' ? {device: {}} : {devices: []} + + results = _.without(results, 'unchanged') + if (!results.length) { + return apiutil.respond(res, 200, `Unchanged (${target})`, result) + } + results = _.without(results, 'not found') + if (!results.length) { + return apiutil.respond(res, 404, `Not Found (${target})`) + } + if (target === 'device') { + result.device = results[0] + } + else { + result.devices = results + } + return apiutil.respond(res, 200, `Updated (${target})`, result) + }) + .catch(function(err) { + if (err !== 'booked' && err !== 'timeout' && err !== 'busy') { + throw err + } + }) + } + + return lockutil.lockGroup(req, res, lock).then(function(lockingSuccessed) { + if (lockingSuccessed) { + const group = lock.group + + if (!apiutil.isOriginGroup(group.class)) { + return apiutil.respond(res, 400, 'Bad Request (this group cannot act as an origin one)') + } + if (typeof serials !== 'undefined') { + return updateDevicesOriginGroup( + group + , _.without(serials.split(','), '').filter(function(serial) { + return group.devices.indexOf(serial) < 0 + }) + ) + } + return dbapi.loadDevicesByOrigin(req.user.groups.subscribed).then(function(devices) { + if (group.class === apiutil.BOOKABLE) { + return devices + } + return extractStandardizableDevices(devices) + }) + .then(function(devices) { + const serials = [] + + devices.forEach(function(device) { + if (group.devices.indexOf(device.serial) < 0) { + serials.push(device.serial) + } + }) + return updateDevicesOriginGroup(group, serials) + }) + } + return false + }) + .catch(function(err) { + apiutil.internalError(res, `Failed to update ${target} origin group: `, err.stack) + }) + .finally(function() { + lockutil.unlockGroup(lock) + }) +} + +function addOriginGroupDevice(req, res) { + apiutil.redirectApiWrapper('serial', addOriginGroupDevices, req, res) +} + +function removeOriginGroupDevices(req, res) { + const lock = {} + + return lockutil.lockGroup(req, res, lock).then(function(lockingSuccessed) { + if (lockingSuccessed) { + const group = lock.group + + if (!apiutil.checkBodyParameter(req.body, 'serials')) { + req.body = {serials: group.devices.join()} + } + return dbapi.getRootGroup().then(function(group) { + req.swagger.params.id = {value: group.id} + return addOriginGroupDevices(req, res) + }) + } + return false + }) + .finally(function() { + lockutil.unlockGroup(lock) + }) +} + +function removeOriginGroupDevice(req, res) { + apiutil.redirectApiWrapper('serial', removeOriginGroupDevices, req, res) +} + +function deleteDevices(req, res) { + const serials = apiutil.getBodyParameter(req.body, 'serials') + const target = apiutil.getQueryParameter(req.swagger.params.redirected) ? 'device' : 'devices' + + function removeDevices(serials) { + let results = [] + + return Promise.each(serials, function(serial) { + return removeDevice(serial, req, res).then(function(result) { + if (result === 'not deleted') { + apiutil.respond(res, 503, 'Server too busy [code: 2], please try again later') + return Promise.reject('busy') + } + return results.push(result) + }) + }) + .then(function() { + results = _.without(results, 'unchanged') + if (!results.length) { + return apiutil.respond(res, 200, `Unchanged (${target})`) + } + if (!_.without(results, 'not found').length) { + return apiutil.respond(res, 404, `Not Found (${target})`) + } + return apiutil.respond(res, 200, `Deleted (${target})`) + }) + .catch(function(err) { + if (err !== 'busy') { + throw err + } + }) + } + + (function() { + if (typeof serials === 'undefined') { + return dbapi.loadDevicesByOrigin(req.user.groups.subscribed).then(function(devices) { + return removeDevices(devices.map(function(device) { + return device.serial + })) + }) + } + else { + return removeDevices(_.without(serials.split(','), '')) + } + })() + .catch(function(err) { + apiutil.internalError(res, `Failed to delete ${target}: `, err.stack) + }) +} + +function deleteDevice(req, res) { + apiutil.redirectApiWrapper('serial', deleteDevices, req, res) +} + +module.exports = { + getDevices: getDevices +, getDeviceBySerial: getDeviceBySerial +, getDeviceGroups: getDeviceGroups +, getDeviceBookings: getDeviceBookings +, addOriginGroupDevice: addOriginGroupDevice +, addOriginGroupDevices: addOriginGroupDevices +, removeOriginGroupDevice: removeOriginGroupDevice +, removeOriginGroupDevices: removeOriginGroupDevices +, deleteDevice: deleteDevice +, deleteDevices: deleteDevices +} diff --git a/lib/units/api/controllers/groups.js b/lib/units/api/controllers/groups.js new file mode 100644 index 00000000..63866998 --- /dev/null +++ b/lib/units/api/controllers/groups.js @@ -0,0 +1,931 @@ +/** +* Copyright © 2019 code initially contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0 +**/ + +const _ = require('lodash') +const dbapi = require('../../../db/api') +const apiutil = require('../../../util/apiutil') +const lockutil = require('../../../util/lockutil') +const util = require('util') +const uuid = require('uuid') +const Promise = require('bluebird') +const usersapi = require('./users') + +/* ---------------------------------- PRIVATE FUNCTIONS --------------------------------- */ + +function groupApiWrapper(email, fn, req, res) { + dbapi.loadUser(email).then(function(user) { + if (!user) { + apiutil.respond(res, 404, 'Not Found (user)') + } + else { + req.user = user + fn(req, res) + } + }) + .catch(function(err) { + apiutil.internalError(res, 'Failed to wrap "%s": ', fn.name, err.stack) + }) +} + +function getDevice(req, serial) { + return dbapi.loadDeviceBySerial(serial).then(function(device) { + if (!device) { + throw new Error(`Device not found: ${serial}`) + } + return apiutil.filterDevice(req, device) + }) +} + +function checkConflicts(id, devices, dates) { + function computeConflicts(conflicts, liteGroup, otherGroup) { + if (otherGroup.id !== liteGroup.id) { + const devices = _.intersection(liteGroup.devices, otherGroup.devices) + + if (devices.length) { + for (let liteGroupDate of liteGroup.dates) { + for (let otherGroupDate of otherGroup.dates) { + if (liteGroupDate.start < otherGroupDate.stop && + liteGroupDate.stop > otherGroupDate.start) { + conflicts.push({ + devices: devices + , date: { + start: new Date( + Math.max(liteGroupDate.start.getTime() + , otherGroupDate.start.getTime())) + , stop: new Date( + Math.min(liteGroupDate.stop.getTime() + , otherGroupDate.stop.getTime())) + } + , group: otherGroup.name + , owner: otherGroup.owner + }) + } + } + } + } + } + } + + return dbapi.getTransientGroups().then(function(groups) { + const conflicts = [] + + groups.forEach(function(otherGroup) { + computeConflicts( + conflicts + , {id: id, devices: devices, dates: dates} + , otherGroup) + }) + return conflicts + }) +} + +function checkSchedule(res, oldGroup, _class, email, repetitions, privilege, start, stop) { + if (oldGroup && oldGroup.devices.length && + (apiutil.isOriginGroup(_class) && !apiutil.isOriginGroup(oldGroup.class) || + apiutil.isOriginGroup(oldGroup.class) && !apiutil.isOriginGroup(_class))) { + return Promise.resolve(apiutil.respond(res, 403, + 'Forbidden (unauthorized class while device list is not empty)')) + } + if (apiutil.isAdminGroup(_class) && privilege === apiutil.USER) { + return Promise.resolve(apiutil.respond(res, 403, 'Forbidden (unauthorized class)')) + } + if (isNaN(start.getTime())) { + return Promise.resolve(apiutil.respond(res, 400, 'Bad Request (Invalid startTime format)')) + } + if (isNaN(stop.getTime())) { + return Promise.resolve(apiutil.respond(res, 400, 'Bad Request (Invalid stopTime format)')) + } + if (start >= stop) { + return Promise.resolve( + apiutil.respond(res, 400, 'Bad Request (Invalid life time: startTime >= stopTime)')) + } + if ((stop - start) > apiutil.CLASS_DURATION[_class]) { + return Promise.resolve(apiutil.respond(res, 400, + 'Bad Request (Invalid Life time & class combination: life time > class duration)' + )) + } + switch(_class) { + case apiutil.BOOKABLE: + case apiutil.STANDARD: + case apiutil.ONCE: + if (repetitions !== 0) { + return Promise.resolve( + apiutil.respond(res, 400, 'Bad Request (Invalid class & repetitions combination)')) + } + break + default: + if (repetitions === 0) { + return Promise.resolve( + apiutil.respond(res, 400, 'Bad Request (Invalid class & repetitions combination)')) + } + break + } + + return dbapi.loadUser(email).then(function(owner) { + if (repetitions > owner.groups.quotas.repetitions) { + return apiutil.respond(res, 400, 'Bad Request (Invalid repetitions value)') + } + return true + }) +} + +/* ---------------------------------- PUBLIC FUNCTIONS ------------------------------------- */ + +function addGroupDevices(req, res) { + const id = req.swagger.params.id.value + const serials = apiutil.getBodyParameter(req.body, 'serials') + const target = apiutil.getQueryParameter(req.swagger.params.redirected) ? 'device' : 'devices' + const lock = {} + let email = null + + function addGroupDevice(group, serial) { + const lock = {} + + return dbapi.lockBookableDevice(req.user.groups.subscribed, serial).then(function(stats) { + if (!stats.replaced) { + return apiutil.lightComputeStats(res, stats) + } + lock.device = stats.changes[0].new_val + + return dbapi.lockGroup(lock.device.group.origin).then(function(stats) { + if (!stats.replaced) { + return apiutil.lightComputeStats(res, stats) + } + lock.group = {id: lock.device.group.origin} + + return checkConflicts(id, [serial], group.dates).then(function(conflicts) { + return conflicts.length ? + Promise.reject(conflicts) : + dbapi.addGroupDevices(group, [serial]) + }) + }) + .finally(function() { + lockutil.unlockGroup(lock) + }) + }) + .finally(function() { + lockutil.unlockDevice(lock) + }) + } + + function _addGroupDevices(lockedGroup, serials) { + let results = [] + let group = lockedGroup + + return Promise.each(serials, function(serial) { + return addGroupDevice(group, serial).then(function(result) { + results.push(result) + if (result.hasOwnProperty('id')) { + group = result + } + }) + }) + .then(function() { + results = _.without(results, 'unchanged') + if (!results.length) { + apiutil.respond(res, 200, `Unchanged (group ${target})`, {group: {}}) + } + else { + results = _.without(results, 'not found') + if (!results.length) { + apiutil.respond(res, 404, `Not Found (group ${target})`) + } + else { + apiutil.respond(res, 200, `Added (group ${target})` + , {group: apiutil.publishGroup(results[results.length - 1])}) + } + } + }) + .catch(function(err) { + if (err === 'quota is reached') { + apiutil.respond(res, 403, 'Forbidden (groups duration quota is reached)') + } + else if (Array.isArray(err)) { + apiutil.respond(res, 409, 'Conflicts Information', {conflicts: err}) + } + else if (err !== 'busy') { + throw err + } + }) + } + + lockutil.lockGroup(req, res, lock).then(function(lockingSuccessed) { + if (lockingSuccessed) { + let group = lock.group + + if (req.user.privilege === apiutil.ADMIN && req.user.email !== group.owner.email) { + email = group.owner.email + return false + } + if (apiutil.isOriginGroup(group.class)) { + return apiutil.respond(res, 400, 'Bad Request (use admin API for bookable/standard groups)') + } + + return (function() { + if (typeof serials === 'undefined') { + return dbapi.loadBookableDevices(req.user.groups.subscribed).then(function(devices) { + const serials = [] + + devices.forEach(function(device) { + if (group.devices.indexOf(device.serial) < 0) { + serials.push(device.serial) + } + }) + return _addGroupDevices(group, serials) + }) + } + else { + return _addGroupDevices( + group + , _.difference( + _.without(serials.split(','), '') + , group.devices) + ) + } + })() + } + return false + }) + .catch(function(err) { + apiutil.internalError(res, `Failed to add group ${target}: `, err.stack) + }) + .finally(function() { + lockutil.unlockGroup(lock) + if (email) { + groupApiWrapper(email, addGroupDevices, req, res) + } + }) +} + +function addGroupDevice(req, res) { + apiutil.redirectApiWrapper('serial', addGroupDevices, req, res) +} + +function removeGroupDevices(req, res) { + const serials = apiutil.getBodyParameter(req.body, 'serials') + const target = apiutil.getQueryParameter(req.swagger.params.redirected) ? 'device' : 'devices' + const lock = {} + + lockutil.lockGroup(req, res, lock).then(function(lockingSuccessed) { + if (lockingSuccessed) { + const group = lock.group + + if (apiutil.isOriginGroup(group.class)) { + return apiutil.respond(res, 400, 'Bad Request (use admin API for bookable/standard groups)') + } + let serialsToRemove = group.devices + + if (typeof serials !== 'undefined') { + serialsToRemove = _.without(serials.split(','), '') + } + if (!serialsToRemove.length) { + return apiutil.respond(res, 200, `Unchanged (group ${target})`, {group: {}}) + } + serialsToRemove = _.intersection(serialsToRemove, group.devices) + if (!serialsToRemove.length) { + return apiutil.respond(res, 404, `Not Found (group ${target})`) + } + return dbapi.removeGroupDevices(group, serialsToRemove).then(function(group) { + apiutil.respond(res, 200, `Removed (group ${target})`, {group: apiutil.publishGroup(group)}) + }) + } + return false + }) + .catch(function(err) { + apiutil.internalError(res, `Failed to remove group ${target}: `, err.stack) + }) + .finally(function() { + lockutil.unlockGroup(lock) + }) +} + +function removeGroupDevice(req, res) { + apiutil.redirectApiWrapper('serial', removeGroupDevices, req, res) +} + +function getGroupDevice(req, res) { + const id = req.swagger.params.id.value + const serial = req.swagger.params.serial.value + + dbapi.getUserGroup(req.user.email, id).then(function(group) { + if (!group) { + apiutil.respond(res, 404, 'Not Found (group)') + } + else if (group.devices.indexOf(serial) < 0) { + apiutil.respond(res, 404, 'Not Found (device)') + } + else { + getDevice(req, serial).then(function(device) { + apiutil.respond(res, 200, 'Device Information', {device: device}) + }) + } + }) + .catch(function(err) { + apiutil.internalError(res, 'Failed to get group device: ', err.stack) + }) +} + +function getGroupUser(req, res) { + const id = req.swagger.params.id.value + const email = req.swagger.params.email.value + + dbapi.getUserGroup(req.user.email, id).then(function(group) { + if (!group) { + apiutil.respond(res, 404, 'Not Found (group)') + } + else if (group.users.indexOf(email) < 0) { + apiutil.respond(res, 404, 'Not Found (user)') + } + else { + usersapi.getUserByEmail(req, res) + } + }) + .catch(function(err) { + apiutil.internalError(res, 'Failed to get group user: ', err.stack) + }) +} + +function getGroupUsers(req, res) { + const id = req.swagger.params.id.value + + dbapi.getUserGroup(req.user.email, id).then(function(group) { + if (!group) { + apiutil.respond(res, 404, 'Not Found (group)') + } + else { + Promise.map(group.users, function(email) { + return usersapi.getUserInfo(req, email).then(function(user) { + return user || Promise.reject(`Group user not found: ${email}`) + }) + }) + .then(function(users) { + apiutil.respond(res, 200, 'Users Information', {users: users}) + }) + } + }) + .catch(function(err) { + apiutil.internalError(res, 'Failed to get group users: ', err.stack) + }) +} + +function removeGroupUsers(req, res) { + const id = req.swagger.params.id.value + const emails = apiutil.getBodyParameter(req.body, 'emails') + const target = apiutil.getQueryParameter(req.swagger.params.redirected) ? 'user' : 'users' + const lock = {} + + function removeGroupUser(email, group, rootGroup) { + if (group.users.indexOf(email) < 0) { + return Promise.resolve('not found') + } + if (email === rootGroup.owner.email || email === group.owner.email) { + return Promise.resolve('forbidden') + } + const lock = {} + + return dbapi.lockUser(email).then(function(stats) { + if (!stats.replaced) { + return apiutil.lightComputeStats(res, stats) + } + lock.user = stats.changes[0].new_val + + return dbapi.isRemoveGroupUserAllowed(email, group) + .then(function(isAllowed) { + return isAllowed ? dbapi.removeGroupUser(id, email) : 'forbidden' + }) + }) + .finally(function() { + lockutil.unlockUser(lock) + }) + } + + lockutil.lockGroup(req, res, lock).then(function(lockingSuccessed) { + if (lockingSuccessed) { + const group = lock.group + + return dbapi.getRootGroup().then(function(rootGroup) { + let emailsToRemove = group.users + let results = [] + + if (typeof emails !== 'undefined') { + emailsToRemove = _.without(emails.split(','), '') + } + return Promise.each(emailsToRemove, function(email) { + return removeGroupUser(email, group, rootGroup).then(function(result) { + results.push(result) + }) + }) + .then(function() { + if (!results.length) { + return apiutil.respond(res, 200, `Unchanged (group ${target})`, {group: {}}) + } + results = _.without(results, 'not found') + if (!results.length) { + return apiutil.respond(res, 404, `Not Found (group ${target})`) + } + if (!_.without(results, 'forbidden').length) { + return apiutil.respond(res, 403, `Forbidden (group ${target})`) + } + return dbapi.getGroup(id).then(function(group) { + apiutil.respond(res, 200, `Removed (group ${target})`, { + group: apiutil.publishGroup(group)}) + }) + }) + }) + .catch(function(err) { + if (err !== 'busy') { + throw err + } + }) + } + return false + }) + .catch(function(err) { + apiutil.internalError(res, `Failed to remove group ${target}: `, err.stack) + }) + .finally(function() { + lockutil.unlockGroup(lock) + }) +} + +function removeGroupUser(req, res) { + apiutil.redirectApiWrapper('email', removeGroupUsers, req, res) +} + +function addGroupUsers(req, res) { + const id = req.swagger.params.id.value + const emails = apiutil.getBodyParameter(req.body, 'emails') + const target = apiutil.getQueryParameter(req.swagger.params.redirected) ? 'user' : 'users' + const lock = {} + + function addGroupUser(email) { + const lock = {} + + return dbapi.lockUser(email).then(function(stats) { + if (!stats.replaced) { + return apiutil.lightComputeStats(res, stats) + } + lock.user = stats.changes[0].new_val + + return dbapi.addGroupUser(id, email) + }) + .finally(function() { + lockutil.unlockUser(lock) + }) + } + + function _addGroupUsers(emails) { + let results = [] + + return Promise.each(emails, function(email) { + return addGroupUser(email).then(function(result) { + results.push(result) + }) + }) + .then(function() { + results = _.without(results, 'unchanged') + if (!results.length) { + return apiutil.respond(res, 200, `Unchanged (group ${target})`, {group: {}}) + } + if (!_.without(results, 'not found').length) { + return apiutil.respond(res, 404, `Not Found (group ${target})`) + } + return dbapi.getGroup(id).then(function(group) { + apiutil.respond(res, 200, `Added (group ${target})`, {group: apiutil.publishGroup(group)}) + }) + }) + .catch(function(err) { + if (err !== 'busy') { + throw err + } + }) + } + + lockutil.lockGroup(req, res, lock).then(function(lockingSuccessed) { + if (!lockingSuccessed) { + return false + } + const group = lock.group + + return (function() { + if (typeof emails === 'undefined') { + return dbapi.getUsers().then(function(users) { + const emails = [] + + users.forEach(function(user) { + if (group.users.indexOf(user.email) < 0) { + emails.push(user.email) + } + }) + return _addGroupUsers(emails) + }) + } + else { + return _addGroupUsers( + _.difference( + _.without(emails.split(','), '') + , group.users) + ) + } + })() + }) + .catch(function(err) { + apiutil.internalError(res, `Failed to add group ${target}: `, err.stack) + }) + .finally(function() { + lockutil.unlockGroup(lock) + }) +} + +function addGroupUser(req, res) { + apiutil.redirectApiWrapper('email', addGroupUsers, req, res) +} + +function getGroup(req, res) { + const id = req.swagger.params.id.value + const fields = req.swagger.params.fields.value + + dbapi.getUserGroup(req.user.email, id).then(function(group) { + if (!group) { + apiutil.respond(res, 404, 'Not Found (group)') + return + } + let publishedGroup = apiutil.publishGroup(group) + + if (fields) { + publishedGroup = _.pick(publishedGroup, fields.split(',')) + } + apiutil.respond(res, 200, 'Group Information', {group: publishedGroup}) + }) + .catch(function(err) { + apiutil.internalError(res, 'Failed to get group: ', err.stack) + }) +} + +function getGroups(req, res) { + const fields = req.swagger.params.fields.value + const owner = req.swagger.params.owner.value + let getGenericGroups + + switch(owner) { + case true: + getGenericGroups = dbapi.getOwnerGroups + break + case false: + getGenericGroups = dbapi.getOnlyUserGroups + break + default: + getGenericGroups = dbapi.getUserGroups + } + getGenericGroups(req.user.email).then(function(groups) { + return apiutil.respond(res, 200, 'Groups Information', { + groups: groups.map(function(group) { + if (fields) { + return _.pick(apiutil.publishGroup(group), fields.split(',')) + } + return apiutil.publishGroup(group) + }) + }) + }) + .catch(function(err) { + apiutil.internalError(res, 'Failed to get groups: ', err.stack) + }) +} + +function createGroup(req, res) { + const _class = typeof req.body.class === 'undefined' ? apiutil.ONCE : req.body.class + const repetitions = + apiutil.isOriginGroup(_class) || typeof req.body.repetitions === 'undefined' ? + 0 : + req.body.repetitions + const now = Date.now() + const start = + apiutil.isOriginGroup(_class) ? + new Date(now) : + new Date(req.body.startTime || now) + const stop = + apiutil.isOriginGroup(_class) ? + new Date(now + apiutil.ONE_YEAR) : + new Date(req.body.stopTime || now + apiutil.ONE_HOUR) + + checkSchedule(res, null, _class, req.user.email, repetitions, req.user.privilege, + start, stop).then(function(checkingSuccessed) { + if (!checkingSuccessed) { + return + } + const name = + typeof req.body.name === 'undefined' ? + 'New_' + util.format('%s', uuid.v4()).replace(/-/g, '') : + req.body.name + const state = + apiutil.isOriginGroup(_class) || typeof req.body.state === 'undefined' ? + apiutil.READY : + req.body.state + const isActive = state === apiutil.READY && apiutil.isOriginGroup(_class) + const duration = 0 + const dates = apiutil.computeGroupDates({start: start, stop: stop}, _class, repetitions) + + dbapi.createUserGroup({ + name: name + , owner: { + email: req.user.email + , name: req.user.name + } + , privilege: req.user.privilege + , class: _class + , repetitions: repetitions + , isActive: isActive + , dates: dates + , duration: duration + , state: state + }) + .then(function(group) { + if (group) { + apiutil.respond(res, 201, 'Created', {group: apiutil.publishGroup(group)}) + } + else { + apiutil.respond(res, 403, 'Forbidden (groups number quota is reached)') + } + }) + .catch(function(err) { + apiutil.internalError(res, 'Failed to create group: ', err.stack) + }) + }) +} + +function deleteGroups(req, res) { + const ids = apiutil.getBodyParameter(req.body, 'ids') + const target = apiutil.getQueryParameter(req.swagger.params.redirected) ? 'group' : 'groups' + + function removeGroup(id) { + const lock = {} + + return dbapi.lockGroupByOwner(req.user.email, id).then(function(stats) { + if (!stats.replaced) { + return apiutil.lightComputeStats(res, stats) + } + const group = lock.group = stats.changes[0].new_val + + if (group.privilege === apiutil.ROOT) { + return 'forbidden' + } + if (group.class === apiutil.BOOKABLE) { + return Promise.each(group.devices, function(serial) { + return dbapi.isDeviceBooked(serial) + .then(function(isBooked) { + return isBooked ? Promise.reject('booked') : true + }) + }) + .then(function() { + return dbapi.deleteUserGroup(id) + }) + .catch(function(err) { + if (err !== 'booked') { + throw err + } + return 'forbidden' + }) + } + else { + return dbapi.deleteUserGroup(id) + } + }) + .finally(function() { + lockutil.unlockGroup(lock) + }) + } + + function removeGroups(ids) { + let results = [] + + return Promise.each(ids, function(id) { + return removeGroup(id).then(function(result) { + results.push(result) + }) + }) + .then(function() { + if (!results.length) { + return apiutil.respond(res, 200, `Unchanged (${target})`) + } + results = _.without(results, 'not found') + if (!results.length) { + return apiutil.respond(res, 404, `Not Found (${target})`) + } + results = _.without(results, 'forbidden') + if (!results.length) { + return apiutil.respond(res, 403, `Forbidden (${target})`) + } + return apiutil.respond(res, 200, `Deleted (${target})`) + }) + .catch(function(err) { + if (err !== 'busy') { + throw err + } + }) + } + + (function() { + if (typeof ids === 'undefined') { + return dbapi.getOwnerGroups(req.user.email).then(function(groups) { + const ids = [] + + groups.forEach(function(group) { + if (group.privilege !== apiutil.ROOT) { + ids.push(group.id) + } + }) + return removeGroups(ids) + }) + } + else { + return removeGroups(_.without(ids.split(','), '')) + } + })() + .catch(function(err) { + apiutil.internalError(res, `Failed to delete ${target}: `, err.stack) + }) +} + +function deleteGroup(req, res) { + apiutil.redirectApiWrapper('id', deleteGroups, req, res) +} + +function updateGroup(req, res) { + const id = req.swagger.params.id.value + const lock = {} + + function updateUserGroup(group, data) { + return dbapi.updateUserGroup(group, data) + .then(function(group) { + if (group) { + apiutil.respond(res, 200, 'Updated (group)', {group: apiutil.publishGroup(group)}) + } + else { + apiutil.respond(res, 403, 'Forbidden (groups duration quota is reached)') + } + }) + } + + lockutil.lockGroup(req, res, lock).then(function(lockingSuccessed) { + if (!lockingSuccessed) { + return false + } + const group = lock.group + const _class = typeof req.body.class === 'undefined' ? group.class : req.body.class + const name = typeof req.body.name === 'undefined' ? group.name : req.body.name + const repetitions = + typeof req.body.repetitions === 'undefined' ? + group.repetitions : + req.body.repetitions + const start = new Date(req.body.startTime || group.dates[0].start) + const stop = new Date(req.body.stopTime || group.dates[0].stop) + let state, isActive + + if (apiutil.isOriginGroup(_class)) { + state = apiutil.READY + isActive = true + } + else { + state = typeof req.body.state === 'undefined' ? apiutil.PENDING : req.body.state + isActive = false + } + + if (group.state === apiutil.READY && state === apiutil.PENDING) { + return apiutil.respond(res, 403, 'Forbidden (group is ready)') + } + + return checkSchedule(res, group, _class, group.owner.email, repetitions, group.privilege, + start, stop).then(function(checkingSuccessed) { + if (!checkingSuccessed) { + return false + } + if (name === group.name && + start.toISOString() === group.dates[0].start.toISOString() && + stop.toISOString() === group.dates[0].stop.toISOString() && + state === group.state && + _class === group.class && + repetitions === group.repetitions) { + return apiutil.respond(res, 200, 'Unchanged (group)', {group: {}}) + } + const duration = group.devices.length * (stop - start) * (repetitions + 1) + const dates = apiutil.computeGroupDates({start: start, stop: stop}, _class, repetitions) + + if (start < group.dates[0].start || + stop > group.dates[0].stop || + repetitions > group.repetitions || + _class !== group.class) { + return checkConflicts(id, group.devices, dates) + .then(function(conflicts) { + if (!conflicts.length) { + return updateUserGroup(group, { + name: name + , state: state + , class: _class + , isActive: isActive + , repetitions: repetitions + , dates: dates + , duration: duration + }) + } + return apiutil.respond(res, 409, 'Conflicts Information', {conflicts: conflicts}) + }) + } + return updateUserGroup(group, { + name: name + , state: state + , class: _class + , isActive: isActive + , repetitions: repetitions + , dates: dates + , duration: duration + }) + }) + }) + .catch(function(err) { + apiutil.internalError(res, 'Failed to update group: ', err.stack) + }) + .finally(function() { + lockutil.unlockGroup(lock) + }) +} + +function getGroupDevices(req, res) { + const id = req.swagger.params.id.value + const bookable = req.swagger.params.bookable.value + + dbapi.getUserGroup(req.user.email, id).then(function(group) { + if (!group) { + apiutil.respond(res, 404, 'Not Found (group)') + return + } + if (bookable) { + if (apiutil.isOriginGroup(group.class)) { + apiutil.respond(res, 400, 'Bad Request (group is not transient)') + return + } + if (req.user.privilege === apiutil.ADMIN && req.user.email !== group.owner.email) { + groupApiWrapper(group.owner.email, getGroupDevices, req, res) + return + } + dbapi.loadBookableDevices(req.user.groups.subscribed).then(function(devices) { + Promise.map(devices, function(device) { + return device.serial + }) + .then(function(serials) { + return checkConflicts(group.id, serials, group.dates) + .then(function(conflicts) { + let bookableSerials = serials + + conflicts.forEach(function(conflict) { + bookableSerials = _.difference(bookableSerials, conflict.devices) + }) + return bookableSerials + }) + }) + .then(function(bookableSerials) { + const deviceList = [] + + devices.forEach(function(device) { + if (bookableSerials.indexOf(device.serial) > -1) { + deviceList.push(apiutil.filterDevice(req, device)) + } + }) + apiutil.respond(res, 200, 'Devices Information', {devices: deviceList}) + }) + }) + } + else { + Promise.map(group.devices, function(serial) { + return getDevice(req, serial) + }) + .then(function(devices) { + apiutil.respond(res, 200, 'Devices Information', {devices: devices}) + }) + } + }) + .catch(function(err) { + apiutil.internalError(res, 'Failed to get group devices: ', err.stack) + }) +} + +module.exports = { + createGroup: createGroup + , updateGroup: updateGroup + , deleteGroup: deleteGroup + , deleteGroups: deleteGroups + , getGroup: getGroup + , getGroups: getGroups + , getGroupUser: getGroupUser + , getGroupUsers: getGroupUsers + , addGroupUser: addGroupUser + , addGroupUsers: addGroupUsers + , removeGroupUser: removeGroupUser + , removeGroupUsers: removeGroupUsers + , getGroupDevice: getGroupDevice + , getGroupDevices: getGroupDevices + , addGroupDevice: addGroupDevice + , addGroupDevices: addGroupDevices + , removeGroupDevice: removeGroupDevice + , removeGroupDevices: removeGroupDevices +} diff --git a/lib/units/api/controllers/user.js b/lib/units/api/controllers/user.js index 76522448..43953c2a 100644 --- a/lib/units/api/controllers/user.js +++ b/lib/units/api/controllers/user.js @@ -1,3 +1,7 @@ +/** +* Copyright © 2019 contains code contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0 +**/ + var util = require('util') var _ = require('lodash') @@ -12,6 +16,9 @@ var wire = require('../../../wire') var wireutil = require('../../../wire/util') var wirerouter = require('../../../wire/router') +const apiutil = require('../../../util/apiutil') +const jwtutil = require('../../../util/jwtutil') + var log = logger.createLogger('api:controllers:user') module.exports = { @@ -24,9 +31,16 @@ module.exports = { , remoteDisconnectUserDeviceBySerial: remoteDisconnectUserDeviceBySerial , getUserAccessTokens: getUserAccessTokens , addAdbPublicKey: addAdbPublicKey +, addUserDeviceV2: addUserDevice +, getAccessTokens: getAccessTokens +, getAccessToken: getAccessToken +, createAccessToken: createAccessToken +, deleteAccessToken: deleteAccessToken +, deleteAccessTokens: deleteAccessTokens } function getUser(req, res) { + // delete req.user.groups.lock res.json({ success: true , user: req.user @@ -53,6 +67,7 @@ function getUserDevices(req, res) { res.json({ success: true + , description: 'Controlled devices information' , devices: deviceList }) }) @@ -61,6 +76,7 @@ function getUserDevices(req, res) { log.error('Failed to load device list: ', err.stack) res.status(500).json({ success: false + , description: 'Internal Server Error' }) }) } @@ -69,113 +85,121 @@ function getUserDeviceBySerial(req, res) { var serial = req.swagger.params.serial.value var fields = req.swagger.params.fields.value - dbapi.loadDevice(serial) - .then(function(device) { - if (!device) { - return res.status(404).json({ - success: false - , description: 'Device not found' + dbapi.loadDevice(req.user.groups.subscribed, serial) + .then(function(cursor) { + cursor.next(function(err, device) { + if (err) { + return res.status(404).json({ + success: false + , description: 'Device not found' + }) + } + + datautil.normalize(device, req.user) + if (!deviceutil.isOwnedByUser(device, req.user)) { + return res.status(403).json({ + success: false + , description: 'Device is not owned by you' + }) + } + + var responseDevice = device + if (fields) { + responseDevice = _.pick(device, fields.split(',')) + } + + res.json({ + success: true + , description: 'Controlled device information' + , device: responseDevice }) - } - - datautil.normalize(device, req.user) - if (!deviceutil.isOwnedByUser(device, req.user)) { - return res.status(403).json({ - success: false - , description: 'Device is not owned by you' - }) - } - - var responseDevice = device - if (fields) { - responseDevice = _.pick(device, fields.split(',')) - } - - res.json({ - success: true - , device: responseDevice }) }) .catch(function(err) { log.error('Failed to load device "%s": ', req.params.serial, err.stack) res.status(500).json({ success: false + , description: 'Internal Server Error' }) }) } function addUserDevice(req, res) { - var serial = req.body.serial - var timeout = req.body.timeout || null + var serial = req.hasOwnProperty('body') ? req.body.serial : req.swagger.params.serial.value + var timeout = req.hasOwnProperty('body') ? req.body.timeout || + null : req.swagger.params.timeout.value || null - dbapi.loadDevice(serial) - .then(function(device) { - if (!device) { - return res.status(404).json({ - success: false - , description: 'Device not found' - }) - } - - datautil.normalize(device, req.user) - if (!deviceutil.isAddable(device, req.user)) { - return res.status(403).json({ - success: false - , description: 'Device is being used or not available' - }) - } - - // Timer will be called if no JoinGroupMessage is received till 5 seconds - var responseTimer = setTimeout(function() { - req.options.channelRouter.removeListener(wireutil.global, messageListener) - return res.status(504).json({ + dbapi.loadDevice(req.user.groups.subscribed, serial) + .then(function(cursor) { + cursor.next(function(err, device) { + if (err) { + return res.status(404).json({ success: false - , description: 'Device is not responding' - }) - }, 5000) + , description: 'Device not found' + }) + } - var messageListener = wirerouter() - .on(wire.JoinGroupMessage, function(channel, message) { - if (message.serial === serial && message.owner.email === req.user.email) { - clearTimeout(responseTimer) - req.options.channelRouter.removeListener(wireutil.global, messageListener) + datautil.normalize(device, req.user) + if (!deviceutil.isAddable(device, req.user)) { + return res.status(403).json({ + success: false + , description: 'Device is being used or not available' + }) + } - return res.json({ - success: true - , description: 'Device successfully added' - }) - } - }) - .handler() + // Timer will be called if no JoinGroupMessage is received till 5 seconds + var responseTimer = setTimeout(function() { + req.options.channelRouter.removeListener(wireutil.global, messageListener) + return res.status(504).json({ + success: false + , description: 'Device is not responding' + }) + }, 5000) - req.options.channelRouter.on(wireutil.global, messageListener) - var usage = 'automation' + var messageListener = wirerouter() + .on(wire.JoinGroupMessage, function(channel, message) { + if (message.serial === serial && message.owner.email === req.user.email) { + clearTimeout(responseTimer) + req.options.channelRouter.removeListener(wireutil.global, messageListener) - req.options.push.send([ - device.channel - , wireutil.envelope( - new wire.GroupMessage( - new wire.OwnerMessage( - req.user.email - , req.user.name - , req.user.group - ) - , timeout - , wireutil.toDeviceRequirements({ - serial: { - value: serial - , match: 'exact' + return res.json({ + success: true + , description: 'Device successfully added' + }) } }) - , usage + .handler() + + req.options.channelRouter.on(wireutil.global, messageListener) + var usage = 'automation' + + req.options.push.send([ + device.channel + , wireutil.envelope( + new wire.GroupMessage( + new wire.OwnerMessage( + req.user.email + , req.user.name + , req.user.group + ) + , timeout + , wireutil.toDeviceRequirements({ + serial: { + value: serial + , match: 'exact' + } + }) + , usage + ) ) - ) - ]) + ]) + }) }) .catch(function(err) { log.error('Failed to load device "%s": ', req.params.serial, err.stack) res.status(500).json({ success: false + , description: 'Internal Server Error' }) }) } @@ -183,66 +207,70 @@ function addUserDevice(req, res) { function deleteUserDeviceBySerial(req, res) { var serial = req.swagger.params.serial.value - dbapi.loadDevice(serial) - .then(function(device) { - if (!device) { - return res.status(404).json({ - success: false - , description: 'Device not found' - }) - } - - datautil.normalize(device, req.user) - if (!deviceutil.isOwnedByUser(device, req.user)) { - return res.status(403).json({ - success: false - , description: 'You cannot release this device. Not owned by you' - }) - } - - // Timer will be called if no JoinGroupMessage is received till 5 seconds - var responseTimer = setTimeout(function() { - req.options.channelRouter.removeListener(wireutil.global, messageListener) - return res.status(504).json({ + dbapi.loadDevice(req.user.groups.subscribed, serial) + .then(function(cursor) { + cursor.next(function(err, device) { + if (err) { + return res.status(404).json({ success: false - , description: 'Device is not responding' - }) - }, 5000) + , description: 'Device not found' + }) + } - var messageListener = wirerouter() - .on(wire.LeaveGroupMessage, function(channel, message) { - if (message.serial === serial && message.owner.email === req.user.email) { - clearTimeout(responseTimer) - req.options.channelRouter.removeListener(wireutil.global, messageListener) + datautil.normalize(device, req.user) + if (!deviceutil.isOwnedByUser(device, req.user)) { + return res.status(403).json({ + success: false + , description: 'You cannot release this device. Not owned by you' + }) + } - return res.json({ - success: true - , description: 'Device successfully removed' - }) - } - }) - .handler() + // Timer will be called if no JoinGroupMessage is received till 5 seconds + var responseTimer = setTimeout(function() { + req.options.channelRouter.removeListener(wireutil.global, messageListener) + return res.status(504).json({ + success: false + , description: 'Device is not responding' + }) + }, 5000) - req.options.channelRouter.on(wireutil.global, messageListener) + var messageListener = wirerouter() + .on(wire.LeaveGroupMessage, function(channel, message) { + if (message.serial === serial && + (message.owner.email === req.user.email || req.user.privilege === 'admin')) { + clearTimeout(responseTimer) + req.options.channelRouter.removeListener(wireutil.global, messageListener) - req.options.push.send([ - device.channel - , wireutil.envelope( - new wire.UngroupMessage( - wireutil.toDeviceRequirements({ - serial: { - value: serial - , match: 'exact' - } - }) + return res.json({ + success: true + , description: 'Device successfully removed' + }) + } + }) + .handler() + + req.options.channelRouter.on(wireutil.global, messageListener) + + req.options.push.send([ + device.channel + , wireutil.envelope( + new wire.UngroupMessage( + wireutil.toDeviceRequirements({ + serial: { + value: serial + , match: 'exact' + } + }) + ) ) - ) - ]) + ]) + }) }) .catch(function(err) { log.error('Failed to load device "%s": ', req.params.serial, err.stack) res.status(500).json({ success: false + , description: 'Internal Server Error' }) }) } @@ -250,65 +278,68 @@ function deleteUserDeviceBySerial(req, res) { function remoteConnectUserDeviceBySerial(req, res) { var serial = req.swagger.params.serial.value - dbapi.loadDevice(serial) - .then(function(device) { - if (!device) { - return res.status(404).json({ - success: false - , description: 'Device not found' - }) - } - - datautil.normalize(device, req.user) - if (!deviceutil.isOwnedByUser(device, req.user)) { - return res.status(403).json({ - success: false - , description: 'Device is not owned by you or is not available' - }) - } - - var responseChannel = 'txn_' + uuid.v4() - req.options.sub.subscribe(responseChannel) - - // Timer will be called if no JoinGroupMessage is received till 5 seconds - var timer = setTimeout(function() { - req.options.channelRouter.removeListener(responseChannel, messageListener) - req.options.sub.unsubscribe(responseChannel) - return res.status(504).json({ + dbapi.loadDevice(req.user.groups.subscribed, serial) + .then(function(cursor) { + cursor.next(function(err, device) { + if (err) { + return res.status(404).json({ success: false - , description: 'Device is not responding' - }) - }, 5000) + , description: 'Device not found' + }) + } - var messageListener = wirerouter() - .on(wire.ConnectStartedMessage, function(channel, message) { - if (message.serial === serial) { - clearTimeout(timer) - req.options.sub.unsubscribe(responseChannel) - req.options.channelRouter.removeListener(responseChannel, messageListener) + datautil.normalize(device, req.user) + if (!deviceutil.isOwnedByUser(device, req.user)) { + return res.status(403).json({ + success: false + , description: 'Device is not owned by you or is not available' + }) + } - return res.json({ - success: true - , remoteConnectUrl: message.url - }) - } - }) - .handler() + var responseChannel = 'txn_' + uuid.v4() + req.options.sub.subscribe(responseChannel) - req.options.channelRouter.on(responseChannel, messageListener) + // Timer will be called if no JoinGroupMessage is received till 5 seconds + var timer = setTimeout(function() { + req.options.channelRouter.removeListener(responseChannel, messageListener) + req.options.sub.unsubscribe(responseChannel) + return res.status(504).json({ + success: false + , description: 'Device is not responding' + }) + }, 5000) - req.options.push.send([ - device.channel - , wireutil.transaction( - responseChannel - , new wire.ConnectStartMessage() - ) - ]) + var messageListener = wirerouter() + .on(wire.ConnectStartedMessage, function(channel, message) { + if (message.serial === serial) { + clearTimeout(timer) + req.options.sub.unsubscribe(responseChannel) + req.options.channelRouter.removeListener(responseChannel, messageListener) + return res.json({ + success: true + , description: 'Remote connection is enabled' + , remoteConnectUrl: message.url + }) + } + }) + .handler() + + req.options.channelRouter.on(responseChannel, messageListener) + + req.options.push.send([ + device.channel + , wireutil.transaction( + responseChannel + , new wire.ConnectStartMessage() + ) + ]) + }) }) .catch(function(err) { log.error('Failed to load device "%s": ', req.params.serial, err.stack) res.status(500).json({ success: false + , description: 'Internal Server Error' }) }) } @@ -316,65 +347,67 @@ function remoteConnectUserDeviceBySerial(req, res) { function remoteDisconnectUserDeviceBySerial(req, res) { var serial = req.swagger.params.serial.value - dbapi.loadDevice(serial) - .then(function(device) { - if (!device) { - return res.status(404).json({ - success: false - , description: 'Device not found' - }) - } + dbapi.loadDevice(req.user.groups.subscribed, serial) + .then(function(cursor) { + cursor.next(function(err, device) { + if (err) { + return res.status(404).json({ + success: false + , description: 'Device not found' + }) + } - datautil.normalize(device, req.user) - if (!deviceutil.isOwnedByUser(device, req.user)) { - return res.status(403).json({ - success: false - , description: 'Device is not owned by you or is not available' - }) - } + datautil.normalize(device, req.user) + if (!deviceutil.isOwnedByUser(device, req.user)) { + return res.status(403).json({ + success: false + , description: 'Device is not owned by you or is not available' + }) + } - var responseChannel = 'txn_' + uuid.v4() - req.options.sub.subscribe(responseChannel) + var responseChannel = 'txn_' + uuid.v4() + req.options.sub.subscribe(responseChannel) - // Timer will be called if no JoinGroupMessage is received till 5 seconds - var timer = setTimeout(function() { - req.options.channelRouter.removeListener(responseChannel, messageListener) - req.options.sub.unsubscribe(responseChannel) - return res.status(504).json({ + // Timer will be called if no JoinGroupMessage is received till 5 seconds + var timer = setTimeout(function() { + req.options.channelRouter.removeListener(responseChannel, messageListener) + req.options.sub.unsubscribe(responseChannel) + return res.status(504).json({ success: false , description: 'Device is not responding' - }) - }, 5000) + }) + }, 5000) - var messageListener = wirerouter() - .on(wire.ConnectStoppedMessage, function(channel, message) { - if (message.serial === serial) { - clearTimeout(timer) - req.options.sub.unsubscribe(responseChannel) - req.options.channelRouter.removeListener(responseChannel, messageListener) + var messageListener = wirerouter() + .on(wire.ConnectStoppedMessage, function(channel, message) { + if (message.serial === serial) { + clearTimeout(timer) + req.options.sub.unsubscribe(responseChannel) + req.options.channelRouter.removeListener(responseChannel, messageListener) + return res.json({ + success: true + , description: 'Device remote disconnected successfully' + }) + } + }) + .handler() - return res.json({ - success: true - , description: 'Device remote disconnected successfully' - }) - } - }) - .handler() + req.options.channelRouter.on(responseChannel, messageListener) - req.options.channelRouter.on(responseChannel, messageListener) - - req.options.push.send([ - device.channel - , wireutil.transaction( - responseChannel - , new wire.ConnectStopMessage() - ) - ]) + req.options.push.send([ + device.channel + , wireutil.transaction( + responseChannel + , new wire.ConnectStopMessage() + ) + ]) + }) }) .catch(function(err) { log.error('Failed to load device "%s": ', req.params.serial, err.stack) res.status(500).json({ success: false + , description: 'Internal Server Error' }) }) } @@ -454,3 +487,111 @@ function addAdbPublicKey(req, res) { }) }) } + +function getAccessToken(req, res) { + const id = req.swagger.params.id.value + + dbapi.loadAccessToken(id).then(function(token) { + if (!token || token.email !== req.user.email) { + apiutil.respond(res, 404, 'Not Found (access token)') + } + else { + apiutil.respond(res, 200, 'Access Token Information', { + token: apiutil.publishAccessToken(token) + }) + } + }) + .catch(function(err) { + apiutil.internalError(res, 'Failed to delete access token "%s": ', id, err.stack) + }) +} + +function getAccessTokens(req, res) { + dbapi.loadAccessTokens(req.user.email).then(function(cursor) { + Promise.promisify(cursor.toArray, cursor)().then(function(tokens) { + const tokenList = [] + + tokens.forEach(function(token) { + tokenList.push(apiutil.publishAccessToken(token)) + }) + apiutil.respond(res, 200, 'Access Tokens Information', {tokens: tokenList}) + }) + }) + .catch(function(err) { + apiutil.internalError(res, 'Failed to get access tokens: ', err.stack) + }) +} + +function createAccessToken(req, res) { + const title = req.swagger.params.title.value + const jwt = jwtutil.encode({ + payload: { + email: req.user.email + , name: req.user.name + } + , secret: req.options.secret + }) + const id = util.format('%s-%s', uuid.v4(), uuid.v4()).replace(/-/g, '') + + dbapi.saveUserAccessToken(req.user.email, { + title: title + , id: id + , jwt: jwt + }) + .then(function(stats) { + req.options.pushdev.send([ + req.user.group + , wireutil.envelope(new wire.UpdateAccessTokenMessage()) + ]) + apiutil.respond(res, 201, 'Created (access token)', + {token: apiutil.publishAccessToken(stats.changes[0].new_val)}) + }) + .catch(function(err) { + apiutil.internalError(res, 'Failed to create access token "%s": ', title, err.stack) + }) +} + +function deleteAccessTokens(req, res) { + dbapi.removeUserAccessTokens(req.user.email).then(function(stats) { + if (!stats.deleted) { + apiutil.respond(res, 200, 'Unchanged (access tokens)') + } + else { + req.options.pushdev.send([ + req.user.group + , wireutil.envelope(new wire.UpdateAccessTokenMessage()) + ]) + apiutil.respond(res, 200, 'Deleted (access tokens)') + } + }) + .catch(function(err) { + apiutil.internalError(res, 'Failed to delete access tokens: ', err.stack) + }) +} + +function deleteAccessToken(req, res) { + const id = req.swagger.params.id.value + + dbapi.loadAccessToken(id).then(function(token) { + if (!token || token.email !== req.user.email) { + apiutil.respond(res, 404, 'Not Found (access token)') + } + else { + dbapi.removeAccessToken(id).then(function(stats) { + if (!stats.deleted) { + apiutil.respond(res, 404, 'Not Found (access token)') + } + else { + req.options.pushdev.send([ + req.user.group + , wireutil.envelope(new wire.UpdateAccessTokenMessage()) + ]) + apiutil.respond(res, 200, 'Deleted (access token)') + } + }) + } + }) + .catch(function(err) { + apiutil.internalError(res, 'Failed to delete access token "%s": ', id, err.stack) + }) +} diff --git a/lib/units/api/controllers/users.js b/lib/units/api/controllers/users.js new file mode 100644 index 00000000..7820710d --- /dev/null +++ b/lib/units/api/controllers/users.js @@ -0,0 +1,398 @@ +/** +* Copyright © 2019 code initially contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0 +**/ + +const dbapi = require('../../../db/api') +const _ = require('lodash') +const apiutil = require('../../../util/apiutil') +const lockutil = require('../../../util/lockutil') +const Promise = require('bluebird') +const wire = require('../../../wire') +const wireutil = require('../../../wire/util') +const userapi = require('./user') + +/* --------------------------------- PRIVATE FUNCTIONS --------------------------------------- */ + +function userApiWrapper(fn, req, res) { + const email = req.swagger.params.email.value + + dbapi.loadUser(email).then(function(user) { + if (!user) { + apiutil.respond(res, 404, 'Not Found (user)') + } + else { + req.user = user + fn(req, res) + } + }) + .catch(function(err) { + apiutil.internalError(res, 'Failed to wrap "%s": ', fn.name, err.stack) + }) +} + +function getPublishedUser(user, userEmail, adminEmail, fields) { + let publishedUser = apiutil.publishUser(user) + if (userEmail !== adminEmail) { + publishedUser = _.pick(user, 'email', 'name', 'privilege') + } + if (fields) { + publishedUser = _.pick(publishedUser, fields.split(',')) + } + return publishedUser +} + +function removeUser(email, req, res) { + const groupOwnerState = req.swagger.params.groupOwner.value + const anyGroupOwnerState = typeof groupOwnerState === 'undefined' + const lock = {} + + function removeGroupUser(owner, id) { + const lock = {} + + return dbapi.lockGroupByOwner(owner, id).then(function(stats) { + if (!stats.replaced) { + return apiutil.lightComputeStats(res, stats) + } + lock.group = stats.changes[0].new_val + + return owner === email ? + dbapi.deleteUserGroup(id) : + dbapi.removeGroupUser(id, email) + }) + .finally(function() { + lockutil.unlockGroup(lock) + }) + } + + function deleteUserInDatabase(channel) { + return dbapi.removeUserAccessTokens(email).then(function() { + return dbapi.deleteUser(email).then(function() { + req.options.pushdev.send([ + channel + , wireutil.envelope(new wire.DeleteUserMessage( + email + )) + ]) + return 'deleted' + }) + }) + } + + function computeUserGroupOwnership(groups) { + if (anyGroupOwnerState) { + return Promise.resolve(true) + } + return Promise.map(groups, function(group) { + if (!groupOwnerState && group.owner.email === email) { + return Promise.reject('filtered') + } + return !groupOwnerState || group.owner.email === email + }) + .then(function(results) { + return _.without(results, false).length > 0 + }) + .catch(function(err) { + if (err === 'filtered') { + return false + } + throw err + }) + } + + if (req.user.email === email) { + return Promise.resolve('forbidden') + } + return dbapi.lockUser(email).then(function(stats) { + if (!stats.replaced) { + return apiutil.lightComputeStats(res, stats) + } + const user = lock.user = stats.changes[0].new_val + + return dbapi.getGroupsByUser(user.email).then(function(groups) { + return computeUserGroupOwnership(groups).then(function(doContinue) { + if (!doContinue) { + return 'unchanged' + } + return Promise.each(groups, function(group) { + return removeGroupUser(group.owner.email, group.id) + }) + .then(function() { + return deleteUserInDatabase(user.group) + }) + }) + }) + }) + .finally(function() { + lockutil.unlockUser(lock) + }) +} + +/* --------------------------------- PUBLIC FUNCTIONS --------------------------------------- */ + +function getUserInfo(req, email) { + const fields = req.swagger.params.fields.value + + return dbapi.loadUser(email).then(function(user) { + if (user) { + return dbapi.getRootGroup().then(function(group) { + return getPublishedUser(user, req.user.email, group.owner.email, fields) + }) + } + return false + }) +} + +function updateUserGroupsQuotas(req, res) { + const email = req.swagger.params.email.value + const duration = + typeof req.swagger.params.duration.value !== 'undefined' ? + req.swagger.params.duration.value : + null + const number = + typeof req.swagger.params.number.value !== 'undefined' ? + req.swagger.params.number.value : + null + const repetitions = + typeof req.swagger.params.repetitions.value !== 'undefined' ? + req.swagger.params.repetitions.value : + null + const lock = {} + + lockutil.lockUser(email, res, lock).then(function(lockingSuccessed) { + if (lockingSuccessed) { + return dbapi.updateUserGroupsQuotas(email, duration, number, repetitions) + .then(function(stats) { + if (stats.replaced) { + return apiutil.respond(res, 200, 'Updated (user quotas)', { + user: apiutil.publishUser(stats.changes[0].new_val) + }) + } + if ((duration === null || duration === lock.user.groups.quotas.allocated.duration) && + (number === null || number === lock.user.groups.quotas.allocated.number) && + (repetitions === null || repetitions === lock.user.groups.quotas.repetitions) + ) { + return apiutil.respond(res, 200, 'Unchanged (user quotas)', {user: {}}) + } + return apiutil.respond( + res + , 400 + , 'Bad Request (quotas must be >= actual consumed resources)') + }) + } + return false + }) + .catch(function(err) { + apiutil.internalError(res, 'Failed to update user groups quotas: ', err.stack) + }) + .finally(function() { + lockutil.unlockUser(lock) + }) +} + +function updateDefaultUserGroupsQuotas(req, res) { + const duration = + typeof req.swagger.params.duration.value !== 'undefined' ? + req.swagger.params.duration.value : + null + const number = + typeof req.swagger.params.number.value !== 'undefined' ? + req.swagger.params.number.value : + null + const repetitions = + typeof req.swagger.params.repetitions.value !== 'undefined' ? + req.swagger.params.repetitions.value : + null + const lock = {} + + lockutil.lockUser(req.user.email, res, lock).then(function(lockingSuccessed) { + if (lockingSuccessed) { + return dbapi.updateDefaultUserGroupsQuotas(req.user.email, duration, number, repetitions) + .then(function(stats) { + if (stats.replaced) { + return apiutil.respond(res, 200, 'Updated (user default quotas)', { + user: apiutil.publishUser(stats.changes[0].new_val) + }) + } + return apiutil.respond(res, 200, 'Unchanged (user default quotas)', {user: {}}) + }) + } + return false + }) + .catch(function(err) { + apiutil.internalError(res, 'Failed to update default user groups quotas: ', err.stack) + }) + .finally(function() { + lockutil.unlockUser(lock) + }) +} + +function getUserByEmail(req, res) { + const email = req.swagger.params.email.value + + getUserInfo(req, email).then(function(user) { + if (user) { + apiutil.respond(res, 200, 'User Information', {user: user}) + } + else { + apiutil.respond(res, 404, 'Not Found (user)') + } + }) + .catch(function(err) { + apiutil.internalError(res, 'Failed to get user: ', err.stack) + }) +} + +function getUsers(req, res) { + const fields = req.swagger.params.fields.value + + dbapi.getUsers().then(function(users) { + return dbapi.getRootGroup().then(function(group) { + apiutil.respond(res, 200, 'Users Information', { + users: users.map(function(user) { + return getPublishedUser(user, req.user.email, group.owner.email, fields) + }) + }) + }) + }) + .catch(function(err) { + apiutil.internalError(res, 'Failed to get users: ', err.stack) + }) +} + +function createUser(req, res) { + const email = req.swagger.params.email.value + const name = req.swagger.params.name.value + + dbapi.createUser(email, name, req.user.ip).then(function(stats) { + if (!stats.inserted) { + apiutil.respond(res, 403, 'Forbidden (user already exists)') + } + else { + apiutil.respond(res, 201, 'Created (user)', { + user: apiutil.publishUser(stats.changes[0].new_val) + }) + } + }) + .catch(function(err) { + apiutil.internalError(res, 'Failed to create user: ', err.stack) + }) +} + +function deleteUsers(req, res) { + const emails = apiutil.getBodyParameter(req.body, 'emails') + const target = apiutil.getQueryParameter(req.swagger.params.redirected) ? 'user' : 'users' + + function removeUsers(emails) { + let results = [] + + return Promise.each(emails, function(email) { + return removeUser(email, req, res).then(function(result) { + results.push(result) + }) + }) + .then(function() { + results = _.without(results, 'unchanged') + if (!results.length) { + return apiutil.respond(res, 200, `Unchanged (${target})`) + } + results = _.without(results, 'not found') + if (!results.length) { + return apiutil.respond(res, 404, `Not Found (${target})`) + } + results = _.without(results, 'forbidden') + if (!results.length) { + apiutil.respond(res, 403, `Forbidden (${target})`) + } + return apiutil.respond(res, 200, `Deleted (${target})`) + }) + .catch(function(err) { + if (err !== 'busy') { + throw err + } + }) + } + + (function() { + if (typeof emails === 'undefined') { + return dbapi.getEmails().then(function(emails) { + return removeUsers(emails) + }) + } + else { + return removeUsers(_.without(emails.split(','), '')) + } + })() + .catch(function(err) { + apiutil.internalError(res, 'Failed to delete ${target}: ', err.stack) + }) +} + +function deleteUser(req, res) { + apiutil.redirectApiWrapper('email', deleteUsers, req, res) +} + +function createUserAccessToken(req, res) { + userApiWrapper(userapi.createAccessToken, req, res) +} + +function deleteUserAccessToken(req, res) { + userApiWrapper(userapi.deleteAccessToken, req, res) +} + +function deleteUserAccessTokens(req, res) { + userApiWrapper(userapi.deleteAccessTokens, req, res) +} + +function getUserAccessToken(req, res) { + userApiWrapper(userapi.getAccessToken, req, res) +} + +function getUserAccessTokens(req, res) { + userApiWrapper(userapi.getAccessTokens, req, res) +} + +function getUserDevices(req, res) { + userApiWrapper(userapi.getUserDevices, req, res) +} + +function getUserDevice(req, res) { + userApiWrapper(userapi.getUserDeviceBySerial, req, res) +} + +function addUserDevice(req, res) { + userApiWrapper(userapi.addUserDevice, req, res) +} + +function deleteUserDevice(req, res) { + userApiWrapper(userapi.deleteUserDeviceBySerial, req, res) +} + +function remoteConnectUserDevice(req, res) { + userApiWrapper(userapi.remoteConnectUserDeviceBySerial, req, res) +} + +function remoteDisconnectUserDevice(req, res) { + userApiWrapper(userapi.remoteDisconnectUserDeviceBySerial, req, res) +} + +module.exports = { + updateUserGroupsQuotas: updateUserGroupsQuotas + , updateDefaultUserGroupsQuotas: updateDefaultUserGroupsQuotas + , getUsers: getUsers + , getUserByEmail: getUserByEmail + , getUserInfo: getUserInfo + , createUser: createUser + , deleteUser: deleteUser + , deleteUsers: deleteUsers + , createUserAccessToken: createUserAccessToken + , deleteUserAccessToken: deleteUserAccessToken + , deleteUserAccessTokens: deleteUserAccessTokens + , getUserAccessTokensV2: getUserAccessTokens + , getUserAccessToken: getUserAccessToken + , getUserDevicesV2: getUserDevices + , getUserDevice: getUserDevice + , addUserDeviceV3: addUserDevice + , deleteUserDevice: deleteUserDevice + , remoteConnectUserDevice: remoteConnectUserDevice + , remoteDisconnectUserDevice: remoteDisconnectUserDevice +} diff --git a/lib/units/api/helpers/securityHandlers.js b/lib/units/api/helpers/securityHandlers.js index 99eedd1c..600be491 100644 --- a/lib/units/api/helpers/securityHandlers.js +++ b/lib/units/api/helpers/securityHandlers.js @@ -1,7 +1,12 @@ +/** +* Copyright © 2019 contains code contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0 +**/ + var dbapi = require('../../../db/api') var jwtutil = require('../../../util/jwtutil') var urlutil = require('../../../util/urlutil') var logger = require('../../../util/logger') +const apiutil = require('../../../util/apiutil') var log = logger.createLogger('api:helpers:securityHandlers') @@ -47,17 +52,27 @@ function accessTokenAuth(req, res, next) { if (!data) { return res.status(500).json({ success: false + , description: 'Internal Server Error' }) } + dbapi.loadUser(data.email) .then(function(user) { if (user) { + if (user.privilege === apiutil.USER && + req.swagger.operation.definition.tags.indexOf('admin') > -1) { + return res.status(403).json({ + success: false + , description: 'Forbidden: privileged operation (admin)' + }) + } req.user = user next() } else { return res.status(500).json({ success: false + , description: 'Internal Server Error' }) } }) @@ -86,6 +101,7 @@ function accessTokenAuth(req, res, next) { else { return res.status(500).json({ success: false + , description: 'Internal Server Error' }) } }) diff --git a/lib/units/api/index.js b/lib/units/api/index.js index 76d8cad7..14929cdd 100644 --- a/lib/units/api/index.js +++ b/lib/units/api/index.js @@ -1,3 +1,7 @@ +/** +* Copyright © 2019 contains code contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0 +**/ + var http = require('http') var path = require('path') var events = require('events') @@ -52,16 +56,51 @@ module.exports = function(options) { lifecycle.fatal() }) + var pushdev = zmqutil.socket('push') + Promise.map(options.endpoints.pushdev, function(endpoint) { + return srv.resolve(endpoint).then(function(records) { + return srv.attempt(records, function(record) { + log.info('Sending output to "%s"', record.url) + pushdev.connect(record.url) + return Promise.resolve(true) + }) + }) + }) + .catch(function(err) { + log.fatal('Unable to connect to pushdev endpoint', err) + lifecycle.fatal() + }) + + var subdev = zmqutil.socket('sub') + Promise.map(options.endpoints.subdev, function(endpoint) { + return srv.resolve(endpoint).then(function(records) { + return srv.attempt(records, function(record) { + log.info('Receiving input from "%s"', record.url) + subdev.connect(record.url) + return Promise.resolve(true) + }) + }) + }) + .catch(function(err) { + log.fatal('Unable to connect to subdev endpoint', err) + lifecycle.fatal() + }) + // Establish always-on channels ;[wireutil.global].forEach(function(channel) { log.info('Subscribing to permanent channel "%s"', channel) sub.subscribe(channel) + subdev.subscribe(channel) }) sub.on('message', function(channel, data) { channelRouter.emit(channel.toString(), channel, data) }) + subdev.on('message', function(channel, data) { + channelRouter.emit(channel.toString(), channel, data) + }) + // Swagger Express Config var config = { appRoot: __dirname @@ -83,6 +122,8 @@ module.exports = function(options) { push: push , sub: sub , channelRouter: channelRouter + , pushdev: pushdev + , subdev: subdev }) req.options = reqOptions @@ -96,7 +137,7 @@ module.exports = function(options) { })) lifecycle.observe(function() { - [push, sub].forEach(function(sock) { + [push, sub, pushdev, subdev].forEach(function(sock) { try { sock.close() } diff --git a/lib/units/api/swagger/api_v1.yaml b/lib/units/api/swagger/api_v1.yaml index 1d04da03..e191fc8d 100644 --- a/lib/units/api/swagger/api_v1.yaml +++ b/lib/units/api/swagger/api_v1.yaml @@ -1,6 +1,10 @@ +## +# Copyright © 2019 contains code contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0 +## + swagger: "2.0" info: - version: "2.3.0" + version: "2.4.0" title: Smartphone Test Farm description: Control and manages real Smartphone devices from browser and restful apis license: @@ -22,9 +26,1271 @@ produces: tags: - name: user description: User Operations + - name: users + description: Users Operations - name: devices description: Device Operations + - name: groups + description: Groups Operations + - name: admin + description: Privileged Operations paths: + /groups: + x-swagger-router-controller: groups + get: + summary: Gets groups + description: Returns the groups to which you belong + operationId: getGroups + tags: + - groups + parameters: + - name: fields + in: query + description: Comma-seperated list of fields; only listed fields will be returned in response + required: false + type: string + - name: owner + in: query + description: Selects the groups for which you are the owner (true) or a simple member (false); note that by not providing this parameter, it means all groups to which you belong are selected + required: false + type: boolean + responses: + "200": + description: Groups information + schema: + $ref: "#/definitions/GroupListResponse" + default: + description: > + Unexpected Error: + * 401: Unauthorized => bad credentials + * 500: Internal Server Error + schema: + $ref: "#/definitions/UnexpectedErrorResponse" + security: + - accessTokenAuth: [] + delete: + summary: Removes groups + description: Removes the groups owned by you + operationId: deleteGroups + tags: + - groups + parameters: + - name: groups + in: body + description: Groups to remove as a comma-separated list of group identifiers; note that by not providing this parameter it means all groups owned by you are removed + required: false + schema: + $ref: "#/definitions/GroupsPayload" + responses: + "200": + description: Groups removing is OK (or no groups to remove) + schema: + $ref: "#/definitions/Response" + default: + description: > + Unexpected Error: + * 401: Unauthorized => bad credentials + * 403: Forbidden => a device is currently booked or unremovable built-in group + * 404: Not Found => unknown groups + * 500: Internal Server Error + * 503: Service Unavailable => server too busy or a lock on a resource is pending + schema: + $ref: "#/definitions/UnexpectedErrorResponse" + security: + - accessTokenAuth: [] + post: + summary: Creates a group + description: Creates a group with you as owner + operationId: createGroup + tags: + - groups + parameters: + - name: group + in: body + description: Group properties; at least one property is required + required: true + schema: + $ref: "#/definitions/GroupPayload" + responses: + "201": + description: Group information + schema: + $ref: "#/definitions/GroupResponse" + default: + description: > + Unexpected Error: + * 400: Bad Request => invalid format or semantic of properties + * 401: Unauthorized => bad credentials + * 500: Internal Server Error + schema: + $ref: "#/definitions/UnexpectedErrorResponse" + security: + - accessTokenAuth: [] + /groups/{id}: + x-swagger-router-controller: groups + get: + summary: Gets a group + description: Returns a group to which you belong + operationId: getGroup + tags: + - groups + parameters: + - name: id + in: path + description: Group identifier + required: true + type: string + - name: fields + in: query + description: Comma-separated list of group fields; only listed fields will be returned in response + required: false + type: string + responses: + "200": + description: Group information + schema: + $ref: "#/definitions/GroupResponse" + default: + description: > + Unexpected Error: + * 401: Unauthorized => bad credentials + * 404: Not Found => unknown group + * 500: Internal Server Error + schema: + $ref: "#/definitions/UnexpectedErrorResponse" + security: + - accessTokenAuth: [] + put: + summary: Updates a group + description: Updates a group owned by you + operationId: updateGroup + tags: + - groups + parameters: + - name: id + in: path + description: Group identifier + required: true + type: string + - name: group + in: body + description: Group properties; at least one property is required + required: true + schema: + $ref: "#/definitions/GroupPayload" + responses: + "200": + description: Group information (an empty group is returned if no change is made) + schema: + $ref: "#/definitions/GroupResponse" + "409": + description: Conflicts information + schema: + $ref: "#/definitions/ConflictsResponse" + default: + description: > + Unexpected Error: + * 400: Bad Request => invalid format or semantic of properties + * 401: Unauthorized => bad credentials + * 403: Forbidden => quota is reached or unauthorized property + * 404: Not Found => unknown group + * 500: Internal Server Error + * 503: Service Unavailable => server too busy or a lock on a resource is pending + schema: + $ref: "#/definitions/UnexpectedErrorResponse" + security: + - accessTokenAuth: [] + delete: + summary: Removes a group + description: Removes a group owned by you + operationId: deleteGroup + tags: + - groups + parameters: + - name: id + in: path + description: Group identifier + required: true + type: string + responses: + "200": + description: Group removing is OK + schema: + $ref: "#/definitions/Response" + default: + description: > + Unexpected Error: + * 401: Unauthorized => bad credentials + * 403: Forbidden => a device is currently booked or unremovable built-in group + * 404: Not Found => unknown group + * 500: Internal Server Error + * 503: Service Unavailable => server too busy or a lock on a resource is pending + schema: + $ref: "#/definitions/UnexpectedErrorResponse" + security: + - accessTokenAuth: [] + /groups/{id}/devices: + x-swagger-router-controller: groups + get: + summary: Gets the devices of a group + description: Returns the devices of the group to which you belong + operationId: getGroupDevices + tags: + - groups + parameters: + - name: id + in: path + description: Group identifier + required: true + type: string + - name: bookable + in: query + description: Selects devices which could be potentially booked by that transient group (true => irrelevant for an origin group!), or selects all devices of the group (false); note that by not providing this parameter all devices of the group are selected + type: boolean + default: false + - name: fields + in: query + description: Comma-separated list of device fields; only listed fields will be returned in response + required: false + type: string + responses: + "200": + description: Group devices information + schema: + $ref: "#/definitions/DeviceListResponse" + default: + description: > + Unexpected Error: + * 400: Bad request => group is not transient + * 401: Unauthorized => bad credentials + * 404: Not Found => unknown group + * 500: Internal Server Error + schema: + $ref: "#/definitions/UnexpectedErrorResponse" + security: + - accessTokenAuth: [] + put: + summary: Adds devices into a transient group + description: Adds devices into a transient group owned by you + operationId: addGroupDevices + tags: + - groups + parameters: + - name: id + in: path + description: Group identifier + required: true + type: string + - name: devices + in: body + description: Devices to add as a comma-separated list of serials; note that by not providing this parameter it means all devices which could be potentially booked by that transient group are added into the latter + required: false + schema: + $ref: "#/definitions/DevicesPayload" + responses: + "200": + description: Group information (an empty group is returned if no change is made) + schema: + $ref: "#/definitions/GroupResponse" + "409": + description: Conflicts information + schema: + $ref: "#/definitions/ConflictsResponse" + default: + description: > + Unexpected Error: + * 400: Bad Request => group is not transient + * 401: Unauthorized => bad credentials + * 403: Forbidden => quota is reached + * 404: Not Found => unknown group or devices + * 500: Internal Server Error + * 503: Service Unavailable => server too busy or a lock on a resource is pending + schema: + $ref: "#/definitions/UnexpectedErrorResponse" + security: + - accessTokenAuth: [] + delete: + summary: Removes devices from a transient group + description: Removes devices from a transient group owned by you + operationId: removeGroupDevices + tags: + - groups + parameters: + - name: id + in: path + description: Group identifier + required: true + type: string + - name: devices + in: body + description: Devices to remove as a comma-separated list of serials; note that by not providing this parameter it means all devices of the group are removed + required: false + schema: + $ref: "#/definitions/DevicesPayload" + responses: + "200": + description: Group information (an empty group is returned if no change is made) + schema: + $ref: "#/definitions/GroupResponse" + default: + description: > + Unexpected Error: + * 400: Bad Request => group is not transient + * 401: Unauthorized => bad credentials + * 404: Not Found => unknown group or devices + * 500: Internal Server Error + * 503: Service Unavailable => server too busy or a lock on a resource is pending + schema: + $ref: "#/definitions/UnexpectedErrorResponse" + security: + - accessTokenAuth: [] + /groups/{id}/devices/{serial}: + x-swagger-router-controller: groups + get: + summary: Gets a device of a group + description: Returns a device of a group to which you belong + operationId: getGroupDevice + tags: + - groups + parameters: + - name: id + in: path + description: Group identifier + required: true + type: string + - name: serial + in: path + description: Device identifier (serial) + required: true + type: string + - name: fields + in: query + description: Comma-separated list of device fields; only listed fields will be returned in response + required: false + type: string + responses: + "200": + description: Group device information + schema: + $ref: "#/definitions/DeviceResponse" + default: + description: > + Unexpected Error: + * 401: Unauthorized => bad credentials + * 404: Not Found => unknown group or device + * 500: Internal Server Error + schema: + $ref: "#/definitions/UnexpectedErrorResponse" + security: + - accessTokenAuth: [] + put: + summary: Adds a device into a transient group + description: Adds a device into a transient group owned by you + operationId: addGroupDevice + tags: + - groups + parameters: + - name: id + in: path + description: Group identifier + required: true + type: string + - name: serial + in: path + description: Device identifier (serial) + required: true + type: string + responses: + "200": + description: Group information (an empty group is returned if no change is made) + schema: + $ref: "#/definitions/GroupResponse" + "409": + description: Conflicts information + schema: + $ref: "#/definitions/ConflictsResponse" + default: + description: > + Unexpected Error: + * 400: Bad Request => group is not transient + * 401: Unauthorized => bad credentials + * 403: Forbidden => quota is reached + * 404: Not Found => unknown group or device + * 500: Internal Server Error + * 503: Service Unavailable => server too busy or a lock on a resource is pending + schema: + $ref: "#/definitions/UnexpectedErrorResponse" + security: + - accessTokenAuth: [] + delete: + summary: Removes a device from a transient group + description: Removes a device from a transient group owned by you + operationId: removeGroupDevice + tags: + - groups + parameters: + - name: id + in: path + description: Group identifier + required: true + type: string + - name: serial + in: path + description: Device identifier (serial) + required: true + type: string + responses: + "200": + description: Group information (an empty group is returned if no change is made) + schema: + $ref: "#/definitions/GroupResponse" + default: + description: > + Unexpected Error: + * 400: Bad Request => group is not transient + * 401: Unauthorized => bad credentials + * 404: Not Found => unknown group or device + * 500: Internal Server Error + * 503: Service Unavailable => server too busy or a lock on a resource is pending + schema: + $ref: "#/definitions/UnexpectedErrorResponse" + security: + - accessTokenAuth: [] + /groups/{id}/users: + x-swagger-router-controller: groups + get: + summary: Gets the users of a group + description: Gets the users of a group to which you belong; if you are the administrator user then all user fields are returned, otherwise only 'email', 'name' and 'privilege' user fields are returned + operationId: getGroupUsers + tags: + - groups + parameters: + - name: id + in: path + description: Group identifier + required: true + type: string + - name: fields + in: query + description: Comma-separated list of user fields; only listed fields will be returned in response + required: false + type: string + responses: + "200": + description: Group users information + schema: + $ref: "#/definitions/UserListResponse" + default: + description: > + Unexpected Error: + * 401: Unauthorized => bad credentials + * 404: Not Found => unknown group + * 500: Internal Server Error + schema: + $ref: "#/definitions/UnexpectedErrorResponse" + security: + - accessTokenAuth: [] + put: + summary: Adds users into a group + description: Adds users into a group owned by you + operationId: addGroupUsers + tags: + - groups + parameters: + - name: id + in: path + description: Group identifier + required: true + type: string + - name: users + in: body + description: Users to add as a comma-separated list of emails; note that by not providing this parameter it means all available users are added into the group + required: false + schema: + $ref: "#/definitions/UsersPayload" + responses: + "200": + description: Group information (an empty group is returned if no change is made) + schema: + $ref: "#/definitions/GroupResponse" + default: + description: > + Unexpected Error: + * 401: Unauthorized => bad credentials + * 404: Not Found => unknown group or device or users + * 500: Internal Server Error + * 503: Service Unavailable => server too busy or a lock on a resource is pending + schema: + $ref: "#/definitions/UnexpectedErrorResponse" + security: + - accessTokenAuth: [] + delete: + summary: Removes users from a group + description: Removes users from a group owned by you + operationId: removeGroupUsers + tags: + - groups + parameters: + - name: id + in: path + description: Group identifier + required: true + type: string + - name: users + in: body + description: Users to remove as a comma-separated list of emails; note that by not providing this parameter it means all users of the group are removed + required: false + schema: + $ref: "#/definitions/UsersPayload" + responses: + "200": + description: Group information (an empty group is returned if no change is made) + schema: + $ref: "#/definitions/GroupResponse" + default: + description: > + Unexpected Error: + * 401: Unauthorized => bad credentials + * 403: Forbidden => owner or administrator user can't be removed + * 404: Not Found => unknown group or device or users + * 500: Internal Server Error + * 503: Service Unavailable => server too busy or a lock on a resource is pending + schema: + $ref: "#/definitions/UnexpectedErrorResponse" + security: + - accessTokenAuth: [] + /groups/{id}/users/{email}: + x-swagger-router-controller: groups + get: + summary: Gets a user of a group + description: Gets a user of a group to which you belong; if you are the administrator user then all user fields are returned, otherwise only 'email', 'name' and 'privilege' user fields are returned + operationId: getGroupUser + tags: + - groups + parameters: + - name: id + in: path + description: Group identifier + required: true + type: string + - name: email + in: path + description: User identifier (email) + required: true + type: string + - name: fields + in: query + description: Comma-separated list of user fields; only listed fields will be returned in response + required: false + type: string + responses: + "200": + description: Group user information + schema: + $ref: "#/definitions/UserResponse" + default: + description: > + Unexpected Error: + * 401: Unauthorized => bad credentials + * 404: Not Found => unknown group or device or user + * 500: Internal Server Error + schema: + $ref: "#/definitions/UnexpectedErrorResponse" + security: + - accessTokenAuth: [] + put: + summary: Adds a user into a group + description: Adds a user into a group owned by you + operationId: addGroupUser + tags: + - groups + parameters: + - name: id + in: path + description: Group identifier + required: true + type: string + - name: email + in: path + description: User identifier (email) + required: true + type: string + responses: + "200": + description: Group information (an empty group is returned if no change is made) + schema: + $ref: "#/definitions/GroupResponse" + default: + description: > + Unexpected Error: + * 401: Unauthorized => bad credentials + * 404: Not Found => unknown group or device or user + * 500: Internal Server Error + * 503: Service Unavailable => server too busy or a lock on a resource is pending + schema: + $ref: "#/definitions/UnexpectedErrorResponse" + security: + - accessTokenAuth: [] + delete: + summary: Removes a user from a group + description: Removes a user from a group owned by you + operationId: removeGroupUser + tags: + - groups + parameters: + - name: id + in: path + description: Group identifier + required: true + type: string + - name: email + in: path + description: User identifier (email) + required: true + type: string + responses: + "200": + description: Group information (an empty group is returned if no change is made) + schema: + $ref: "#/definitions/GroupResponse" + default: + description: > + Unexpected Error: + * 401: Unauthorized => bad credentials + * 403: Forbidden => owner or administrator user can't be removed + * 404: Not Found => unknown group or device or user + * 500: Internal Server Error + * 503: Service Unavailable => server too busy or a lock on a resource is pending + schema: + $ref: "#/definitions/UnexpectedErrorResponse" + security: + - accessTokenAuth: [] + /users: + x-swagger-router-controller: users + get: + summary: Gets users + description: gets users; if you are the administrator user then all user fields are returned, otherwise only 'email', 'name' and 'privilege' user fields are returned + operationId: getUsers + tags: + - users + parameters: + - name: fields + in: query + description: Comma-separated list of user fields; only listed fields will be returned in response + required: false + type: string + responses: + "200": + description: Users information + schema: + $ref: "#/definitions/UserListResponse" + default: + description: > + Unexpected Error: + * 401: Unauthorized => bad credentials + * 500: Internal Server Error + schema: + $ref: "#/definitions/UnexpectedErrorResponse" + security: + - accessTokenAuth: [] + delete: + summary: Removes users + description: Removes users from the database + operationId: deleteUsers + tags: + - admin + parameters: + - name: groupOwner + in: query + description: Allows or not the removing of each user depending respectively if the user is a group owner (true) or not (false); note that by not providing the groupOwner parameter it means an unconditionally removing + required: false + type: boolean + - name: users + in: body + description: Users to remove as a comma-separated list of emails; note that by not providing this parameter it means all users are selected for removing + required: false + schema: + $ref: "#/definitions/UsersPayload" + responses: + "200": + description: Users removing is OK (or no users to remove) + schema: + $ref: "#/definitions/Response" + default: + description: > + Unexpected Error: + * 401: Unauthorized => bad credentials + * 403: Forbidden => administrator user can't be removed + * 404: Not Found => unknown users + * 500: Internal Server Error + * 503: Service Unavailable => server too busy or a lock on a resource is pending + schema: + $ref: "#/definitions/UnexpectedErrorResponse" + security: + - accessTokenAuth: [] + /users/groupsQuotas: + x-swagger-router-controller: users + put: + summary: Updates the default groups quotas of users + description: Updates the default groups quotas allocated to each new user + operationId: updateDefaultUserGroupsQuotas + tags: + - admin + parameters: + - name: number + in: query + description: Number of groups + required: false + type: integer + minimum: 0 + - name: duration + in: query + description: Total duration of groups (milliseconds) + required: false + type: integer + minimum: 0 + - name: repetitions + in: query + description: Number of repetitions per Group + required: false + type: integer + minimum: 0 + responses: + "200": + description: Administrator user information (an empty user is returned if no change is made) + schema: + $ref: "#/definitions/UserResponse" + default: + description: > + Unexpected Error: + * 401: Unauthorized => bad credentials + * 500: Internal Server Error + * 503: Service Unavailable => server too busy or a lock on a resource is pending + schema: + $ref: "#/definitions/UnexpectedErrorResponse" + security: + - accessTokenAuth: [] + /users/{email}: + x-swagger-router-controller: users + post: + summary: Creates a user + description: Creates a user in the database + operationId: createUser + tags: + - admin + parameters: + - name: email + in: path + description: User identifier (email) + required: true + type: string + - name: name + in: query + description: User name + required: true + type: string + responses: + "201": + description: User information + schema: + $ref: "#/definitions/UserResponse" + default: + description: > + Unexpected Error: + * 401: Unauthorized => bad credentials + * 403: Forbidden => user already exists + * 500: Internal Server Error + schema: + $ref: "#/definitions/UnexpectedErrorResponse" + security: + - accessTokenAuth: [] + get: + summary: Gets a user + description: Gets a user; if you are the administrator user then all user fields are returned, otherwise only 'email', 'name' and 'privilege' user fields are returned + operationId: getUserByEmail + tags: + - users + parameters: + - name: email + in: path + description: User identifier (email) + required: true + type: string + - name: fields + in: query + description: Comma-separated list of user fields; only listed fields will be returned in response + required: false + type: string + responses: + "200": + description: User information + schema: + $ref: "#/definitions/UserResponse" + default: + description: > + Unexpected Error: + * 401: Unauthorized => bad credentials + * 404: Not Found => unknown user + * 500: Internal Server Error + schema: + $ref: "#/definitions/UnexpectedErrorResponse" + security: + - accessTokenAuth: [] + delete: + summary: Removes a user + description: Removes a user from the database + operationId: deleteUser + tags: + - admin + parameters: + - name: email + in: path + description: User identifier (email) + required: true + type: string + - name: groupOwner + in: query + description: Allows or not the removing of the user depending respectively if the user is a group owner (true) or not (false); note that by not providing this parameter it means an unconditionally removing + required: false + type: boolean + responses: + "200": + description: User removing is OK + schema: + $ref: "#/definitions/Response" + default: + description: > + Unexpected Error: + * 401: Unauthorized => bad credentials + * 403: Forbidden => administrator user can't be removed + * 404: Not Found => unknown user + * 500: Internal Server Error + * 503: Service Unavailable => server too busy or a lock on a resource is pending + schema: + $ref: "#/definitions/UnexpectedErrorResponse" + security: + - accessTokenAuth: [] + /users/{email}/groupsQuotas: + x-swagger-router-controller: users + put: + summary: Updates the groups quotas of a user + description: Updates the groups quotas of a user + operationId: updateUserGroupsQuotas + tags: + - admin + parameters: + - name: email + in: path + description: User identifier (email) + required: true + type: string + - name: number + in: query + description: Number of groups + required: false + type: integer + minimum: 0 + - name: duration + in: query + description: Total duration of groups (milliseconds) + required: false + type: integer + minimum: 0 + - name: repetitions + in: query + description: Number of repetitions per Group + required: false + type: integer + minimum: 0 + responses: + "200": + description: User information (an empty user is returned if no change is made) + schema: + $ref: "#/definitions/UserResponse" + default: + description: > + Unexpected Error: + * 400: Bad Request => quotas must be >= actual consumed resources + * 401: Unauthorized => bad credentials + * 404: Not Found => unknown user + * 500: Internal Server Error + * 503: Service Unavailable => server too busy or a lock on a resource is pending + schema: + $ref: "#/definitions/UnexpectedErrorResponse" + security: + - accessTokenAuth: [] + /users/{email}/devices: + x-swagger-router-controller: users + get: + summary: Gets the devices controlled by a user + description: Gets the devices controlled by a user + operationId: getUserDevicesV2 + tags: + - admin + parameters: + - name: email + in: path + description: User identifier (email) + required: true + type: string + - name: fields + in: query + description: Comma-separated list of device fields; only listed fields will be returned in response + required: false + type: string + responses: + "200": + description: Controlled devices information + schema: + $ref: "#/definitions/DeviceListResponse" + default: + description: > + Unexpected Error: + * 401: Unauthorized => bad credentials + * 404: Not Found => unknown user + * 500: Internal Server Error + schema: + $ref: "#/definitions/UnexpectedErrorResponse" + security: + - accessTokenAuth: [] + /users/{email}/devices/{serial}: + x-swagger-router-controller: users + post: + summary: Places a device under user control + description: Places a device under user control; note this is not completely analogous to press the 'Use' button in the UI because that does not authorize remote connection through ADB + operationId: addUserDeviceV3 + tags: + - admin + parameters: + - name: email + in: path + description: User identifier (email) + required: true + type: string + - name: serial + in: path + description: Device identifier (serial) + required: true + type: string + - name: timeout + in: query + description: Means the device will be automatically removed from the user control if it is kept idle for this period (in milliseconds); default value is provided by the provider 'group timeout' + required: false + type: integer + minimum: 0 + responses: + "200": + description: Device controlling is OK + schema: + $ref: "#/definitions/Response" + default: + description: > + Unexpected Error: + * 401: Unauthorized => bad credentials + * 403: Forbidden => Device is already controlled or is not available + * 404: Not Found => unknown user or device + * 500: Internal Server Error + * 504: Gateway Time-out => device is not responding + schema: + $ref: "#/definitions/UnexpectedErrorResponse" + security: + - accessTokenAuth: [] + get: + summary: Gets a device controlled by a user + description: Gets a device controlled by a user + operationId: getUserDevice + tags: + - admin + parameters: + - name: email + in: path + description: User identifier (email) + required: true + type: string + - name: serial + in: path + description: Device identifier (Serial) + required: true + type: string + - name: fields + in: query + description: Comma-separated list of device fields; only listed fields will be returned in response + required: false + type: string + responses: + "200": + description: Controlled device information + schema: + $ref: "#/definitions/DeviceResponse" + default: + description: > + Unexpected Error: + * 401: Unauthorized => bad credentials + * 403: Forbidden => device is not controlled by the user + * 404: Not Found => unknown user or device + * 500: Internal Server Error + schema: + $ref: "#/definitions/UnexpectedErrorResponse" + security: + - accessTokenAuth: [] + delete: + summary: Remove a device from the user control + description: Remove a device from the user control; note this is analogous to press the 'Stop Using' button in the UI because that forbids also remote connection through ADB + operationId: deleteUserDevice + tags: + - admin + parameters: + - name: email + in: path + description: User identifier (email) + required: true + type: string + - name: serial + in: path + description: Device identifier (serial) + required: true + type: string + responses: + "200": + description: Device releasing is OK + schema: + $ref: "#/definitions/Response" + default: + description: > + Unexpected Error: + * 401: Unauthorized => bad credentials + * 403: Forbidden => device is not controlled by the user + * 404: Not Found => unknown user or device + * 500: Internal Server Error + * 504: Gateway Time-out => device is not responding + schema: + $ref: "#/definitions/UnexpectedErrorResponse" + security: + - accessTokenAuth: [] + /users/{email}/devices/{serial}/remoteConnect: + x-swagger-router-controller: users + post: + summary: Allows to remotely connect to a device controlled by a user + description: Allows to remotely connect to a device controlled by a user; returns the remote debug URL in response for use with ADB + operationId: remoteConnectUserDevice + tags: + - admin + parameters: + - name: email + in: path + description: User identifier (email) + required: true + type: string + - name: serial + in: path + description: Device identifier (serial) + required: true + type: string + responses: + "200": + description: Remote debug URL + schema: + $ref: "#/definitions/RemoteConnectUserDeviceResponse" + default: + description: > + Unexpected Error: + * 401: Unauthorized => bad credentials + * 403: Forbidden => device is not controlled by the user + * 404: Not Found => unknown user or device + * 500: Internal Server Error + * 504: Gateway Time-out => device is not responding + schema: + $ref: "#/definitions/UnexpectedErrorResponse" + security: + - accessTokenAuth: [] + delete: + summary: Forbids to remotely connect to a device controlled by a user + description: Forbids using ADB to remotely connect to a device controlled by a user + operationId: remoteDisconnectUserDevice + tags: + - admin + parameters: + - name: email + in: path + description: User identifier (email) + required: true + type: string + - name: serial + in: path + description: Device identifier (serial) + required: true + type: string + responses: + "200": + description: Remote debug URL disabling is OK + schema: + $ref: "#/definitions/Response" + default: + description: > + Unexpected Error: + * 401: Unauthorized => bad credentials + * 403: Forbidden => device is not controlled by the user + * 404: Not Found => unknown user or device + * 500: Internal Server Error + * 504: Gateway Time-out => device is not responding + schema: + $ref: "#/definitions/UnexpectedErrorResponse" + security: + - accessTokenAuth: [] + /users/{email}/accessTokens: + x-swagger-router-controller: users + post: + summary: Create an access token for a user + description: Creates an access token for a user. + operationId: createUserAccessToken + tags: + - admin + parameters: + - name: email + in: path + description: User identifier (email) + required: true + type: string + - name: title + in: query + description: Access token title + required: true + type: string + responses: + "200": + description: Access token information + schema: + $ref: "#/definitions/UserAccessTokenResponse" + default: + description: > + Unexpected Error: + * 401: Unauthorized => bad credentials + * 404: Not Found => unknown user + * 500: Internal Server Error + schema: + $ref: "#/definitions/UnexpectedErrorResponse" + security: + - accessTokenAuth: [] + get: + summary: Gets the access tokens of a user + description: Gets the access tokens of a user + operationId: getUserAccessTokensV2 + tags: + - admin + parameters: + - name: email + in: path + description: User identifier (email) + required: true + type: string + responses: + "200": + description: Access tokens information + schema: + $ref: "#/definitions/UserAccessTokensResponse" + default: + description: > + Unexpected Error: + * 401: Unauthorized => bad credentials + * 404: Not Found => unknown user + * 500: Internal Server Error + schema: + $ref: "#/definitions/UnexpectedErrorResponse" + security: + - accessTokenAuth: [] + delete: + summary: Remove the access tokens of a user + description: Remove the access tokens of a user + operationId: deleteUserAccessTokens + tags: + - admin + parameters: + - name: email + in: path + description: User identifier (email) + required: true + type: string + responses: + "200": + description: Access tokens removing is OK + schema: + $ref: "#/definitions/Response" + default: + description: > + Unexpected Error: + * 401: Unauthorized => bad credentials + * 404: Not Found => unknown user + * 500: Internal Server Error + schema: + $ref: "#/definitions/UnexpectedErrorResponse" + security: + - accessTokenAuth: [] + /users/{email}/accessTokens/{id}: + x-swagger-router-controller: users + get: + summary: Gets an access token of a user + description: Gets an access token of a user + operationId: getUserAccessToken + tags: + - admin + parameters: + - name: email + in: path + description: User identifier (email) + required: true + type: string + - name: id + in: path + description: Access token identifier + required: true + type: string + responses: + "200": + description: Access token information + schema: + $ref: "#/definitions/UserAccessTokenResponse" + default: + description: > + Unexpected Error: + * 401: Unauthorized => bad credentials + * 404: Not Found => unknown user or token + * 500: Internal Server Error + schema: + $ref: "#/definitions/UnexpectedErrorResponse" + security: + - accessTokenAuth: [] + delete: + summary: Removes an access token of a user + description: Removes an access token of a user + operationId: deleteUserAccessToken + tags: + - admin + parameters: + - name: email + in: path + description: User identifier (email) + required: true + type: string + - name: id + in: path + description: Access token identifier + required: true + type: string + responses: + "200": + description: Access token removing is OK + schema: + $ref: "#/definitions/Response" + default: + description: > + Unexpected Error: + * 401: Unauthorized => bad credentials + * 404: Not Found => unknown user or token + * 500: Internal Server Error + schema: + $ref: "#/definitions/UnexpectedErrorResponse" + security: + - accessTokenAuth: [] /user: x-swagger-router-controller: user get: @@ -64,9 +1330,12 @@ paths: schema: $ref: "#/definitions/DeviceListResponse" default: - description: Unexpected Error + description: > + Unexpected Error: + * 401: Unauthorized => bad credentials + * 500: Internal Server Error schema: - $ref: "#/definitions/ErrorResponse" + $ref: "#/definitions/UnexpectedErrorResponse" security: - accessTokenAuth: [] post: @@ -86,13 +1355,54 @@ paths: "200": description: Add User Device Status default: - description: Unexpected Error + description: > + Unexpected Error: + * 401: Unauthorized => bad credentials + * 403: Forbidden => device is already controlled or is not available + * 404: Not Found => unknown device + * 500: Internal Server Error + * 504: Gateway Time-out => device is not responding schema: - $ref: "#/definitions/ErrorResponse" + $ref: "#/definitions/UnexpectedErrorResponse" security: - accessTokenAuth: [] /user/devices/{serial}: x-swagger-router-controller: user + post: + summary: Places a device under user control + description: Places a device under user control; note this is not completely analogous to press the 'Use' button in the UI because that does not authorize remote connection through ADB + operationId: addUserDeviceV2 + tags: + - user + parameters: + - name: serial + in: path + description: Device identifier (serial) + required: true + type: string + - name: timeout + in: query + description: Means the device will be automatically removed from the user control if it is kept idle for this period (in milliseconds); default value is provided by the provider 'group timeout' + required: false + type: integer + minimum: 0 + responses: + "200": + description: Device controlling is OK + schema: + $ref: "#/definitions/Response" + default: + description: > + Unexpected Error: + * 401: Unauthorized => bad credentials + * 403: Forbidden => device is already controlled or is not available + * 404: Not Found => unknown device + * 500: Internal Server Error + * 504: Gateway Time-out => device is not responding + schema: + $ref: "#/definitions/UnexpectedErrorResponse" + security: + - accessTokenAuth: [] get: summary: User Device description: The devices enpoint return information about device owned by user @@ -116,9 +1426,14 @@ paths: schema: $ref: "#/definitions/DeviceResponse" default: - description: Unexpected Error + description: > + Unexpected Error: + * 401: Unauthorized => bad credentials + * 403: Forbidden => device is not controlled by the user + * 404: Not Found => unknown device + * 500: Internal Server Error schema: - $ref: "#/definitions/ErrorResponse" + $ref: "#/definitions/UnexpectedErrorResponse" security: - accessTokenAuth: [] delete: @@ -136,10 +1451,18 @@ paths: responses: "200": description: Delete User Device Status - default: - description: Unexpected Error schema: - $ref: "#/definitions/ErrorResponse" + $ref: "#/definitions/Response" + default: + description: > + Unexpected Error: + * 401: Unauthorized => bad credentials + * 403: Forbidden => device is not controlled by the user + * 404: Not Found => unknown device + * 500: Internal Server Error + * 504: Gateway Time-out => device is not responding + schema: + $ref: "#/definitions/UnexpectedErrorResponse" security: - accessTokenAuth: [] # I do know this is against REST principal to use verb as endpoint. But I feel it is more easy to @@ -164,9 +1487,15 @@ paths: schema: $ref: "#/definitions/RemoteConnectUserDeviceResponse" default: - description: Unexpected Error + description: > + Unexpected Error: + * 401: Unauthorized => bad credentials + * 403: Forbidden => device is not controlled by the user + * 404: Not Found => unknown device + * 500: Internal Server Error + * 504: Gateway Time-out => device is not responding schema: - $ref: "#/definitions/ErrorResponse" + $ref: "#/definitions/UnexpectedErrorResponse" security: - accessTokenAuth: [] delete: @@ -184,14 +1513,70 @@ paths: responses: "200": description: Remote Disonnect User Device Request Status - default: - description: Unexpected Error schema: - $ref: "#/definitions/ErrorResponse" + $ref: "#/definitions/Response" + default: + description: > + Unexpected Error: + * 401: Unauthorized => bad credentials + * 403: Forbidden => device is not controlled by the user + * 404: Not Found => unknown device + * 500: Internal Server Error + * 504: Gateway Time-out => device is not responding + schema: + $ref: "#/definitions/UnexpectedErrorResponse" + security: + - accessTokenAuth: [] + /user/fullAccessTokens: + x-swagger-router-controller: user + get: + summary: Gets your access tokens + description: Gets your access tokens; note that all fields are returned in reponse including the 'id' one + operationId: getAccessTokens + tags: + - user + responses: + "200": + description: Access tokens information + schema: + $ref: "#/definitions/UserAccessTokensResponse" + default: + description: > + Unexpected Error: + * 401: Unauthorized => bad credentials + * 500: Internal Server Error + schema: + $ref: "#/definitions/UnexpectedErrorResponse" security: - accessTokenAuth: [] /user/accessTokens: x-swagger-router-controller: user + post: + summary: Create an access token + description: Create an access token for you + operationId: createAccessToken + tags: + - user + parameters: + - name: title + in: query + description: Access token title + required: true + type: string + responses: + "201": + description: Access token information + schema: + $ref: "#/definitions/UserAccessTokenResponse" + default: + description: > + Unexpected Error: + * 401: Unauthorized => bad credentials + * 500: Internal Server Error + schema: + $ref: "#/definitions/UnexpectedErrorResponse" + security: + - accessTokenAuth: [] get: summary: Access Tokens description: The Access Tokens endpoints returns titles of all the valid access tokens @@ -209,6 +1594,82 @@ paths: $ref: "#/definitions/ErrorResponse" security: - accessTokenAuth: [] + delete: + summary: Removes your access tokens + description: Removes your access tokens + operationId: deleteAccessTokens + tags: + - user + responses: + "200": + description: Access tokens removing is OK + schema: + $ref: "#/definitions/Response" + default: + description: > + Unexpected Error: + * 401: Unauthorized => bad credentials + * 500: Internal Server Error + schema: + $ref: "#/definitions/UnexpectedErrorResponse" + security: + - accessTokenAuth: [] + /user/accessTokens/{id}: + x-swagger-router-controller: user + get: + summary: Gets an access token + description: Gets one of your access tokens + operationId: getAccessToken + tags: + - user + parameters: + - name: id + in: path + description: Access token identifier + required: true + type: string + responses: + "200": + description: Access token information + schema: + $ref: "#/definitions/UserAccessTokenResponse" + default: + description: > + Unexpected Error: + * 401: Unauthorized => bad credentials + * 404: Not Found => unknown token + * 500: Internal Server Error + schema: + $ref: "#/definitions/UnexpectedErrorResponse" + security: + - accessTokenAuth: [] + delete: + summary: Removes an access token + description: Removes one of your access tokens + operationId: deleteAccessToken + tags: + - user + parameters: + - name: id + in: path + description: Access token identifier + required: true + type: string + responses: + "200": + description: Access token removing is OK + schema: + $ref: "#/definitions/Response" + default: + description: > + Unexpected Error: + * 401: Unauthorized => bad credentials + * 404: Not Found => unknown token + * 500: Internal Server Error + schema: + $ref: "#/definitions/UnexpectedErrorResponse" + security: + - accessTokenAuth: [] /user/adbPublicKeys: x-swagger-router-controller: user post: @@ -243,9 +1704,9 @@ paths: schema: $ref: "#/definitions/ErrorResponse" security: - - accessTokenAuth: [] + - accessTokenAuth: [] /devices: - x-swagger-router-controller: devices + x-swagger-router-controller: devices get: summary: Device List description: The devices endpoint return list of all the STF devices including Disconnected and Offline @@ -253,6 +1714,23 @@ paths: tags: - devices parameters: + - name: target + in: query + description: > + Targets devices of your universe: + * bookable - devices belonging to a bookable group + * standard - devices belonging to a standard group + * origin - all devices + * standardizable - devices which are not yet booked including those belonging to a standard group + * user (default value) - devices which are accessible by you at a given time + type: string + enum: + - bookable + - standard + - origin + - standardizable + - user + default: user - name: fields in: query description: Fields query parameter takes a comma seperated list of fields. Only listed field will be return in response @@ -260,20 +1738,72 @@ paths: type: string responses: "200": - description: List of Devices + description: Devices information schema: $ref: "#/definitions/DeviceListResponse" default: - description: Unexpected Error + description: > + Unexpected Error: + * 401: Unauthorized => bad credentials + * 500: Internal Server Error schema: - $ref: "#/definitions/ErrorResponse" + $ref: "#/definitions/UnexpectedErrorResponse" + security: + - accessTokenAuth: [] + delete: + summary: Removes devices + description: Removes devices from the database + operationId: deleteDevices + tags: + - admin + parameters: + - name: present + in: query + description: Allows or not the removing of each device depending respectively if the device is present (true) or not (false); note that by not providing this parameter it means an unconditional removing + required: false + type: boolean + - name: booked + in: query + description: Allows or not the removing of each device depending respectively if the device is booked (true) or not (false); note that by not providing this parameter it means an unconditional removing + required: false + type: boolean + - name: annotated + in: query + description: Allows or not the removing of each device depending respectively if the device is annotated (true) or not (false); note that by not providing this parameter it means an unconditional removing + required: false + type: boolean + - name: controlled + in: query + description: Allows or not the removing of each device depending respectively if the device is controlled (true) or not (false); note that by not providing this parameter it means an unconditional removing + required: false + type: boolean + - name: devices + in: body + description: Devices to remove as a comma-separated list of serials; note that by not providing this parameter it means all devices are selected for removing + required: false + schema: + $ref: "#/definitions/DevicesPayload" + responses: + "200": + description: Devices removing is OK (or no devices to remove) + schema: + $ref: "#/definitions/Response" + default: + description: > + Unexpected Error: + * 401: Unauthorized => bad credentials + * 404: Not Found => unknown devices + * 500: Internal Server Error + * 503: Service Unavailable => server too busy or a lock on a resource is pending + schema: + $ref: "#/definitions/UnexpectedErrorResponse" security: - accessTokenAuth: [] /devices/{serial}: x-swagger-router-controller: devices get: summary: Device Information - description: The device enpoint return information about a single device + description: The devices serial enpoint return information about a single device operationId: getDeviceBySerial tags: - devices @@ -299,45 +1829,492 @@ paths: $ref: "#/definitions/ErrorResponse" security: - accessTokenAuth: [] + delete: + summary: Removes a device + description: Removes a device from the database + operationId: deleteDevice + tags: + - admin + parameters: + - name: serial + in: path + description: Device identifier (serial) + required: true + type: string + - name: present + in: query + description: Allows or not the removing of the device depending respectively if the device is present (true) or not (false); note that by not providing this parameter it means an unconditional removing + required: false + type: boolean + - name: booked + in: query + description: Allows or not the removing of the device depending respectively if the device is booked (true) or not (false); note that by not providing this parameter it means an unconditional removing + required: false + type: boolean + - name: annotated + in: query + description: Allows or not the removing of the device depending respectively if the device is annotated (true) or not (false); note that by not providing this parameter it means an unconditional removing + required: false + type: boolean + - name: controlled + in: query + description: Allows or not the removing of the device depending respectively if the device is controlled (true) or not (false); note that by not providing this parameter it means an unconditional removing + required: false + type: boolean + responses: + "200": + description: Device removing is OK + schema: + $ref: "#/definitions/Response" + default: + description: > + Unexpected Error: + * 401: Unauthorized => bad credentials + * 404: Not Found => unknown device + * 500: Internal Server Error + * 503: Service Unavailable => server too busy or a lock on a resource is pending + schema: + $ref: "#/definitions/UnexpectedErrorResponse" + security: + - accessTokenAuth: [] + /devices/groups/{id}: + x-swagger-router-controller: devices + put: + summary: Adds devices into an origin group + description: Adds devices into an origin group along with updating each added device; returns the updated devices + operationId: addOriginGroupDevices + tags: + - admin + parameters: + - name: id + in: path + description: Group identifier + required: true + type: string + - name: devices + in: body + description: > + Devices to add as a comma-separated list of serials; note that by not providing this parameter it means all 'available devices' are selected for adding: + * 'availables devices' means all devices in case of a bookable group + * 'availables devices' means all not yet booked devices in case of a standard group + required: false + schema: + $ref: "#/definitions/DevicesPayload" + - name: fields + in: query + description: Comma-seperated list of fields; only listed fields will be returned in response + required: false + type: string + responses: + "200": + description: Devices information (an empty device list is returned if no change is made) + schema: + $ref: "#/definitions/DeviceListResponse" + default: + description: > + Unexpected Error: + * 400: Bad Request => group is not an origin one + * 401: Unauthorized => bad credentials + * 403: Fobidden => a device is currently booked + * 404: Not Found => unknown group or devices + * 500: Internal Server Error + * 503: Service Unavailable => server too busy or a lock on a resource is pending + * 504: Gateway Time-out => server is not responding + schema: + $ref: "#/definitions/UnexpectedErrorResponse" + security: + - accessTokenAuth: [] + delete: + summary: Removes devices from an origin group + description: Removes devices from an origin group along with updating each removed device; returns the updated devices + operationId: removeOriginGroupDevices + tags: + - admin + parameters: + - name: id + in: path + description: Group identifier + required: true + type: string + - name: devices + in: body + description: Devices to remove as a comma-separated list of serials; note that by not providing this parameter it means all devices of the group are selected for removing + required: false + schema: + $ref: "#/definitions/DevicesPayload" + - name: fields + in: query + description: Comma-seperated list of fields; only listed fields will be returned in response + required: false + type: string + responses: + "200": + description: Devices information (an empty device list is returned if no change is made) + schema: + $ref: "#/definitions/DeviceListResponse" + default: + description: > + Unexpected Error: + * 400: Bad Request => group is not an origin one + * 401: Unauthorized => bad credentials + * 403: Fobidden => a device is currently booked + * 404: Not Found => unknown group or devices + * 500: Internal Server Error + * 503: Service Unavailable => server too busy or a lock on a resource is pending + * 504: Gateway Time-out => server is not responding + schema: + $ref: "#/definitions/UnexpectedErrorResponse" + security: + - accessTokenAuth: [] + /devices/{serial}/groups: + x-swagger-router-controller: devices + get: + summary: Gets the groups to which the device belongs + description: Gets the groups to which the device belongs + operationId: getDeviceGroups + tags: + - admin + parameters: + - name: serial + in: path + description: Device identifier (serial) + required: true + type: string + - name: fields + in: query + description: Comma-seperated list of fields; only listed fields will be returned in response + required: false + type: string + responses: + "200": + description: Groups information + schema: + $ref: "#/definitions/GroupListResponse" + default: + description: > + Unexpected Error: + * 401: Unauthorized => bad credentials + * 404: Not Found => unknown device + * 500: Internal Server Error + schema: + $ref: "#/definitions/UnexpectedErrorResponse" + security: + - accessTokenAuth: [] + /devices/{serial}/bookings: + x-swagger-router-controller: devices + get: + summary: Gets the bookings to which the device belongs + description: Gets the bookings (i.e. transient groups) to which the device belongs + operationId: getDeviceBookings + tags: + - devices + parameters: + - name: serial + in: path + description: Device identifier (serial) + required: true + type: string + - 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: Bookings information + schema: + $ref: "#/definitions/GroupListResponse" + default: + description: > + Unexpected Error: + * 401: Unauthorized => bad credentials + * 404: Not Found => unknown device + * 500: Internal Server Error + schema: + $ref: "#/definitions/UnexpectedErrorResponse" + security: + - accessTokenAuth: [] + /devices/{serial}/groups/{id}: + x-swagger-router-controller: devices + put: + summary: Adds a device into an origin group + description: Adds a device into an origin group along with updating the added device; returns the updated device + operationId: addOriginGroupDevice + tags: + - admin + parameters: + - name: serial + in: path + description: Device identifier (serial) + required: true + type: string + - name: id + in: path + description: Group identifier + required: true + type: string + responses: + "200": + description: Device information + schema: + $ref: "#/definitions/DeviceResponse" + default: + description: > + Unexpected Error: + * 400: Bad Request => group is not an origin one + * 401: Unauthorized => bad credentials + * 403: Fobidden => the device is currently booked + * 404: Not Found => unknown group or device + * 500: Internal Server Error + * 503: Service Unavailable => server too busy or a lock on a resource is pending + * 504: Gateway Time-out => server is not responding + schema: + $ref: "#/definitions/UnexpectedErrorResponse" + security: + - accessTokenAuth: [] + delete: + summary: Removes a device from an origin group + description: Removes a device from an origin group along with updating the removed device; returns the updated device + operationId: removeOriginGroupDevice + tags: + - admin + parameters: + - name: serial + in: path + description: Device identifier (serial) + required: true + type: string + - name: id + in: path + description: Group identifier + required: true + type: string + responses: + "200": + description: Device information + schema: + $ref: "#/definitions/DeviceResponse" + default: + description: > + Unexpected Error: + * 400: Bad Request => group is not an origin one + * 401: Unauthorized => bad credentials + * 403: Fobidden => the device is currently booked + * 404: Not Found => unknown group or device + * 500: Internal Server Error + * 503: Service Unavailable => server too busy or a lock on a resource is pending + * 504: Gateway Time-out => server is not responding + schema: + $ref: "#/definitions/UnexpectedErrorResponse" + security: + - accessTokenAuth: [] /swagger.json: x-swagger-pipe: swagger_raw definitions: + UnexpectedErrorResponse: + required: + - success + - description + properties: + success: + type: boolean + default: false + description: + type: string + Response: + required: + - success + - description + properties: + success: + type: boolean + default: true + description: + type: string + GroupResponse: + required: + - success + - description + - group + properties: + success: + type: boolean + description: + type: string + group: + description: A null value means the group is unchanged + type: object + Conflict: + type: object + properties: + devices: + description: Devices in conflict + type: array + items: + type: string + date: + description: Timeslot in conflict + type: object + properties: + start: + type: string + format: date-time + stop: + type: string + format: date-time + group: + description: Name of the group in conflict + type: string + owner: + description: Owner of the group in conflict + type: object + properties: + email: + type: string + name: + type: string + ConflictsResponse: + required: + - success + - description + - conflicts + properties: + success: + type: boolean + description: + type: string + conflicts: + description: > + List of conflicts with the current group operation: + * adding a device into the group + * updating the schedule of the group + type: array + items: + $ref: '#/definitions/Conflict' + GroupListResponse: + required: + - success + - description + - groups + properties: + success: + type: boolean + description: + type: string + groups: + type: array + items: + type: object + UserListResponse: + required: + - success + - description + - users + properties: + success: + type: boolean + description: + type: string + users: + type: array + items: + type: object UserResponse: required: + - success + - description - user properties: + success: + type: boolean + description: + type: string user: type: object - AccessTokensResponse: + Token: + type: object + properties: + id: + type: string + title: + type: string + UserAccessTokenResponse: required: + - success + - description + - token + properties: + success: + type: boolean + description: + type: string + token: + $ref: '#/definitions/Token' + UserAccessTokensResponse: + required: + - success + - description - tokens properties: + success: + type: boolean + description: + type: string + tokens: + type: array + items: + $ref: '#/definitions/Token' + AccessTokensResponse: + required: + - success + - description + - tokens + properties: + success: + type: boolean + description: + type: string tokens: type: array items: type: string DeviceListResponse: required: + - success + - description - devices properties: + success: + type: boolean + description: + type: string devices: type: array items: type: object DeviceResponse: required: + - success + - description - device properties: + success: + type: boolean + description: + type: string device: type: object RemoteConnectUserDeviceResponse: required: + - success + - description - remoteConnectUrl - - serial properties: - remoteConnectUrl: + success: + type: boolean + description: type: string - serial: + remoteConnectUrl: type: string AddUserDevicePayload: description: payload object for adding device to user @@ -350,6 +2327,65 @@ definitions: timeout: description: Device timeout in ms. If device is kept idle for this period, it will be automatically disconnected. Default is provider group timeout type: integer + GroupPayload: + description: Payload object for creating/updating a group + properties: + name: + description: Group Name; default value => generated at runtime + type: string + pattern: '^[0-9a-zA-Z-_./: ]{1,50}$' + startTime: + description: Group starting time; default value => group creation time + type: string + format: date-time + stopTime: + description: Group expiration time; default value => startTime + 1 hour + type: string + format: date-time + class: + description: Group class; privileged value => debug, bookable, standard + type: string + enum: + - once + - bookable + - hourly + - daily + - weekly + - monthly + - quaterly + - halfyearly + - yearly + - debug + - standard + default: once + repetitions: + description: Group repetitions; default value => 0 + type: integer + minimum: 0 + state: + description: Group state; default value => pending or ready for bookable/standard classes + type: string + enum: + - pending + - ready + GroupsPayload: + description: Payload object for adding/removing groups + properties: + ids: + description: Comma-separated list of identifiers + type: string + UsersPayload: + description: Payload object for adding/removing users + properties: + emails: + description: Comma-separated list of emails + type: string + DevicesPayload: + description: Payload object for adding/removing devices + properties: + serials: + description: Comma-separated list of serials + type: string ErrorResponse: required: - message diff --git a/lib/units/app/middleware/auth.js b/lib/units/app/middleware/auth.js index e960f4f3..bf3a5595 100644 --- a/lib/units/app/middleware/auth.js +++ b/lib/units/app/middleware/auth.js @@ -1,3 +1,7 @@ +/** +* Copyright © 2019 contains code contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0 +**/ + var jwtutil = require('../../../util/jwtutil') var urlutil = require('../../../util/urlutil') @@ -18,6 +22,7 @@ module.exports = function(options) { }) .then(function() { req.session.jwt = data + req.sessionOptions.httpOnly = false res.redirect(redir) }) .catch(next) diff --git a/lib/units/auth/ldap.js b/lib/units/auth/ldap.js index 5388407e..c0f6b61c 100644 --- a/lib/units/auth/ldap.js +++ b/lib/units/auth/ldap.js @@ -1,3 +1,7 @@ +/** +* Copyright © 2019 contains code contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0 +**/ + var http = require('http') var express = require('express') @@ -16,6 +20,8 @@ var pathutil = require('../../util/pathutil') var urlutil = require('../../util/urlutil') var lifecycle = require('../../util/lifecycle') +const dbapi = require('../../db/api') + module.exports = function(options) { var log = logger.createLogger('auth-ldap') var app = express() @@ -54,6 +60,24 @@ module.exports = function(options) { res.redirect('/auth/ldap/') }) + app.get('/auth/contact', function(req, res) { + dbapi.getRootGroup().then(function(group) { + res.status(200) + .json({ + success: true + , contact: group.owner + }) + }) + .catch(function(err) { + log.error('Unexpected error', err.stack) + res.status(500) + .json({ + success: false + , error: 'ServerError' + }) + }) + }) + app.get('/auth/ldap/', function(req, res) { res.render('index') }) diff --git a/lib/units/auth/mock.js b/lib/units/auth/mock.js index dc65b770..f3ed4932 100644 --- a/lib/units/auth/mock.js +++ b/lib/units/auth/mock.js @@ -1,3 +1,7 @@ +/** +* Copyright © 2019 contains code contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0 +**/ + var http = require('http') var express = require('express') @@ -16,6 +20,8 @@ var pathutil = require('../../util/pathutil') var urlutil = require('../../util/urlutil') var lifecycle = require('../../util/lifecycle') +const dbapi = require('../../db/api') + module.exports = function(options) { var log = logger.createLogger('auth-mock') var app = express() @@ -80,6 +86,24 @@ module.exports = function(options) { res.redirect('/auth/mock/') }) + app.get('/auth/contact', function(req, res) { + dbapi.getRootGroup().then(function(group) { + res.status(200) + .json({ + success: true + , contact: group.owner + }) + }) + .catch(function(err) { + log.error('Unexpected error', err.stack) + res.status(500) + .json({ + success: false + , error: 'ServerError' + }) + }) + }) + app.get('/auth/mock/', function(req, res) { res.render('index') }) diff --git a/lib/units/device/plugins/connect.js b/lib/units/device/plugins/connect.js index 90bf3f79..b70efc34 100644 --- a/lib/units/device/plugins/connect.js +++ b/lib/units/device/plugins/connect.js @@ -1,3 +1,7 @@ +/** +* Copyright © 2019 contains code contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0 +**/ + var util = require('util') var syrup = require('stf-syrup') @@ -117,6 +121,7 @@ module.exports = syrup.serial() if (plugin.isRunning()) { activeServer.close() activeServer.end() + activeServer = null } }) @@ -131,7 +136,7 @@ module.exports = syrup.serial() } lifecycle.observe(plugin.stop) - group.on('leave', plugin.end) + group.on('leave', plugin.stop) router .on(wire.ConnectStartMessage, function(channel) { @@ -163,7 +168,7 @@ module.exports = syrup.serial() }) .on(wire.ConnectStopMessage, function(channel) { var reply = wireutil.reply(options.serial) - plugin.end() + plugin.stop() .then(function() { push.send([ channel @@ -187,6 +192,5 @@ module.exports = syrup.serial() }) }) - return plugin.start() - .return(plugin) + return(plugin) }) diff --git a/lib/units/device/plugins/service.js b/lib/units/device/plugins/service.js index 459c1e5a..b2e132b5 100644 --- a/lib/units/device/plugins/service.js +++ b/lib/units/device/plugins/service.js @@ -33,8 +33,9 @@ module.exports = syrup.serial() .dependency(require('../support/adb')) .dependency(require('../support/router')) .dependency(require('../support/push')) + .dependency(require('../support/sdk')) .dependency(require('../resources/service')) - .define(function(options, adb, router, push, apk) { + .define(function(options, adb, router, push, sdk, apk) { var log = logger.createLogger('device:plugins:service') var messageResolver = new MessageResolver() var plugin = new events.EventEmitter() @@ -62,9 +63,11 @@ module.exports = syrup.serial() } function callService(intent) { + var startServiceCmd = (sdk.level < 26) ? 'startservice' : 'start-foreground-service' + log.info('using \'%s\' command for API %s', startServiceCmd, sdk.level) return adb.shell(options.serial, util.format( - 'am startservice --user 0 %s' - , intent + 'am %s --user 0 %s' + , startServiceCmd, intent )) .timeout(15000) .then(function(out) { @@ -76,8 +79,8 @@ module.exports = syrup.serial() .then(function(line) { if (line.indexOf('--user') !== -1) { return adb.shell(options.serial, util.format( - 'am startservice %s' - , intent + 'am %s %s' + , startServiceCmd, intent )) .timeout(15000) .then(function() { diff --git a/lib/units/device/plugins/solo.js b/lib/units/device/plugins/solo.js index ae78a2d0..e3ba1f40 100644 --- a/lib/units/device/plugins/solo.js +++ b/lib/units/device/plugins/solo.js @@ -44,6 +44,7 @@ module.exports = syrup.serial() , identity.product , identity.cpuPlatform , identity.openGLESVersion + , identity.marketName )) ]) }) diff --git a/lib/units/device/resources/minicap.js b/lib/units/device/resources/minicap.js index fb8c47c6..509e700e 100644 --- a/lib/units/device/resources/minicap.js +++ b/lib/units/device/resources/minicap.js @@ -62,7 +62,7 @@ module.exports = syrup.serial() } function removeResource(res) { - return adb.shell(options.serial, ['rm', res.dest]) + return adb.shell(options.serial, ['rm', '-f', res.dest]) .timeout(10000) .then(function(out) { return streamutil.readAll(out) diff --git a/lib/units/device/resources/minirev.js b/lib/units/device/resources/minirev.js index 37b28401..802998c3 100644 --- a/lib/units/device/resources/minirev.js +++ b/lib/units/device/resources/minirev.js @@ -36,7 +36,7 @@ module.exports = syrup.serial() } function removeResource(res) { - return adb.shell(options.serial, ['rm', res.dest]) + return adb.shell(options.serial, ['rm', '-f', res.dest]) .timeout(10000) .then(function(out) { return streamutil.readAll(out) diff --git a/lib/units/device/resources/minitouch.js b/lib/units/device/resources/minitouch.js index ba1327fc..c06404fb 100644 --- a/lib/units/device/resources/minitouch.js +++ b/lib/units/device/resources/minitouch.js @@ -35,7 +35,7 @@ module.exports = syrup.serial() } function removeResource(res) { - return adb.shell(options.serial, ['rm', res.dest]) + return adb.shell(options.serial, ['rm', '-f', res.dest]) .timeout(10000) .then(function(out) { return streamutil.readAll(out) diff --git a/lib/units/groups-engine/index.js b/lib/units/groups-engine/index.js new file mode 100644 index 00000000..2a5ad76b --- /dev/null +++ b/lib/units/groups-engine/index.js @@ -0,0 +1,115 @@ +/** +* Copyright © 2019 code initially contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0 +**/ + +const events = require('events') +const Promise = require('bluebird') +const logger = require('../../util/logger') +const zmqutil = require('../../util/zmqutil') +const srv = require('../../util/srv') +const lifecycle = require('../../util/lifecycle') +const wireutil = require('../../wire/util') + +const groupsScheduler = require('./scheduler') +const groupsWatcher = require('./watchers/groups') +const devicesWatcher = require('./watchers/devices') +const usersWatcher = require('./watchers/users') + +module.exports = function(options) { + const log = logger.createLogger('groups-engine') + const channelRouter = new events.EventEmitter() + + const push = zmqutil.socket('push') + Promise.map(options.endpoints.push, function(endpoint) { + return srv.resolve(endpoint).then(function(records) { + return srv.attempt(records, function(record) { + log.info('Sending output to "%s"', record.url) + push.connect(record.url) + return Promise.resolve(true) + }) + }) + }) + .catch(function(err) { + log.fatal('Unable to connect to push endpoint', err) + lifecycle.fatal() + }) + + // Input + const sub = zmqutil.socket('sub') + Promise.map(options.endpoints.sub, function(endpoint) { + return srv.resolve(endpoint).then(function(records) { + return srv.attempt(records, function(record) { + log.info('Receiving input from "%s"', record.url) + sub.connect(record.url) + return Promise.resolve(true) + }) + }) + }) + .catch(function(err) { + log.fatal('Unable to connect to sub endpoint', err) + lifecycle.fatal() + }) + + const pushdev = zmqutil.socket('push') + Promise.map(options.endpoints.pushdev, function(endpoint) { + return srv.resolve(endpoint).then(function(records) { + return srv.attempt(records, function(record) { + log.info('Sending output to "%s"', record.url) + pushdev.connect(record.url) + return Promise.resolve(true) + }) + }) + }) + .catch(function(err) { + log.fatal('Unable to connect to pushdev endpoint', err) + lifecycle.fatal() + }) + + const subdev = zmqutil.socket('sub') + Promise.map(options.endpoints.subdev, function(endpoint) { + return srv.resolve(endpoint).then(function(records) { + return srv.attempt(records, function(record) { + log.info('Receiving input from "%s"', record.url) + subdev.connect(record.url) + return Promise.resolve(true) + }) + }) + }) + .catch(function(err) { + log.fatal('Unable to connect to subdev endpoint', err) + lifecycle.fatal() + }) + + // Establish always-on channels + ;[wireutil.global].forEach(function(channel) { + log.info('Subscribing to permanent channel "%s"', channel) + sub.subscribe(channel) + subdev.subscribe(channel) + }) + + sub.on('message', function(channel, data) { + channelRouter.emit(channel.toString(), channel, data) + }) + + subdev.on('message', function(channel, data) { + channelRouter.emit(channel.toString(), channel, data) + }) + + groupsScheduler() + groupsWatcher(push, pushdev, channelRouter) + devicesWatcher(push, pushdev, channelRouter) + usersWatcher(pushdev) + + lifecycle.observe(function() { + [push, sub, pushdev, subdev].forEach(function(sock) { + try { + sock.close() + } + catch (err) { + // No-op + } + }) + }) + + log.info('Groups engine started') +} diff --git a/lib/units/groups-engine/scheduler/index.js b/lib/units/groups-engine/scheduler/index.js new file mode 100644 index 00000000..67e70738 --- /dev/null +++ b/lib/units/groups-engine/scheduler/index.js @@ -0,0 +1,156 @@ +/** +* Copyright © 2019 code initially contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0 +**/ + +const Promise = require('bluebird') +const logger = require('../../../util/logger') +const apiutil = require('../../../util/apiutil') +const db = require('../../../db') +const dbapi = require('../../../db/api') +const r = require('rethinkdb') + +module.exports = function() { + const log = logger.createLogger('groups-scheduler') + + function updateOriginGroupLifetime(group) { + const lock = {} + + return dbapi.adminLockGroup(group.id, lock).then(function(lockingSuccessed) { + if (lockingSuccessed) { + const now = Date.now() + + return db.run(r.table('groups').get(group.id).update({ + dates: [{ + start: new Date(now) + , stop: new Date(now + (group.dates[0].stop - group.dates[0].start)) + }] + })) + } + return false + }) + .finally(function() { + return dbapi.adminUnlockGroup(lock) + }) + } + + function deleteUserGroup(group) { + const lock = {} + + return dbapi.adminLockGroup(group.id, lock).then(function(lockingSuccessed) { + if (lockingSuccessed) { + return dbapi.deleteUserGroup(group.id) + } + else { + return db.run(r.table('groups').get(group.id).update({ + isActive: false + , state: apiutil.WAITING + })) + } + }) + .finally(function() { + return dbapi.adminUnlockGroup(lock) + }) + } + + function updateGroupDates(group, incr, isActive) { + const repetitions = group.repetitions - incr + const dates = group.dates.slice(incr) + const duration = group.devices.length * (dates[0].stop - dates[0].start) * (repetitions + 1) + + return db.run(r.table('groups').get(group.id).update({ + dates: dates + , repetitions: repetitions + , duration: duration + , isActive: isActive + , state: apiutil.READY + })) + .then(function() { + return dbapi.updateUserGroupDuration(group.owner.email, group.duration, duration) + }) + } + + function doBecomeUnactiveGroup(group) { + const lock = {} + + return dbapi.adminLockGroup(group.id, lock).then(function(lockingSuccessed) { + if (lockingSuccessed) { + return updateGroupDates(group, 1, false) + } + else { + return db.run(r.table('groups').get(group.id).update({ + isActive: false + , state: apiutil.WAITING + })) + } + }) + .finally(function() { + return dbapi.adminUnlockGroup(lock) + }) + } + + function doCleanElapsedGroupDates(group, incr) { + const lock = {} + + return dbapi.adminLockGroup(group.id, lock).then(function(lockingSuccessed) { + return lockingSuccessed ? updateGroupDates(group, incr, false) : false + }) + .finally(function() { + return dbapi.adminUnlockGroup(lock) + }) + } + + function doBecomeActiveGroup(group, incr) { + const lock = {} + + return dbapi.adminLockGroup(group.id, lock).then(function(lockingSuccessed) { + return lockingSuccessed ? updateGroupDates(group, incr, true) : false + }) + .finally(function() { + return dbapi.adminUnlockGroup(lock) + }) + } + + dbapi.unlockBookingObjects().then(function() { + setInterval(function() { + const now = Date.now() + + dbapi.getReadyGroupsOrderByIndex('startTime').then(function(groups) { + Promise.each(groups, (function(group) { + if (apiutil.isOriginGroup(group.class)) { + if (now >= group.dates[0].stop.getTime()) { + return updateOriginGroupLifetime(group) + } + } + else if ((group.isActive || group.state === apiutil.WAITING) && + now >= group.dates[0].stop.getTime()) { + if (group.dates.length === 1) { + return deleteUserGroup(group) + } + else { + return doBecomeUnactiveGroup(group) + } + } + else if (!group.isActive) { + for(const i in group.dates) { + if (now >= group.dates[i].stop.getTime()) { + if (group.dates[i].stop === group.dates[group.dates.length - 1].stop) { + return deleteUserGroup(group) + } + } + else if (now < group.dates[i].start.getTime()) { + return i > 0 ? doCleanElapsedGroupDates(group, i) : false + } + else { + return doBecomeActiveGroup(group, i) + } + } + } + return false + })) + }) + .catch(function(err) { + log.error('An error occured during groups scheduling', err.stack) + }) + }, 1000) + }) +} diff --git a/lib/units/groups-engine/watchers/devices.js b/lib/units/groups-engine/watchers/devices.js new file mode 100644 index 00000000..b3100a28 --- /dev/null +++ b/lib/units/groups-engine/watchers/devices.js @@ -0,0 +1,255 @@ +/** +* Copyright © 2019 code initially contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0 +**/ + +const wirerouter = require('../../../wire/router') +const _ = require('lodash') +const r = require('rethinkdb') +const util = require('util') +const uuid = require('uuid') +const logger = require('../../../util/logger') +const timeutil = require('../../../util/timeutil') +const wireutil = require('../../../wire/util') +const wire = require('../../../wire') +const dbapi = require('../../../db/api') +const db = require('../../../db') + +module.exports = function(push, pushdev, channelRouter) { + const log = logger.createLogger('watcher-devices') + + function sendReleaseDeviceControl(serial, channel) { + push.send([ + channel + , wireutil.envelope( + new wire.UngroupMessage( + wireutil.toDeviceRequirements({ + serial: { + value: serial + , match: 'exact' + } + }) + ) + ) + ]) + } + + function sendDeviceGroupChange(id, group, serial, originName) { + pushdev.send([ + wireutil.global + , wireutil.envelope( + new wire.DeviceGroupChangeMessage( + id + , new wire.DeviceGroupMessage( + group.id + , group.name + , new wire.DeviceGroupOwnerMessage( + group.owner.email + , group.owner.name + ) + , new wire.DeviceGroupLifetimeMessage( + group.dates[0].start.getTime() + , group.dates[0].stop.getTime() + ) + , group.class + , group.repetitions + , originName + ) + , serial + ) + ) + ]) + } + + function sendDeviceChange(device1, device2, action) { + function publishDevice() { + const device = _.cloneDeep(device1) + + delete device.channel + delete device.owner + delete device.group.id + delete device.group.lifeTime + return device + } + + pushdev.send([ + wireutil.global + , wireutil.envelope( + new wire.DeviceChangeMessage( + publishDevice() + , action + , device2.group.origin + , timeutil.now('nano') + ) + ) + ]) + } + + function sendReleaseDeviceControlAndDeviceGroupChange( + device + , sendDeviceGroupChangeWrapper) { + let messageListener + const responseTimer = setTimeout(function() { + channelRouter.removeListener(wireutil.global, messageListener) + sendDeviceGroupChangeWrapper() + }, 5000) + + messageListener = wirerouter() + .on(wire.LeaveGroupMessage, function(channel, message) { + if (message.serial === device.serial && + message.owner.email === device.owner.email) { + clearTimeout(responseTimer) + channelRouter.removeListener(wireutil.global, messageListener) + sendDeviceGroupChangeWrapper() + } + }) + .handler() + + channelRouter.on(wireutil.global, messageListener) + sendReleaseDeviceControl(device.serial, device.channel) + } + + db.run(r + .table('devices') + .pluck( + 'serial' + , 'channel' + , 'owner' + , 'model' + , 'operator' + , 'manufacturer' + , {group: ['id', 'origin', 'originName', 'lifeTime']} + , {provider: ['name']} + , {network: ['type', 'subtype']} + , {display: ['height', 'width']} + , 'version' + , 'sdk' + , 'abi' + , 'cpuPlatform' + , 'openGLESVersion' + , {phone: ['imei']} + , 'marketName' + ) + .changes(), function(err, cursor) { + if (err) { + throw err + } + return cursor + }) + .then(function(cursor) { + cursor.each(function(err, data) { + if (err) { + throw err + } + if (data.old_val === null) { + return sendDeviceChange(data.new_val, data.new_val, 'created') + } + else if (data.new_val === null) { + sendDeviceChange(data.old_val, data.old_val, 'deleted') + } + else if (data.new_val.model !== data.old_val.model || + data.new_val.group.origin !== data.old_val.group.origin || + data.new_val.operator !== data.old_val.operator || + data.new_val.hasOwnProperty('network') && + (!data.old_val.hasOwnProperty('network') || + data.new_val.network.type !== data.old_val.network.type || + data.new_val.network.subtype !== data.old_val.network.subtype + ) || + data.new_val.provider.name !== data.old_val.provider.name) { + sendDeviceChange(data.new_val, data.old_val, 'updated') + } + + const isDeleted = data.new_val === null + const id = isDeleted ? data.old_val.group.id : data.new_val.group.id + + return dbapi.getGroup(id).then(function(group) { + function sendDeviceGroupChangeOnDeviceDeletion() { + const fakeGroup = Object.assign({}, group) + + fakeGroup.id = util.format('%s', uuid.v4()).replace(/-/g, '') + fakeGroup.name = 'none' + sendDeviceGroupChange( + group.id + , fakeGroup + , data.old_val.serial + , data.old_val.group.originName + ) + } + + function sendDeviceGroupChangeOnDeviceCurrentGroupUpdating() { + sendDeviceGroupChange( + data.old_val.group.id + , group + , data.new_val.serial + , data.new_val.group.originName + ) + } + + if (group) { + if (isDeleted) { + if (data.old_val.owner) { + sendReleaseDeviceControlAndDeviceGroupChange( + data.old_val + , sendDeviceGroupChangeOnDeviceDeletion + ) + return + } + sendDeviceGroupChangeOnDeviceDeletion() + return + } + + const isChangeCurrentGroup = data.new_val.group.id !== data.old_val.group.id + const isChangeOriginGroup = data.new_val.group.origin !== data.old_val.group.origin + const isChangeLifeTime = + data.new_val.group.lifeTime.start.getTime() !== + data.old_val.group.lifeTime.start.getTime() + + if (isChangeLifeTime && !isChangeCurrentGroup && !isChangeOriginGroup) { + sendDeviceGroupChange( + data.old_val.group.id + , group + , data.new_val.serial + , data.new_val.group.originName + ) + return + } + + if (isChangeCurrentGroup) { + if (data.new_val.owner && group.users.indexOf(data.new_val.owner.email) < 0) { + sendReleaseDeviceControlAndDeviceGroupChange( + data.new_val + , sendDeviceGroupChangeOnDeviceCurrentGroupUpdating + ) + } + else { + sendDeviceGroupChangeOnDeviceCurrentGroupUpdating() + } + } + + if (isChangeOriginGroup) { + dbapi.getGroup(data.old_val.group.origin).then(function(originGroup) { + if (originGroup) { + dbapi.removeOriginGroupDevice(originGroup, data.new_val.serial) + } + }) + dbapi.getGroup(data.new_val.group.origin).then(function(originGroup) { + if (originGroup) { + dbapi.addOriginGroupDevice(originGroup, data.new_val.serial) + } + }) + if (!isChangeCurrentGroup) { + sendDeviceGroupChange( + data.new_val.group.id + , group + , data.new_val.serial + , data.new_val.group.originName + ) + } + } + } + }) + }) + }) + .catch(function(err) { + log.error('An error occured during DEVICES table watching', err.stack) + }) +} diff --git a/lib/units/groups-engine/watchers/groups.js b/lib/units/groups-engine/watchers/groups.js new file mode 100644 index 00000000..7bb7e9af --- /dev/null +++ b/lib/units/groups-engine/watchers/groups.js @@ -0,0 +1,346 @@ +/** +* Copyright © 2019 code initially contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0 +**/ + +const wirerouter = require('../../../wire/router') +const Promise = require('bluebird') +const _ = require('lodash') +const r = require('rethinkdb') +const logger = require('../../../util/logger') +const timeutil = require('../../../util/timeutil') +const apiutil = require('../../../util/apiutil') +const wireutil = require('../../../wire/util') +const wire = require('../../../wire') +const dbapi = require('../../../db/api') +const db = require('../../../db') + +module.exports = function(push, pushdev, channelRouter) { + const log = logger.createLogger('watcher-groups') + + function sendReleaseDeviceControl(serial, channel) { + push.send([ + channel + , wireutil.envelope( + new wire.UngroupMessage( + wireutil.toDeviceRequirements({ + serial: { + value: serial + , match: 'exact' + } + }) + ) + ) + ]) + } + + function sendGroupChange( + group + , subscribers + , isChangedDates + , isChangedClass + , isAddedUser + , users + , isAddedDevice + , devices + , action) { + function dates2String(dates) { + return dates.map(function(date) { + return { + start: date.start.toJSON() + , stop: date.stop.toJSON() + } + }) + } + pushdev.send([ + wireutil.global + , wireutil.envelope( + new wire.GroupChangeMessage( + new wire.GroupField( + group.id + , group.name + , group.class + , group.privilege + , group.owner + , dates2String(group.dates) + , group.duration + , group.repetitions + , group.devices + , group.users + , group.state + , group.isActive + ) + , action + , subscribers + , isChangedDates + , isChangedClass + , isAddedUser + , users + , isAddedDevice + , devices + , timeutil.now('nano') + ) + ) + ]) + } + + function sendGroupUsersChange(group, users, devices, isAdded, action) { + const isDeletedLater = action === 'GroupDeletedLater' + + pushdev.send([ + wireutil.global + , wireutil.envelope( + new wire.GroupUserChangeMessage(users, isAdded, group.id, isDeletedLater, devices)) + ]) + } + + function doUpdateDeviceOriginGroup(group) { + return dbapi.updateDeviceOriginGroup(group.ticket.serial, group).then(function() { + push.send([ + wireutil.global + , wireutil.envelope( + new wire.DeviceOriginGroupMessage(group.ticket.signature) + ) + ]) + }) + } + + function doUpdateDevicesCurrentGroup(group, devices) { + return Promise.map(devices, function(serial) { + return dbapi.updateDeviceCurrentGroup(serial, group) + }) + } + + function doUpdateDevicesCurrentGroupFromOrigin(devices) { + return Promise.map(devices, function(serial) { + return dbapi.updateDeviceCurrentGroupFromOrigin(serial) + }) + } + + function doUpdateDevicesCurrentGroupDates(group) { + if (apiutil.isOriginGroup(group.class)) { + return Promise.map(group.devices, function(serial) { + return dbapi.loadDeviceBySerial(serial).then(function(device) { + return device.group.id === group.id ? + doUpdateDevicesCurrentGroup(group, [serial]) : + false + }) + }) + } + else { + return Promise.map(group.devices, function(serial) { + return doUpdateDevicesCurrentGroup(group, [serial]) + }) + } + } + + function treatGroupUsersChange(group, users, isActive, isAddedUser) { + if (isActive) { + return Promise.map(users, function(email) { + return Promise.map(group.devices, function(serial) { + return dbapi.loadDeviceBySerial(serial).then(function(device) { + if (device && device.group.id === group.id) { + if (!isAddedUser && device.owner && device.owner.email === email) { + return new Promise(function(resolve) { + let messageListener + const responseTimer = setTimeout(function() { + channelRouter.removeListener(wireutil.global, messageListener) + resolve(serial) + }, 5000) + + messageListener = wirerouter() + .on(wire.LeaveGroupMessage, function(channel, message) { + if (message.serial === serial && + message.owner.email === email) { + clearTimeout(responseTimer) + channelRouter.removeListener(wireutil.global, messageListener) + resolve(serial) + } + }) + .handler() + + channelRouter.on(wireutil.global, messageListener) + sendReleaseDeviceControl(serial, device.channel) + }) + } + return serial + } + return false + }) + }) + .then(function(devices) { + sendGroupUsersChange( + group, [email], _.without(devices, false), isAddedUser, 'GroupUser(s)Updated') + }) + }) + } + else { + return sendGroupUsersChange(group, users, [], isAddedUser, 'GroupUser(s)Updated') + } + } + + function treatGroupDevicesChange(oldGroup, group, devices, isAddedDevice) { + if (isAddedDevice) { + return doUpdateDevicesCurrentGroup(group, devices) + } + else { + return doUpdateDevicesCurrentGroupFromOrigin(devices) + .then(function() { + if (group === null) { + sendGroupUsersChange(oldGroup, oldGroup.users, [], false, 'GroupDeletedLater') + } + }) + } + } + + function treatGroupDeletion(group) { + if (apiutil.isOriginGroup(group.class)) { + return dbapi.getRootGroup().then(function(rootGroup) { + return Promise.map(group.devices, function(serial) { + return dbapi.updateDeviceOriginGroup(serial, rootGroup) + }) + .then(function() { + sendGroupUsersChange(group, group.users, [], false, 'GroupDeletedLater') + }) + }) + } + else { + return sendGroupUsersChange(group, group.users, [], false, 'GroupDeleted') + } + } + + + db.run(r + .table('groups') + .pluck( + 'id' + , 'name' + , 'class' + , 'privilege' + , 'owner' + , 'dates' + , 'duration' + , 'repetitions' + , 'devices' + , 'users' + , 'state' + , 'isActive' + , 'ticket' + ) + .changes(), function(err, cursor) { + if (err) { + throw err + } + return cursor + }) + .then(function(cursor) { + cursor.each(function(err, data) { + let users, devices, isBecomeActive, isBecomeUnactive, isActive + , isAddedUser, isAddedDevice, isUpdatedDeviceOriginGroup, isChangedDates + + if (err) { + throw err + } + if (data.old_val === null) { + sendGroupChange( + data.new_val + , data.new_val.users + , false + , false + , false + , [] + , false + , [] + , 'created' + ) + return sendGroupUsersChange( + data.new_val + , data.new_val.users + , data.new_val.devices + , true + , 'GroupCreated' + ) + } + + if (data.new_val === null) { + sendGroupChange( + data.old_val + , data.old_val.users + , false + , false + , false + , [] + , false + , [] + , 'deleted' + ) + + users = data.old_val.users + devices = data.old_val.devices + isChangedDates = false + isActive = data.old_val.isActive + isBecomeActive = isBecomeUnactive = false + isAddedUser = isAddedDevice = false + isUpdatedDeviceOriginGroup = false + } + else { + users = _.xor(data.new_val.users, data.old_val.users) + devices = _.xor(data.new_val.devices, data.old_val.devices) + isChangedDates = + data.old_val.dates.length !== data.new_val.dates.length || + data.old_val.dates[0].start.getTime() !== + data.new_val.dates[0].start.getTime() || + data.old_val.dates[0].stop.getTime() !== + data.new_val.dates[0].stop.getTime() + isActive = data.new_val.isActive + isBecomeActive = !data.old_val.isActive && data.new_val.isActive + isBecomeUnactive = data.old_val.isActive && !data.new_val.isActive + isAddedUser = data.new_val.users.length > data.old_val.users.length + isAddedDevice = data.new_val.devices.length > data.old_val.devices.length + isUpdatedDeviceOriginGroup = + data.new_val.ticket !== null && + (data.old_val.ticket === null || + data.new_val.ticket.signature !== data.old_val.ticket.signature) + + if (!isUpdatedDeviceOriginGroup) { + sendGroupChange( + data.new_val + , _.union(data.old_val.users, data.new_val.users) + , isChangedDates + , data.old_val.class !== data.new_val.class + , isAddedUser + , users + , isAddedDevice + , devices + , 'updated' + ) + } + } + + if (isUpdatedDeviceOriginGroup) { + return doUpdateDeviceOriginGroup(data.new_val) + } + else if (isBecomeActive && data.new_val.devices.length) { + return doUpdateDevicesCurrentGroup(data.new_val, data.new_val.devices) + } + else if (isBecomeUnactive && data.new_val.devices.length) { + return doUpdateDevicesCurrentGroupFromOrigin(data.new_val.devices) + } + else if (devices.length && isActive && !apiutil.isOriginGroup(data.old_val.class)) { + return treatGroupDevicesChange(data.old_val, data.new_val, devices, isAddedDevice) + } + else if (data.new_val === null) { + return treatGroupDeletion(data.old_val) + } + else if (isChangedDates && isActive) { + return doUpdateDevicesCurrentGroupDates(data.new_val) + } + else if (users.length) { + return treatGroupUsersChange(data.old_val, users, isActive, isAddedUser) + } + return true + }) + }) + .catch(function(err) { + log.error('An error occured during GROUPS table watching', err.stack) + }) +} diff --git a/lib/units/groups-engine/watchers/users.js b/lib/units/groups-engine/watchers/users.js new file mode 100644 index 00000000..ce23553c --- /dev/null +++ b/lib/units/groups-engine/watchers/users.js @@ -0,0 +1,94 @@ +/** +* Copyright © 2019 code initially contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0 +**/ + +const timeutil = require('../../../util/timeutil') +const r = require('rethinkdb') +const _ = require('lodash') +const logger = require('../../../util/logger') +const wireutil = require('../../../wire/util') +const wire = require('../../../wire') +const db = require('../../../db') + +module.exports = function(pushdev) { + const log = logger.createLogger('watcher-users') + + function sendUserChange(user, isAddedGroup, groups, action, targets) { + pushdev.send([ + wireutil.global + , wireutil.envelope( + new wire.UserChangeMessage( + user + , isAddedGroup + , groups + , action + , targets + , timeutil.now('nano'))) + ]) + } + + db.run(r + .table('users') + .pluck( + 'email' + , 'name' + , 'privilege' + , {groups: ['quotas', 'subscribed'] + }) + .changes(), function(err, cursor) { + if (err) { + throw err + } + return cursor + }) + .then(function(cursor) { + cursor.each(function(err, data) { + if (err) { + throw err + } + if (data.old_val === null) { + sendUserChange(data.new_val, false, [], 'created', ['settings']) + } + else if (data.new_val === null) { + sendUserChange(data.old_val, false, [], 'deleted', ['settings']) + } + else { + const targets = [] + + if (!_.isEqual( + data.new_val.groups.quotas.allocated + , data.old_val.groups.quotas.allocated)) { + targets.push('settings') + targets.push('view') + } + else if (!_.isEqual( + data.new_val.groups.quotas.consumed + , data.old_val.groups.quotas.consumed)) { + targets.push('view') + } + else if (data.new_val.groups.quotas.defaultGroupsNumber !== + data.old_val.groups.quotas.defaultGroupsNumber || + data.new_val.groups.quotas.defaultGroupsDuration !== + data.old_val.groups.quotas.defaultGroupsDuration || + data.new_val.groups.quotas.defaultGroupsRepetitions !== + data.old_val.groups.quotas.defaultGroupsRepetitions || + data.new_val.groups.quotas.repetitions !== + data.old_val.groups.quotas.repetitions || + !_.isEqual(data.new_val.groups.subscribed, data.old_val.groups.subscribed)) { + targets.push('settings') + } + if (targets.length) { + sendUserChange( + data.new_val + , data.new_val.groups.subscribed.length > data.old_val.groups.subscribed.length + , _.xor(data.new_val.groups.subscribed, data.old_val.groups.subscribed) + , 'updated' + , targets) + } + } + }) + }) + .catch(function(err) { + log.error('An error occured during USERS table watching', err.stack) + }) +} diff --git a/lib/units/processor/index.js b/lib/units/processor/index.js index d2a866c1..a2c89952 100644 --- a/lib/units/processor/index.js +++ b/lib/units/processor/index.js @@ -1,3 +1,7 @@ +/** +* Copyright © 2019 contains code contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0 +**/ + var Promise = require('bluebird') var logger = require('../../util/logger') @@ -55,17 +59,70 @@ module.exports = db.ensureConnectivity(function(options) { }) devDealer.on('message', wirerouter() + .on(wire.UpdateAccessTokenMessage, function(channel, message, data) { + appDealer.send([channel, data]) + }) + .on(wire.DeleteUserMessage, function(channel, message, data) { + appDealer.send([channel, data]) + }) + .on(wire.DeviceChangeMessage, function(channel, message, data) { + appDealer.send([channel, data]) + }) + .on(wire.UserChangeMessage, function(channel, message, data) { + appDealer.send([channel, data]) + }) + .on(wire.GroupChangeMessage, function(channel, message, data) { + appDealer.send([channel, data]) + }) + .on(wire.DeviceGroupChangeMessage, function(channel, message, data) { + appDealer.send([channel, data]) + }) + .on(wire.GroupUserChangeMessage, function(channel, message, data) { + appDealer.send([channel, data]) + }) // Initial device message - .on(wire.DeviceIntroductionMessage, function(channel, message, data) { + .on(wire.DeviceIntroductionMessage, function(channel, message) { dbapi.saveDeviceInitialState(message.serial, message) - .then(function() { + .then(function(device) { devDealer.send([ message.provider.channel , wireutil.envelope(new wire.DeviceRegisteredMessage( message.serial )) ]) - appDealer.send([channel, data]) + appDealer.send([ + channel + , wireutil.envelope(new wire.DeviceIntroductionMessage( + message.serial + , message.status + , new wire.ProviderMessage( + message.provider.channel + , message.provider.name + ) + , new wire.DeviceGroupMessage( + device.group.id + , device.group.name + , new wire.DeviceGroupOwnerMessage( + device.group.owner.email + , device.group.owner.name + ) + , new wire.DeviceGroupLifetimeMessage( + device.group.lifeTime.start.getTime() + , device.group.lifeTime.stop.getTime() + ) + , device.group.class + , device.group.repetitions + , device.group.originName + ) + )) + ]) + }) + .catch(function(err) { + log.error( + 'Unable to save the initial state of Device "%s"' + , message.serial + , err.stack + ) }) }) // Workerless messages diff --git a/lib/units/storage/plugins/apk/index.js b/lib/units/storage/plugins/apk/index.js index 3d1f93ba..9ed22bca 100644 --- a/lib/units/storage/plugins/apk/index.js +++ b/lib/units/storage/plugins/apk/index.js @@ -37,7 +37,7 @@ module.exports = function(options) { }) .catch(function(err) { log.error('Unable to read manifest of "%s"', req.params.id, err.stack) - res.status(500) + res.status(200) .json({ success: false }) diff --git a/lib/units/websocket/index.js b/lib/units/websocket/index.js index 5ed69194..51e3cbd7 100644 --- a/lib/units/websocket/index.js +++ b/lib/units/websocket/index.js @@ -1,3 +1,7 @@ +/** +* Copyright © 2019 contains code contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0 +**/ + var http = require('http') var events = require('events') var util = require('util') @@ -23,6 +27,8 @@ var ip = require('./middleware/remote-ip') var auth = require('./middleware/auth') var jwtutil = require('../../util/jwtutil') +const apiutil = require('../../util/apiutil') + module.exports = function(options) { var log = logger.createLogger('websocket') var server = http.createServer() @@ -118,23 +124,99 @@ module.exports = function(options) { } } + let disconnectSocket var messageListener = wirerouter() + .on(wire.UpdateAccessTokenMessage, function() { + socket.emit('user.keys.accessToken.updated') + }) + .on(wire.DeleteUserMessage, function() { + disconnectSocket(true) + }) + .on(wire.DeviceChangeMessage, function(channel, message) { + if (user.groups.subscribed.indexOf(message.device.group.origin) > -1 || + user.groups.subscribed.indexOf(message.oldOriginGroupId) > -1) { + socket.emit('user.settings.devices.' + message.action, message) + } + }) + .on(wire.UserChangeMessage, function(channel, message) { + Promise.map(message.targets, function(target) { + socket.emit('user.' + target + '.users.' + message.action, message) + }) + }) + .on(wire.GroupChangeMessage, function(channel, message) { + if (user.privilege === 'admin' || + user.email === message.group.owner.email || + !apiutil.isOriginGroup(message.group.class) && + (message.action === 'deleted' || + message.action === 'updated' && + (message.isChangedDates || message.isChangedClass || message.devices.length))) { + socket.emit('user.settings.groups.' + message.action, message) + } + if (message.subscribers.indexOf(user.email) > -1) { + socket.emit('user.view.groups.' + message.action, message) + } + }) + .on(wire.DeviceGroupChangeMessage, function(channel, message) { + if (user.groups.subscribed.indexOf(message.id) > -1) { + if (user.groups.subscribed.indexOf(message.group.id) > -1) { + socket.emit('device.updateGroupDevice', { + important: true + , data: { + serial: message.serial + , group: message.group + } + }) + } + else { + socket.emit('device.removeGroupDevices', {important: true, devices: [message.serial]}) + } + } + else if (user.groups.subscribed.indexOf(message.group.id) > -1) { + socket.emit('device.addGroupDevices', {important: true, devices: [message.serial]}) + } + }) + .on(wire.GroupUserChangeMessage, function(channel, message) { + if (message.users.indexOf(user.email) > -1) { + if (message.isAdded) { + user.groups.subscribed = _.union(user.groups.subscribed, [message.id]) + if (message.devices.length) { + socket.emit('device.addGroupDevices', {important: true, devices: message.devices}) + } + } + else { + if (message.devices.length) { + socket.emit('device.removeGroupDevices', {important: true, devices: message.devices}) + } + if (message.isDeletedLater) { + setTimeout(function() { + user.groups.subscribed = _.without(user.groups.subscribed, message.id) + }, 5000) + } + else { + user.groups.subscribed = _.without(user.groups.subscribed, message.id) + } + } + } + }) .on(wire.DeviceLogMessage, function(channel, message) { socket.emit('device.log', message) }) .on(wire.DeviceIntroductionMessage, function(channel, message) { - socket.emit('device.add', { - important: true - , data: { - serial: message.serial - , present: false - , provider: message.provider - , owner: null - , status: message.status - , ready: false - , reverseForwards: [] - } - }) + if (user.groups.subscribed.indexOf(message.group.id) > -1) { + socket.emit('device.add', { + important: true + , data: { + serial: message.serial + , present: true + , provider: message.provider + , owner: null + , status: message.status + , ready: false + , reverseForwards: [] + , group: message.group + } + }) + } }) .on(wire.DeviceReadyMessage, function(channel, message) { socket.emit('device.change', { @@ -307,6 +389,7 @@ module.exports = function(options) { joinChannel(user.group) new Promise(function(resolve) { + disconnectSocket = resolve socket.on('disconnect', resolve) // Global messages for all clients using socket.io // @@ -314,15 +397,19 @@ module.exports = function(options) { .on('device.note', function(data) { return dbapi.setDeviceNote(data.serial, data.note) .then(function() { - return dbapi.loadDevice(data.serial) + return dbapi.loadDevice(user.groups.subscribed, data.serial) }) - .then(function(device) { - if (device) { - io.emit('device.change', { - important: true - , data: { - serial: device.serial - , notes: device.notes + .then(function(cursor) { + if (cursor) { + cursor.next(function(err, device) { + if (!err) { + io.emit('device.change', { + important: true + , data: { + serial: device.serial + , notes: device.notes + } + }) } }) } @@ -364,7 +451,7 @@ module.exports = function(options) { .on('user.keys.accessToken.remove', function(data) { return dbapi.removeUserAccessToken(user.email, data.title) .then(function() { - socket.emit('user.keys.accessToken.removed', data.title) + socket.emit('user.keys.accessToken.updated') }) }) .on('user.keys.adb.add', function(data) { @@ -916,6 +1003,7 @@ module.exports = function(options) { channelRouter.removeListener(channel, messageListener) sub.unsubscribe(channel) }) + socket.disconnect(true) }) .catch(function(err) { // Cannot guarantee integrity of client @@ -923,8 +1011,7 @@ module.exports = function(options) { 'Client had an error, disconnecting due to probable loss of integrity' , err.stack ) - - socket.disconnect(true) + // move 'socket.disconnect(true)' statement to finally block instead! }) }) diff --git a/lib/util/apiutil.js b/lib/util/apiutil.js new file mode 100644 index 00000000..016e0f18 --- /dev/null +++ b/lib/util/apiutil.js @@ -0,0 +1,257 @@ +/** +* Copyright © 2019 code initially contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0 +**/ + +const Promise = require('bluebird') +const _ = require('lodash') +const logger = require('./logger') +const datautil = require('./datautil') + +const apiutil = Object.create(null) +const log = logger.createLogger('api:controllers:apiutil') + +apiutil.PENDING = 'pending' +apiutil.READY = 'ready' +apiutil.WAITING = 'waiting' + +apiutil.BOOKABLE = 'bookable' +apiutil.STANDARD = 'standard' +apiutil.ONCE = 'once' +apiutil.DEBUG = 'debug' +apiutil.ORIGIN = 'origin' +apiutil.STANDARDIZABLE = 'standardizable' + +apiutil.ROOT = 'root' +apiutil.ADMIN = 'admin' +apiutil.USER = 'user' + +apiutil.FIVE_MN = 300 * 1000 +apiutil.ONE_HOUR = 3600 * 1000 +apiutil.ONE_DAY = 24 * apiutil.ONE_HOUR +apiutil.ONE_WEEK = 7 * apiutil.ONE_DAY +apiutil.ONE_MONTH = 30 * apiutil.ONE_DAY +apiutil.ONE_QUATER = 3 * apiutil.ONE_MONTH +apiutil.ONE_HALF_YEAR = 6 * apiutil.ONE_MONTH +apiutil.ONE_YEAR = 365 * apiutil.ONE_DAY + +apiutil.MAX_USER_GROUPS_NUMBER = 5 +apiutil.MAX_USER_GROUPS_DURATION = 15 * apiutil.ONE_DAY +apiutil.MAX_USER_GROUPS_REPETITIONS = 10 + +apiutil.CLASS_DURATION = { + once: Infinity +, bookable: Infinity +, standard: Infinity +, hourly: apiutil.ONE_HOUR +, daily: apiutil.ONE_DAY +, weekly: apiutil.ONE_WEEK +, monthly: apiutil.ONE_MONTH +, quaterly: apiutil.ONE_QUATER +, halfyearly: apiutil.ONE_HALF_YEAR +, yearly: apiutil.ONE_YEAR +, debug: apiutil.FIVE_MN +} + +apiutil.isOriginGroup = function(_class) { + return _class === apiutil.BOOKABLE || _class === apiutil.STANDARD +} + +apiutil.isAdminGroup = function(_class) { + return apiutil.isOriginGroup(_class) || _class === apiutil.DEBUG +} + +apiutil.internalError = function(res, ...args) { + log.error.apply(log, args) + apiutil.respond(res, 500, 'Internal Server Error') +} + +apiutil.respond = function(res, code, message, data) { + const status = code >= 200 && code < 300 + const response = { + success: status + , description: message + } + + if (data) { + for (const key in data) { + if (data.hasOwnProperty(key)) { + response[key] = data[key] + } + } + } + res.status(code).json(response) + return status +} + +apiutil.publishGroup = function(group) { +// delete group.lock + delete group.createdAt + delete group.ticket + return group +} + +apiutil.publishDevice = function(device, user) { + datautil.normalize(device, user) +// delete device.group.lock + return device +} + +apiutil.publishUser = function(user) { +// delete user.groups.lock + return user +} + +apiutil.publishAccessToken = function(token) { + delete token.email + delete token.jwt + return token +} + +apiutil.filterDevice = function(req, device) { + const fields = req.swagger.params.fields.value + + if (fields) { + return _.pick(apiutil.publishDevice(device, req.user), fields.split(',')) + } + return apiutil.publishDevice(device, req.user) +} + +apiutil.computeDuration = function(group, deviceNumber) { + return (group.devices.length + deviceNumber) * + (group.dates[0].stop - group.dates[0].start) * + (group.repetitions + 1) +} + +apiutil.lightComputeStats = function(res, stats) { + if (stats.locked) { + apiutil.respond(res, 503, 'Server too busy, please try again later') + return Promise.reject('busy') + } + return 'not found' +} + +apiutil.computeStats = function(res, stats, objectName, ...lock) { + if (!stats.replaced) { + if (stats.skipped) { + return apiutil.respond(res, 404, `Not Found (${objectName})`) + } + if (stats.locked) { + return apiutil.respond(res, 503, 'Server too busy, please try again later') + } + return apiutil.respond(res, 403, `Forbidden (${objectName})`) + } + if (lock.length) { + lock[0][objectName] = stats.changes[0].new_val + } + return true +} + +apiutil.lockResult = function(stats) { + const result = {status: false, data: stats} + + if (stats.replaced || stats.skipped) { + result.status = true + result.data.locked = false + } + else { + result.data.locked = true + } + return result +} + +apiutil.lockDeviceResult = function(stats, fn, groups, serial) { + const result = apiutil.lockResult(stats) + if (!result.status) { + return fn(groups, serial).then(function(devices) { + if (!devices.length) { + result.data.locked = false + result.status = true + } + return result + }) + } + return result +} + +apiutil.setIntervalWrapper = function(fn, numTimes, delay) { + return fn().then(function(result) { + if (result.status) { + return result.data + } + return new Promise(function(resolve, reject) { + let counter = 0 + const interval = setInterval(function() { + return fn().then(function(result) { + if (result.status || ++counter === numTimes) { + if (!result.status && counter === numTimes) { + log.debug('%s() failed %s times in a loop!', fn.name, counter) + } + clearInterval(interval) + resolve(result.data) + } + }) + .catch(function(err) { + clearInterval(interval) + reject(err) + }) + }, delay) + }) + }) +} + +apiutil.redirectApiWrapper = function(field, fn, req, res) { + if (typeof req.body === 'undefined') { + req.body = {} + } + req.body[field + 's'] = req.swagger.params[field].value + req.swagger.params.redirected = {value: true} + fn(req, res) +} + +apiutil.computeGroupDates = function(lifeTime, _class, repetitions) { + const dates = new Array(lifeTime) + + for(let repetition = 1 + , currentLifeTime = { + start: new Date(lifeTime.start.getTime()) + , stop: new Date(lifeTime.stop.getTime()) + } + ; repetition <= repetitions + ; repetition++) { + currentLifeTime.start = new Date( + currentLifeTime.start.getTime() + + apiutil.CLASS_DURATION[_class] + ) + currentLifeTime.stop = new Date( + currentLifeTime.stop.getTime() + + apiutil.CLASS_DURATION[_class] + ) + dates.push({ + start: new Date(currentLifeTime.start.getTime()) + , stop: new Date(currentLifeTime.stop.getTime()) + }) + } + return dates +} + +apiutil.checkBodyParameter = function(body, parameter) { + return typeof body !== 'undefined' && typeof body[parameter] !== 'undefined' +} + +apiutil.getBodyParameter = function(body, parameter) { + let undef + + return apiutil.checkBodyParameter(body, parameter) ? body[parameter] : undef +} + +apiutil.checkQueryParameter = function(parameter) { + return typeof parameter !== 'undefined' && typeof parameter.value !== 'undefined' +} + +apiutil.getQueryParameter = function(parameter) { + let undef + + return apiutil.checkQueryParameter(parameter) ? parameter.value : undef +} + +module.exports = apiutil diff --git a/lib/util/datautil.js b/lib/util/datautil.js index d1cfaa27..eb691d74 100644 --- a/lib/util/datautil.js +++ b/lib/util/datautil.js @@ -1,3 +1,7 @@ +/** +* Copyright © 2019 contains code contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0 +**/ + var deviceData = require('stf-device-db') var browserData = require('stf-browser-db') @@ -41,13 +45,14 @@ datautil.applyBrowsers = function(device) { } datautil.applyOwner = function(device, user) { - device.using = !!device.owner && device.owner.email === user.email + device.using = !!device.owner && + (device.owner.email === user.email || user.privilege === 'admin') return device } // Only owner can see this information datautil.applyOwnerOnlyInfo = function(device, user) { - if (device.owner && device.owner.email === user.email) { + if (device.owner && (device.owner.email === user.email || user.privilege === 'admin')) { // No-op } else { diff --git a/lib/util/deviceutil.js b/lib/util/deviceutil.js index 7b125d9a..257d9249 100644 --- a/lib/util/deviceutil.js +++ b/lib/util/deviceutil.js @@ -1,3 +1,7 @@ +/** +* Copyright © 2019 contains code contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0 +**/ + var logger = require('./logger') var log = logger.createLogger('util:deviceutil') @@ -8,7 +12,7 @@ deviceutil.isOwnedByUser = function(device, user) { return device.present && device.ready && device.owner && - device.owner.email === user.email && + (device.owner.email === user.email || user.privilege === 'admin') && device.using } diff --git a/lib/util/devutil.js b/lib/util/devutil.js index da61b0c3..0772cfd7 100644 --- a/lib/util/devutil.js +++ b/lib/util/devutil.js @@ -2,6 +2,7 @@ var util = require('util') var split = require('split') var Promise = require('bluebird') +var androidDeviceList = require('android-device-list') var devutil = module.exports = Object.create(null) @@ -135,6 +136,7 @@ devutil.makeIdentity = function(serial, properties) { var product = properties['ro.product.name'] var cpuPlatform = properties['ro.board.platform'] var openGLESVersion = properties['ro.opengles.version'] + var marketName = properties['ro.product.device'] openGLESVersion = parseInt(openGLESVersion, 10) if (isNaN(openGLESVersion)) { @@ -157,6 +159,13 @@ devutil.makeIdentity = function(serial, properties) { model = model.substr(manufacturer.length) } + if (marketName) { + var devices = androidDeviceList.getDevicesByDeviceId(marketName) + if (devices.length > 0) { + marketName = devices[0].name + } + } + // Clean up remaining model name // model = model.replace(/[_ ]/g, '') return { @@ -171,5 +180,6 @@ devutil.makeIdentity = function(serial, properties) { , product: product , cpuPlatform: cpuPlatform , openGLESVersion: openGLESVersion + , marketName: marketName } } diff --git a/lib/util/fakedevice.js b/lib/util/fakedevice.js index 966f2d27..6a6de91f 100644 --- a/lib/util/fakedevice.js +++ b/lib/util/fakedevice.js @@ -1,3 +1,7 @@ +/** +* Copyright © 2019 contains code contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0 +**/ + var util = require('util') var uuid = require('uuid') @@ -7,10 +11,8 @@ var dbapi = require('../db/api') var devices = require('stf-device-db/dist/devices-latest') module.exports.generate = function(wantedModel) { - var serial = util.format( - 'fake-%s' - , uuid.v4(null, new Buffer(16)).toString('base64') - ) + // no base64 because some characters as '=' or '/' are not compatible through API (delete devices) + const serial = 'fake-' + util.format('%s', uuid.v4()).replace(/-/g, '') return dbapi.saveDeviceInitialState(serial, { provider: { @@ -28,7 +30,7 @@ module.exports.generate = function(wantedModel) { , model: model , version: '4.1.2' , abi: 'armeabi-v7a' - , sdk: 8 + Math.floor(Math.random() * 12) + , sdk: (8 + Math.floor(Math.random() * 12)).toString() // string required! , display: { density: 3 , fps: 60 @@ -49,6 +51,9 @@ module.exports.generate = function(wantedModel) { , phoneNumber: '0000000000' } , product: model + , cpuPlatform: 'msm8996' + , openGLESVersion: '3.1' + , marketName: 'Bar F9+' }) }) .then(function() { diff --git a/lib/util/fakegroup.js b/lib/util/fakegroup.js new file mode 100644 index 00000000..00ad6f20 --- /dev/null +++ b/lib/util/fakegroup.js @@ -0,0 +1,42 @@ +/** +* Copyright © 2019 code initially contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0 +**/ + +const util = require('util') +const uuid = require('uuid') +const dbapi = require('../db/api') +const apiutil = require('./apiutil') + +module.exports.generate = function() { + return dbapi.getRootGroup().then(function(rootGroup) { + const now = Date.now() + + return dbapi.createUserGroup({ + name: 'fakegroup-' + util.format('%s', uuid.v4()).replace(/-/g, '') + , owner: { + email: rootGroup.owner.email + , name: rootGroup.owner.name + } + , privilege: apiutil.ADMIN + , class: apiutil.BOOKABLE + , repetitions: 0 + , isActive: true + , dates: apiutil.computeGroupDates( + { + start: new Date(now) + , stop: new Date(now + apiutil.ONE_YEAR) + } + , apiutil.BOOKABLE + , 0 + ) + , duration: 0 + , state: apiutil.READY + }) + .then(function(group) { + if (group) { + return group.id + } + throw new Error('Forbidden (groups number quota is reached)') + }) + }) +} diff --git a/lib/util/fakeuser.js b/lib/util/fakeuser.js new file mode 100644 index 00000000..8ca850da --- /dev/null +++ b/lib/util/fakeuser.js @@ -0,0 +1,14 @@ +/** +* Copyright © 2019 code initially contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0 +**/ + +const util = require('util') +const uuid = require('uuid') +const dbapi = require('../db/api') + +module.exports.generate = function() { + const name = 'fakeuser-' + util.format('%s', uuid.v4()).replace(/-/g, '') + const email = name + '@openstf.com' + + return dbapi.createUser(email, name, '127.0.0.1').return(email) +} diff --git a/lib/util/lockutil.js b/lib/util/lockutil.js new file mode 100644 index 00000000..1e2c77e7 --- /dev/null +++ b/lib/util/lockutil.js @@ -0,0 +1,69 @@ +/** +* Copyright © 2019 code initially contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0 +**/ + +const apiutil = require('./apiutil') +const dbapi = require('../db/api') + +const lockutil = Object.create(null) + +lockutil.unlockDevice = function(lock) { + if (lock.device) { + dbapi.unlockDevice(lock.device.serial) + } +} + +lockutil.lockUser = function(email, res, lock) { + return dbapi.lockUser(email) + .then(function(stats) { + return apiutil.computeStats(res, stats, 'user', lock) + }) +} + +lockutil.unlockUser = function(lock) { + if (lock.user) { + dbapi.unlockUser(lock.user.email) + } +} + +lockutil.lockGroupAndUser = function(req, res, lock) { + return lockutil.lockGroup(req, res, lock).then(function(lockingSuccessed) { + return lockingSuccessed ? + lockutil.lockUser(req.user.email, res, lock) : + false + }) +} + +lockutil.unlockGroupAndUser = function(lock) { + lockutil.unlockGroup(lock) + lockutil.unlockUser(lock) +} + +lockutil.lockGroup = function(req, res, lock) { + const id = req.swagger.params.id.value + const email = req.user.email + + return dbapi.lockGroupByOwner(email, id).then(function(stats) { + return apiutil.computeStats(res, stats, 'group', lock) + }) +} + +lockutil.unlockGroup = function(lock) { + if (lock.group) { + dbapi.unlockGroup(lock.group.id) + } +} + +lockutil.unlockGroupAndDevice = function(lock) { + lockutil.unlockGroup(lock) + lockutil.unlockDevice(lock) +} + +lockutil.lockGenericDevice = function(req, res, lock, lockDevice) { + return lockDevice(req.user.groups.subscribed, req.swagger.params.serial.value) + .then(function(stats) { + return apiutil.computeStats(res, stats, 'device', lock) + }) +} + +module.exports = lockutil diff --git a/lib/util/streamutil.js b/lib/util/streamutil.js index 4c6fb284..118071ac 100644 --- a/lib/util/streamutil.js +++ b/lib/util/streamutil.js @@ -36,6 +36,8 @@ module.exports.readAll = function(stream) { stream.on('readable', readableListener) stream.on('end', endListener) + readableListener() + return resolver.promise.finally(function() { stream.removeListener('error', errorListener) stream.removeListener('readable', readableListener) diff --git a/lib/util/timeutil.js b/lib/util/timeutil.js new file mode 100644 index 00000000..98992389 --- /dev/null +++ b/lib/util/timeutil.js @@ -0,0 +1,22 @@ +/** +* Copyright © 2019 code initially contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0 +**/ + +const timeutil = Object.create(null) + +timeutil.now = function(unit) { + const hrTime = process.hrtime() + + switch (unit) { + case 'milli': + return hrTime[0] * 1000 + hrTime[1] / 1000000 + case 'micro': + return hrTime[0] * 1000000 + hrTime[1] / 1000 + case 'nano': + return hrTime[0] * 1000000000 + hrTime[1] + default: + return hrTime[0] * 1000000000 + hrTime[1] + } +} + +module.exports = timeutil diff --git a/lib/wire/wire.proto b/lib/wire/wire.proto index 2b2c60ca..3bc8cdf5 100644 --- a/lib/wire/wire.proto +++ b/lib/wire/wire.proto @@ -1,3 +1,7 @@ +// +// Copyright © 2019 contains code contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0 +// + // Message wrapper enum MessageType { @@ -78,6 +82,160 @@ enum MessageType { FileSystemGetMessage = 82; ConnectStartedMessage = 92; ConnectStoppedMessage = 93; + GroupUserChangeMessage = 1200; + DeviceGroupChangeMessage = 1201; + DeviceOriginGroupMessage = 1202; + DeleteUserMessage = 1203; + UpdateAccessTokenMessage = 1204; + GroupChangeMessage = 1205; + UserChangeMessage = 1206; + DeviceChangeMessage = 1207; +} + +message UpdateAccessTokenMessage { +} + +message DeleteUserMessage { + required string email = 1; +} + +message DeviceOriginGroupMessage { + required string signature = 1; +} + +message UserQuotasDetailField { + required double duration = 1; + required uint32 number = 2; +} + +message UserQuotasField { + required UserQuotasDetailField allocated = 1; + required UserQuotasDetailField consumed = 2; + required uint32 defaultGroupsDuration = 3; + required uint32 defaultGroupsNumber = 4; + required uint32 defaultGroupsRepetitions = 5; + required uint32 repetitions = 6; +} + +message UserGroupsField { + required UserQuotasField quotas = 1; + repeated string subscribed = 2; +} + +message UserField { + required string email = 1; + required string name = 2; + required string privilege = 3; + required UserGroupsField groups = 4; +} + +message UserChangeMessage { + required UserField user = 1; + required bool isAddedGroup = 2; + repeated string groups = 3; + required string action = 4; + repeated string targets = 5; + required double timeStamp = 6; +} + +message DeviceNetworkField { + optional string type = 1; + optional string subtype = 2; +} + +message DeviceDisplayField { + optional uint32 height = 1; + optional uint32 width = 2; +} + +message DevicePhoneField { + optional string imei = 1; +} + +message DeviceProviderField { + optional string name = 1; +} + +message DeviceGroupField { + optional string origin = 1; + optional string originName = 2; +} + +message DeviceField { + required string serial = 1; + optional string model = 2; + optional string version = 3; + optional string operator = 4; + optional DeviceNetworkField network = 5; + optional DeviceDisplayField display = 6; + optional string manufacturer = 7; + optional string sdk = 8; + optional string abi = 9; + optional string cpuPlatform = 10; + optional string openGLESVersion = 11; + optional DevicePhoneField phone = 12; + optional DeviceProviderField provider = 13; + optional DeviceGroupField group = 14; + optional string marketName = 15; +} + +message DeviceChangeMessage { + required DeviceField device = 1; + required string action = 2; + required string oldOriginGroupId = 3; + required double timeStamp = 4; +} + +message GroupDateField { + required string start = 1; + required string stop = 2; +} + +message GroupOwnerField { + required string email = 1; + required string name = 2; +} + +message GroupField { + required string id = 1; + required string name = 2; + required string class = 3; + required string privilege = 4; + required GroupOwnerField owner = 5; + repeated GroupDateField dates = 6; + required uint32 duration = 7; + required uint32 repetitions = 8; + repeated string devices = 9; + repeated string users = 10; + required string state = 11; + required bool isActive = 12; +} + +message GroupChangeMessage { + required GroupField group = 1; + required string action = 2; + repeated string subscribers = 3; + required bool isChangedDates = 4; + required bool isChangedClass = 5; + required bool isAddedUser = 6; + repeated string users = 7; + required bool isAddedDevice = 8; + repeated string devices = 9; + required double timeStamp = 10; +} + +message DeviceGroupChangeMessage { + required string id = 1; + required DeviceGroupMessage group = 2; + required string serial = 3; +} + +message GroupUserChangeMessage { + repeated string users = 1; + required bool isAdded = 2; + required string id = 3; + required bool isDeletedLater = 4; + repeated string devices = 5; } message ConnectStartedMessage { @@ -132,6 +290,26 @@ message DeviceLogMessage { // Introductions +message DeviceGroupOwnerMessage { + required string email = 1; + required string name = 2; +} + +message DeviceGroupLifetimeMessage { + required double start = 1; + required double stop = 2; +} + +message DeviceGroupMessage { + required string id = 1; + required string name = 2; + required DeviceGroupOwnerMessage owner = 3; + required DeviceGroupLifetimeMessage lifeTime = 4; + required string class = 5; + required uint32 repetitions = 6; + required string originName = 7; +} + message ProviderMessage { required string channel = 1; required string name = 2; @@ -145,6 +323,7 @@ message DeviceIntroductionMessage { required string serial = 1; required DeviceStatus status = 2; required ProviderMessage provider = 3; + optional DeviceGroupMessage group = 4; } message DeviceRegisteredMessage { @@ -230,6 +409,7 @@ message DeviceIdentityMessage { optional string product = 12; optional string cpuPlatform = 13; optional string openGLESVersion = 14; + optional string marketName = 15; } message DeviceProperty { diff --git a/package.json b/package.json index 8fa80940..cb64de6d 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,7 @@ "adbkit": "^2.11.1", "adbkit-apkreader": "^3.1.1", "adbkit-monkey": "^1.0.1", + "android-device-list": "^1.2.1", "aws-sdk": "^2.4.13", "basic-auth": "^1.0.3", "bluebird": "^2.10.1", @@ -62,9 +63,9 @@ "lodash": "^4.14.2", "markdown-serve": "^0.3.2", "mime": "^1.3.4", - "minicap-prebuilt": "^2.3.0", + "minicap-prebuilt-beta": "^2.4.0", "minimatch": "^3.0.3", - "minitouch-prebuilt": "^1.2.0", + "minitouch-prebuilt-beta": "^1.3.0", "my-local-ip": "^1.0.0", "openid": "^2.0.1", "passport": "^0.3.2", @@ -109,6 +110,7 @@ "exports-loader": "^0.6.2", "extract-text-webpack-plugin": "^1.0.1", "file-loader": "^0.9.0", + "fs-extra": "^8.1.0", "gulp": "^3.8.11", "gulp-angular-gettext": "^2.1.0", "gulp-eslint": "^3.0.1", @@ -118,15 +120,16 @@ "gulp-run": "^1.6.12", "gulp-util": "^3.0.7", "html-loader": "^0.4.0", + "http-https": "^1.0.0", "imports-loader": "^0.6.5", "jasmine-core": "^2.4.1", - "jasmine-reporters": "^2.1.1", + "jasmine-reporters": "^2.3.2", "json-loader": "^0.5.4", - "karma": "^1.1.2", - "karma-chrome-launcher": "^1.0.1", + "karma": "^1.7.1", + "karma-chrome-launcher": "^2.2.0", "karma-firefox-launcher": "^1.0.0", "karma-ie-launcher": "^1.0.0", - "karma-jasmine": "^1.0.2", + "karma-jasmine": "^2.0.1", "karma-junit-reporter": "^1.1.0", "karma-opera-launcher": "^1.0.0", "karma-phantomjs-launcher": "^1.0.0", @@ -138,8 +141,8 @@ "node-libs-browser": "^1.0.0", "node-sass": "^3.4.2", "phantomjs-prebuilt": "^2.1.11", - "protractor": "^4.0.3", - "protractor-html-screenshot-reporter": "0.0.21", + "protractor": "^5.4.1", + "protractor-html-reporter-2": "1.0.4", "raw-loader": "^0.5.1", "sass-loader": "^4.0.0", "script-loader": "^0.7.0", @@ -151,7 +154,7 @@ "then-jade": "^2.4.1", "url-loader": "^0.5.7", "webpack": "^1.12.11", - "webpack-dev-server": "^1.14.1" + "webpack-dev-server": "^3.1.11" }, "engines": { "node": ">= 6.9" diff --git a/res/app/app.js b/res/app/app.js index 5a3a4b77..f5073847 100644 --- a/res/app/app.js +++ b/res/app/app.js @@ -1,3 +1,7 @@ +/** +* Copyright © 2019 contains code contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0 +**/ + require.ensure([], function(require) { require('angular') require('angular-route') @@ -10,13 +14,15 @@ require.ensure([], function(require) { require('angular-hotkeys').name, require('./layout').name, require('./device-list').name, + require('./group-list').name, require('./control-panes').name, require('./menu').name, require('./settings').name, require('./docs').name, require('./user').name, require('./../common/lang').name, - require('stf/standalone').name + require('stf/standalone').name, + require('./group-list').name ]) .config(function($routeProvider, $locationProvider) { $locationProvider.hashPrefix('!') diff --git a/res/app/components/stf/column-choice/column-choice-directive.js b/res/app/components/stf/column-choice/column-choice-directive.js new file mode 100644 index 00000000..997ce854 --- /dev/null +++ b/res/app/components/stf/column-choice/column-choice-directive.js @@ -0,0 +1,15 @@ +/** +* Copyright © 2019 code initially contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0 +**/ + +module.exports = function() { + return { + restrict: 'E', + scope: { + buttonStyle: '@?', + columnData: '=', + resetData: '&' + }, + template: require('./column-choice.pug'), + } +} diff --git a/res/app/components/stf/column-choice/column-choice.css b/res/app/components/stf/column-choice/column-choice.css new file mode 100644 index 00000000..b27a8f39 --- /dev/null +++ b/res/app/components/stf/column-choice/column-choice.css @@ -0,0 +1,23 @@ +/** +* Copyright © 2019 code initially contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0 +**/ + +.stf-column-choice .stf-column-customize { + white-space: nowrap; + padding: 10px; + padding-bottom: 0; + column-count: 2; + -moz-column-count: 2; + -webkit-column-count: 2; + max-width: 800px; +} + +.stf-column-choice .stf-column-customize .checkbox { + margin-bottom: 10px; +} + +.stf-column-choice .stf-column-customize .checkbox-label { + margin-left: 10px; +} + + diff --git a/res/app/components/stf/column-choice/column-choice.pug b/res/app/components/stf/column-choice/column-choice.pug new file mode 100644 index 00000000..1dd5b6a7 --- /dev/null +++ b/res/app/components/stf/column-choice/column-choice.pug @@ -0,0 +1,24 @@ +// + Copyright © 2019 code initially contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0 +// + +.stf-column-choice + .btn-group(uib-dropdown auto-close='outsideClick') + button.btn.btn-sm.btn-primary-outline( + style='margin-top: 5px; {{buttonStyle}}' + type='button' + uib-dropdown-toggle) + i.fa.fa-columns + span(translate) Customize + ul.dropdown-menu.pointer.stf-column-customize( + uib-dropdown-menu role='menu' + ng-click='$event.stopPropagation()') + li(ng-repeat='column in columnData') + label.checkbox.pointer + input(type='checkbox' ng-model='column.selected') + span.checkbox-label(ng-bind-template='{{::column.name | translate}}') + li + button.btn.btn-xs.btn-danger-outline.checkbox(ng-click='resetData()') + i.fa.fa-trash-o + span(ng-bind='"Reset"|translate') + diff --git a/res/app/components/stf/column-choice/index.js b/res/app/components/stf/column-choice/index.js new file mode 100644 index 00000000..33a2ddd6 --- /dev/null +++ b/res/app/components/stf/column-choice/index.js @@ -0,0 +1,12 @@ +/** +* Copyright © 2019 code initially contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0 +**/ + +require('./column-choice.css') + +module.exports = angular.module('stf.column-choice', [ + require('stf/common-ui').name +]) + .directive('stfColumnChoice', require('./column-choice-directive')) + + diff --git a/res/app/components/stf/common-ui/index.js b/res/app/components/stf/common-ui/index.js index 6264cf60..8b468bce 100644 --- a/res/app/components/stf/common-ui/index.js +++ b/res/app/components/stf/common-ui/index.js @@ -1,4 +1,9 @@ +/** +* Copyright © 2019 contains code contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0 +**/ + module.exports = angular.module('stf/common-ui', [ + require('./pagination').name, require('./safe-apply').name, require('./clear-button').name, require('./filter-button').name, diff --git a/res/app/components/stf/common-ui/modals/generic-modal/generic-modal-service.js b/res/app/components/stf/common-ui/modals/generic-modal/generic-modal-service.js new file mode 100644 index 00000000..166527c1 --- /dev/null +++ b/res/app/components/stf/common-ui/modals/generic-modal/generic-modal-service.js @@ -0,0 +1,38 @@ +/** +* Copyright © 2019 code initially contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0 +**/ + +module.exports = + function GenericModalServiceFactory($uibModal) { + const service = {} + + const ModalInstanceCtrl = function($scope, $uibModalInstance, data) { + $scope.data = data + + $scope.ok = function() { + $uibModalInstance.close(true) + } + + $scope.cancel = function() { + $uibModalInstance.dismiss('cancel') + } + } + + service.open = function(data) { + var modalInstance = $uibModal.open({ + template: require('./generic-modal.pug'), + controller: ModalInstanceCtrl, + size: data.size, + animation: true, + resolve: { + data: function() { + return data + } + } + }) + + return modalInstance.result + } + + return service + } diff --git a/res/app/components/stf/common-ui/modals/generic-modal/generic-modal-spec.js b/res/app/components/stf/common-ui/modals/generic-modal/generic-modal-spec.js new file mode 100644 index 00000000..6ce25b01 --- /dev/null +++ b/res/app/components/stf/common-ui/modals/generic-modal/generic-modal-spec.js @@ -0,0 +1,15 @@ +/** +* Copyright © 2019 code initially contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0 +**/ + +describe('GenericModalService', function() { + + beforeEach(angular.mock.module(require('./').name)) + + it('should ...', inject(function() { + + //expect(FatalMessageService.doSomething()).toEqual('something'); + + })) + +}) diff --git a/res/app/components/stf/common-ui/modals/generic-modal/generic-modal.pug b/res/app/components/stf/common-ui/modals/generic-modal/generic-modal.pug new file mode 100644 index 00000000..ca43b2aa --- /dev/null +++ b/res/app/components/stf/common-ui/modals/generic-modal/generic-modal.pug @@ -0,0 +1,36 @@ +// + Copyright © 2019 code initially contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0 +// + +.stf-generic-modal.stf-modal + .modal-header + h4.modal-title.text-warning(ng-if="data.type === 'Warning'") + i.fa.fa-warning + .button-spacer + span(translate) {{data.type}} + + h4.modal-title.text-info(ng-if="data.type === 'Information'") + i.fa.fa-info-circle + .button-spacer + span(translate) {{data.type}} + + h4.modal-title.text-danger(ng-if="data.type === 'Error'") + i.fa.fa-times-circle + .button-spacer + span(translate) {{data.type}} + + .modal-body + label.control-label + span(translate) {{data.message}} + + .modal-footer + button.btn.btn-primary( + type='button' + ng-click='ok()') + span(translate) OK + + button.btn.btn-warning( + type='button' + ng-if='data.cancel' + ng-click='cancel()') + span(translate) Cancel diff --git a/res/app/components/stf/common-ui/modals/generic-modal/index.js b/res/app/components/stf/common-ui/modals/generic-modal/index.js new file mode 100644 index 00000000..529fb6bd --- /dev/null +++ b/res/app/components/stf/common-ui/modals/generic-modal/index.js @@ -0,0 +1,8 @@ +/** +* Copyright © 2019 code initially contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0 +**/ + +module.exports = angular.module('stf.generic-modal', [ + require('stf/common-ui/modals/common').name +]) + .factory('GenericModalService', require('./generic-modal-service')) diff --git a/res/app/components/stf/common-ui/modals/index.js b/res/app/components/stf/common-ui/modals/index.js index 3014aa1a..5413fb6d 100644 --- a/res/app/components/stf/common-ui/modals/index.js +++ b/res/app/components/stf/common-ui/modals/index.js @@ -1,4 +1,9 @@ +/** +* Copyright © 2019 contains code contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0 +**/ + module.exports = angular.module('stf.modals', [ + require('./generic-modal').name, require('./fatal-message').name, require('./socket-disconnected').name, require('./version-update').name, diff --git a/res/app/components/stf/common-ui/pagination/index.js b/res/app/components/stf/common-ui/pagination/index.js new file mode 100644 index 00000000..f8609619 --- /dev/null +++ b/res/app/components/stf/common-ui/pagination/index.js @@ -0,0 +1,12 @@ +/** +* Copyright © 2019 code initially contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0 +**/ + +require('./pagination.css') + +module.exports = angular.module('stf.pagination', [ +]) + .filter('pagedObjectsFilter', require('./pagination-filter')) + .directive('stfPager', require('./pagination-directive')) + .factory('ItemsPerPageOptionsService', require('./pagination-service')) + diff --git a/res/app/components/stf/common-ui/pagination/pagination-directive.js b/res/app/components/stf/common-ui/pagination/pagination-directive.js new file mode 100644 index 00000000..3199179a --- /dev/null +++ b/res/app/components/stf/common-ui/pagination/pagination-directive.js @@ -0,0 +1,24 @@ +/** +* Copyright © 2019 code initially contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0 +**/ + +module.exports = function() { + return { + restrict: 'E', + scope: { + tooltipLabel: '@', + iconStyle: '@?', + itemsSearchStyle: '@?', + itemsSearch: '=', + itemsPerPageOptions: '<', + itemsPerPage: '=', + totalItems: '<', + totalItemsStyle: '@?', + currentPage: '=' + }, + template: require('./pagination.pug'), + link: function(scope, element, attrs) { + scope.currentPage = 1 + } + } +} diff --git a/res/app/components/stf/common-ui/pagination/pagination-filter.js b/res/app/components/stf/common-ui/pagination/pagination-filter.js new file mode 100644 index 00000000..23c2337e --- /dev/null +++ b/res/app/components/stf/common-ui/pagination/pagination-filter.js @@ -0,0 +1,16 @@ +/** +* Copyright © 2019 code initially contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0 +**/ + +module.exports = function() { + return function(objects, scope, currentPage, maxItems, searchItems) { + scope[searchItems] = objects + if (scope[maxItems].value === 0) { + return objects + } + return objects.slice( + (scope[currentPage] - 1) * scope[maxItems].value + , scope[currentPage] * scope[maxItems].value + ) + } +} diff --git a/res/app/components/stf/common-ui/pagination/pagination-service.js b/res/app/components/stf/common-ui/pagination/pagination-service.js new file mode 100644 index 00000000..1b4f7d72 --- /dev/null +++ b/res/app/components/stf/common-ui/pagination/pagination-service.js @@ -0,0 +1,21 @@ +/** +* Copyright © 2019 code initially contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0 +**/ + +module.exports = function ItemsPerPageOptionsServiceFactory() { + const service = [ + {name: '1', value: 1} + , {name: '5', value: 5} + , {name: '10', value: 10} + , {name: '20', value: 20} + , {name: '50', value: 50} + , {name: '100', value: 100} + , {name: '200', value: 200} + , {name: '500', value: 500} + , {name: '1000', value: 1000} + , {name: '*', value: 0} + ] + + return service +} + diff --git a/res/app/components/stf/common-ui/pagination/pagination.css b/res/app/components/stf/common-ui/pagination/pagination.css new file mode 100644 index 00000000..cd1fa8d8 --- /dev/null +++ b/res/app/components/stf/common-ui/pagination/pagination.css @@ -0,0 +1,4 @@ +/** +* Copyright © 2019 code initially contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0 +**/ + diff --git a/res/app/components/stf/common-ui/pagination/pagination.pug b/res/app/components/stf/common-ui/pagination/pagination.pug new file mode 100644 index 00000000..1f3d8e58 --- /dev/null +++ b/res/app/components/stf/common-ui/pagination/pagination.pug @@ -0,0 +1,34 @@ +// + Copyright © 2019 code initially contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0 +// + +.input-group(style='margin-right: 5px; {{itemsSearchStyle}}' class='{{itemsSearchStyle}}') + .input-group-addon.input-sm + i.glyphicon.glyphicon-search( + class='{{iconStyle}}' + uib-tooltip='{{tooltipLabel}}' + tooltip-placement='auto top-right' + tooltip-popup-delay='500') + input.form-control.input-sm(type='text' placeholder='Search' ng-model='itemsSearch') + +select.custon-select.form-control.input-sm( + ng-model='itemsPerPage' + ng-options='option as option.name for option in itemsPerPageOptions track by option.value') + +uib-pagination( + style='vertical-align: middle; width: -moz-max-content' + total-items='totalItems' + items-per-page='itemsPerPage.value' + class='pagination-sm' + max-size='1' + boundary-links='true' + boundary-link-numbers='false' + previous-text='<' next-text='>' first-text='First' last-text='Last' + rotate='true' + ng-model='currentPage') + +button.btn.btn-sm.btn-info( + type='button' + class='{{totalItemsStyle}}' + style='pointer-events: none') + span {{totalItems}} diff --git a/res/app/components/stf/device/device-service.js b/res/app/components/stf/device/device-service.js index a5672a46..53009906 100644 --- a/res/app/components/stf/device/device-service.js +++ b/res/app/components/stf/device/device-service.js @@ -1,6 +1,11 @@ +/** +* Copyright © 2019 contains code contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0 +**/ + var oboe = require('oboe') var _ = require('lodash') var EventEmitter = require('eventemitter3') +let Promise = require('bluebird') module.exports = function DeviceServiceFactory($http, socket, EnhanceDeviceService) { var deviceService = {} @@ -93,6 +98,12 @@ module.exports = function DeviceServiceFactory($http, socket, EnhanceDeviceServi if (index >= 0) { devices.splice(index, 1) delete devicesBySerial[data.serial] + for (var serial in devicesBySerial) { + if (devicesBySerial[serial] > index) { + devicesBySerial[serial]-- + } + } + sync(data) this.emit('remove', data) } }.bind(this) @@ -131,6 +142,8 @@ module.exports = function DeviceServiceFactory($http, socket, EnhanceDeviceServi } notify(event) } + + /** code removed to avoid to show forbidden devices in user view! else { if (options.filter(event.data)) { insert(event.data) @@ -139,6 +152,7 @@ module.exports = function DeviceServiceFactory($http, socket, EnhanceDeviceServi notify(event) } } + **/ } scopedSocket.on('device.add', addListener) @@ -153,6 +167,43 @@ module.exports = function DeviceServiceFactory($http, socket, EnhanceDeviceServi } this.devices = devices + + function addGroupDevicesListener(event) { + return Promise.map(event.devices, function(serial) { + return deviceService.load(serial).then(function(device) { + return device + }) + }) + .then(function(_devices) { + _devices.forEach(function(device) { + if (device && typeof devicesBySerial[device.serial] === 'undefined') { + insert(device) + notify(event) + } + }) + }) + } + + function removeGroupDevicesListener(event) { + event.devices.forEach(function(serial) { + if (typeof devicesBySerial[serial] !== 'undefined') { + remove(devices[devicesBySerial[serial]]) + notify(event) + } + }) + } + + function updateGroupDeviceListener(event) { + let device = get(event.data) + if (device) { + modify(device, event.data) + notify(event) + } + } + + scopedSocket.on('device.addGroupDevices', addGroupDevicesListener) + scopedSocket.on('device.removeGroupDevices', removeGroupDevicesListener) + scopedSocket.on('device.updateGroupDevice', updateGroupDeviceListener) } Tracker.prototype = new EventEmitter() diff --git a/res/app/components/stf/device/enhance-device/enhance-device-service.js b/res/app/components/stf/device/enhance-device/enhance-device-service.js index b55701a1..4346dffd 100644 --- a/res/app/components/stf/device/enhance-device/enhance-device-service.js +++ b/res/app/components/stf/device/enhance-device/enhance-device-service.js @@ -1,3 +1,7 @@ +/** +* Copyright © 2019 contains code contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0 +**/ + module.exports = function EnhanceDeviceServiceFactory($filter, AppState) { var service = {} @@ -62,6 +66,8 @@ module.exports = function EnhanceDeviceServiceFactory($filter, AppState) { device.enhancedUserProfileUrl = enhanceUserProfileUrl(device.owner.email) device.enhancedUserName = device.owner.name || 'No name' } + + device.enhancedGroupOwnerProfileUrl = enhanceUserProfileUrl(device.group.owner.email) } function enhanceUserProfileUrl(email) { diff --git a/res/app/components/stf/devices/devices-service.js b/res/app/components/stf/devices/devices-service.js new file mode 100644 index 00000000..bd0b551c --- /dev/null +++ b/res/app/components/stf/devices/devices-service.js @@ -0,0 +1,107 @@ +/** +* Copyright © 2019 code initially contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0 +**/ + +const oboe = require('oboe') + +module.exports = function DevicesServiceFactory( + $rootScope +, $http +, socket +, CommonService +) { + const DevicesService = {} + + function buildQueryParameters(filters) { + var query = '' + + if (filters.present !== 'Any') { + query += 'present=' + filters.present.toLowerCase() + } + if (filters.booked !== 'Any') { + query += (query === '' ? '' : '&') + 'booked=' + filters.booked.toLowerCase() + } + if (filters.annotated !== 'Any') { + query += (query === '' ? '' : '&') + 'annotated=' + filters.annotated.toLowerCase() + } + if (filters.controlled !== 'Any') { + query += (query === '' ? '' : '&') + 'controlled=' + filters.controlled.toLowerCase() + } + return query === '' ? query : '?' + query + } + + DevicesService.getOboeDevices = function(target, fields, addDevice) { + return oboe(CommonService.getBaseUrl() + + '/api/v1/devices?target=' + target + '&fields=' + fields) + .node('devices[*]', function(device) { + addDevice(device) + }) + } + + DevicesService.getDevices = function(target, fields) { + return $http.get('/api/v1/devices?target=' + target + '&fields=' + fields) + } + + DevicesService.getDevice = function(serial, fields) { + return $http.get('/api/v1/devices/' + serial + '?fields=' + fields) + } + + DevicesService.removeDevice = function(serial, filters) { + return $http.delete('/api/v1/devices/' + serial + buildQueryParameters(filters)) + } + + DevicesService.removeDevices = function(filters, serials) { + return $http({ + method: 'DELETE', + url: '/api/v1/devices' + buildQueryParameters(filters), + headers: { + 'Content-Type': 'application/json;charset=utf-8' + }, + data: typeof serials === 'undefined' ? serials : JSON.stringify({serials: serials}) + }) + } + + DevicesService.addOriginGroupDevice = function(id, serial) { + return $http.put('/api/v1/devices/' + serial + '/groups/' + id) + } + + DevicesService.addOriginGroupDevices = function(id, serials) { + return $http({ + method: 'PUT', + url: '/api/v1/devices/groups/' + id + '?fields=""', + data: typeof serials === 'undefined' ? serials : JSON.stringify({serials: serials}) + }) + } + + DevicesService.removeOriginGroupDevice = function(id, serial) { + return $http.delete('/api/v1/devices/' + serial + '/groups/' + id) + } + + DevicesService.removeOriginGroupDevices = function(id, serials) { + return $http({ + method: 'DELETE', + url: '/api/v1/devices/groups/' + id + '?fields=""', + headers: { + 'Content-Type': 'application/json;charset=utf-8' + }, + data: typeof serials === 'undefined' ? serials : JSON.stringify({serials: serials}) + }) + } + + socket.on('user.settings.devices.created', function(device) { + $rootScope.$broadcast('user.settings.devices.created', device) + $rootScope.$apply() + }) + + socket.on('user.settings.devices.deleted', function(device) { + $rootScope.$broadcast('user.settings.devices.deleted', device) + $rootScope.$apply() + }) + + socket.on('user.settings.devices.updated', function(device) { + $rootScope.$broadcast('user.settings.devices.updated', device) + $rootScope.$apply() + }) + + return DevicesService +} diff --git a/res/app/components/stf/devices/index.js b/res/app/components/stf/devices/index.js new file mode 100644 index 00000000..622da50c --- /dev/null +++ b/res/app/components/stf/devices/index.js @@ -0,0 +1,9 @@ +/** +* Copyright © 2019 code initially contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0 +**/ + +module.exports = angular.module('stf.devices', [ + require('stf/util/common').name, + require('stf/socket').name +]) +.factory('DevicesService', require('./devices-service')) diff --git a/res/app/components/stf/groups/groups-service.js b/res/app/components/stf/groups/groups-service.js new file mode 100644 index 00000000..8f4a4c2d --- /dev/null +++ b/res/app/components/stf/groups/groups-service.js @@ -0,0 +1,184 @@ +/** +* Copyright © 2019 code initially contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0 +**/ + +const oboe = require('oboe') + +module.exports = function GroupsServiceFactory( + $rootScope +, $http +, socket +, CommonService +) { + const GroupsService = {} + + GroupsService.getGroupUsers = function(id, fields) { + return $http.get('/api/v1/groups/' + id + '/users?fields=' + fields) + } + + GroupsService.getOboeGroupUsers = function(id, fields, addGroupUser) { + return oboe(CommonService.getBaseUrl() + + '/api/v1/groups/' + id + '/users?fields=' + fields) + .node('users[*]', function(user) { + addGroupUser(user) + }) + } + + GroupsService.getGroupDevices = function(id, bookable, fields) { + return $http.get('/api/v1/groups/' + id + '/devices?bookable=' + bookable + '&fields=' + fields) + } + + GroupsService.getOboeGroupDevices = function(id, bookable, fields, addGroupDevice) { + return oboe(CommonService.getBaseUrl() + + '/api/v1/groups/' + id + '/devices?bookable=' + bookable + '&fields=' + fields) + .node('devices[*]', function(device) { + addGroupDevice(device) + }) + } + + GroupsService.getGroupDevice = function(id, serial, fields) { + return $http.get('/api/v1/groups/' + id + '/devices/' + serial + '?fields=' + fields) + } + + GroupsService.addGroupDevice = function(id, serial) { + return $http.put('/api/v1/groups/' + id + '/devices/' + serial) + } + + GroupsService.addGroupDevices = function(id, serials) { + return $http({ + method: 'PUT', + url: '/api/v1/groups/' + id + '/devices', + data: typeof serials === 'undefined' ? serials : JSON.stringify({serials: serials}) + }) + } + + GroupsService.removeGroupDevice = function(id, serial) { + return $http.delete('/api/v1/groups/' + id + '/devices/' + serial) + } + + GroupsService.removeGroupDevices = function(id, serials) { + return $http({ + method: 'DELETE', + url: '/api/v1/groups/' + id + '/devices', + headers: { + 'Content-Type': 'application/json;charset=utf-8' + }, + data: typeof serials === 'undefined' ? serials : JSON.stringify({serials: serials}) + }) + } + + GroupsService.addGroupUser = function(id, email) { + return $http.put('/api/v1/groups/' + id + '/users/' + email) + } + + GroupsService.addGroupUsers = function(id, emails) { + return $http({ + method: 'PUT', + url: '/api/v1/groups/' + id + '/users', + data: typeof emails === 'undefined' ? emails : JSON.stringify({emails: emails}) + }) + } + + GroupsService.removeGroupUser = function(id, email) { + return $http.delete('/api/v1/groups/' + id + '/users/' + email) + } + + GroupsService.removeGroupUsers = function(id, emails) { + return $http({ + method: 'DELETE', + url: '/api/v1/groups/' + id + '/users', + headers: { + 'Content-Type': 'application/json;charset=utf-8' + }, + data: typeof emails === 'undefined' ? emails : JSON.stringify({emails: emails}) + }) + } + + GroupsService.getOboeGroups = function(addGroup) { + return oboe(CommonService.getBaseUrl() + '/api/v1/groups') + .node('groups[*]', function(group) { + addGroup(group) + }) + } + + GroupsService.getGroups = function() { + return $http.get('/api/v1/groups') + } + + GroupsService.getOboeMyGroups = function(addGroup) { + return oboe(CommonService.getBaseUrl() + '/api/v1/groups?owner=true') + .node('groups[*]', function(group) { + addGroup(group) + }) + } + + GroupsService.getMyGroups = function() { + return $http.get('/api/v1/groups?owner=true') + } + + GroupsService.getGroup = function(id) { + return $http.get('/api/v1/groups/' + id) + } + + GroupsService.removeGroup = function(id) { + return $http.delete('/api/v1/groups/' + id) + } + + GroupsService.removeGroups = function(ids) { + return $http({ + method: 'DELETE', + url: '/api/v1/groups?_=' + Date.now(), + headers: { + 'Content-Type': 'application/json;charset=utf-8' + }, + data: typeof ids === 'undefined' ? ids : JSON.stringify({ids: ids}) + }) + } + + GroupsService.createGroup = function() { + return $http({ + method: 'POST', + url: '/api/v1/groups', + data: JSON.stringify({'state': 'pending'}) + }) + } + + GroupsService.updateGroup = function(id, data) { + return $http({ + method: 'PUT', + url: '/api/v1/groups/' + id, + data: JSON.stringify(data) + }) + } + socket.on('user.settings.groups.created', function(group) { + $rootScope.$broadcast('user.settings.groups.created', group) + $rootScope.$apply() + }) + + socket.on('user.settings.groups.deleted', function(group) { + $rootScope.$broadcast('user.settings.groups.deleted', group) + $rootScope.$apply() + }) + + socket.on('user.settings.groups.updated', function(group) { + $rootScope.$broadcast('user.settings.groups.updated', group) + $rootScope.$apply() + }) + + socket.on('user.view.groups.created', function(group) { + $rootScope.$broadcast('user.view.groups.created', group) + $rootScope.$apply() + }) + + socket.on('user.view.groups.deleted', function(group) { + $rootScope.$broadcast('user.view.groups.deleted', group) + $rootScope.$apply() + }) + + socket.on('user.view.groups.updated', function(group) { + $rootScope.$broadcast('user.view.groups.updated', group) + $rootScope.$apply() + }) + + return GroupsService +} diff --git a/res/app/components/stf/groups/index.js b/res/app/components/stf/groups/index.js new file mode 100644 index 00000000..0966131d --- /dev/null +++ b/res/app/components/stf/groups/index.js @@ -0,0 +1,8 @@ +/** +* Copyright © 2019 code initially contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0 +**/ + +module.exports = angular.module('stf.groups', [ + require('stf/util/common').name +]) +.factory('GroupsService', require('./groups-service')) diff --git a/res/app/components/stf/tokens/access-token-service.js b/res/app/components/stf/tokens/access-token-service.js index 214aa8cf..00a4e388 100644 --- a/res/app/components/stf/tokens/access-token-service.js +++ b/res/app/components/stf/tokens/access-token-service.js @@ -1,3 +1,7 @@ +/** +* Copyright © 2019 contains code contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0 +**/ + module.exports = function AccessTokenServiceFactory( $rootScope , $http @@ -26,7 +30,7 @@ module.exports = function AccessTokenServiceFactory( $rootScope.$apply() }) - socket.on('user.keys.accessToken.removed', function() { + socket.on('user.keys.accessToken.updated', function() { $rootScope.$broadcast('user.keys.accessTokens.updated') $rootScope.$apply() }) diff --git a/res/app/components/stf/user/index.js b/res/app/components/stf/user/index.js index d547328d..899bfb36 100644 --- a/res/app/components/stf/user/index.js +++ b/res/app/components/stf/user/index.js @@ -1,4 +1,6 @@ module.exports = angular.module('stf/user', [ + require('stf/socket').name, + require('stf/common-ui').name, require('stf/app-state').name ]) .factory('UserService', require('./user-service')) diff --git a/res/app/components/stf/user/user-service.js b/res/app/components/stf/user/user-service.js index 0c9e449f..9bba785c 100644 --- a/res/app/components/stf/user/user-service.js +++ b/res/app/components/stf/user/user-service.js @@ -1,5 +1,10 @@ +/** +* Copyright © 2019 contains code contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0 +**/ + module.exports = function UserServiceFactory( $rootScope +, $http , socket , AppState , AddAdbKeyModalService @@ -8,6 +13,10 @@ module.exports = function UserServiceFactory( var user = UserService.currentUser = AppState.user + UserService.getUser = function() { + return $http.get('/api/v1/user') + } + UserService.getAdbKeys = function() { return (user.adbKeys || (user.adbKeys = [])) } diff --git a/res/app/components/stf/users/index.js b/res/app/components/stf/users/index.js new file mode 100644 index 00000000..7e8a57ae --- /dev/null +++ b/res/app/components/stf/users/index.js @@ -0,0 +1,8 @@ +/** +* Copyright © 2019 code initially contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0 +**/ + +module.exports = angular.module('stf.users', [ + require('stf/util/common').name +]) +.factory('UsersService', require('./users-service')) diff --git a/res/app/components/stf/users/users-service.js b/res/app/components/stf/users/users-service.js new file mode 100644 index 00000000..ef6df566 --- /dev/null +++ b/res/app/components/stf/users/users-service.js @@ -0,0 +1,96 @@ +/** +* Copyright © 2019 code initially contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0 +**/ + +const oboe = require('oboe') + +module.exports = function UsersServiceFactory( + $rootScope +, $http +, socket +, CommonService +) { + const UsersService = {} + + function buildQueryParameters(filters) { + var query = '' + + if (filters.groupOwner !== 'Any') { + query += 'groupOwner=' + filters.groupOwner.toLowerCase() + } + return query === '' ? query : '?' + query + } + + UsersService.getOboeUsers = function(fields, addUser) { + return oboe(CommonService.getBaseUrl() + '/api/v1/users?fields=' + fields) + .node('users[*]', function(user) { + addUser(user) + }) + } + + UsersService.getUsers = function(fields) { + return $http.get('/api/v1/users?fields=' + fields) + } + + UsersService.getUser = function(email, fields) { + return $http.get('/api/v1/users/' + email + '?fields=' + fields) + } + + UsersService.removeUser = function(email, filters) { + return $http.delete('/api/v1/users/' + email + buildQueryParameters(filters)) + } + + UsersService.removeUsers = function(filters, emails) { + return $http({ + method: 'DELETE', + url: '/api/v1/users' + buildQueryParameters(filters), + headers: { + 'Content-Type': 'application/json;charset=utf-8' + }, + data: typeof emails === 'undefined' ? emails : JSON.stringify({emails: emails}) + }) + } + + UsersService.updateUserGroupsQuotas = function(email, number, duration, repetitions) { + return $http.put( + '/api/v1/users/' + email + + '/groupsQuotas?number=' + number + + '&duration=' + duration + + '&repetitions=' + repetitions + ) + } + + UsersService.updateDefaultUserGroupsQuotas = function(number, duration, repetitions) { + return $http.put( + '/api/v1/users/groupsQuotas?number=' + number + + '&duration=' + duration + + '&repetitions=' + repetitions + ) + } + + UsersService.createUser = function(name, email) { + return $http.post('/api/v1/users/' + email + '?name=' + name) + } + + socket.on('user.settings.users.created', function(user) { + $rootScope.$broadcast('user.settings.users.created', user) + $rootScope.$apply() + }) + + socket.on('user.settings.users.deleted', function(user) { + $rootScope.$broadcast('user.settings.users.deleted', user) + $rootScope.$apply() + }) + + socket.on('user.view.users.updated', function(user) { + $rootScope.$broadcast('user.view.users.updated', user) + $rootScope.$apply() + }) + + socket.on('user.settings.users.updated', function(user) { + $rootScope.$broadcast('user.settings.users.updated', user) + $rootScope.$apply() + }) + + return UsersService +} diff --git a/res/app/components/stf/util/common/common-service.js b/res/app/components/stf/util/common/common-service.js new file mode 100644 index 00000000..b4f6b5f5 --- /dev/null +++ b/res/app/components/stf/util/common/common-service.js @@ -0,0 +1,223 @@ +/** +* Copyright © 2019 code initially contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0 +**/ + +const _ = require('lodash') + +module.exports = function CommonServiceFactory( + $window, + $location, + GenericModalService +) { + const service = {} + + const FIVE_MN = 300 * 1000 + const ONE_HOUR = 3600 * 1000 + const ONE_DAY = 24 * ONE_HOUR + const ONE_WEEK = 7 * ONE_DAY + const ONE_MONTH = 30 * ONE_DAY + const ONE_QUATER = 3 * ONE_MONTH + const ONE_HALF_YEAR = 6 * ONE_MONTH + const ONE_YEAR = 365 * ONE_DAY + + function getClassOptionsField(id, field) { + for(var i in service.classOptions) { + if (service.classOptions[i].id === id) { + return service.classOptions[i][field] + } + } + return '' + } + + service.classOptions = [ + {name: 'Once', id: 'once', privilege: 'user', duration: Infinity}, + {name: 'Hourly', id: 'hourly', privilege: 'user', duration: ONE_HOUR}, + {name: 'Daily', id: 'daily', privilege: 'user', duration: ONE_DAY}, + {name: 'Weekly', id: 'weekly', privilege: 'user', duration: ONE_WEEK}, + {name: 'Monthly', id: 'monthly', privilege: 'user', duration: ONE_MONTH}, + {name: 'Quaterly', id: 'quaterly', privilege: 'user', duration: ONE_QUATER}, + {name: 'Halfyearly', id: 'halfyearly', privilege: 'user', duration: ONE_HALF_YEAR}, + {name: 'Yearly', id: 'yearly', privilege: 'user', duration: ONE_YEAR}, + {name: 'Debug', id: 'debug', privilege: 'admin', duration: FIVE_MN}, + {name: 'Bookable', id: 'bookable', privilege: 'admin', duration: Infinity}, + {name: 'Standard', id: 'standard', privilege: 'admin', duration: Infinity} + ] + + service.getClassName = function(id) { + return getClassOptionsField(id, 'name') + } + + service.getClassDuration = function(id) { + return getClassOptionsField(id, 'duration') + } + + service.getDuration = function(ms) { + if (ms < 1000) { + return '0s' + } + var s = Math.floor(ms / 1000) + var m = Math.floor(s / 60) + + s %= 60 + var h = Math.floor(m / 60) + + m %= 60 + var d = Math.floor(h / 24) + + h %= 24 + return (d === 0 ? '' : d + 'd') + + (h === 0 ? '' : (d === 0 ? '' : ' ') + h + 'h') + + (m === 0 ? '' : (h === 0 ? '' : ' ') + m + 'm') + + (s === 0 ? '' : (m === 0 ? '' : ' ') + s + 's') + } + + service.errorWrapper = function(fn, args) { + return fn.apply(null, args).catch(function(error) { + return GenericModalService.open({ + message: error.data ? + error.data.description : + error.status + ' ' + error.statusText + , type: 'Error' + , size: 'lg' + , cancel: false + }) + .then(function() { + return error + }) + }) + } + + service.getIndex = function(array, value, property) { + for(var i in array) { + if (array[i][property] === value) { + return i + } + } + return -1 + } + + service.merge = function(oldObject, newObject) { + var undefinedValue + + return _.merge(oldObject, newObject, function(a, b) { + return _.isArray(b) ? b : undefinedValue + }) + } + + service.isAddable = function(object, timeStamp) { + return typeof object === 'undefined' || + timeStamp >= object.timeStamp && object.index === -1 + } + + service.isExisting = function(object) { + return typeof object !== 'undefined' && + object.index !== -1 + } + + service.isRemovable = function(object, timeStamp) { + return service.isExisting(object) && + timeStamp >= object.timeStamp + } + + service.add = function(array, objects, value, property, timeStamp) { + if (service.isAddable(objects[value[property]], timeStamp)) { + objects[value[property]] = { + index: array.push(value) - 1 + , timeStamp: timeStamp + } + return array[objects[value[property]].index] + } + return null + } + + service.update = function(array, objects, value, property, timeStamp, noAdding) { + if (service.isExisting(objects[value[property]])) { + service.merge(array[objects[value[property]].index], value) + objects[value[property]].timeStamp = timeStamp + return array[objects[value[property]].index] + } + else if (!noAdding) { + return service.add(array, objects, value, property, timeStamp) + } + return null + } + + service.delete = function(array, objects, key, timeStamp) { + if (service.isRemovable(objects[key], timeStamp)) { + const index = objects[key].index + const value = array.splice(index, 1)[0] + + objects[key].index = -1 + objects[key].timeStamp = timeStamp + for (var k in objects) { + if (objects[k].index > index) { + objects[k].index-- + } + } + return value + } + else if (typeof objects[key] === 'undefined') { + objects[key] = { + index: -1 + , timeStamp: timeStamp + } + } + return null + } + + service.sortBy = function(data, column) { + const index = service.getIndex(data.columns, column.name, 'name') + + if (index !== data.sort.index) { + data.sort.reverse = false + column.sort = 'sort-asc' + data.columns[data.sort.index].sort = 'none' + data.sort.index = index + } + else { + data.sort.reverse = !data.sort.reverse + column.sort = column.sort === 'sort-asc' ? 'sort-desc' : 'sort-asc' + } + return service + } + + service.isOriginGroup = function(_class) { + return _class === 'bookable' || _class === 'standard' + } + + service.isNoRepetitionsGroup = function(_class) { + return service.isOriginGroup(_class) || _class === 'once' + } + + service.url = function(url) { + const a = $window.document.createElement('a') + + $window.document.body.appendChild(a) + a.href = url + a.click() + $window.document.body.removeChild(a) + return service + } + + service.copyToClipboard = function(data) { + const input = $window.document.createElement('input') + + $window.document.body.appendChild(input) + input.value = data + input.select() + $window.document.execCommand('copy') + $window.document.body.removeChild(input) + return service + } + + service.getBaseUrl = function() { + return $location.protocol() + + '://' + + $location.host() + + ':' + + $location.port() + } + + return service +} + diff --git a/res/app/components/stf/util/common/index.js b/res/app/components/stf/util/common/index.js new file mode 100644 index 00000000..394bc761 --- /dev/null +++ b/res/app/components/stf/util/common/index.js @@ -0,0 +1,8 @@ +/** +* Copyright © 2019 code initially contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0 +**/ + +module.exports = angular.module('stf.util.common', [ + require('stf/common-ui').name +]) +.factory('CommonService', require('./common-service')) diff --git a/res/app/control-panes/automation/store-account/store-account-spec.js b/res/app/control-panes/automation/store-account/store-account-spec.js index e0a3d550..c013cfe9 100644 --- a/res/app/control-panes/automation/store-account/store-account-spec.js +++ b/res/app/control-panes/automation/store-account/store-account-spec.js @@ -13,5 +13,4 @@ describe('StoreAccountCtrl', function() { expect(1).toEqual(1) })) - }) diff --git a/res/app/control-panes/control-panes-controller.js b/res/app/control-panes/control-panes-controller.js index 5a5db164..1fccfdf7 100644 --- a/res/app/control-panes/control-panes-controller.js +++ b/res/app/control-panes/control-panes-controller.js @@ -1,3 +1,7 @@ +/** +* Copyright © 2019 contains code contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0 +**/ + module.exports = function ControlPanesController($scope, $http, gettext, $routeParams, $timeout, $location, DeviceService, GroupService, ControlService, @@ -85,7 +89,9 @@ module.exports = $scope.$watch('device.state', function(newValue, oldValue) { if (newValue !== oldValue) { - if (oldValue === 'using') { +/*************** fix bug: it seems automation state was forgotten ? *************/ + if (oldValue === 'using' || oldValue === 'automation') { +/******************************************************************************/ FatalMessageService.open($scope.device, false) } } diff --git a/res/app/device-list/column/device-column-service.js b/res/app/device-list/column/device-column-service.js index 96dafd30..1dd2b58f 100644 --- a/res/app/device-list/column/device-column-service.js +++ b/res/app/device-list/column/device-column-service.js @@ -1,3 +1,7 @@ +/** +* Copyright © 2019 contains code contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0 +**/ + var _ = require('lodash') var filterOps = { @@ -18,7 +22,7 @@ var filterOps = { } } -module.exports = function DeviceColumnService($filter, gettext) { +module.exports = function DeviceColumnService($filter, gettext, SettingsService, AppState) { // Definitions for all possible values. return { state: DeviceStatusCell({ @@ -27,6 +31,52 @@ module.exports = function DeviceColumnService($filter, gettext) { return $filter('translate')(device.enhancedStateAction) } }) + , group: TextCell({ + title: gettext('Group Name') + , value: function(device) { + return $filter('translate')(device.group.name) + } + }) + , groupSchedule: TextCell({ + title: gettext('Group Class') + , value: function(device) { + return $filter('translate')(device.group.class) + } + }) + , groupOwner: LinkCell({ + title: gettext('Group Owner') + , target: '_blank' + , value: function(device) { + return $filter('translate')(device.group.owner.name) + } + , link: function(device) { + return device.enhancedGroupOwnerProfileUrl + } + }) + , groupEndTime: TextCell({ + title: gettext('Group Expiration Date') + , value: function(device) { + return $filter('date')(device.group.lifeTime.stop, SettingsService.get('dateFormat')) + } + }) + , groupStartTime: TextCell({ + title: gettext('Group Starting Date') + , value: function(device) { + return $filter('date')(device.group.lifeTime.start, SettingsService.get('dateFormat')) + } + }) + , groupRepetitions: TextCell({ + title: gettext('Group Repetitions') + , value: function(device) { + return device.group.repetitions + } + }) + , groupOrigin: TextCell({ + title: gettext('Group Origin') + , value: function(device) { + return $filter('translate')(device.group.originName) + } + }) , model: DeviceModelCell({ title: gettext('Model') , value: function(device) { @@ -38,7 +88,7 @@ module.exports = function DeviceColumnService($filter, gettext) { , value: function(device) { return device.name || device.model || device.serial } - }) + }, AppState.user.email) , operator: TextCell({ title: gettext('Carrier') , value: function(device) { @@ -179,6 +229,12 @@ module.exports = function DeviceColumnService($filter, gettext) { return device.manufacturer || '' } }) + , marketName: TextCell({ + title: gettext('Market name') + , value: function(device) { + return device.marketName || '' + } + }) , sdk: NumberCell({ title: gettext('SDK') , defaultOrder: 'desc' @@ -305,8 +361,10 @@ function zeroPadTwoDigit(digit) { } function compareIgnoreCase(a, b) { - var la = (a || '').toLowerCase() - var lb = (b || '').toLowerCase() +/***** fix bug: cast to String for Safari compatibility ****/ + var la = (String(a) || '').toLowerCase() + var lb = (String(b) || '').toLowerCase() +/***********************************************************/ if (la === lb) { return 0 } @@ -316,8 +374,10 @@ function compareIgnoreCase(a, b) { } function filterIgnoreCase(a, filterValue) { - var va = (a || '').toLowerCase() - var vb = filterValue.toLowerCase() +/***** fix bug: cast to String for Safari compatibility ****/ + var va = (String(a) || '').toLowerCase() + var vb = String(filterValue).toLowerCase() +/***********************************************************/ return va.indexOf(vb) !== -1 } @@ -551,7 +611,7 @@ function DeviceModelCell(options) { }) } -function DeviceNameCell(options) { +function DeviceNameCell(options, ownerEmail) { return _.defaults(options, { title: options.title , defaultOrder: 'asc' @@ -566,11 +626,11 @@ function DeviceNameCell(options) { var a = td.firstChild var t = a.firstChild - if (device.using) { + if (device.using && device.owner.email === ownerEmail) { a.className = 'device-product-name-using' a.href = '#!/control/' + device.serial } - else if (device.usable) { + else if (device.usable && !device.using) { a.className = 'device-product-name-usable' a.href = '#!/control/' + device.serial } diff --git a/res/app/device-list/column/index.js b/res/app/device-list/column/index.js index 8f4323a5..e1aa0fdf 100644 --- a/res/app/device-list/column/index.js +++ b/res/app/device-list/column/index.js @@ -1,4 +1,10 @@ +/** +* Copyright © 2019 contains code contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0 +**/ + module.exports = angular.module('stf.device-list.column', [ - require('gettext').name + require('gettext').name, + require('stf/settings').name, + require('stf/app-state').name ]) .service('DeviceColumnService', require('./device-column-service')) diff --git a/res/app/device-list/device-list-controller.js b/res/app/device-list/device-list-controller.js index f14109e4..afcff67f 100644 --- a/res/app/device-list/device-list-controller.js +++ b/res/app/device-list/device-list-controller.js @@ -1,3 +1,7 @@ +/** +* Copyright © 2019 contains code contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0 +**/ + var QueryParser = require('./util/query-parser') module.exports = function DeviceListCtrl( @@ -55,6 +59,10 @@ module.exports = function DeviceListCtrl( name: 'manufacturer' , selected: false } + , { + name: 'marketName' + , selected: false + } , { name: 'sdk' , selected: false @@ -123,6 +131,34 @@ module.exports = function DeviceListCtrl( name: 'owner' , selected: true } + , { + name: 'group' + , selected: false + } + , { + name: 'groupSchedule' + , selected: false + } + , { + name: 'groupStartTime' + , selected: false + } + , { + name: 'groupEndTime' + , selected: false + } + , { + name: 'groupRepetitions' + , selected: false + } + , { + name: 'groupOwner' + , selected: false + } + , { + name: 'groupOrigin' + , selected: false + } ] $scope.columns = defaultColumns diff --git a/res/app/device-list/icons/device-list-icons-directive.js b/res/app/device-list/icons/device-list-icons-directive.js index 19212479..1ab79335 100644 --- a/res/app/device-list/icons/device-list-icons-directive.js +++ b/res/app/device-list/icons/device-list-icons-directive.js @@ -96,7 +96,6 @@ module.exports = function DeviceListIconsDirective( a.removeAttribute('href') li.classList.add('device-is-busy') } - return li } } @@ -169,8 +168,7 @@ module.exports = function DeviceListIconsDirective( } if (device.using) { - if (e.target.classList.contains('btn') && - e.target.classList.contains('state-using')) { + if (e.target.classList.contains('btn') && e.target.classList.contains('state-using')) { kickDevice(device) e.preventDefault() } diff --git a/res/app/device-list/stats/device-list-stats-directive.js b/res/app/device-list/stats/device-list-stats-directive.js index bd8c6f6c..ac47dda8 100644 --- a/res/app/device-list/stats/device-list-stats-directive.js +++ b/res/app/device-list/stats/device-list-stats-directive.js @@ -1,3 +1,7 @@ +/** +* Copyright © 2019 contains code contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0 +**/ + module.exports = function DeviceListStatsDirective( UserService ) { @@ -73,8 +77,11 @@ module.exports = function DeviceListStatsDirective( var newStats = updateStats(device) scope.counter.total -= 1 - scope.counter.busy += newStats.busy - oldStats.busy - scope.counter.using += newStats.using - oldStats.using + scope.counter.usable -= newStats.usable + scope.counter.busy -= newStats.busy + scope.counter.using -= newStats.using + //scope.counter.busy += newStats.busy - oldStats.busy + //scope.counter.using += newStats.using - oldStats.using delete mapping[device.serial] diff --git a/res/app/group-list/group-list-controller.js b/res/app/group-list/group-list-controller.js new file mode 100644 index 00000000..d86315df --- /dev/null +++ b/res/app/group-list/group-list-controller.js @@ -0,0 +1,464 @@ +/** +* Copyright © 2019 code initially contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0 +**/ + +const _ = require('lodash') + +module.exports = function GroupListCtrl( + $scope +, $filter +, GroupsService +, UserService +, UsersService +, DevicesService +, SettingsService +, ItemsPerPageOptionsService +, CommonService +) { + const users = [] + const usersByEmail = {} + const devices = [] + const devicesBySerial = {} + const groupsById = {} + const groupsEnv = {} + const groupUserToAdd = {} + const userFields = + 'email,' + + 'name,' + + 'privilege' + const deviceFields = + 'serial,' + + 'version,' + + 'manufacturer,' + + 'marketName,' + + 'sdk,' + + 'display.width,' + + 'display.height,' + + 'model' + + function incrStateStats(group, incr) { + if (group.isActive) { + $scope.activeGroups += incr + } + else if (group.state === 'pending') { + $scope.pendingGroups += incr + } + $scope.readyGroups = $scope.groups.length - $scope.activeGroups - $scope.pendingGroups + } + + function updateStateStats(oldGroup, newGroup) { + if (oldGroup === null) { + incrStateStats(newGroup, 1) + } + else if (newGroup === null) { + incrStateStats(oldGroup, -1) + } + else { + if (newGroup.isActive && !oldGroup.isActive) { + incrStateStats(newGroup, 1) + } + else if (!newGroup.isActive && oldGroup.isActive) { + incrStateStats(oldGroup, -1) + } + else if (newGroup.state === 'ready' && oldGroup.state === 'pending') { + incrStateStats(oldGroup, -1) + } + } + } + + function updateGroupExtraProperties(group) { + const status = {pending: 'Pending', waiting: 'Waiting', ready: 'Ready'} + + group.status = group.isActive ? 'Active' : status[group.state] + group.startTime = $filter('date')(group.dates[0].start, SettingsService.get('dateFormat')) + group.stopTime = $filter('date')(group.dates[0].stop, SettingsService.get('dateFormat')) + + } + + function updateQuotaBar(bar, consumed, allocated) { + bar.value = (consumed / allocated) * 100 | 0 + if (bar.value < 25) { + bar.type = 'success' + } + else if (bar.value < 50) { + bar.type = 'info' + } + else if (bar.value < 75) { + bar.type = 'warning' + } + else { + bar.type = 'danger' + } + } + + function updateQuotaBars() { + updateQuotaBar( + $scope.numberBar + , $scope.user.groups.quotas.consumed.number + , $scope.user.groups.quotas.allocated.number + ) + updateQuotaBar( + $scope.durationBar + , $scope.user.groups.quotas.consumed.duration + , $scope.user.groups.quotas.allocated.duration + ) + } + + function addGroup(group, timeStamp) { + if (CommonService.add( + $scope.groups + , groupsById + , group + , 'id' + , timeStamp)) { + $scope.groupsEnv[group.id] = { + devices: [] + , users: [] + } + groupsEnv[group.id] = { + devicesBySerial: {} + , usersByEmail: {} + } + updateStateStats(null, group) + updateGroupExtraProperties(group) + return group + } + return null + } + + function updateGroup(group, timeStamp) { + return CommonService.update( + $scope.groups + , groupsById + , group + , 'id' + , timeStamp) + } + + function deleteGroup(id, timeStamp) { + const group = CommonService.delete( + $scope.groups + , groupsById + , id + , timeStamp) + + if (group) { + updateStateStats(group, null) + delete $scope.groupsEnv[group.id] + delete groupsEnv[group.id] + } + return group + } + + function addUser(user, timeStamp) { + if (CommonService.add( + users + , usersByEmail + , user + , 'email' + , timeStamp + ) && typeof groupUserToAdd[user.email] !== 'undefined') { + addGroupUser( + groupUserToAdd[user.email].id + , user.email + , groupUserToAdd[user.email].timeStamp) + delete groupUserToAdd[user.email] + } + } + + function deleteUser(email, timeStamp) { + return CommonService.delete( + users + , usersByEmail + , email + , timeStamp) + } + + function addDevice(device, timeStamp) { + return CommonService.add( + devices + , devicesBySerial + , device + , 'serial' + , timeStamp) + } + + function updateDevice(device, timeStamp) { + return CommonService.update( + devices + , devicesBySerial + , device + , 'serial' + , timeStamp) + } + + function deleteDevice(serial, timeStamp) { + return CommonService.delete( + devices + , devicesBySerial + , serial + , timeStamp) + } + + function addGroupUser(id, email, timeStamp) { + if (CommonService.isExisting(usersByEmail[email])) { + CommonService.add( + $scope.groupsEnv[id].users + , groupsEnv[id].usersByEmail + , users[usersByEmail[email].index] + , 'email' + , timeStamp) + } + else { + groupUserToAdd[email] = {id: id, timeStamp: timeStamp} + } + } + + function deleteGroupUser(id, email, timeStamp) { + CommonService.delete( + $scope.groupsEnv[id].users + , groupsEnv[id].usersByEmail + , email + , timeStamp) + } + + function addGroupDevice(id, serial, timeStamp) { + if (CommonService.isExisting(devicesBySerial[serial])) { + CommonService.add( + $scope.groupsEnv[id].devices + , groupsEnv[id].devicesBySerial + , devices[devicesBySerial[serial].index] + , 'serial' + , timeStamp) + } + else { + GroupsService.getGroupDevice(id, serial, deviceFields) + .then(function(response) { + if (addDevice(response.data.device, timeStamp)) { + CommonService.add( + $scope.groupsEnv[id].devices + , groupsEnv[id].devicesBySerial + , devices[devicesBySerial[serial].index] + , 'serial' + , timeStamp) + } + }) + } + } + + function deleteGroupDevice(id, serial, timeStamp) { + CommonService.delete( + $scope.groupsEnv[id].devices + , groupsEnv[id].devicesBySerial + , serial + , timeStamp) + } + + function updateGroupDevices(group, isAddedDevice, devices, timeStamp) { + if (devices.length) { + if (isAddedDevice) { + devices.forEach(function(serial) { + addGroupDevice(group.id, serial, timeStamp) + }) + } + else { + devices.forEach(function(serial) { + deleteGroupDevice(group.id, serial, timeStamp) + }) + } + } + } + + function updateGroupUsers(group, isAddedUser, users, timeStamp) { + if (users.length) { + if (isAddedUser) { + users.forEach(function(email) { + addGroupUser(group.id, email, timeStamp) + }) + } + else { + users.forEach(function(email) { + deleteGroupUser(group.id, email, timeStamp) + }) + } + } + } + + function initScope() { + GroupsService.getOboeGroups(function(group) { + addGroup(group, -1) + }) + .done(function() { + $scope.$digest() + }) + + UserService.getUser().then(function(response) { + $scope.user = response.data.user + updateQuotaBars() + }) + + UsersService.getOboeUsers(userFields, function(user) { + addUser(user, -1) + }) + } + + $scope.scopeGroupListCtrl = $scope + $scope.sortBy = CommonService.sortBy + $scope.getDuration = CommonService.getDuration + $scope.getClassName = CommonService.getClassName + $scope.user = UserService.currentUser + $scope.numberBar = {} + $scope.durationBar = {} + $scope.groupsEnv = {} + $scope.groups = [] + $scope.activeGroups = $scope.readyGroups = $scope.pendingGroups = 0 + $scope.itemsPerPageOptions = ItemsPerPageOptionsService + SettingsService.bind($scope, { + target: 'groupItemsPerPage' + , source: 'groupViewItemsPerPage' + , defaultValue: $scope.itemsPerPageOptions[2] + }) + $scope.groupColumns = [ + {name: 'Status', property: 'status'} + , {name: 'Name', property: 'name'} + , {name: 'Identifier', property: 'id'} + , {name: 'Owner', property: 'owner.name'} + , {name: 'Devices', property: 'devices.length'} + , {name: 'Users', property: 'users.length'} + , {name: 'Class', property: 'class'} + , {name: 'Repetitions', property: 'repetitions'} + , {name: 'Duration', property: 'duration'} + , {name: 'Starting Date', property: 'startTime'} + , {name: 'Expiration Date', property: 'stopTime'} + ] + $scope.defaultGroupData = { + columns: [ + {name: 'Status', selected: true, sort: 'none'} + , {name: 'Name', selected: true, sort: 'sort-asc'} + , {name: 'Identifier', selected: false, sort: 'none'} + , {name: 'Owner', selected: true, sort: 'none'} + , {name: 'Devices', selected: true, sort: 'none'} + , {name: 'Users', selected: true, sort: 'none'} + , {name: 'Class', selected: true, sort: 'none'} + , {name: 'Repetitions', selected: true, sort: 'none'} + , {name: 'Duration', selected: true, sort: 'none'} + , {name: 'Starting Date', selected: true, sort: 'none'} + , {name: 'Expiration Date', selected: true, sort: 'none'} + ] + , sort: {index: 1, reverse: false} + } + SettingsService.bind($scope, { + target: 'groupData' + , source: 'groupData' + , defaultValue: $scope.defaultGroupData + }) + + $scope.mailToGroupOwners = function(groups) { + CommonService.copyToClipboard(_.uniq(groups.map(function(group) { + return group.owner.email + })) + .join(SettingsService.get('emailSeparator'))) + .url('mailto:?body=*** Paste the email addresses from the clipboard! ***') + } + + $scope.mailToGroupUsers = function(group, users) { + // group unused actually.. + CommonService.copyToClipboard(users.map(function(user) { + return user.email + }) + .join(SettingsService.get('emailSeparator'))) + .url('mailto:?body=*** Paste the email addresses from the clipboard! ***') + } + + $scope.getTooltip = function(objects) { + var tooltip = '' + + objects.forEach(function(object) { + tooltip += object + '\n' + }) + return tooltip + } + + $scope.resetData = function() { + $scope.groupData = JSON.parse(JSON.stringify($scope.defaultGroupData)) + } + + $scope.initGroupUsers = function(group) { + if (typeof $scope.groupsEnv[group.id].userCurrentPage === 'undefined') { + $scope.groupsEnv[group.id].userCurrentPage = 1 + $scope.groupsEnv[group.id].userItemsPerPage = $scope.itemsPerPageOptions[1] + } + group.users.forEach(function(email) { + addGroupUser(group.id, email, -1) + }) + } + + $scope.initGroupDevices = function(group) { + if (typeof $scope.groupsEnv[group.id].deviceCurrentPage === 'undefined') { + $scope.groupsEnv[group.id].deviceCurrentPage = 1 + $scope.groupsEnv[group.id].deviceItemsPerPage = $scope.itemsPerPageOptions[1] + } + GroupsService.getOboeGroupDevices(group.id, false, deviceFields, function(device) { + addDevice(device, -1) + addGroupDevice(group.id, device.serial, -1) + }) + .done(function() { + $scope.$digest() + }) + } + + $scope.$on('user.view.groups.created', function(event, message) { + addGroup(message.group, message.timeStamp) + }) + + $scope.$on('user.view.groups.deleted', function(event, message) { + deleteGroup(message.group.id, message.timeStamp) + }) + + $scope.$on('user.view.groups.updated', function(event, message) { + if (CommonService.isExisting(groupsById[message.group.id])) { + if (message.group.users.indexOf(UserService.currentUser.email) < 0) { + deleteGroup(message.group.id, message.timeStamp) + } + else { + updateStateStats($scope.groups[groupsById[message.group.id].index], message.group) + updateGroupDevices(message.group, message.isAddedDevice, message.devices, message.timeStamp) + updateGroupUsers(message.group, message.isAddedUser, message.users, message.timeStamp) + updateGroup(message.group, message.timeStamp) + updateGroupExtraProperties($scope.groups[groupsById[message.group.id].index]) + } + } + else { + addGroup(message.group, message.timeStamp) + } + }) + + $scope.$on('user.settings.users.created', function(event, message) { + addUser(message.user, message.timeStamp) + }) + + $scope.$on('user.settings.users.deleted', function(event, message) { + deleteUser(message.user.email, message.timeStamp) + }) + + $scope.$on('user.view.users.updated', function(event, message) { + if (message.user.email === $scope.user.email) { + $scope.user = message.user + updateQuotaBars() + } + }) + + $scope.$on('user.settings.devices.created', function(event, message) { + addDevice(message.device, message.timeStamp) + }) + + $scope.$on('user.settings.devices.deleted', function(event, message) { + deleteDevice(message.device.serial, message.timeStamp) + }) + + $scope.$on('user.settings.devices.updated', function(event, message) { + updateDevice(message.device, message.timeStamp) + }) + + initScope() +} diff --git a/res/app/group-list/group-list.css b/res/app/group-list/group-list.css new file mode 100644 index 00000000..f6a9c4d6 --- /dev/null +++ b/res/app/group-list/group-list.css @@ -0,0 +1,196 @@ +/** +* Copyright © 2019 code initially contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0 +**/ + +.stf-pager-group-devices-search { + width: 160px; +} + +.stf-pager-group-list-total-items { + margin-top: 5px; +} + +.stf-group-list .selectable { + user-select: text; +} + +.stf-group-list .group-list .stf-pager-group-devices-search i.stf-pager-group-devices-search-icon { + font-size: 12px; + margin-right: 0px; +} + +.stf-group-list .group-list { + min-height: 600px; +} + +.stf-group-list .group-list-header { + margin: 20px 0px 20px 15px; +} + +.stf-group-list .btn-devices, .btn-users { + padding: 0px; + margin: 0px; +} + +.stf-group-list .group-devices { + width: auto; + min-width: 475px; + max-width: 600px; +} + +.stf-group-list .group-users { + width: auto; + min-width: 475px; + max-width: 600px; +} + +.stf-group-list .group-icon { + font-size: 15px; + vertical-align: middle; + margin-right: 10px; +} + +.stf-group-list .group-device-icon { + font-size: 25px; + vertical-align: middle; + margin-right: 10px; +} + +.stf-group-list .group-user-icon { + font-size: 20px; + vertical-align: middle; + margin-right: 10px; +} + +.stf-group-list .group-device-details, .group-user-details { + display: inline-block; + line-height: 2; + margin-left: 10px; +} + +.stf-group-list .group-device-name, .group-user-name { + color: #007aff; + font-size: 14px; + font-weight: 300; + margin: 2px 0 6px; +} + +.stf-group-list .group-device-id, .group-user-id { + font-family: Monaco, Menlo, Consolas, "Courier New", monospace; + font-size: 10px; + margin-bottom: 2px; + color: #999999; + font-weight: 300; +} + +.stf-group-list td,span { + white-space: nowrap; +} + +.stf-group-list .user-line, .device-line { + padding: 9px 15px 9px 2px; + margin-left: 14px; + border-bottom: 1px solid #dddddd; +} + +.stf-group-list .mailto { + padding-bottom: 1px; + margin-top: 5px; +} + +.stf-group-list i.mailto { + vertical-align: initial; +} + +.group-stats { + min-height: 100px; + height: 100px; + text-align: center; +} + + +.group-stats [class^="col-"], +.group-stats [class*="col-"] { + height: 100%; + margin-bottom: 0; +} + +.group-stats [class^="col-"]:last-child, +.group-stats [class*="col-"]:last-child { + border: 0; +} + +.group-stats [class^="col-"] .number, +.group-stats [class*="col-"] .number { + font-size: 3.4em; + font-weight: 100; + line-height: 1.5em; + letter-spacing: -0.06em; +} + +.group-stats [class^="col-"] .number .icon, +.group-stats [class*="col-"] .number .icon { + width: 50px; + height: 38px; + display: inline-block; + vertical-align: top; + margin: 20px 12px 0 0; +} + +.group-quota-stats { + min-height: 75px; + height: 75px; + text-align: center; +} + +.group-quota-stats .bar { + height: 20px; + vertical-align: top; + margin: 14px 12px 12px 12px; +} + +.group-quota-stats .text, +.group-stats [class^="col-"] .text, +.group-stats [class*="col-"] .text { + font-weight: 300; + color: #aeaeae; + text-transform: uppercase; + font-size: 12px; +} + +.group-stats .fa { + font-size: 0.8em; +} + + +@media (max-width: 600px) { + .group-stats { + min-height: 60px; + height: 60px; + text-align: center; + } + + .group-stats .fa { + font-size: 0.6em; + } + + .group-stats [class^="col-"] .number, + .group-stats [class*="col-"] .number { + font-size: 1.8em; + line-height: normal; + font-weight: 300; + } + + .group-stats [class^="col-"] .number .icon, + .group-stats [class*="col-"] .number .icon { + width: 25px; + height: 19px; + margin: 10px 6px 0 0; + } + + .group-stats [class^="col-"] .text, + .group-stats [class*="col-"] .text { + font-size: 0.8em; + font-weight: 500; + } +} diff --git a/res/app/group-list/group-list.pug b/res/app/group-list/group-list.pug new file mode 100644 index 00000000..7a109cee --- /dev/null +++ b/res/app/group-list/group-list.pug @@ -0,0 +1,14 @@ +// + Copyright © 2019 code initially contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0 +// + +.stf-group-list + .row.unselectable + .col-md-12 + div(ng-include="'group-list/stats/group-stats.pug'") + .row.unselectable + .col-md-12 + div(ng-include="'group-list/stats/group-quota-stats.pug'") + .row.unselectable + .col-md-12 + div(ng-include="'group-list/groups/groups.pug'") diff --git a/res/app/group-list/groups/groups.pug b/res/app/group-list/groups/groups.pug new file mode 100644 index 00000000..ad18cf0d --- /dev/null +++ b/res/app/group-list/groups/groups.pug @@ -0,0 +1,167 @@ +// + Copyright © 2019 code initially contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0 +// + +.widget-container.fluid-height.overflow-auto.group-list + .heading + nothing-to-show( + icon='fa-object-group' + message='{{"No Groups" | translate}}' ng-if='!groups.length') + + div(ng-if='groups.length') + form.form-inline + .form-group.group-list-header + stf-pager( + tooltip-label="{{'Group selection' | translate}}" + total-items='filteredGroups.length' + total-items-style='stf-pager-group-list-total-items' + items-per-page='scopeGroupListCtrl.groupItemsPerPage' + items-per-page-options='itemsPerPageOptions' + current-page='scopeGroupListCtrl.groupCurrentPage' + items-search='search') + + .form-group.group-list-header + stf-column-choice(reset-data='resetData()' column-data='groupData.columns') + + button.btn.btn-xs.btn-primary-outline.pull-right( + type='button' + uib-tooltip="{{'Write an email to the group owner selection' | translate}}" + ng-disabled='!filteredGroups.length' + ng-click='mailToGroupOwners(filteredGroups)' + tooltip-placement='top' + tooltip-popup-delay='500') + i.fa.fa-envelope-o + span(translate) Contact Owners + + table.table.table-hover.dataTable.ng-table + thead + tr + th.header.sortable( + ng-class='[column.sort]' + ng-repeat='column in groupData.columns | filter: {selected: true}' + ng-click='sortBy(groupData, column)') + div.strong(ng-bind-template='{{::column.name | translate}}') + tbody + tr(ng-repeat="group in groups \ + | filter:search \ + | orderBy:groupColumns[groupData.sort.index].property:groupData.sort.reverse \ + | pagedObjectsFilter:scopeGroupListCtrl:'groupCurrentPage':'groupItemsPerPage':'filteredGroups' \ + track by group.id") + + td(ng-if='groupData.columns[0].selected' + ng-class="{'color-green': group.status === 'Active', \ + 'color-red': group.status === 'Pending', \ + 'color-orange': group.status === 'Ready'}") {{group.status | translate}} + td.selectable(ng-if='groupData.columns[1].selected') + i.fa.fa-object-group.group-icon + span {{group.name}} + td.selectable(ng-if='groupData.columns[2].selected') {{::group.id}} + td(ng-if='groupData.columns[3].selected') + a(ng-href="{{::'mailto:' + group.owner.email}}") {{::group.owner.name}} + + td(ng-if='groupData.columns[4].selected') + .btn-group.btn-devices(uib-dropdown auto-close='outsideClick') + button.btn.btn-sm.btn-primary-outline.btn-devices( + type='button' + ng-disabled='!group.devices.length' + ng-click='initGroupDevices(group)' + uib-dropdown-toggle) + span {{group.devices.length}} + + ul.dropdown-menu.group-devices( + ng-if='groupsEnv[group.id].deviceCurrentPage && groupsEnv[group.id].devices.length' + uib-dropdown-menu role='menu' ng-click='$event.stopPropagation()') + li + a + form.form-inline + .form-group + stf-pager( + items-search-style='stf-pager-group-devices-search' + icon-style='stf-pager-group-devices-search-icon' + tooltip-label="{{'Device selection' | translate}}" + total-items='groupsEnv[group.id].filteredDevices.length' + items-per-page='groupsEnv[group.id].deviceItemsPerPage' + items-per-page-options='itemsPerPageOptions' + current-page='groupsEnv[group.id].deviceCurrentPage' + items-search='deviceSearch') + + li(ng-repeat="device in groupsEnv[group.id].devices \ + | filter:deviceSearch \ + | orderBy: 'model' \ + | pagedObjectsFilter:groupsEnv[group.id]:'deviceCurrentPage':'deviceItemsPerPage':'filteredDevices' \ + track by device.serial") + + .device-line + i.fa.fa-mobile.group-device-icon + .group-device-details.selectable + a.group-device-name(ng-bind-template="{{device.manufacturer + ' ' + device.model + ' (' + device.marketName + ')'}}") + .group-device-id + span(translate) Serial + span(ng-bind-template="{{::': ' + device.serial + ' - '}}") + span(translate) OS + span(ng-bind-template="{{': ' + device.version + ' - '}}") + span(translate) Screen + span(ng-bind-template="{{': ' + device.display.width + 'x' + device.display.height + ' - '}}") + span(translate) SDK + span(ng-bind-template="{{': ' + device.sdk}}") + + td(ng-if='groupData.columns[5].selected') + .btn-group.btn-users(uib-dropdown auto-close='outsideClick') + button.btn.btn-sm.btn-primary-outline.btn-users( + type='button' + ng-disabled='!group.users.length' + ng-click='initGroupUsers(group)' + uib-dropdown-toggle) + span {{group.users.length}} + + ul.dropdown-menu.group-users( + ng-if='groupsEnv[group.id].userCurrentPage && groupsEnv[group.id].users' + uib-dropdown-menu role='menu' ng-click='$event.stopPropagation()') + li + .user-line + form + .form-group.mailto + button.btn.btn-xs.btn-primary-outline.pull-right( + type='button' + uib-tooltip="{{'Write an email to the group user selection' | translate}}" + ng-disabled='!groupsEnv[group.id].filteredUsers.length' + ng-click='mailToGroupUsers(group, groupsEnv[group.id].filteredUsers)' + tooltip-placement='top' + tooltip-popup-delay='500') + i.fa.fa-envelope-o + span(translate) Contact Users + + form.form-inline + .form-group + stf-pager( + items-search-style='stf-pager-group-devices-search' + icon-style='stf-pager-group-devices-search-icon' + tooltip-label="{{'User selection' | translate}}" + total-items='groupsEnv[group.id].filteredUsers.length' + items-per-page='groupsEnv[group.id].userItemsPerPage' + items-per-page-options='itemsPerPageOptions' + current-page='groupsEnv[group.id].userCurrentPage' + items-search='userSearch') + + li(ng-repeat="user in groupsEnv[group.id].users \ + | filter:userSearch \ + | orderBy: 'name' \ + | pagedObjectsFilter:groupsEnv[group.id]:'userCurrentPage':'userItemsPerPage':'filteredUsers' \ + track by user.email") + .user-line + i.fa.fa-user.group-user-icon + .group-user-details.selectable + a.group-user-name( + ng-href="{{::'mailto:' + user.email}}" + ng-bind-template="{{::user.name}}") + .group-user-id + span(translate) Email + span(ng-bind-template="{{::': ' + user.email + ' - '}}") + span(translate) Privilege + span(ng-bind-template="{{::': ' + user.privilege}}") + + td(ng-if='groupData.columns[6].selected') {{getClassName(group.class) | translate}} + td(ng-if='groupData.columns[7].selected') {{group.repetitions}} + td(ng-if='groupData.columns[8].selected') {{getDuration(group.duration)}} + td(ng-if='groupData.columns[9].selected') {{group.startTime}} + td(ng-if='groupData.columns[10].selected') {{group.stopTime}} diff --git a/res/app/group-list/index.js b/res/app/group-list/index.js new file mode 100644 index 00000000..ca9608e8 --- /dev/null +++ b/res/app/group-list/index.js @@ -0,0 +1,35 @@ +/** +* Copyright © 2019 code initially contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0 +**/ + +require('./group-list.css') + +module.exports = angular.module('group-list', [ + require('stf/column-choice').name, + require('stf/groups').name, + require('stf/user').name, + require('stf/users').name, + require('stf/devices').name, + require('stf/settings').name, + require('stf/util/common').name, + require('stf/common-ui').name +]) + .config(['$routeProvider', function($routeProvider) { + $routeProvider + .when('/groups', { + template: require('./group-list.pug'), + controller: 'GroupListCtrl' + }) + }]) + .run(['$templateCache', function($templateCache) { + $templateCache.put( + 'group-list/stats/group-stats.pug', require('./stats/group-stats.pug') + ) + $templateCache.put( + 'group-list/stats/group-quota-stats.pug', require('./stats/group-quota-stats.pug') + ) + $templateCache.put( + 'group-list/groups/groups.pug', require('./groups/groups.pug') + ) + }]) + .controller('GroupListCtrl', require('./group-list-controller')) diff --git a/res/app/group-list/stats/group-quota-stats.pug b/res/app/group-list/stats/group-quota-stats.pug new file mode 100644 index 00000000..08ca3973 --- /dev/null +++ b/res/app/group-list/stats/group-quota-stats.pug @@ -0,0 +1,14 @@ +// + Copyright © 2019 code initially contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0 +// + +.widget-container.group-quota-stats + .col-xs-6 + uib-progressbar.bar(class='progress-striped' value='numberBar.value' type='{{numberBar.type}}') + b {{numberBar.value}}% + .text(translate) {{user.name}} groups number use + .col-xs-6 + uib-progressbar.bar(class='progress-striped' value='durationBar.value' type='{{durationBar.type}}') + b {{durationBar.value}}% + .text(translate) {{user.name}} groups duration use + diff --git a/res/app/group-list/stats/group-stats.pug b/res/app/group-list/stats/group-stats.pug new file mode 100644 index 00000000..1638c619 --- /dev/null +++ b/res/app/group-list/stats/group-stats.pug @@ -0,0 +1,25 @@ +// + Copyright © 2019 code initially contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0 +// + +.widget-container.group-stats + .col-xs-3 + .number.color-blue + .icon.fa.fa-globe + span(ng-bind='groups.length') + .text(translate) Total groups + .col-xs-3 + .number.color-green + .icon.fa.fa-play + span(ng-bind='activeGroups') + .text(translate) Active groups + .col-xs-3 + .number.color-orange + .icon.fa.fa-pause + span(ng-bind='readyGroups') + .text(translate) Ready groups + .col-xs-3 + .number.color-pink + .icon.fa.fa-stop + span(ng-bind='pendingGroups') + .text(translate) Pending groups diff --git a/res/app/menu/index.js b/res/app/menu/index.js index 89f72ea5..88ab1128 100644 --- a/res/app/menu/index.js +++ b/res/app/menu/index.js @@ -1,6 +1,14 @@ +/** +* Copyright © 2019 contains code contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0 +**/ + require('./menu.css') +require('angular-cookies') module.exports = angular.module('stf.menu', [ + 'ngCookies', + require('stf/socket').name, + require('stf/util/common').name, require('stf/nav-menu').name, require('stf/settings').name, require('stf/common-ui/modals/external-url-modal').name, diff --git a/res/app/menu/menu-controller.js b/res/app/menu/menu-controller.js index 9af67429..1e93aab1 100644 --- a/res/app/menu/menu-controller.js +++ b/res/app/menu/menu-controller.js @@ -1,5 +1,18 @@ -module.exports = function MenuCtrl($scope, $rootScope, SettingsService, - $location, LogcatService) { +/** +* Copyright © 2019 contains code contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0 +**/ + +module.exports = function MenuCtrl( + $scope +, $rootScope +, SettingsService +, $location +, $http +, CommonService +, LogcatService +, socket +, $cookies +, $window) { SettingsService.bind($scope, { target: 'lastUsedDevice' @@ -15,4 +28,21 @@ module.exports = function MenuCtrl($scope, $rootScope, SettingsService, $scope.isControlRoute = $location.path().search('/control') !== -1 }) + $scope.mailToSupport = function() { + CommonService.url('mailto:' + $scope.contactEmail) + } + + $http.get('/auth/contact').then(function(response) { + $scope.contactEmail = response.data.contact.email + }) + + $scope.logout = function() { + $cookies.remove('XSRF-TOKEN', {path: '/'}) + $cookies.remove('ssid', {path: '/'}) + $cookies.remove('ssid.sig', {path: '/'}) + $window.location = '/' + setTimeout(function() { + socket.disconnect() + }, 100) + } } diff --git a/res/app/menu/menu.pug b/res/app/menu/menu.pug index 17b83e8c..49a241a0 100644 --- a/res/app/menu/menu.pug +++ b/res/app/menu/menu.pug @@ -1,3 +1,7 @@ +// + Copyright © 2019 contains code contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0 +// + .navbar.stf-menu(ng-controller='MenuCtrl') .container-fluid.stf-top-bar a.stf-logo(ng-href="/#!/devices") STF @@ -9,6 +13,9 @@ a(ng-href='/#!/devices', accesskey='1') span.fa.fa-sitemap span(ng-if='!$root.basicMode', translate) Devices + a(ng-href='/#!/groups') + span.fa.fa-object-group + span(ng-if='!$root.basicMode', translate) Groups a(ng-href='/#!/settings') span.fa.fa-gears span(ng-if='!$root.basicMode', translate) Settings @@ -18,7 +25,22 @@ button(type='button', ng-model='$root.platform', uib-btn-radio="'web'", translate).btn.btn-sm.btn-default-outline Web button(type='button', ng-model='$root.platform', uib-btn-radio="'native'", translate).btn.btn-sm.btn-default-outline Native + li.stf-nav-web-native-button(ng-if='!$root.basicMode') + button.btn.btn-sm.btn-default-outline( + type='button' + ng-click='mailToSupport()') + i.fa.fa-envelope-o + span(translate) Contact Support + + li.stf-nav-web-native-button(ng-if='!$root.basicMode') + button.btn.btn-sm.btn-default-outline( + type='button' + ng-click='logout()') + i.fa.fa-sign-out + span(translate) Logout + li(ng-show='!$root.basicMode') a(ng-href='/#!/help', accesskey='6') i.fa.fa-question-circle.fa-fw | {{ "Help" | translate }} + diff --git a/res/app/settings/devices/devices-controller.js b/res/app/settings/devices/devices-controller.js new file mode 100644 index 00000000..0d539a0c --- /dev/null +++ b/res/app/settings/devices/devices-controller.js @@ -0,0 +1,169 @@ +/** +* Copyright © 2019 code initially contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0 +**/ + +const _ = require('lodash') + +module.exports = function DevicesCtrl( + $scope +, DevicesService +, SettingsService +, ItemsPerPageOptionsService +, GenericModalService +, CommonService +) { + const devicesBySerial = {} + const deviceFields = + 'model,' + + 'serial,' + + 'version,' + + 'display.height,' + + 'display.width,' + + 'manufacturer,' + + 'sdk,' + + 'abi,' + + 'cpuPlatform,' + + 'openGLESVersion,' + + 'marketName,' + + 'phone.imei,' + + 'provider.name,' + + 'group.originName' + + + function publishDevice(device) { + if (!device.model) { + device.display = {} + } + else { + device.displayStr = device.display.width + 'x' + device.display.height + } + for (var i in device) { + if (device[i] === null) { + device[i] = '' + } + } + return device + } + + function addDevice(device, timeStamp) { + return CommonService.add( + $scope.devices + , devicesBySerial + , device + , 'serial' + , timeStamp) + } + + function updateDevice(device, timeStamp) { + return CommonService.update( + $scope.devices + , devicesBySerial + , device + , 'serial' + , timeStamp) + } + + function deleteDevice(serial, timeStamp) { + return CommonService.delete( + $scope.devices + , devicesBySerial + , serial + , timeStamp) + } + + function initScope() { + DevicesService.getOboeDevices('user', deviceFields, function(device) { + addDevice(publishDevice(device), -1) + }) + .done(function() { + $scope.$digest() + }) + } + + SettingsService.bind($scope, { + target: 'removingFilters' + , source: 'DevicesRemovingFilters' + , defaultValue: { + present: 'False' + , booked: 'False' + , annotated: 'False' + , controlled: 'False' + } + }) + $scope.devices = [] + $scope.confirmRemove = {value: true} + $scope.scopeDevicesCtrl = $scope + $scope.itemsPerPageOptions = ItemsPerPageOptionsService + SettingsService.bind($scope, { + target: 'deviceItemsPerPage' + , source: 'deviceItemsPerPage' + , defaultValue: $scope.itemsPerPageOptions[2] + }) + $scope.removingFilterOptions = ['True', 'False', 'Any'] + + $scope.removeDevice = function(serial, askConfirmation) { + if (askConfirmation) { + GenericModalService.open({ + message: 'Really delete this device?' + , type: 'Warning' + , size: 'sm' + , cancel: true + }) + .then(function() { + CommonService.errorWrapper( + DevicesService.removeDevice + , [serial, $scope.removingFilters] + ) + }) + } + else { + CommonService.errorWrapper( + DevicesService.removeDevice + , [serial, $scope.removingFilters] + ) + } + } + + $scope.removeDevices = function(search, filteredDevices, askConfirmation) { + function removeDevices() { + CommonService.errorWrapper( + DevicesService.removeDevices + , search ? + [$scope.removingFilters, filteredDevices.map(function(device) { + return device.serial + }) + .join()] : + [$scope.removingFilters] + ) + } + + if (askConfirmation) { + GenericModalService.open({ + message: 'Really delete this selection of devices?' + , type: 'Warning' + , size: 'sm' + , cancel: true + }) + .then(function() { + removeDevices() + }) + } + else { + removeDevices() + } + } + + $scope.$on('user.settings.devices.created', function(event, message) { + addDevice(publishDevice(message.device), message.timeStamp) + }) + + $scope.$on('user.settings.devices.deleted', function(event, message) { + deleteDevice(message.device.serial, message.timeStamp) + }) + + $scope.$on('user.settings.devices.updated', function(event, message) { + updateDevice(publishDevice(message.device), message.timeStamp) + }) + + initScope() +} diff --git a/res/app/settings/devices/devices-spec.js b/res/app/settings/devices/devices-spec.js new file mode 100644 index 00000000..b6371fc6 --- /dev/null +++ b/res/app/settings/devices/devices-spec.js @@ -0,0 +1,21 @@ +/** +* Copyright © 2019 code initially contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0 +**/ + +describe('DevicesCtrl', function() { + + beforeEach(angular.mock.module(require('./index').name)) + + var scope, ctrl + + beforeEach(inject(function($rootScope, $controller) { + scope = $rootScope.$new() + ctrl = $controller('DevicesCtrl', {$scope: scope}) + })) + + it('should ...', inject(function() { + expect(1).toEqual(1) + + })) + +}) diff --git a/res/app/settings/devices/devices.css b/res/app/settings/devices/devices.css new file mode 100644 index 00000000..11ff8c08 --- /dev/null +++ b/res/app/settings/devices/devices.css @@ -0,0 +1,65 @@ +/** +* Copyright © 2019 code initially contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0 +**/ + +.stf-devices .selectable { + user-select: text; +} + +.stf-pager-devices-total-items { + margin-top: 5px; +} + +.stf-devices .device-header { + margin-left: 10px; +} + +.stf-devices .heading .device-header-icon { + font-size: 16px; +} + +.stf-devices .device-list-icon { + margin-right: 10px; +} + +.stf-devices .device-filters-items { + margin-top: 5px; +} + +.stf-devices .device-filters-item { + margin: 0px 10px 15px 15px; +} + +.stf-devices .devices-list .device-line { + padding: 10px; + border-bottom: 1px solid #dddddd; + margin-left: 0px; +} + +.stf-devices .devices-list .device-line.device-actions { + padding-bottom: 23px; +} + +.stf-devices .device-list-details { + display: inline-block; +} + +.stf-devices .device-list-label { + font-weight: bold; + margin-right: 10px; +} + +.stf-devices .device-list-name { + color: #007aff; + font-size: 14px; + font-weight: 300; + margin: 2px 0 6px; +} + +.stf-devices .device-list-id { + font-family: Monaco, Menlo, Consolas, "Courier New", monospace; + font-size: 10px; + margin-bottom: 2px; + color: #999999; + font-weight: 300; +} diff --git a/res/app/settings/devices/devices.pug b/res/app/settings/devices/devices.pug new file mode 100644 index 00000000..a7f90f4d --- /dev/null +++ b/res/app/settings/devices/devices.pug @@ -0,0 +1,137 @@ +// + Copyright © 2019 code initially contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0 +// + +.widget-container.fluid-height.stf-devices(ng-controller='DevicesCtrl') + .heading + i.fa.fa-mobile.device-header-icon + span(translate) Device list + + a.pull-right.btn.btn-sm(ng-href='') + i.fa.fa-question-circle.fa-fw(uib-tooltip='{{"More about Devices" | translate}}' tooltip-placement='left') + + .widget-content.padded + + nothing-to-show(icon='fa-mobile' message='{{"No Devices" | translate}}' ng-if='!devices.length') + + div(ng-if='devices.length') + ul.list-group.devices-list + li.list-group-item + .device-line.device-actions + form.form-inline.device-header + .form-group + stf-pager( + tooltip-label="{{'Device selection' | translate}}" + total-items='filteredDevices.length' + total-items-style='stf-pager-devices-total-items' + items-per-page='scopeDevicesCtrl.deviceItemsPerPage' + items-per-page-options='itemsPerPageOptions' + current-page='scopeDevicesCtrl.deviceCurrentPage' + items-search='search') + + button.btn.btn-xs.btn-danger.pull-right( + type='button' + uib-tooltip="{{'Remove the device selection' | translate}}" + tooltip-placement='bottom' + tooltip-popup-delay='500' + ng-disabled='!filteredDevices.length' + ng-click='removeDevices(search, filteredDevices, confirmRemove.value)') + i.fa.fa-trash-o + span(translate) Remove + + button.btn.btn-xs.btn-success.pull-right( + type='button' + uib-tooltip="{{'Enable/Disable confirmation for device removing' | translate}}" + tooltip-placement='top' + tooltip-popup-delay='500' + ng-click='confirmRemove.value = !confirmRemove.value' + ng-class='{"btn-warning-outline": !confirmRemove.value, "btn-success": confirmRemove.value}') + i.fa.fa-lock(ng-if='confirmRemove.value') + i.fa.fa-unlock(ng-if='!confirmRemove.value') + span(translate) Confirm Remove + + button.btn.btn-xs.btn-danger-outline.pull-right( + type='button' + uib-tooltip="{{'Set filters for device removing' | translate}}" + tooltip-placement='top' + tooltip-popup-delay='500' + ng-click='showFilters = !showFilters' + ng-class='{"btn-danger-outline": !showFilters, "btn-danger": showFilters}') + i.fa.fa-trash-o + span(translate) Filters + + li.list-group-item(ng-if='showFilters') + .device-line + .heading + i.fa.fa-trash-o + span(translate) Removing filters + + form.form-inline.device-filters-items + .form-group.device-filters-item + label.device-list-label( + translate + uib-tooltip="{{'Device presence state' | translate}}" + tooltip-placement='top' + tooltip-popup-delay='500') Present + select( + ng-model='removingFilters.present' + ng-options='option for option in removingFilterOptions') + + .form-group.device-filters-item + label.device-list-label( + translate + uib-tooltip="{{'Device booking state' | translate}}" + tooltip-placement='top' + tooltip-popup-delay='500') Booked + select( + ng-model='removingFilters.booked' + ng-options='option for option in removingFilterOptions') + + .form-group.device-filters-item + label.device-list-label( + translate + uib-tooltip="{{'Device notes state' | translate}}" + tooltip-placement='top' + tooltip-popup-delay='500') Annotated + select( + ng-model='removingFilters.annotated' + ng-options='option for option in removingFilterOptions') + + .form-group.device-filters-item + label.device-list-label( + translate + uib-tooltip="{{'Device controlling state' | translate}}" + tooltip-placement='top' + tooltip-popup-delay='500') Controlled + select( + ng-model='removingFilters.controlled' + ng-options='option for option in removingFilterOptions') + + li.list-group-item(ng-repeat="device in devices \ + | filter:search \ + | orderBy: 'model' \ + | pagedObjectsFilter:scopeDevicesCtrl:'deviceCurrentPage':'deviceItemsPerPage':'filteredDevices' \ + track by device.serial") + .device-line.device-actions + i.fa.fa-mobile.fa-2x.fa-fw.device-list-icon + .device-list-details.selectable + .device-list-name(ng-bind-template="{{device.manufacturer + ' ' + device.model + ' (' + device.marketName + ')'}}") + .device-list-id + span(translate) Serial + span(ng-bind-template="{{::': ' + device.serial + ' - '}}") + span(translate) OS + span(ng-bind-template="{{': ' + device.version + ' - '}}") + span(translate) Screen + span(ng-bind-template="{{': ' + device.displayStr + ' - '}}") + span(translate) SDK + span(ng-bind-template="{{': ' + device.sdk + ' - '}}") + span(translate) Location + span(ng-bind-template="{{': ' + device.provider.name + ' - '}}") + span(translate) Group Origin + span(ng-bind-template="{{': ' + device.group.originName}}") + + button.btn.btn-xs.btn-danger-outline.pull-right( + type='button' + ng-click='removeDevice(device.serial, confirmRemove.value)') + i.fa.fa-trash-o + span(translate) Remove diff --git a/res/app/settings/devices/index.js b/res/app/settings/devices/index.js new file mode 100644 index 00000000..3b6a8267 --- /dev/null +++ b/res/app/settings/devices/index.js @@ -0,0 +1,18 @@ +/** +* Copyright © 2019 code initially contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0 +**/ + +require('./devices.css') + +module.exports = angular.module('stf.settings.devices', [ + require('stf/common-ui').name, + require('stf/settings').name, + require('stf/util/common').name, + require('stf/devices').name +]) + .run(['$templateCache', function($templateCache) { + $templateCache.put( + 'settings/devices/devices.pug', require('./devices.pug') + ) + }]) + .controller('DevicesCtrl', require('./devices-controller')) diff --git a/res/app/settings/general/date-format/date-format-controller.js b/res/app/settings/general/date-format/date-format-controller.js new file mode 100644 index 00000000..3f10f0c4 --- /dev/null +++ b/res/app/settings/general/date-format/date-format-controller.js @@ -0,0 +1,27 @@ +/** +* Copyright © 2019 code initially contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0 +**/ + +module.exports = function DateFormatCtrl( + $scope +, SettingsService +) { + + $scope.defaultDateFormat = 'M/d/yy h:mm:ss a' + SettingsService.bind($scope, { + target: 'dateFormat' + , source: 'dateFormat' + , defaultValue: $scope.defaultDateFormat + }) + + $scope.$watch( + function() { + return SettingsService.get('dateFormat') + } + , function(newvalue) { + if (typeof newvalue === 'undefined') { + SettingsService.set('dateFormat', $scope.defaultDateFormat) + } + } + ) +} diff --git a/res/app/settings/general/date-format/date-format.pug b/res/app/settings/general/date-format/date-format.pug new file mode 100644 index 00000000..24c83968 --- /dev/null +++ b/res/app/settings/general/date-format/date-format.pug @@ -0,0 +1,17 @@ +// + Copyright © 2019 code initially contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0 +// + +.widget-container.fluid-height(ng-controller='DateFormatCtrl') + .heading + i.fa.fa-clock-o + span(translate) Date format + .widget-content.padded + .form-horizontal + .form-group + .input-group + .input-group-addon.input-sm + i.fa.fa-clock-o( + uib-tooltip="{{'Define your own Date format' | translate}}" tooltip-placement='auto top-right' tooltip-popup-delay='500') + input.form-control.input-sm(size='30' type='text' placeholder='M/d/yy h:mm:ss a' ng-model='dateFormat') + diff --git a/res/app/settings/general/date-format/index.js b/res/app/settings/general/date-format/index.js new file mode 100644 index 00000000..bbc20d5e --- /dev/null +++ b/res/app/settings/general/date-format/index.js @@ -0,0 +1,13 @@ +/** +* Copyright © 2019 code initially contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0 +**/ + +module.exports = angular.module('stf.settings.general.date-format', [ + require('stf/settings').name +]) + .run(['$templateCache', function($templateCache) { + $templateCache.put( + 'settings/general/date-format/date-format.pug', require('./date-format.pug') + ) + }]) + .controller('DateFormatCtrl', require('./date-format-controller')) diff --git a/res/app/settings/general/email-address-separator/email-address-separator-controller.js b/res/app/settings/general/email-address-separator/email-address-separator-controller.js new file mode 100644 index 00000000..6c8a940b --- /dev/null +++ b/res/app/settings/general/email-address-separator/email-address-separator-controller.js @@ -0,0 +1,27 @@ +/** +* Copyright © 2019 code initially contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0 +**/ + +module.exports = function EmailAddressSeparatorCtrl( + $scope +, SettingsService +) { + + $scope.defaultEmailAddressSeparator = ',' + SettingsService.bind($scope, { + target: 'emailAddressSeparator' + , source: 'emailAddressSeparator' + , defaultValue: $scope.defaultEmailAddressSeparator + }) + + $scope.$watch( + function() { + return SettingsService.get('emailAddressSeparator') + } + , function(newvalue) { + if (typeof newvalue === 'undefined') { + SettingsService.set('emailAddressSeparator', $scope.defaultEmailAddressSeparator) + } + } + ) +} diff --git a/res/app/settings/general/email-address-separator/email-address-separator.pug b/res/app/settings/general/email-address-separator/email-address-separator.pug new file mode 100644 index 00000000..7f4e59d2 --- /dev/null +++ b/res/app/settings/general/email-address-separator/email-address-separator.pug @@ -0,0 +1,17 @@ +// + Copyright © 2019 code initially contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0 +// + +.widget-container.fluid-height(ng-controller='EmailAddressSeparatorCtrl') + .heading + i.fa.fa-envelope-o + span(translate) Email address separator + .widget-content.padded + .form-horizontal + .form-group + .input-group + .input-group-addon.input-sm + i.fa.fa-envelope-o( + uib-tooltip="{{'Define your own Email address separator' | translate}}" tooltip-placement='auto top-right' tooltip-popup-delay='500') + input.form-control.input-sm(size='2' type='text' placeholder=',' ng-model='emailAddressSeparator') + diff --git a/res/app/settings/general/email-address-separator/index.js b/res/app/settings/general/email-address-separator/index.js new file mode 100644 index 00000000..3e2cc4fa --- /dev/null +++ b/res/app/settings/general/email-address-separator/index.js @@ -0,0 +1,14 @@ +/** +* Copyright © 2019 code initially contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0 +**/ + +module.exports = angular.module('stf.settings.general.email-address-separator', [ + require('stf/settings').name +]) + .run(['$templateCache', function($templateCache) { + $templateCache.put( + 'settings/general/email-address-separator/email-address-separator.pug' + , require('./email-address-separator.pug') + ) + }]) + .controller('EmailAddressSeparatorCtrl', require('./email-address-separator-controller')) diff --git a/res/app/settings/general/general.pug b/res/app/settings/general/general.pug index 26a26a96..0fa05bdb 100644 --- a/res/app/settings/general/general.pug +++ b/res/app/settings/general/general.pug @@ -1,5 +1,13 @@ +// + Copyright © 2019 contains code contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0 +// + .row - .col-md-6 + .col-md-3 div(ng-include='"settings/general/local/local-settings.pug"') - .col-md-6 + .col-md-3 div(ng-include='"settings/general/language/language.pug"') + .col-md-3 + 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"') diff --git a/res/app/settings/general/index.js b/res/app/settings/general/index.js index f2df94da..5033197a 100644 --- a/res/app/settings/general/index.js +++ b/res/app/settings/general/index.js @@ -1,8 +1,14 @@ +/** +* Copyright © 2019 contains code contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0 +**/ + require('./general.css') module.exports = angular.module('stf.settings.general', [ require('./language').name, - require('./local').name + require('./local').name, + require('./email-address-separator').name, + require('./date-format').name ]) .run(['$templateCache', function($templateCache) { $templateCache.put( diff --git a/res/app/settings/groups/conflicts/conflicts.pug b/res/app/settings/groups/conflicts/conflicts.pug new file mode 100644 index 00000000..c250317f --- /dev/null +++ b/res/app/settings/groups/conflicts/conflicts.pug @@ -0,0 +1,31 @@ +// + Copyright © 2019 code initially contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0 +// + +li.list-group-item.groups-list + .heading.group-action-body + i.fa.fa-ban + span(translate) Conflicts + + .widget-container.fluid-height.overflow-auto.group-conflicts + table.table.table-hover.dataTable.ng-table + thead + tr + th.header.sortable( + ng-class='[column.sort]' + ng-repeat='column in conflictData.columns' + ng-click='sortBy(conflictData, column)') + div.strong(ng-bind-template='{{column.name | translate}}') + + tbody + tr.selectable( + ng-repeat='conflict in groupsEnv[group.id].conflicts \ + | orderBy:conflictColumns[conflictData.sort.index].property:conflictData.sort.reverse') + td {{conflict.serial}} + td {{conflict.startDate}} + td {{conflict.stopDate}} + td {{conflict.group}} + td + a.link(ng-href="{{'mailto:' + conflict.ownerEmail}}" + ng-click='$event.stopPropagation()') {{conflict.ownerName}} + diff --git a/res/app/settings/groups/devices/devices.pug b/res/app/settings/groups/devices/devices.pug new file mode 100644 index 00000000..0a39d331 --- /dev/null +++ b/res/app/settings/groups/devices/devices.pug @@ -0,0 +1,197 @@ +// + Copyright © 2019 code initially contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0 +// + +li.list-group-item.groups-list + .heading.group-action-body + i.fa.fa-mobile + span(translate) Devices + + .row + .panel-group + .panel.panel-default + .panel-heading.text-center + button.btn.btn-xs.btn-primary.btn-group-devices-action( + type='button' + ng-click='showGroupDevices = !showGroupDevices' + ng-class='{"btn-primary-outline": showGroupDevices, "btn-primary": !showGroupDevices}') + i.fa.fa-mobile + span(translate) Group devices + + .panel-body(ng-show='!showGroupDevices') + nothing-to-show( + icon='fa-mobile' message='{{"No group devices" | translate}}' + ng-if='!groupsEnv[group.id].filteredGroupDevices.length && \ + (!groupsEnv[group.id].availableDevices.length || !group.devices.length)') + + div(ng-show='groupsEnv[group.id].filteredGroupDevices.length || \ + groupsEnv[group.id].availableDevices.length && group.devices.length') + .form-inline + .form-group.group-devices-header + stf-pager( + tooltip-label="{{'Group device selection' | translate}}" + total-items='groupsEnv[filteredGroups[getGroupIndex($parent.$index)].id].filteredGroupDevices.length' + total-items-style='stf-pager-groups-total-items' + items-per-page='groupsEnv[filteredGroups[getGroupIndex($parent.$index)].id].groupDeviceItemsPerPage' + items-per-page-options='itemsPerPageOptions' + current-page='groupsEnv[filteredGroups[getGroupIndex($parent.$index)].id].groupDeviceCurrentPage' + items-search='groupDeviceSearch') + + .form-group.group-devices-header + stf-column-choice( + button-style='margin: 5px 0px 0px 15px' + reset-data='resetGroupDeviceData()' + column-data='groupDeviceData.columns') + + .widget-container.fluid-height.overflow-auto + table.table.table-hover.dataTable.ng-table + thead + tr + th.header + button.btn.btn-sm.btn-danger.btn-group-devices-action( + type='button' + ng-if="groupsEnv[filteredGroups[getGroupIndex($parent.$index)].id].hasOwnProperty('filteredGroupDevices')" + ng-disabled="!groupsEnv[filteredGroups[getGroupIndex($parent.$index)].id].filteredGroupDevices.length || \ + group.privilege === 'root'" + ng-click='removeGroupDevices(\ + group, \ + groupDeviceSearch, \ + groupsEnv[filteredGroups[getGroupIndex($parent.$index)].id].filteredGroupDevices)') + i.fa.fa-trash-o + th.header.sortable( + ng-class='[column.sort]' + ng-repeat='column in groupDeviceData.columns | filter: {selected: true}' + ng-click='sortBy(groupDeviceData, column)') + div.strong(ng-bind-template="{{column.name | translate}}") + + tbody + tr.selectable(ng-repeat="device in filteredGroups[getGroupIndex($parent.$index)].devices \ + | groupObjectsFilter:\ + groupsEnv[filteredGroups[getGroupIndex($parent.$index)].id].availableDevices:\ + groupsEnv[filteredGroups[getGroupIndex($parent.$index)].id].availableDevicesBySerial \ + | filter:groupDeviceSearch \ + | orderBy:deviceColumns[groupDeviceData.sort.index].property:groupDeviceData.sort.reverse \ + | pagedObjectsFilter:\ + groupsEnv[filteredGroups[getGroupIndex($parent.$index)].id]:\ + 'groupDeviceCurrentPage':'groupDeviceItemsPerPage':'filteredGroupDevices' \ + track by device.serial") + td + button.btn.btn-danger-outline.btn-xs( + ng-disabled="filteredGroups[getGroupIndex($parent.$index)].privilege === 'root'" + ng-click='removeGroupDevice(filteredGroups[getGroupIndex($parent.$index)], device)') + i.fa.fa-trash-o.fa-fw + td(ng-if='groupDeviceData.columns[0].selected') {{device.model}} + td(ng-if='groupDeviceData.columns[1].selected') {{device.serial}} + td(ng-if='groupDeviceData.columns[2].selected') {{device.operator}} + td(ng-if='groupDeviceData.columns[3].selected') {{device.version}} + td(ng-if='groupDeviceData.columns[4].selected') {{device.networkStr}} + td(ng-if='groupDeviceData.columns[5].selected') {{device.displayStr}} + td(ng-if='groupDeviceData.columns[6].selected') {{device.manufacturer}} + td(ng-if='groupDeviceData.columns[7].selected') {{device.sdk}} + td(ng-if='groupDeviceData.columns[8].selected') {{device.abi}} + td(ng-if='groupDeviceData.columns[9].selected') {{device.cpuPlatform}} + td(ng-if='groupDeviceData.columns[10].selected') {{device.openGLESVersion}} + td(ng-if='groupDeviceData.columns[11].selected') {{device.marketName}} + td(ng-if='groupDeviceData.columns[12].selected') {{device.phone.imei}} + td(ng-if='groupDeviceData.columns[13].selected') {{device.provider.name}} + td(ng-if='groupDeviceData.columns[14].selected') {{device.group.originName}} + + .panel.panel-default + .panel-heading.text-center + button.btn.btn-xs.btn-primary-outline.btn-group-devices-action( + type='button' + ng-click='showAvailableDevices = !showAvailableDevices' + ng-class='{"btn-primary-outline": !showAvailableDevices, "btn-primary": showAvailableDevices}') + i.fa.fa-mobile + span(translate) Available devices + + .panel-body(ng-show='showAvailableDevices') + nothing-to-show( + icon='fa-mobile' message='{{"No available devices" | translate}}' + ng-if='!(groupsEnv[group.id].filteredAvailableDevices && \ + groupsEnv[group.id].filteredAvailableDevices.length || \ + groupsEnv[group.id].availableDevices.length !== group.devices.length)') + + div(ng-if='groupsEnv[group.id].filteredAvailableDevices && \ + groupsEnv[group.id].filteredAvailableDevices.length || \ + groupsEnv[group.id].availableDevices.length !== group.devices.length') + .form-inline + .form-group.group-devices-header + stf-pager( + tooltip-label="{{'Available device selection' | translate}}" + total-items='groupsEnv[filteredGroups[getGroupIndex($parent.$index)].id].filteredAvailableDevices.length' + total-items-style='stf-pager-groups-total-items' + items-per-page='groupsEnv[filteredGroups[getGroupIndex($parent.$index)].id].availableDeviceItemsPerPage' + items-per-page-options='itemsPerPageOptions' + current-page='groupsEnv[filteredGroups[getGroupIndex($parent.$index)].id].availableDeviceCurrentPage' + items-search='deviceSearch') + + .form-group.group-devices-header + stf-column-choice( + button-style='margin: 5px 0px 0px 15px' + reset-data='resetDeviceData()' + column-data='deviceData.columns') + + .widget-container.fluid-height.overflow-auto + table.table.table-hover.dataTable.ng-table + thead + tr + th.header + button.btn.btn-sm.btn-primary.btn-group-devices-action( + type='button' + ng-if="groupsEnv[filteredGroups[getGroupIndex($parent.$index)].id].hasOwnProperty('filteredAvailableDevices')" + ng-disabled='!groupsEnv[filteredGroups[getGroupIndex($parent.$index)].id].filteredAvailableDevices.length || \ + !conditionForDevicesAddition(\ + filteredGroups[getGroupIndex($parent.$index)], \ + groupsEnv[filteredGroups[getGroupIndex($parent.$index)].id].filteredAvailableDevices.length)' + uib-tooltip="{{'Groups duration quota is reached' | translate}}" + tooltip-placement='auto top-right' + tooltip-enable="groupsEnv[filteredGroups[getGroupIndex($parent.$index)].id].filteredAvailableDevices.length && \ + !conditionForDevicesAddition(\ + filteredGroups[getGroupIndex($parent.$index)], \ + groupsEnv[filteredGroups[getGroupIndex($parent.$index)].id].filteredAvailableDevices.length)" + tooltip-popup-delay='500' + ng-click='addGroupDevices(group, \ + deviceSearch, \ + groupsEnv[filteredGroups[getGroupIndex($parent.$index)].id].filteredAvailableDevices)') + i.fa.fa-cart-plus + th.header.sortable( + ng-class='[column.sort]' + ng-repeat="column in deviceData.columns | filter: {selected: true}" + ng-click='sortBy(deviceData, column)') + div.strong(ng-bind-template="{{column.name | translate}}") + + tbody + tr.selectable(ng-repeat="device in groupsEnv[filteredGroups[getGroupIndex($parent.$index)].id].availableDevices \ + | availableObjectsFilter:filteredGroups[getGroupIndex($parent.$index)]:'devices':'serial' \ + | filter:deviceSearch \ + | orderBy:deviceColumns[deviceData.sort.index].property:deviceData.sort.reverse \ + | pagedObjectsFilter:\ + groupsEnv[filteredGroups[getGroupIndex($parent.$index)].id]:\ + 'availableDeviceCurrentPage':'availableDeviceItemsPerPage':'filteredAvailableDevices' \ + track by device.serial") + td + button.btn.btn-primary-outline.btn-xs( + type='button' + ng-disabled='!conditionForDevicesAddition(filteredGroups[getGroupIndex($parent.$index)], 1)' + uib-tooltip="{{'Groups duration quota is reached' | translate}}" + tooltip-placement='auto top-right' + tooltip-enable="!conditionForDevicesAddition(filteredGroups[getGroupIndex($parent.$index)], 1)" + tooltip-popup-delay='500' + ng-click='addGroupDevice(filteredGroups[getGroupIndex($parent.$index)], device)') + i.fa.fa-cart-plus.fa-fw + td(ng-if='deviceData.columns[0].selected') {{device.model}} + td(ng-if='deviceData.columns[1].selected') {{device.serial}} + td(ng-if='deviceData.columns[2].selected') {{device.operator}} + td(ng-if='deviceData.columns[3].selected') {{device.version}} + td(ng-if='deviceData.columns[4].selected') {{device.networkStr}} + td(ng-if='deviceData.columns[5].selected') {{device.displayStr}} + td(ng-if='deviceData.columns[6].selected') {{device.manufacturer}} + td(ng-if='deviceData.columns[7].selected') {{device.sdk}} + td(ng-if='deviceData.columns[8].selected') {{device.abi}} + td(ng-if='deviceData.columns[9].selected') {{device.cpuPlatform}} + td(ng-if='deviceData.columns[10].selected') {{device.openGLESVersion}} + td(ng-if='deviceData.columns[11].selected') {{device.marketName}} + td(ng-if='deviceData.columns[12].selected') {{device.phone.imei}} + td(ng-if='deviceData.columns[13].selected') {{device.provider.name}} + td(ng-if='deviceData.columns[14].selected') {{device.group.originName}} diff --git a/res/app/settings/groups/filters/available-objects-filter.js b/res/app/settings/groups/filters/available-objects-filter.js new file mode 100644 index 00000000..54b1bb41 --- /dev/null +++ b/res/app/settings/groups/filters/available-objects-filter.js @@ -0,0 +1,17 @@ +/** +* Copyright © 2019 code initially contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0 +**/ + +module.exports = function() { + return function(objects, group, groupKey, objectKey) { + const objectList = [] + + objects.forEach(function(object) { + if (group[groupKey].indexOf(object[objectKey]) < 0) { + objectList.push(object) + } + }) + return objectList + } +} + diff --git a/res/app/settings/groups/filters/group-objects-filter.js b/res/app/settings/groups/filters/group-objects-filter.js new file mode 100644 index 00000000..f102b001 --- /dev/null +++ b/res/app/settings/groups/filters/group-objects-filter.js @@ -0,0 +1,17 @@ +/** +* Copyright © 2019 code initially contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0 +**/ + +module.exports = function(CommonService) { + return function(keys, objects, objectsIndex) { + const objectList = [] + + keys.forEach(function(key) { + if (CommonService.isExisting(objectsIndex[key])) { + objectList.push(objects[objectsIndex[key].index]) + } + }) + return objectList + } +} + diff --git a/res/app/settings/groups/groups-controller.js b/res/app/settings/groups/groups-controller.js new file mode 100644 index 00000000..3dbee0f5 --- /dev/null +++ b/res/app/settings/groups/groups-controller.js @@ -0,0 +1,911 @@ +/** +* Copyright © 2019 code initially contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0 +**/ + +const _ = require('lodash') +const Promise = require('bluebird') + +module.exports = function GroupsCtrl( + $scope +, $filter +, GroupsService +, UserService +, UsersService +, DevicesService +, SettingsService +, ItemsPerPageOptionsService +, GenericModalService +, CommonService +) { + const originDevices = [] + const originDevicesBySerial = {} + const standardizableDevices = [] + const standardizableDevicesBySerial = {} + const groupsById = {} + const cachedGroupsClass = {} + const deviceFields = + 'serial,' + + 'model,' + + 'version,' + + 'operator,' + + 'network.type,' + + 'network.subtype,' + + 'display.height,' + + 'display.width,' + + 'manufacturer,' + + 'sdk,' + + 'abi,' + + 'cpuPlatform,' + + 'openGLESVersion,' + + 'marketName,' + + 'phone.imei,' + + 'provider.name,' + + 'group.originName' + const userFields = + 'email,' + + 'name,' + + 'privilege,' + + 'groups.subscribed,' + + 'groups.quotas.allocated,' + + 'groups.quotas.consumed' + var rootGroupId + + function publishDevice(device) { + if (!device.model) { + device.display = device.phone = device.network = {} + } + else { + device.displayStr = device.display.width + 'x' + device.display.height + device.networkStr = $scope.computeNetwork(device) + } + return device + } + + function initAvailableGroupDevices(group, availableDevices, availableDevicesBySerial) { + $scope.groupsEnv[group.id].availableDevices = availableDevices + $scope.groupsEnv[group.id].availableDevicesBySerial = availableDevicesBySerial + $scope.groupsEnv[group.id].availableDevices.forEach(function(device) { + publishDevice(device) + }) + } + + function getAvailableGroupDevices(group) { + if (group.class === 'bookable') { + initAvailableGroupDevices(group, originDevices, originDevicesBySerial) + } + else if (group.class === 'standard') { + initAvailableGroupDevices(group, standardizableDevices, standardizableDevicesBySerial) + } + else if ($scope.groupsEnv[group.id].showDevices) { + GroupsService.getGroupDevices(group.id, true, deviceFields).then(function(response) { + if (CommonService.isExisting($scope.groupsEnv[group.id])) { + $scope.groupsEnv[group.id].availableDevicesBySerial = {} + $scope.groupsEnv[group.id].availableDevices = [] + response.data.devices.forEach(function(device) { + addAvailableGroupDevice(group.id, device, -1) + }) + initAvailableGroupDevices( + group + , $scope.groupsEnv[group.id].availableDevices + , $scope.groupsEnv[group.id].availableDevicesBySerial) + } + }) + } + } + + function checkDurationQuota(group, deviceNumber, startDate, stopDate, repetitions) { + if (CommonService.isOriginGroup(group.class)) { + return true + } + if (CommonService.isExisting($scope.usersByEmail[group.owner.email])) { + const duration = + (group.devices.length + deviceNumber) * + ((new Date(stopDate)) - (new Date(startDate))) * + (repetitions + 1) + + if (duration <= + $scope.users[$scope.usersByEmail[group.owner.email].index] + .groups.quotas.allocated.duration) { + return true + } + } + return false + } + + function isBookedDevice(serial) { + if (CommonService.isExisting(originDevicesBySerial[serial])) { + for(var i in $scope.groups) { + if (!CommonService.isOriginGroup($scope.groups[i].class) && + $scope.groups[i].devices.indexOf(serial) > -1) { + return true + } + } + } + return false + } + + function addStandardizableDevicesIfNotBooked(devices, timeStamp) { + devices.forEach(function(serial) { + if (!isBookedDevice(serial)) { + addStandardizableDevice( + originDevices[originDevicesBySerial[serial].index] + , timeStamp + ) + } + }) + } + + function updateStandardizableDeviceIfNotBooked(device, timeStamp) { + if (!isBookedDevice(device.serial)) { + updateStandardizableDevice(device, timeStamp) + } + } + + function initGroup(group) { + cachedGroupsClass[group.id] = group.class + if (typeof $scope.groupsEnv[group.id] === 'undefined') { + $scope.groupsEnv[group.id] = {} + initAvailableGroupDevices(group, [], {}) + if (group.privilege === 'root') { + rootGroupId = group.id + } + } + return group + } + + function addGroup(group, timeStamp) { + if (CommonService.add($scope.groups, groupsById, group, 'id', timeStamp)) { + return initGroup(group) + } + return null + } + + function updateGroup(group, timeStamp, noAdding) { + if (CommonService.update($scope.groups, groupsById, group, 'id', timeStamp, noAdding)) { + return initGroup($scope.groups[groupsById[group.id].index]) + } + return null + } + + function deleteGroup(id, timeStamp) { + const group = CommonService.delete($scope.groups, groupsById, id, timeStamp) + + if (group) { + delete $scope.groupsEnv[group.id] + } + return group + } + + function addOriginDevice(device, timeStamp) { + return CommonService.add(originDevices, originDevicesBySerial, device, 'serial', timeStamp) + } + + function updateOriginDevice(device, timeStamp) { + return CommonService.update(originDevices, originDevicesBySerial, device, 'serial', timeStamp) + } + + function deleteOriginDevice(serial, timeStamp) { + return CommonService.delete(originDevices, originDevicesBySerial, serial, timeStamp) + } + + function addStandardizableDevice(device, timeStamp) { + return CommonService.add( + standardizableDevices, standardizableDevicesBySerial, device, 'serial', timeStamp) + } + + function updateStandardizableDevice(device, timeStamp) { + return CommonService.update( + standardizableDevices, standardizableDevicesBySerial, device, 'serial', timeStamp) + } + + function deleteStandardizableDevice(serial, timeStamp) { + return CommonService.delete( + standardizableDevices, standardizableDevicesBySerial, serial, timeStamp) + } + + function addAvailableGroupDevice(id, device, timeStamp) { + return CommonService.add( + $scope.groupsEnv[id].availableDevices + , $scope.groupsEnv[id].availableDevicesBySerial, device, 'serial', timeStamp) + } + + function updateAvailableGroupDevice(id, device, timeStamp, noAdding) { + return CommonService.update( + $scope.groupsEnv[id].availableDevices + , $scope.groupsEnv[id].availableDevicesBySerial, device, 'serial', timeStamp, noAdding) + } + + function deleteAvailableGroupDevice(id, serial, timeStamp) { + return CommonService.delete( + $scope.groupsEnv[id].availableDevices + , $scope.groupsEnv[id].availableDevicesBySerial, serial, timeStamp) + } + + function addUser(user, timeStamp) { + return CommonService.add($scope.users, $scope.usersByEmail, user, 'email', timeStamp) + } + + function updateUser(user, timeStamp) { + return CommonService.update($scope.users, $scope.usersByEmail, user, 'email', timeStamp) + } + + function deleteUser(email, timeStamp) { + return CommonService.delete($scope.users, $scope.usersByEmail, email, timeStamp) + } + + function initScope() { + GroupsService.getOboeMyGroups(function(group) { + addGroup(group, -1) + }) + .done(function() { + $scope.$digest() + }) + + UsersService.getOboeUsers(userFields, function(user) { + addUser(user, -1) + }) + .done(function() { + if (CommonService.isExisting($scope.usersByEmail[$scope.currentUser.email])) { + $scope.users[$scope.usersByEmail[$scope.currentUser.email].index] = $scope.currentUser + } + }) + + UserService.getUser().then(function(response) { + CommonService.merge($scope.currentUser, response.data.user) + }) + + if ($scope.isAdmin()) { + DevicesService.getOboeDevices('origin', deviceFields, function(device) { + addOriginDevice(device, -1) + }) + DevicesService.getOboeDevices('standardizable', deviceFields, function(device) { + addStandardizableDevice(device, -1) + }) + } + } + + $scope.currentUser = CommonService.merge({}, UserService.currentUser) + $scope.users = [] + $scope.usersByEmail = {} + $scope.groups = [] + $scope.groupsEnv = {} + $scope.confirmRemove = {value: true} + $scope.scopeGroupsCtrl = $scope + $scope.itemsPerPageOptions = ItemsPerPageOptionsService + + SettingsService.bind($scope, { + target: 'groupItemsPerPage' + , source: 'groupItemsPerPage' + , defaultValue: $scope.itemsPerPageOptions[2] + }) + + $scope.userColumns = [ + {name: 'Name', property: 'name'} + , {name: 'Email', property: 'email'} + , {name: 'Privilege', property: 'privilege'} + ] + $scope.defaultUserData = { + columns: [ + {name: 'Name', sort: 'sort-asc'} + , {name: 'Email', sort: 'none'} + , {name: 'Privilege', sort: 'none'} + ] + , sort: {index: 0, reverse: false} + } + SettingsService.bind($scope, { + target: 'userData' + , source: 'userData' + , defaultValue: $scope.defaultUserData + }) + SettingsService.bind($scope, { + target: 'groupUserData' + , source: 'groupUserData' + , defaultValue: $scope.defaultUserData + }) + + $scope.conflictColumns = [ + {name: 'Serial', property: 'serial'} + , {name: 'Starting Date', property: 'startDate'} + , {name: 'Expiration Date', property: 'stopDate'} + , {name: 'Group Name', property: 'group'} + , {name: 'Group Owner', property: 'ownerName'} + ] + $scope.defaultConflictData = { + columns: [ + {name: 'Serial', sort: 'sort-asc'} + , {name: 'Starting Date', sort: 'none'} + , {name: 'Expiration Date', sort: 'none'} + , {name: 'Group Name', sort: 'none'} + , {name: 'Group Owner', sort: 'none'} + ] + , sort: {index: 0, reverse: false} + } + SettingsService.bind($scope, { + target: 'conflictData' + , source: 'conflictData' + , defaultValue: $scope.defaultConflictData + }) + + $scope.mailToGroupOwners = function(groups) { + CommonService.copyToClipboard(_.uniq(groups.map(function(group) { + return group.owner.email + })) + .join(SettingsService.get('emailSeparator'))) + .url('mailto:?body=*** Paste the email addresses from the clipboard! ***') + } + + $scope.mailToGroupUsers = function(group, users) { + // group unused actually.. + $scope.mailToAvailableUsers(users) + } + + $scope.mailToAvailableUsers = function(users) { + CommonService.copyToClipboard(users.map(function(user) { + return user.email + }) + .join(SettingsService.get('emailSeparator'))) + .url('mailto:?body=*** Paste the email addresses from the clipboard! ***') + } + + $scope.getGroupIndex = function(relativeIndex) { + return relativeIndex + ($scope.groupCurrentPage - 1) * $scope.groupItemsPerPage.value + } + + $scope.computeDisplay = function(device) { + return device.display.width * device.display.height + } + + $scope.computeNetwork = function(device) { + if (!device.network || !device.network.type) { + return '' + } + else if (device.network.subtype) { + return device.network.type + ' (' + device.network.subtype + ')' + } + return device.network.type + } + + $scope.resetDeviceData = function() { + $scope.deviceData = JSON.parse(JSON.stringify($scope.defaultDeviceData)) + } + + $scope.resetGroupDeviceData = function() { + $scope.groupDeviceData = JSON.parse(JSON.stringify($scope.defaultDeviceData)) + } + + $scope.deviceColumns = [ + {name: 'Model', property: 'model'} + , {name: 'Serial', property: 'serial'} + , {name: 'Carrier', property: 'operator'} + , {name: 'OS', property: 'version'} + , {name: 'Network', property: $scope.computeNetwork} + , {name: 'Screen', property: $scope.computeDisplay} + , {name: 'Manufacturer', property: 'manufacturer'} + , {name: 'SDK', property: 'sdk'} + , {name: 'ABI', property: 'abi'} + , {name: 'CPU Platform', property: 'cpuPlatform'} + , {name: 'OpenGL ES version', property: 'openGLESVersion'} + , {name: 'Market name', property: 'marketName'} + , {name: 'Phone IMEI', property: 'phone.imei'} + , {name: 'Location', property: 'provider.name'} + , {name: 'Group Origin', property: 'group.originName'} + ] + $scope.defaultDeviceData = { + columns: [ + {name: 'Model', selected: true, sort: 'sort-asc'} + , {name: 'Serial', selected: true, sort: 'none'} + , {name: 'Carrier', selected: false, sort: 'none'} + , {name: 'OS', selected: true, sort: 'none'} + , {name: 'Network', selected: false, sort: 'none'} + , {name: 'Screen', selected: true, sort: 'none'} + , {name: 'Manufacturer', selected: true, sort: 'none'} + , {name: 'SDK', selected: true, sort: 'none'} + , {name: 'ABI', selected: false, sort: 'none'} + , {name: 'CPU Platform', selected: false, sort: 'none'} + , {name: 'OpenGL ES version', selected: false, sort: 'none'} + , {name: 'Market name', selected: true, sort: 'none'} + , {name: 'Phone IMEI', selected: false, sort: 'none'} + , {name: 'Location', selected: true, sort: 'none'} + , {name: 'Group Origin', selected: true, sort: 'none'} + ] + , sort: {index: 0, reverse: false} + } + SettingsService.bind($scope, { + target: 'deviceData' + , source: 'deviceData' + , defaultValue: $scope.defaultDeviceData + }) + SettingsService.bind($scope, { + target: 'groupDeviceData' + , source: 'groupDeviceData' + , defaultValue: $scope.defaultDeviceData + }) + $scope.nameRegex = /^[0-9a-zA-Z-_./: ]{1,50}$/ + $scope.nameRegexStr = '/^[0-9a-zA-Z-_./: ]{1,50}$/' + $scope.classOptions = CommonService.classOptions + $scope.getClassName = CommonService.getClassName + $scope.sortBy = CommonService.sortBy + + $scope.isAdmin = function() { + return $scope.currentUser.privilege === 'admin' + } + + $scope.getRepetitionsQuotas = function(email) { + if (CommonService.isExisting($scope.usersByEmail[email])) { + return $scope.users[$scope.usersByEmail[email].index].groups.quotas.repetitions + } + return null + } + + $scope.initShowDevices = function(group, showDevices) { + if (typeof $scope.groupsEnv[group.id].groupDeviceCurrentPage === 'undefined') { + $scope.groupsEnv[group.id].groupDeviceCurrentPage = 1 + $scope.groupsEnv[group.id].groupDeviceItemsPerPage = $scope.itemsPerPageOptions[1] + $scope.groupsEnv[group.id].availableDeviceCurrentPage = 1 + $scope.groupsEnv[group.id].availableDeviceItemsPerPage = $scope.itemsPerPageOptions[1] + } + $scope.groupsEnv[group.id].showDevices = showDevices + getAvailableGroupDevices(group) + } + + $scope.initShowUsers = function(group) { + if (typeof $scope.groupsEnv[group.id].groupUserCurrentPage === 'undefined') { + $scope.groupsEnv[group.id].groupUserCurrentPage = 1 + $scope.groupsEnv[group.id].groupUserItemsPerPage = $scope.itemsPerPageOptions[1] + $scope.groupsEnv[group.id].availableUserCurrentPage = 1 + $scope.groupsEnv[group.id].availableUserItemsPerPage = $scope.itemsPerPageOptions[1] + } + } + + $scope.watchGroupClass = function(group) { + if (CommonService.isNoRepetitionsGroup($scope.groupsEnv[group.id].tmpClass)) { + $scope.groupsEnv[group.id].tmpRepetitions = 0 + } + else if ($scope.groupsEnv[group.id].tmpRepetitions === 0) { + $scope.groupsEnv[group.id].tmpRepetitions = 1 + } + } + + $scope.initTemporaryName = function(group) { + $scope.groupsEnv[group.id].tmpName = group.name + $scope.groupsEnv[group.id].tmpNameTooltip = 'No change' + } + + $scope.initTemporarySchedule = function(group) { + $scope.groupsEnv[group.id].tmpClass = group.class + $scope.groupsEnv[group.id].tmpRepetitions = group.repetitions + $scope.groupsEnv[group.id].tmpStartDate = new Date(group.dates[0].start) + $scope.groupsEnv[group.id].tmpStopDate = new Date(group.dates[0].stop) + $scope.groupsEnv[group.id].tmpScheduleTooltip = 'No change' + } + + $scope.conditionForDevicesAddition = function(group, deviceNumber) { + return checkDurationQuota( + group + , deviceNumber + , group.dates[0].start + , group.dates[0].stop + , group.repetitions + ) + } + + $scope.conditionForGroupCreation = function() { + return $scope.currentUser.groups.quotas.consumed.number < + $scope.currentUser.groups.quotas.allocated.number + } + + $scope.conditionForGroupUsersRemoving = function(group, users) { + return !(users.length === 0 || + group.privilege === 'root' && users.length === 1 && users[0].privilege === 'admin' || + group.privilege !== 'root' && + (users.length === 2 && + (users[0].privilege === 'admin' && users[1].email === group.owner.email || + users[0].email === group.owner.email && users[1].privilege === 'admin') || + users.length === 1 && + (users[0].email === group.owner.email || users[0].privilege === 'admin')) + ) + } + + $scope.conditionForNameSaving = function(group, formInvalidStatus) { + return !formInvalidStatus && $scope.groupsEnv[group.id].tmpName !== group.name + } + + $scope.conditionForScheduleSaving = function(group, formInvalidStatus) { + if (formInvalidStatus) { + $scope.groupsEnv[group.id].tmpScheduleTooltip = 'Bad syntax' + return false + } + if ($scope.groupsEnv[group.id].tmpClass !== group.class || + parseInt($scope.groupsEnv[group.id].tmpRepetitions, 10) !== group.repetitions || + $scope.groupsEnv[group.id].tmpStartDate.getTime() !== + (new Date(group.dates[0].start)).getTime() || + $scope.groupsEnv[group.id].tmpStopDate.getTime() !== + (new Date(group.dates[0].stop)).getTime()) { + if (!CommonService.isNoRepetitionsGroup($scope.groupsEnv[group.id].tmpClass)) { + if (parseInt($scope.groupsEnv[group.id].tmpRepetitions, 10) === 0) { + $scope.groupsEnv[group.id].tmpScheduleTooltip = 'Repetitions must be > 0 for this Class' + return false + } + } + if ($scope.groupsEnv[group.id].tmpStartDate >= $scope.groupsEnv[group.id].tmpStopDate) { + $scope.groupsEnv[group.id].tmpScheduleTooltip = 'Starting date >= Expiration date' + return false + } + if (($scope.groupsEnv[group.id].tmpStopDate - $scope.groupsEnv[group.id].tmpStartDate) > + CommonService.getClassDuration($scope.groupsEnv[group.id].tmpClass)) { + $scope.groupsEnv[group.id].tmpScheduleTooltip = + '(Expiration date - Starting date) must be <= Class duration' + return false + } + if ($scope.isAdmin() && + group.devices.length && + (CommonService.isOriginGroup(group.class) && + !CommonService.isOriginGroup($scope.groupsEnv[group.id].tmpClass) || + CommonService.isOriginGroup($scope.groupsEnv[group.id].tmpClass) && + !CommonService.isOriginGroup(group.class))) { + $scope.groupsEnv[group.id].tmpScheduleTooltip = + 'Unauthorized class while device list is not empty' + return false + } + if (!checkDurationQuota( + group + , 0 + , $scope.groupsEnv[group.id].tmpStartDate + , $scope.groupsEnv[group.id].tmpStopDate + , $scope.groupsEnv[group.id].tmpRepetitions)) { + $scope.groupsEnv[group.id].tmpScheduleTooltip = 'Group duration quotas is reached' + return false + } + $scope.groupsEnv[group.id].tmpScheduleTooltip = '' + return true + } + $scope.groupsEnv[group.id].tmpScheduleTooltip = 'No change' + return false + } + + $scope.conditionForRepetitions = function(group) { + return !CommonService.isNoRepetitionsGroup($scope.groupsEnv[group.id].tmpClass) + } + + $scope.addGroupDevice = function(group, device) { + if (CommonService.isOriginGroup(group.class)) { + CommonService.errorWrapper( + DevicesService.addOriginGroupDevice + , [group.id, device.serial]) + } + else { + CommonService.errorWrapper( + GroupsService.addGroupDevice + , [group.id, device.serial]) + .then(function(response) { + if (!response.success && + response.status === 409 && + response.data.hasOwnProperty('conflicts')) { + $scope.groupsEnv[group.id].showConflicts = true + $scope.groupsEnv[group.id].conflicts = response.data.conflicts + } + }) + } + } + + $scope.addGroupDevices = function(group, deviceSearch, filteredDevices) { + CommonService.errorWrapper( + CommonService.isOriginGroup(group.class) ? + DevicesService.addOriginGroupDevices : + GroupsService.addGroupDevices + , deviceSearch ? + [group.id, filteredDevices.map(function(device) { return device.serial }).join()] : + [group.id]) + } + + $scope.removeGroupDevice = function(group, device) { + CommonService.errorWrapper( + CommonService.isOriginGroup(group.class) ? + DevicesService.removeOriginGroupDevice : + GroupsService.removeGroupDevice + , [group.id, device.serial]) + } + + $scope.removeGroupDevices = function(group, deviceSearch, filteredDevices) { + CommonService.errorWrapper( + CommonService.isOriginGroup(group.class) ? + DevicesService.removeOriginGroupDevices : + GroupsService.removeGroupDevices + , deviceSearch ? + [group.id, filteredDevices.map(function(device) { return device.serial }).join()] : + [group.id]) + } + + $scope.addGroupUser = function(group, user) { + CommonService.errorWrapper( + GroupsService.addGroupUser + , [group.id, user.email]) + } + + $scope.addGroupUsers = function(group, userSearch, filteredUsers) { + CommonService.errorWrapper( + GroupsService.addGroupUsers + , userSearch ? + [group.id, filteredUsers.map(function(user) { return user.email }).join()] : + [group.id]) + } + + $scope.removeGroupUser = function(group, user) { + CommonService.errorWrapper( + GroupsService.removeGroupUser + , [group.id, user.email]) + } + + $scope.removeGroupUsers = function(group, userSearch, filteredUsers) { + CommonService.errorWrapper( + GroupsService.removeGroupUsers + , userSearch ? + [group.id, filteredUsers.map(function(user) { return user.email }).join()] : + [group.id]) + } + + $scope.removeGroup = function(group, askConfirmation) { + if (askConfirmation) { + GenericModalService.open({ + message: 'Really delete this group?' + , type: 'Warning' + , size: 'sm' + , cancel: true + }) + .then(function() { + CommonService.errorWrapper( + GroupsService.removeGroup + , [group.id]) + }) + } + else { + CommonService.errorWrapper( + GroupsService.removeGroup + , [group.id]) + } + } + + $scope.removeGroups = function(search, filteredGroups, askConfirmation) { + function removeGroups() { + if (!search) { + CommonService.errorWrapper(GroupsService.removeGroups) + } + else { + CommonService.errorWrapper( + GroupsService.removeGroups + , [filteredGroups.map(function(group) { return group.id }).join()]) + } + } + + if (askConfirmation) { + GenericModalService.open({ + message: 'Really delete this selection of groups?' + , type: 'Warning' + , size: 'sm' + , cancel: true + }) + .then(function() { + removeGroups() + }) + } + else { + removeGroups() + } + } + + $scope.createGroup = function() { + $scope.hideGroupCreation = true + CommonService.errorWrapper(GroupsService.createGroup) + .then(function() { + delete $scope.hideGroupCreation + }) + } + + $scope.updateGroupSchedule = function(group) { + CommonService.errorWrapper(GroupsService.updateGroup, [group.id, { + 'class': $scope.groupsEnv[group.id].tmpClass + , 'repetitions': parseInt($scope.groupsEnv[group.id].tmpRepetitions, 10) + , 'startTime': $scope.groupsEnv[group.id].tmpStartDate + , 'stopTime': $scope.groupsEnv[group.id].tmpStopDate + }]) + .then(function(response) { + if (!response.success && + response.status === 409 && + response.data.hasOwnProperty('conflicts')) { + $scope.groupsEnv[group.id].conflicts = [] + response.data.conflicts.forEach(function(conflict) { + conflict.devices.forEach(function(serial) { + $scope.groupsEnv[group.id].conflicts.push({ + serial: serial + , startDate: $filter('date')(conflict.date.start, SettingsService.get('dateFormat')) + , stopDate: $filter('date')(conflict.date.stop, SettingsService.get('dateFormat')) + , group: conflict.group + , ownerName: conflict.owner.name + , ownerEmail: conflict.owner.email + }) + }) + }) + $scope.groupsEnv[group.id].showConflicts = true + } + }) + } + + $scope.updateGroupState = function(group) { + CommonService.errorWrapper( + GroupsService.updateGroup + , [group.id, {'state': 'ready'}]) + } + + $scope.updateGroupName = function(group) { + CommonService.errorWrapper( + GroupsService.updateGroup + , [group.id, {'name': $scope.groupsEnv[group.id].tmpName}]) + } + + $scope.$on('user.settings.groups.updated', function(event, message) { + const isChangedSchedule = message.isChangedDates || message.isChangedClass + const doGetDevices = + !CommonService.isOriginGroup(message.group.class) && + (isChangedSchedule || message.devices.length) + const isGroupOwner = $scope.isAdmin() || $scope.currentUser.email === message.group.owner.email + const group = updateGroup( + message.group + , message.timeStamp + , !isGroupOwner) + + if (group) { + if ($scope.isAdmin()) { + if (!CommonService.isOriginGroup(group.class)) { + if (message.devices.length) { + if (!message.isAddedDevice) { + addStandardizableDevicesIfNotBooked(message.devices, message.timeStamp) + } + else { + message.devices.forEach(function(serial) { + deleteStandardizableDevice(serial, message.timeStamp) + }) + } + } + } + else if (message.isChangedClass) { + getAvailableGroupDevices(group) + } + } + if (isChangedSchedule && group.state !== 'pending') { + $scope.initTemporarySchedule(group) + } + if (doGetDevices) { + $scope.groups.forEach(function(group) { + if (group.id !== message.group.id || isChangedSchedule) { + getAvailableGroupDevices(group) + } + }) + } + } + else if (!isGroupOwner && doGetDevices) { // a completer ... soit propriétaire et event obsolete, soit non propriétaire donc non admin + $scope.groups.forEach(function(group) { + getAvailableGroupDevices(group) + }) + } + }) + + $scope.$on('user.settings.groups.created', function(event, message) { + addGroup(message.group, message.timeStamp) + }) + + $scope.$on('user.settings.groups.deleted', function(event, message) { + const group = message.group + + if (deleteGroup(group.id, message.timeStamp)) { + if ($scope.isAdmin() && !CommonService.isOriginGroup(group.class)) { + addStandardizableDevicesIfNotBooked(group.devices, message.timeStamp) + } + } + if (!CommonService.isOriginGroup(group.class) && group.devices.length) { + $scope.groups.forEach(function(group) { + if (!CommonService.isOriginGroup(group.class)) { + getAvailableGroupDevices(group) + } + }) + } + }) + + $scope.$on('user.settings.users.updated', function(event, message) { + function getGroupClass(id) { + if (CommonService.isExisting(groupsById[id])) { + return Promise.resolve($scope.groups[groupsById[id].index].class) + } + else if (cachedGroupsClass[id]) { + return Promise.resolve(cachedGroupsClass[id]) + } + else { + return GroupsService.getGroup(id).then(function(response) { + cachedGroupsClass[id] = response.data.group.class + return cachedGroupsClass[id] + }) + .catch(function(error) { + return false + }) + } + } + + if (($scope.isAdmin() && + CommonService.isExisting($scope.usersByEmail[message.user.email]) || + message.user.email === $scope.currentUser.email + ) && + updateUser(message.user, message.timeStamp) && + message.groups.length) { + + Promise.map(message.groups, function(groupId) { + return getGroupClass(groupId).then(function(_class) { + return !_class || _class === 'bookable' + }) + }) + .then(function(results) { + if (_.without(results, false).length) { + Promise.map($scope.groups, function(group) { + if (group.owner.email === message.user.email && + !CommonService.isOriginGroup(group.class)) { + getAvailableGroupDevices(group) + } + }) + } + }) + } + }) + + $scope.$on('user.settings.users.created', function(event, message) { + addUser(message.user, message.timeStamp) + }) + + $scope.$on('user.settings.users.deleted', function(event, message) { + deleteUser(message.user.email, message.timeStamp) + }) + + $scope.$on('user.settings.devices.deleted', function(event, message) { + if ($scope.isAdmin()) { + deleteOriginDevice(message.device.serial, message.timeStamp) + deleteStandardizableDevice(message.device.serial, message.timeStamp) + } + $scope.groups.forEach(function(group) { + if (!CommonService.isOriginGroup(group.class)) { + deleteAvailableGroupDevice(group.id, message.device.serial, message.timeStamp) + } + }) + }) + + $scope.$on('user.settings.devices.created', function(event, message) { + const device = publishDevice(message.device) + + if ($scope.isAdmin()) { + addOriginDevice(device, message.timeStamp) + addStandardizableDevice(device, message.timeStamp) + } + }) + + $scope.$on('user.settings.devices.updated', function(event, message) { + const device = publishDevice(message.device) + + if ($scope.isAdmin()) { + updateOriginDevice(device, message.timeStamp) + updateStandardizableDeviceIfNotBooked(device, message.timeStamp) + } + $scope.groups.forEach(function(group) { + if (!CommonService.isOriginGroup(group.class)) { + if (device.group.origin !== message.oldOriginGroupId) { + if ($scope.currentUser.groups.subscribed.indexOf(device.group.origin) > -1) { + getAvailableGroupDevices(group, message.timeStamp) + } + else { + deleteAvailableGroupDevice(group.id, device.serial, message.timeStamp) + } + } + else { + updateAvailableGroupDevice(group.id, device, message.timeStamp, true) + } + } + }) + }) + + initScope() +} diff --git a/res/app/settings/groups/groups-spec.js b/res/app/settings/groups/groups-spec.js new file mode 100644 index 00000000..39587018 --- /dev/null +++ b/res/app/settings/groups/groups-spec.js @@ -0,0 +1,21 @@ +/** +* Copyright © 2019 code initially contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0 +**/ + +describe('GroupsCtrl', function() { + + beforeEach(angular.mock.module(require('./index').name)) + + var scope, ctrl + + beforeEach(inject(function($rootScope, $controller) { + scope = $rootScope.$new() + ctrl = $controller('GroupsCtrl', {$scope: scope}) + })) + + it('should ...', inject(function() { + expect(1).toEqual(1) + + })) + +}) diff --git a/res/app/settings/groups/groups.css b/res/app/settings/groups/groups.css new file mode 100644 index 00000000..6c9cf631 --- /dev/null +++ b/res/app/settings/groups/groups.css @@ -0,0 +1,107 @@ +/** +* Copyright © 2019 code initially contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0 +**/ + +.stf-groups .selectable { + user-select: text; +} + +.stf-pager-groups-total-items { + margin-top: 5px; +} + +.stf-groups .groups-header { + margin-left: 10px; +} + +.stf-groups .group-users-header, .group-devices-header { + margin-bottom: 15px; +} + +.stf-groups .btn-check-name, .btn-group-devices-action, .btn-group-users-action { + margin-top: 5px; +} + +.stf-groups .groups-action { + margin-top: 10px; +} + +.stf-groups .group-schedule-item { + margin: 0px 10px 15px 15px; +} + +.stf-groups td,th { + padding: 0px; + white-space: nowrap; + font-size: small; +} + +.stf-groups .group-list-icon { + margin-right: 10px; +} + +.stf-groups .group-list-label { + font-weight: bold; + margin-right: 10px; +} + +.stf-groups .group-span-label, .group-conflicts { + margin-left: 10px; +} + +.stf-groups .group-span-label-error { + margin-left: 10px; + color: #FF2D55; +} + +.stf-groups .group-span-label-warning { + margin-left: 10px; + color: #FFA101; +} + +.stf-groups .group-span-label-success { + margin-left: 10px; + color: #60c561; +} + +.stf-groups input.ng-invalid { + border-color: red; +} + +.stf-groups .groups-list a.link { + padding: 0px; + border-bottom: none; + color: #167FFC; +} + +.stf-groups .groups-list .group-line { + padding: 10px; + border-bottom: 1px solid #dddddd; +} + +.stf-groups .groups-list .group-line.group-actions { + padding-bottom: 23px; +} + +.stf-groups .groups-list .heading.group-action-body { + margin-top: 22px; +} + +.stf-groups .group-list-details { + display: inline-block; +} + +.stf-groups .group-list-name { + color: #007aff; + font-size: 14px; + font-weight: 300; + margin: 2px 0 6px; +} + +.stf-groups .group-list-id { + font-family: Monaco, Menlo, Consolas, "Courier New", monospace; + font-size: 10px; + margin-bottom: 2px; + color: #999999; + font-weight: 300; +} diff --git a/res/app/settings/groups/groups.pug b/res/app/settings/groups/groups.pug new file mode 100644 index 00000000..cf2b9023 --- /dev/null +++ b/res/app/settings/groups/groups.pug @@ -0,0 +1,193 @@ +// + Copyright © 2019 code initially contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0 +// + +.widget-container.fluid-height.stf-groups(ng-controller='GroupsCtrl') + .heading + i.fa.fa-object-group + span(translate) Group list + + button.btn.btn-xs.btn-primary-outline.pull-right( + type='button' + ng-disabled='!conditionForGroupCreation() || hideGroupCreation' + uib-tooltip="{{'Groups number quota is reached' | translate}}" + tooltip-placement='bottom' + tooltip-enable="!conditionForGroupCreation()" + tooltip-popup-delay='500' + ng-click='createGroup()') + i.fa.fa-plus.fa-fw + + a.pull-right.btn.btn-xs(ng-href='') + i.fa.fa-question-circle.fa-fw( + uib-tooltip='{{"More about Groups" | translate}}' + tooltip-placement='left' + tooltip-popup-delay='500') + + .widget-content.padded + + nothing-to-show( + icon='fa-object-group' + message='{{"No Groups" | translate}}' ng-if='!groups.length') + + div(ng-if='groups.length') + ul.list-group.groups-list + li.list-group-item + .group-line.group-actions + form.form-inline.groups-header + .form-group + stf-pager( + tooltip-label="{{'Group selection' | translate}}" + total-items='filteredGroups.length' + total-items-style='stf-pager-groups-total-items' + items-per-page='scopeGroupsCtrl.groupItemsPerPage' + items-per-page-options='itemsPerPageOptions' + current-page='scopeGroupsCtrl.groupCurrentPage' + items-search='search') + + button.btn.btn-xs.btn-danger.pull-right( + type='button' + ng-disabled="!filteredGroups.length || filteredGroups.length === 1 && filteredGroups[0].privilege === 'root'" + uib-tooltip="{{'Remove the group selection' | translate}}" + tooltip-placement='bottom' + tooltip-popup-delay='500' + ng-click='removeGroups(search, filteredGroups, confirmRemove.value)') + i.fa.fa-trash-o + span(translate) Remove + + button.btn.btn-xs.btn-success.pull-right( + type='button' + uib-tooltip="{{'Enable/Disable confirmation for group removing' | translate}}" + tooltip-placement='top' + tooltip-popup-delay='500' + ng-click='confirmRemove.value = !confirmRemove.value' + ng-class='{"btn-warning-outline": !confirmRemove.value, "btn-success": confirmRemove.value}') + i.fa.fa-lock(ng-if='confirmRemove.value') + i.fa.fa-unlock(ng-if='!confirmRemove.value') + span(translate) Confirm Remove + + button.btn.btn-xs.btn-primary-outline.pull-right( + ng-if='isAdmin()' + type='button' + uib-tooltip="{{'Write an email to the group owner selection' | translate}}" + ng-disabled='!filteredGroups.length' + ng-click='mailToGroupOwners(filteredGroups)' + tooltip-placement='top' + tooltip-popup-delay='500') + i.fa.fa-envelope-o + span(translate) Contact Owners + + li.list-group-item(ng-repeat="group in groups \ + | filter:search \ + | orderBy: 'name' \ + | pagedObjectsFilter:scopeGroupsCtrl:'groupCurrentPage':'groupItemsPerPage':'filteredGroups' \ + track by group.id") + .group-line.group-actions + i.fa.fa-object-group.fa-2x.fa-fw.group-list-icon + .group-list-details.selectable + form.form-inline(name='nameForm' ng-if="group.state === 'pending' && showName") + input.form-control.input-sm( + size='35' type='text' placeholder="Name" + ng-model='groupsEnv[group.id].tmpName' + ng-pattern="nameRegex" + uib-tooltip="{{'Regex syntax' | translate}}: {{::nameRegexStr}}" + tooltip-placement='top' + tooltip-popup-delay='500' + tooltip-enable="group.state === 'pending' && nameForm.$invalid" + required) + + button.btn.btn-sm.btn-primary.btn-check-name( + type='button' + ng-click='updateGroupName(group)' + ng-disabled='!conditionForNameSaving(group, nameForm.$invalid)') + i.fa.fa-check + + .group-list-name( + ng-bind-template='{{group.name}}' + ng-if="group.state !== 'pending' || !showName") + + .group-list-id + span(translate) Identifier + span(ng-bind-template="{{::': ' + group.id + ' - '}}") + span(translate) Class + span(ng-bind-template="{{': ' + getClassName(group.class) + ' - '}}") + span(translate) Devices + span(ng-bind-template="{{': ' + group.devices.length + ' - '}}") + span(translate) Users + span(ng-bind-template="{{': ' + group.users.length}}") + span(ng-if='isAdmin()' ng-bind-template="{{::' - '}}") + span(ng-if='isAdmin()' translate) Owner + span(ng-if='isAdmin()' ng-bind-template="{{::': ' + group.owner.name}}") + + button.btn.btn-xs.btn-danger-outline.pull-right( + type='button' + ng-click='removeGroup(group, confirmRemove.value)' + ng-disabled='group.privilege === "root"') + i.fa.fa-trash-o + span(translate) Remove + + button.btn.btn-xs.btn-danger-outline.pull-right( + type='button' + ng-if="group.state === 'pending'" + ng-click='updateGroupState(group)') + i.fa.fa-unlock + span(translate) Get ready + + button.btn.btn-xs.pull-right( + type='button' + ng-show="group.state === 'pending'" + ng-click='initTemporaryName(group); showName = !showName' + ng-class='{"btn-primary-outline": !showName && group.state === "pending",\ + "btn-primary": showName && group.state === "pending"}') + i.fa.fa-tag + span(translate) Name + + button.btn.btn-xs.pull-right( + type='button' + ng-click='initTemporarySchedule(group); showSchedule = !showSchedule' + ng-class='{"btn-primary-outline": !showSchedule && group.state === "pending",\ + "btn-primary": showSchedule && group.state === "pending",\ + "btn-warning-outline": !showSchedule && !group.isActive && group.state !== "pending",\ + "btn-warning": showSchedule && !group.isActive && group.state !== "pending",\ + "btn-success-outline": !showSchedule && group.isActive && group.state !== "pending",\ + "btn-success": showSchedule && group.isActive && group.state !== "pending"}') + i.fa.fa-clock-o + span(translate) Schedule + + button.btn.btn-xs.btn-primary-outline.pull-right( + type='button' + ng-click='initShowDevices(group, !showDevices); showDevices = !showDevices' + ng-class='{"btn-primary-outline": !showDevices, "btn-primary": showDevices}') + i.fa.fa-mobile + span(translate) Devices + + button.btn.btn-xs.btn-primary-outline.pull-right( + type='button' + ng-click='initShowUsers(group); showUsers = !showUsers' + ng-class='{"btn-primary-outline": !showUsers, "btn-primary": showUsers}') + i.fa.fa-user + span(translate) Users + + button.btn.btn-xs.btn-danger.pull-right( + type='button' + ng-if='groupsEnv[group.id].showConflicts' + ng-click='groupsEnv[group.id].showConflicts = !groupsEnv[group.id].showConflicts' + ng-class='{"btn-danger-outline": !groupsEnv[group.id].showConflicts, \ + "btn-danger": groupsEnv[group.id].showConflicts}') + i.fa.fa-ban + span(translate) Conflicts + + ul.list-group.groups-action( + ng-if='groupsEnv[group.id].showConflicts') + div(ng-include="'settings/groups/conflicts/conflicts.pug'") + + ul.list-group.groups-action( + ng-if='showSchedule') + div(ng-include="'settings/groups/schedule/schedule.pug'") + + ul.list-group.groups-action( + ng-if='showDevices') + div(ng-include="'settings/groups/devices/devices.pug'") + + ul.list-group.groups-action( + ng-if='showUsers') + div(ng-include="'settings/groups/users/users.pug'") diff --git a/res/app/settings/groups/index.js b/res/app/settings/groups/index.js new file mode 100644 index 00000000..10620dbf --- /dev/null +++ b/res/app/settings/groups/index.js @@ -0,0 +1,35 @@ +/** +* Copyright © 2019 code initially contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0 +**/ + +require('./groups.css') + +module.exports = angular.module('stf.settings.groups', [ + require('stf/users').name, + require('stf/devices').name, + require('stf/user').name, + require('stf/groups').name, + require('stf/settings').name, + require('stf/util/common').name, + require('stf/common-ui').name +]) + .run(['$templateCache', function($templateCache) { + $templateCache.put( + 'settings/groups/groups.pug', require('./groups.pug') + ) + $templateCache.put( + 'settings/groups/schedule/schedule.pug', require('./schedule/schedule.pug') + ) + $templateCache.put( + 'settings/groups/devices/devices.pug', require('./devices/devices.pug') + ) + $templateCache.put( + 'settings/groups/users/users.pug', require('./users/users.pug') + ) + $templateCache.put( + 'settings/groups/conflicts/conflicts.pug', require('./conflicts/conflicts.pug') + ) + }]) + .controller('GroupsCtrl', require('./groups-controller')) + .filter('availableObjectsFilter', require('./filters/available-objects-filter')) + .filter('groupObjectsFilter', require('./filters/group-objects-filter')) diff --git a/res/app/settings/groups/schedule/schedule.pug b/res/app/settings/groups/schedule/schedule.pug new file mode 100644 index 00000000..2810d22c --- /dev/null +++ b/res/app/settings/groups/schedule/schedule.pug @@ -0,0 +1,68 @@ +// + Copyright © 2019 code initially contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0 +// + +li.list-group-item.groups-list + .heading.group-action-body + i.fa.fa-clock-o + span(translate) Schedule + + form.form-inline(name='scheduleForm') + fieldset(ng-disabled="group.state !== 'pending'") + .form-group.group-schedule-item + label.group-list-label(translate) Class + select(ng-model='groupsEnv[group.id].tmpClass' ng-change='watchGroupClass(group)') + option( + ng-if="option.privilege === 'user' ||\ + option.privilege === currentUser.privilege && currentUser.email === group.owner.email" + ng-repeat='option in classOptions' + value='{{option.id}}') {{option.name}} + + .form-group.group-schedule-item(ng-if='conditionForRepetitions(group)') + label.group-list-label(translate) Repetitions + input.form-control.input-sm( + type='range' + min='0' + max='{{getRepetitionsQuotas(group.owner.email)}}' + ng-model='groupsEnv[group.id].tmpRepetitions' + required) + span.group-span-label {{groupsEnv[group.id].tmpRepetitions}} + + .form-group.group-schedule-item + label.group-list-label(translate) Starting Date + input.form-control.input-sm( + size='21' + type='datetime-local' + ng-model='groupsEnv[group.id].tmpStartDate' + placeholder='yyyy-MM-ddTHH:mm:ss:sss' + required) + + .form-group.group-schedule-item + label.group-list-label(translate) Expiration Date + input.form-control.input-sm( + size='21' + type='datetime-local' + ng-model='groupsEnv[group.id].tmpStopDate' + placeholder='yyyy-MM-ddTHH:mm:ss:sss' + required) + + .form-group.group-schedule-item + button.btn.btn-sm.btn-primary( + type='button' + ng-click='updateGroupSchedule(group)' + ng-disabled='!conditionForScheduleSaving(group, scheduleForm.$invalid)') + span(translate) Save + + span.group-span-label-warning( + translate + ng-if="group.state === 'pending' && \ + conditionForScheduleSaving(group, scheduleForm.$invalid) && \ + (groupsEnv[group.id].tmpClass === 'bookable' || \ + groupsEnv[group.id].tmpClass === 'standard')") Saving will also get ready the group! + + span.group-span-label-error( + translate + ng-if="group.state === 'pending' && \ + !conditionForScheduleSaving(group, scheduleForm.$invalid) && \ + groupsEnv[group.id].tmpScheduleTooltip !== 'No change'") {{groupsEnv[group.id].tmpScheduleTooltip}} + diff --git a/res/app/settings/groups/users/users.pug b/res/app/settings/groups/users/users.pug new file mode 100644 index 00000000..9c6aae4d --- /dev/null +++ b/res/app/settings/groups/users/users.pug @@ -0,0 +1,166 @@ +// + Copyright © 2019 code initially contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0 +// + +li.list-group-item.groups-list + .heading.group-action-body + i.fa.fa-user + span(translate) Users + + .row + .panel-group + .panel.panel-default + .panel-heading.text-center + button.btn.btn-xs.btn-primary.btn-group-users-action( + type='button' + ng-click='showGroupUsers = !showGroupUsers' + ng-class='{"btn-primary-outline": showGroupUsers, "btn-primary": !showGroupUsers}') + i.fa.fa-user + span(translate) Group users + + .panel-body(ng-show='!showGroupUsers') + div + .form-inline + .form-group.group-users-header + stf-pager( + tooltip-label="{{'Group user selection' | translate}}" + total-items='groupsEnv[filteredGroups[getGroupIndex($parent.$index)].id].filteredGroupUsers.length' + total-items-style='stf-pager-groups-total-items' + items-per-page='groupsEnv[filteredGroups[getGroupIndex($parent.$index)].id].groupUserItemsPerPage' + items-per-page-options='itemsPerPageOptions' + current-page='groupsEnv[filteredGroups[getGroupIndex($parent.$index)].id].groupUserCurrentPage' + items-search='groupUserSearch') + + button.btn.btn-xs.btn-primary-outline.pull-right( + type='button' + uib-tooltip="{{'Write a mail to the group user selection' | translate}}" + ng-disabled='!groupsEnv[filteredGroups[getGroupIndex($parent.$index)].id].filteredGroupUsers.length' + ng-click='mailToGroupUsers(\ + filteredGroups[getGroupIndex($parent.$index)],\ + groupsEnv[filteredGroups[getGroupIndex($parent.$index)].id].filteredGroupUsers)' + tooltip-placement='top' + tooltip-popup-delay='500') + i.fa.fa-envelope-o + span(translate) Contact Users + + .widget-container.fluid-height.overflow-auto + table.table.table-hover.dataTable.ng-table + thead + tr + th.header + button.btn.btn-sm.btn-danger.btn-group-users-action( + type='button' + ng-if="groupsEnv[filteredGroups[getGroupIndex($parent.$index)].id].hasOwnProperty('filteredGroupUsers')" + ng-disabled="!conditionForGroupUsersRemoving(\ + group, \ + groupsEnv[filteredGroups[getGroupIndex($parent.$index)].id].filteredGroupUsers)" + ng-click='removeGroupUsers(\ + group,\ + groupUserSearch,\ + groupsEnv[filteredGroups[getGroupIndex($parent.$index)].id].filteredGroupUsers)') + i.fa.fa-trash-o + th.header.sortable( + ng-class='[column.sort]' + ng-repeat="column in groupUserData.columns" + ng-click='sortBy(groupUserData, column)') + div.strong(ng-bind-template="{{column.name | translate}}") + + tbody + tr.selectable(ng-repeat="user in group.users \ + | groupObjectsFilter:users:usersByEmail \ + | filter:groupUserSearch \ + | orderBy:userColumns[groupUserData.sort.index].property:groupUserData.sort.reverse \ + | pagedObjectsFilter:\ + groupsEnv[filteredGroups[getGroupIndex($parent.$index)].id]:\ + 'groupUserCurrentPage':'groupUserItemsPerPage':'filteredGroupUsers' \ + track by user.email") + td + button.btn.btn-danger-outline.btn-xs( + type='button' + ng-disabled="user.privilege === 'admin' || \ + user.email === filteredGroups[getGroupIndex($parent.$index)].owner.email" + ng-click='removeGroupUser(filteredGroups[getGroupIndex($parent.$index)], user)') + i.fa.fa-trash-o.fa-fw + td {{user.name}} + td + a.link(ng-href="{{'mailto:' + user.email}}" + ng-click='$event.stopPropagation()') {{user.email}} + td {{user.privilege}} + + .panel.panel-default + .panel-heading.text-center + button.btn.btn-xs.btn-primary-outline.btn-group-users-action( + type='button' + ng-click='showAvailableUsers = !showAvailableUsers' + ng-class='{"btn-primary-outline": !showAvailableUsers, "btn-primary": showAvailableUsers}') + i.fa.fa-user + span(translate) Available users + + .panel-body(ng-show='showAvailableUsers') + nothing-to-show( + icon='fa-user' + message='{{"No available users" | translate}}' + ng-if='!groupsEnv[group.id].filteredAvailableUsers.length && users.length === group.users.length') + + div(ng-if='groupsEnv[group.id].filteredAvailableUsers.length || users.length !== group.users.length') + .form-inline + .form-group.group-users-header + stf-pager( + tooltip-label="{{'Available user selection' | translate}}" + total-items='groupsEnv[filteredGroups[getGroupIndex($parent.$index)].id].filteredAvailableUsers.length' + total-items-style='stf-pager-groups-total-items' + items-per-page='groupsEnv[filteredGroups[getGroupIndex($parent.$index)].id].availableUserItemsPerPage' + items-per-page-options='itemsPerPageOptions' + current-page='groupsEnv[filteredGroups[getGroupIndex($parent.$index)].id].availableUserCurrentPage' + items-search='userSearch') + + button.btn.btn-xs.btn-primary-outline.pull-right( + type='button' + uib-tooltip="{{'Write a mail to the available user selection' | translate}}" + ng-disabled='!groupsEnv[filteredGroups[getGroupIndex($parent.$index)].id].filteredAvailableUsers.length' + ng-click='mailToAvailableUsers(groupsEnv[filteredGroups[getGroupIndex($parent.$index)].id].filteredAvailableUsers)' + tooltip-placement='top' + tooltip-popup-delay='500') + i.fa.fa-envelope-o + span(translate) Contact Users + + + .widget-container.fluid-height.overflow-auto + table.table.table-hover.dataTable.ng-table + thead + tr + th.header + button.btn.btn-sm.btn-primary.btn-group-users-action( + type='button' + ng-if="groupsEnv[filteredGroups[getGroupIndex($parent.$index)].id].hasOwnProperty('filteredAvailableUsers')" + ng-disabled='!groupsEnv[filteredGroups[getGroupIndex($parent.$index)].id].filteredAvailableUsers.length' + ng-click='addGroupUsers(\ + group,\ + userSearch,\ + groupsEnv[filteredGroups[getGroupIndex($parent.$index)].id].filteredAvailableUsers)') + i.fa.fa-cart-plus + th.header.sortable( + ng-class='[column.sort]' + ng-repeat="column in userData.columns" + ng-click='sortBy(userData, column)') + div.strong(ng-bind-template="{{column.name | translate}}") + + tbody + tr.selectable(ng-repeat="user in users \ + | availableObjectsFilter:filteredGroups[getGroupIndex($parent.$index)]:'users':'email' \ + | filter:userSearch \ + | orderBy:userColumns[userData.sort.index].property:userData.sort.reverse \ + | pagedObjectsFilter:\ + groupsEnv[filteredGroups[getGroupIndex($parent.$index)].id]:\ + 'availableUserCurrentPage':'availableUserItemsPerPage':'filteredAvailableUsers' \ + track by user.email") + td + button.btn.btn-primary-outline.btn-xs( + type='button' + ng-click='addGroupUser(filteredGroups[getGroupIndex($parent.$index)], user)') + i.fa.fa-cart-plus.fa-fw + td {{user.name}} + td + a.link(ng-href="{{'mailto:' + user.email}}" + ng-click='$event.stopPropagation()') {{user.email}} + td {{user.privilege}} diff --git a/res/app/settings/index.js b/res/app/settings/index.js index 8fc74525..ec0ddd38 100644 --- a/res/app/settings/index.js +++ b/res/app/settings/index.js @@ -1,6 +1,16 @@ +/** +* Copyright © 2019 contains code contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0 +**/ + +require('./settings.css') + module.exports = angular.module('ui-settings', [ require('./general').name, require('./keys').name, + require('./groups').name, + require('./devices').name, + require('./users').name, + require('stf/app-state').name, require('stf/common-ui/nice-tabs').name //require('./notifications').name ]) diff --git a/res/app/settings/keys/access-tokens/index.js b/res/app/settings/keys/access-tokens/index.js index 5e5b7a65..c1fb719d 100644 --- a/res/app/settings/keys/access-tokens/index.js +++ b/res/app/settings/keys/access-tokens/index.js @@ -1,6 +1,7 @@ require('./access-tokens.css') module.exports = angular.module('stf.settings.keys.access-tokens', [ + require('stf/socket').name, require('stf/common-ui').name, require('stf/tokens').name, require('stf/tokens/generate-access-token').name diff --git a/res/app/settings/keys/adb-keys/index.js b/res/app/settings/keys/adb-keys/index.js index 49877d98..5be17fa3 100644 --- a/res/app/settings/keys/adb-keys/index.js +++ b/res/app/settings/keys/adb-keys/index.js @@ -1,6 +1,7 @@ require('./adb-keys.css') module.exports = angular.module('stf.settings.keys.adb-keys', [ + require('stf/user').name, require('stf/common-ui').name, require('stf/keys/add-adb-key').name ]) diff --git a/res/app/settings/settings-controller.js b/res/app/settings/settings-controller.js index d157fc12..35529345 100644 --- a/res/app/settings/settings-controller.js +++ b/res/app/settings/settings-controller.js @@ -1,15 +1,45 @@ -module.exports = function SettingsCtrl($scope, gettext) { +/** +* Copyright © 2019 contains code contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0 +**/ - $scope.settingTabs = [ +module.exports = function SettingsCtrl($scope, gettext, AppState) { + + $scope.settingTabs = [] + $scope.settingTabs.push( { title: gettext('General'), icon: 'fa-gears fa-fw', templateUrl: 'settings/general/general.pug' - }, + } + ) + $scope.settingTabs.push( { title: gettext('Keys'), icon: 'fa-key fa-fw', templateUrl: 'settings/keys/keys.pug' } - ] + ) + $scope.settingTabs.push( + { + title: gettext('Groups'), + icon: 'fa-object-group fa-fw', + templateUrl: 'settings/groups/groups.pug' + } + ) + if (AppState.user.privilege === 'admin') { + $scope.settingTabs.push( + { + title: gettext('Devices'), + icon: 'fa-mobile stf-settings-tabs-device-icon fa-fw', + templateUrl: 'settings/devices/devices.pug' + } + ) + $scope.settingTabs.push( + { + title: gettext('Users'), + icon: 'fa-user fa-fw', + templateUrl: 'settings/users/users.pug' + } + ) + } } diff --git a/res/app/settings/settings.css b/res/app/settings/settings.css new file mode 100644 index 00000000..7991ce90 --- /dev/null +++ b/res/app/settings/settings.css @@ -0,0 +1,8 @@ +/** +* Copyright © 2019 code initially contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0 +**/ + +.stf-settings-tabs-device-icon { + font-size: 15px; +} + diff --git a/res/app/settings/users/index.js b/res/app/settings/users/index.js new file mode 100644 index 00000000..e8e460ee --- /dev/null +++ b/res/app/settings/users/index.js @@ -0,0 +1,18 @@ +/** +* Copyright © 2019 code initially contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0 +**/ + +require('./users.css') + +module.exports = angular.module('stf.settings.users', [ + require('stf/app-state').name, + require('stf/settings').name, + require('stf/util/common').name, + require('stf/users').name +]) + .run(['$templateCache', function($templateCache) { + $templateCache.put( + 'settings/users/users.pug', require('./users.pug') + ) + }]) + .controller('UsersCtrl', require('./users-controller')) diff --git a/res/app/settings/users/users-controller.js b/res/app/settings/users/users-controller.js new file mode 100644 index 00000000..a437dae8 --- /dev/null +++ b/res/app/settings/users/users-controller.js @@ -0,0 +1,229 @@ +/** +* Copyright © 2019 code initially contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0 +**/ + +const _ = require('lodash') + +module.exports = function UsersCtrl( + $scope +, UsersService +, AppState +, SettingsService +, ItemsPerPageOptionsService +, GenericModalService +, CommonService +) { + const usersByEmail = {} + const userFields = + 'email,' + + 'name,' + + 'privilege,' + + 'groups.quotas' + + function addUser(user, timeStamp) { + return CommonService.add( + $scope.users + , usersByEmail + , user + , 'email' + , timeStamp) + } + + function updateUser(user, timeStamp) { + return CommonService.update( + $scope.users + , usersByEmail + , user + , 'email' + , timeStamp) + } + + function deleteUser(email, timeStamp) { + return CommonService.delete( + $scope.users + , usersByEmail + , email + , timeStamp) + } + + function initScope() { + UsersService.getOboeUsers(userFields, function(user) { + addUser(user, -1) + }) + .done(function() { + $scope.$digest() + if (CommonService.isExisting(usersByEmail[AppState.user.email])) { + $scope.adminUser = $scope.users[usersByEmail[AppState.user.email].index] + } + }) + } + + SettingsService.bind($scope, { + target: 'removingFilters' + , source: 'UsersRemovingFilters' + , defaultValue: { + groupOwner: 'False' + } + }) + $scope.users = [] + $scope.confirmRemove = {value: true} + $scope.scopeUsersCtrl = $scope + $scope.itemsPerPageOptions = ItemsPerPageOptionsService + SettingsService.bind($scope, { + target: 'userItemsPerPage' + , source: 'userItemsPerPage' + , defaultValue: $scope.itemsPerPageOptions[2] + }) + $scope.tmpEnv = {} + $scope.nameRegex = /^[0-9a-zA-Z-_. ]{1,50}$/ + $scope.nameRegexStr = '/^[0-9a-zA-Z-_. ]{1,50}$/' + $scope.removingFilterOptions = ['True', 'False', 'Any'] + + $scope.mailTo = function(users) { + CommonService.copyToClipboard(users.map(function(user) { + return user.email + }) + .join(SettingsService.get('emailSeparator'))) + .url('mailto:?body=*** Paste the email addresses from the clipboard! ***') + } + + $scope.removeUser = function(email, askConfirmation) { + if (askConfirmation) { + GenericModalService.open({ + message: 'Really delete this user?' + , type: 'Warning' + , size: 'sm' + , cancel: true + }) + .then(function() { + CommonService.errorWrapper( + UsersService.removeUser + , [email, $scope.removingFilters] + ) + }) + } + else { + CommonService.errorWrapper( + UsersService.removeUser + , [email, $scope.removingFilters] + ) + } + } + + $scope.removeUsers = function(search, filteredUsers, askConfirmation) { + function removeUsers() { + CommonService.errorWrapper( + UsersService.removeUsers + , search ? + [$scope.removingFilters, filteredUsers.map(function(user) { return user.email }).join()] : + [$scope.removingFilters] + ) + } + + if (askConfirmation) { + GenericModalService.open({ + message: 'Really delete this selection of users?' + , type: 'Warning' + , size: 'sm' + , cancel: true + }) + .then(function() { + removeUsers() + }) + } + else { + removeUsers() + } + } + + $scope.conditionForDefaultQuotasSaving = function(formInvalidStatus) { + if (formInvalidStatus) { + $scope.tmpEnv.defaultQuotasTooltip = 'Bad syntax' + return false + } + if ($scope.tmpEnv.defaultGroupsNumber + !== $scope.adminUser.groups.quotas.defaultGroupsNumber || + $scope.tmpEnv.defaultGroupsDuration + !== $scope.adminUser.groups.quotas.defaultGroupsDuration || + $scope.tmpEnv.defaultGroupsRepetitions + !== $scope.adminUser.groups.quotas.defaultGroupsRepetitions + ) { + $scope.tmpEnv.defaultQuotasTooltip = '' + return true + } + $scope.tmpEnv.defaultQuotasTooltip = 'No change' + return false + } + + $scope.initTemporaryDefaultQuotas = function() { + $scope.tmpEnv.defaultGroupsNumber = $scope.adminUser.groups.quotas.defaultGroupsNumber + $scope.tmpEnv.defaultGroupsDuration = $scope.adminUser.groups.quotas.defaultGroupsDuration + $scope.tmpEnv.defaultGroupsRepetitions = $scope.adminUser.groups.quotas.defaultGroupsRepetitions + $scope.tmpEnv.defaultQuotasTooltip = 'No change' + } + + $scope.updateDefaultUserGroupsQuotas = function() { + CommonService.errorWrapper(UsersService.updateDefaultUserGroupsQuotas, [ + $scope.tmpEnv.defaultGroupsNumber + , $scope.tmpEnv.defaultGroupsDuration + , $scope.tmpEnv.defaultGroupsRepetitions + ]) + } + + $scope.updateUserGroupsQuotas = function(user) { + CommonService.errorWrapper(UsersService.updateUserGroupsQuotas, [ + user.email + , user.groupsNumber + , user.groupsDuration + , user.groupsRepetitions + ]) + } + + $scope.initTemporaryUser = function() { + $scope.tmpEnv.userName = $scope.tmpEnv.userEmail = '' + $scope.tmpEnv.userTooltip = 'Bad syntax' + } + + $scope.conditionForQuotasSaving = function(user, formInvalidStatus) { + if (formInvalidStatus) { + user.quotasTooltip = 'Bad syntax' + return false + } + if (user.groupsNumber !== user.groups.quotas.allocated.number || + user.groupsDuration !== user.groups.quotas.allocated.duration || + user.groupsRepetitions !== user.groups.quotas.repetitions) { + user.quotasTooltip = '' + return true + } + user.quotasTooltip = 'No change' + return false + } + + $scope.initTemporaryQuotas = function(user) { + user.groupsNumber = user.groups.quotas.allocated.number + user.groupsDuration = user.groups.quotas.allocated.duration + user.groupsRepetitions = user.groups.quotas.repetitions + user.quotasTooltip = 'No change' + } + + $scope.createUser = function() { + CommonService.errorWrapper( + UsersService.createUser + , [$scope.tmpEnv.userName, $scope.tmpEnv.userEmail] + ) + } + + $scope.$on('user.settings.users.created', function(event, message) { + addUser(message.user, message.timeStamp) + }) + + $scope.$on('user.settings.users.deleted', function(event, message) { + deleteUser(message.user.email, message.timeStamp) + }) + + $scope.$on('user.settings.users.updated', function(event, message) { + updateUser(message.user, message.timeStamp) + }) + + initScope() +} diff --git a/res/app/settings/users/users-spec.js b/res/app/settings/users/users-spec.js new file mode 100644 index 00000000..76fd1a54 --- /dev/null +++ b/res/app/settings/users/users-spec.js @@ -0,0 +1,21 @@ +/** +* Copyright © 2019 code initially contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0 +**/ + +describe('UsersCtrl', function() { + + beforeEach(angular.mock.module(require('./index').name)) + + var scope, ctrl + + beforeEach(inject(function($rootScope, $controller) { + scope = $rootScope.$new() + ctrl = $controller('UsersCtrl', {$scope: scope}) + })) + + it('should ...', inject(function() { + expect(1).toEqual(1) + + })) + +}) diff --git a/res/app/settings/users/users.css b/res/app/settings/users/users.css new file mode 100644 index 00000000..f5e43d54 --- /dev/null +++ b/res/app/settings/users/users.css @@ -0,0 +1,87 @@ +/** +* Copyright © 2019 code initially contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0 +**/ + +.stf-users .selectable { + user-select: text; +} + +.stf-pager-users-total-items { + margin-top: 5px; +} + +.stf-users .user-creation, .user-default-quotas-item, .user-filters-item, .form-group.user-quotas-item { + margin: 0px 10px 15px 15px; +} + +.stf-users .user-save, .user-default-quotas-save, .form-group.user-quotas-save { + margin: 5px 10px 15px 15px; +} + +.stf-users .user-header { + margin-left: 10px; +} + +.stf-users .user-filters-items { + margin-top: 5px; + margin-bottom: 15px; +} + +.stf-users .user-default-quotas-items, .user-quotas-items { + margin: 0px 0px 15px 0px; +} + +.stf-users .user-list-icon { + margin-right: 10px; +} + +.stf-users .user-list-label { + font-weight: bold; + margin-right: 10px; +} + +.stf-users input.ng-invalid { + border-color: red; +} + +.stf-users .user-list .user-list-items { + margin: 10px 0px 0px 0px; +} + +.stf-users .user-list .user-line { + padding: 10px; + border-bottom: 1px solid #dddddd; +} + +.stf-users .user-list .user-line.user-actions { + padding-bottom: 23px; +} + +.stf-users .user-list .heading.user-action-body { + margin-top: 22px; +} + +.stf-users .user-list-details.selectable a { + padding: 0px; + border-bottom: none; + color: #167FFC; +} + +.stf-users .user-list-details { + display: inline-block; +} + +.stf-users .user-list-name { + color: #007aff; + font-size: 14px; + font-weight: 300; + margin: 2px 0 6px; +} + +.stf-users .user-list-id { + font-family: Monaco, Menlo, Consolas, "Courier New", monospace; + font-size: 10px; + margin-bottom: 2px; + color: #999999; + font-weight: 300; +} diff --git a/res/app/settings/users/users.pug b/res/app/settings/users/users.pug new file mode 100644 index 00000000..00d201c4 --- /dev/null +++ b/res/app/settings/users/users.pug @@ -0,0 +1,216 @@ +// + Copyright © 2019 code initially contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0 +// + +.widget-container.fluid-height.stf-users(ng-controller='UsersCtrl') + .heading + i.fa.fa-user + span(translate) User list + + button.btn.btn-primary-outline.pull-right.btn-sm( + ng-click='showCreateUser = !showCreateUser; initTemporaryUser()' + ng-class='{ "btn-primary-outline": !showCreateUser, "btn-primary": showCreateUser }') + i.fa.fa-plus.fa-fw + + a.pull-right.btn.btn-sm(ng-href='') + i.fa.fa-question-circle.fa-fw(uib-tooltip='{{"More about Users" | translate}}' tooltip-placement='left') + + .widget-content.padded + + nothing-to-show(icon='fa-user' message='{{"No Users" | translate}}' ng-if='!users.length') + + div(ng-if='users.length') + ul.list-group.user-list + li.list-group-item(ng-if='showCreateUser') + .user-line + .heading + i.fa.fa-user + span(translate) Create new user + + form.form-inline(name='userForm') + .form-group.user-creation + label.user-list-label(translate) Name + input.form-control.input-sm( + name='nameForm' + uib-tooltip="{{'Regex syntax' | translate}}: {{::nameRegexStr}}" + tooltip-placement='top' + tooltip-popup-delay='500' + tooltip-enable='userForm.nameForm.$invalid' + type='text' ng-model='tmpEnv.userName' ng-pattern="nameRegex" required) + + .form-group.user-creation + label.user-list-label(translate) Email + input.form-control.input-sm(size='35' type='email' ng-model='tmpEnv.userEmail' required) + + .form-group.user-save + button.btn.btn-sm.btn-primary( + type='button' + ng-click='createUser()' + ng-disabled='userForm.$invalid') + span(translate) Save + + li.list-group-item + .user-line.user-actions + form.form-inline.user-header + .form-group + stf-pager( + tooltip-label="{{'User selection' | translate}}" + total-items='filteredUsers.length' + total-items-style='stf-pager-users-total-items' + items-per-page='scopeUsersCtrl.userItemsPerPage' + items-per-page-options='itemsPerPageOptions' + current-page='scopeUsersCtrl.userCurrentPage' + items-search='search') + + button.btn.btn-xs.btn-danger.pull-right( + type='button' + uib-tooltip="{{'Remove the user selection' | translate}}" + tooltip-placement='bottom' + tooltip-popup-delay='500' + ng-disabled="!filteredUsers.length || filteredUsers.length === 1 && filteredUsers[0].privilege === 'admin'" + ng-click='removeUsers(search, filteredUsers, confirmRemove.value)') + i.fa.fa-trash-o + span(translate) Remove + + button.btn.btn-xs.btn-success.pull-right( + type='button' + uib-tooltip="{{'Enable/Disable confirmation for user removing' | translate}}" + tooltip-placement='top' + tooltip-popup-delay='500' + ng-click='confirmRemove.value = !confirmRemove.value' + ng-class='{"btn-warning-outline": !confirmRemove.value, "btn-success": confirmRemove.value}') + i.fa.fa-lock(ng-if='confirmRemove.value') + i.fa.fa-unlock(ng-if='!confirmRemove.value') + span(translate) Confirm Remove + + button.btn.btn-xs.btn-danger-outline.pull-right( + type='button' + uib-tooltip="{{'Set filters for user removing' | translate}}" + tooltip-placement='top' + tooltip-popup-delay='500' + ng-click='showFilters = !showFilters' + ng-class='{"btn-danger-outline": !showFilters, "btn-danger": showFilters}') + i.fa.fa-trash-o + span(translate) Filters + + button.btn.btn-xs.btn-primary-outline.pull-right( + type='button' + uib-tooltip="{{'Set groups quotas for new users' | translate}}" + tooltip-placement='top' + tooltip-popup-delay='500' + ng-click='showDefaultGroupsQuotas = !showDefaultGroupsQuotas; initTemporaryDefaultQuotas()' + ng-class='{"btn-primary-outline": !showDefaultGroupsQuotas, "btn-primary": showDefaultGroupsQuotas}') + i.fa.fa-object-group + span(translate) Default Groups Quotas + + button.btn.btn-xs.btn-primary-outline.pull-right( + type='button' + uib-tooltip="{{'Write an email to the user selection' | translate}}" + ng-disabled='!filteredUsers.length' + ng-click='mailTo(filteredUsers)' + tooltip-placement='top' + tooltip-popup-delay='500') + i.fa.fa-envelope-o + span(translate) Contact Users + + li.list-group-item(ng-if='showFilters') + .user-line + .heading + i.fa.fa-trash-o + span(translate) Removing filters + + form.form-inline.user-filters-items + .form-group.user-filters-item + label.user-list-label( + translate + uib-tooltip="{{'Filter on user group ownership' | translate}}" + tooltip-placement='top' + tooltip-popup-delay='500') Group Owner + select(ng-model='removingFilters.groupOwner' ng-options='option for option in removingFilterOptions') + + li.list-group-item(ng-if='showDefaultGroupsQuotas') + .user-line + .heading + i.fa.fa-object-group + span(translate) Default groups quotas + + form.form-inline.user-default-quotas-items(name='dafaultQuotasForm') + .form-group.user-default-quotas-item + label.user-list-label(translate) Number of groups + input.form-control.input-sm(type='number' min='0' ng-model='tmpEnv.defaultGroupsNumber' required) + + .form-group.user-default-quotas-item + label.user-list-label Total duration of groups (ms) + input.form-control.input-sm(type='number' min='0' ng-model='tmpEnv.defaultGroupsDuration' required) + + .form-group.user-default-quotas-item + label.user-list-label(translate) Number of repetitions per group + input.form-control.input-sm(type='number' min='0' ng-model='tmpEnv.defaultGroupsRepetitions' required) + + .form-group.user-default-quotas-save + button.btn.btn-sm.btn-primary( + uib-tooltip='{{tmpEnv.defaultQuotasTooltip | translate}}' + tooltip-enable='tmpEnv.defaultQuotasTooltip' + tooltip-placement='top' + tooltip-popup-delay='500' + ng-click='updateDefaultUserGroupsQuotas()' + ng-disabled='!conditionForDefaultQuotasSaving(defaultQuotasForm.$invalid)') + span(translate) Save + + li.list-group-item(ng-repeat="user in users \ + | filter:search \ + | orderBy: 'name' \ + | pagedObjectsFilter:scopeUsersCtrl:'userCurrentPage':'userItemsPerPage':'filteredUsers' \ + track by user.email") + .user-line.user-actions + i.fa.fa-user.fa-2x.fa-fw.user-list-icon + .user-list-details.selectable + a.user-list-name(ng-href="{{::'mailto:' + user.email}}") {{::user.name}} + .user-list-id + span(translate) Email + span(ng-bind-template="{{::': ' + user.email + ' - '}}") + span(translate) Privilege + span(ng-bind-template="{{::': ' + user.privilege}}") + + button.btn.btn-xs.btn-danger-outline.pull-right( + type='button' + ng-click='removeUser(user.email, confirmRemove.value)' + ng-disabled='user.privilege === "admin"') + i.fa.fa-trash-o + span(translate) Remove + + button.btn.btn-xs.btn-primary-outline.pull-right( + type='button' + ng-click='showGroupsQuotas = !showGroupsQuotas; initTemporaryQuotas(user)' + ng-class='{"btn-primary-outline": !showGroupsQuotas, "btn-primary": showGroupsQuotas}') + i.fa.fa-object-group + span(translate) Groups Quotas + + ul.list-group.user-list.user-list-items(ng-if='showGroupsQuotas') + li.list-group-item + .heading.user-action-body + i.fa.fa-object-group + span(translate) Groups Quotas + + form.form-inline(name='quotasForm') + .form-group.user-quotas-item + label.user-list-label(translate) Number of groups + input.form-control.input-sm(type='number' min='0' ng-max-length='5' ng-model='user.groupsNumber' required) + + .form-group.user-quotas-item + label.user-list-label(translate) Total duration of groups (ms) + input.form-control.input-sm(type='number' min='0' ng-model='user.groupsDuration' required) + + .form-group.user-quotas-item + label.user-list-label(translate) Number of repetitions per group + input.form-control.input-sm(type='number' min='0' ng-model='user.groupsRepetitions' required) + + .form-group.user-quotas-save + button.btn.btn-sm.btn-primary( + uib-tooltip='{{user.quotasTooltip | translate}}' + tooltip-enable='user.quotasTooltip' + tooltip-placement='top' + tooltip-popup-delay='500' + ng-click='updateUserGroupsQuotas(user)' + ng-disabled='!conditionForQuotasSaving(user, quotasForm.$invalid)') + span(translate) Save diff --git a/res/auth/ldap/scripts/signin/index.js b/res/auth/ldap/scripts/signin/index.js index 0e6d9fde..4525ac97 100644 --- a/res/auth/ldap/scripts/signin/index.js +++ b/res/auth/ldap/scripts/signin/index.js @@ -1,6 +1,13 @@ +/** +* Copyright © 2019 contains code contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0 +**/ + require('./signin.css') -module.exports = angular.module('stf.signin', []) +module.exports = angular.module('stf.signin', [ + require('stf/util/common').name, + require('stf/common-ui').name +]) .config(function($routeProvider) { $routeProvider .when('/auth/ldap/', { diff --git a/res/auth/ldap/scripts/signin/signin-controller.js b/res/auth/ldap/scripts/signin/signin-controller.js index 25aee8d4..dd44c247 100644 --- a/res/auth/ldap/scripts/signin/signin-controller.js +++ b/res/auth/ldap/scripts/signin/signin-controller.js @@ -1,4 +1,8 @@ -module.exports = function SignInCtrl($scope, $http) { +/** +* Copyright © 2019 contains code contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0 +**/ + +module.exports = function SignInCtrl($scope, $http, CommonService) { $scope.error = null @@ -33,4 +37,12 @@ module.exports = function SignInCtrl($scope, $http) { } }) } + + $scope.mailToSupport = function() { + CommonService.url('mailto:' + $scope.contactEmail) + } + + $http.get('/auth/contact').then(function(response) { + $scope.contactEmail = response.data.contact.email + }) } diff --git a/res/auth/ldap/scripts/signin/signin.pug b/res/auth/ldap/scripts/signin/signin.pug index b73d4d44..125f8768 100644 --- a/res/auth/ldap/scripts/signin/signin.pug +++ b/res/auth/ldap/scripts/signin/signin.pug @@ -1,3 +1,7 @@ +// + Copyright © 2019 contains code contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0 +// + .login2(ng-controller='SignInCtrl') .login-wrapper a(href='./') @@ -28,3 +32,15 @@ span(translate) Please enter your password input.btn.btn-lg.btn-primary.btn-block(type='submit', value='Log In') + + button.btn.btn-sm.btn-default-outline( + type='button' + uib-tooltip="{{'Write a mail to the support team' | translate}}" + ng-disabled='!contactEmail' + ng-click='mailToSupport()' + tooltip-placement='top' + tooltip-popup-delay='500') + i.fa.fa-envelope-o + span(translate) Contact Support + + diff --git a/res/auth/mock/scripts/signin/index.js b/res/auth/mock/scripts/signin/index.js index 2f5afe3c..6becbb50 100644 --- a/res/auth/mock/scripts/signin/index.js +++ b/res/auth/mock/scripts/signin/index.js @@ -1,6 +1,13 @@ +/** +* Copyright © 2019 contains code contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0 +**/ + require('./signin.css') -module.exports = angular.module('stf.signin', []) +module.exports = angular.module('stf.signin', [ + require('stf/util/common').name, + require('stf/common-ui').name +]) .config(function($routeProvider) { $routeProvider .when('/auth/mock/', { diff --git a/res/auth/mock/scripts/signin/signin-controller.js b/res/auth/mock/scripts/signin/signin-controller.js index 70ce9011..410c4d35 100644 --- a/res/auth/mock/scripts/signin/signin-controller.js +++ b/res/auth/mock/scripts/signin/signin-controller.js @@ -1,4 +1,8 @@ -module.exports = function SignInCtrl($scope, $http) { +/** +* Copyright © 2019 contains code contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0 +**/ + +module.exports = function SignInCtrl($scope, $http, CommonService) { $scope.error = null @@ -33,4 +37,12 @@ module.exports = function SignInCtrl($scope, $http) { } }) } + + $scope.mailToSupport = function() { + CommonService.url('mailto:' + $scope.contactEmail) + } + + $http.get('/auth/contact').then(function(response) { + $scope.contactEmail = response.data.contact.email + }) } diff --git a/res/auth/mock/scripts/signin/signin.pug b/res/auth/mock/scripts/signin/signin.pug index 1baa352a..0e5076f9 100644 --- a/res/auth/mock/scripts/signin/signin.pug +++ b/res/auth/mock/scripts/signin/signin.pug @@ -1,3 +1,7 @@ +// + Copyright © 2019 contains code contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0 +// + .login2(ng-controller='SignInCtrl') .login-wrapper a(href='./') @@ -29,3 +33,14 @@ span(ng-show='signin.email.$error.required', translate) Please enter your email input.btn.btn-lg.btn-primary.btn-block(type='submit', value='Log In') + + button.btn.btn-sm.btn-default-outline( + type='button' + uib-tooltip="{{'Write a mail to the support team' | translate}}" + ng-disabled='!contactEmail' + ng-click='mailToSupport()' + tooltip-placement='top' + tooltip-popup-delay='500') + i.fa.fa-envelope-o + span(translate) Contact Support + diff --git a/res/common/lang/langs.json b/res/common/lang/langs.json index cb6429a0..dd2fab07 100644 --- a/res/common/lang/langs.json +++ b/res/common/lang/langs.json @@ -2,6 +2,7 @@ "en": "English", "es": "Español", "fr": "Français", + "pt_BR": "Português (Brasil)", "pl": "Język polski", "ja": "日本語", "zh_CN": "简体中文", diff --git a/res/common/lang/po/stf.es.po b/res/common/lang/po/stf.es.po index bfc9224d..86e2f447 100644 --- a/res/common/lang/po/stf.es.po +++ b/res/common/lang/po/stf.es.po @@ -2,13 +2,15 @@ # Translators: # Gunther Brunner, 2015 # Gunther Brunner, 2015 +# lodopidolo, 2018 # Luis Calvo , 2016 # takeshimiya , 2015 +# takeshimiya , 2015 msgid "" msgstr "" "Project-Id-Version: STF\n" -"PO-Revision-Date: 2016-01-27 09:34+0000\n" -"Last-Translator: Luis Calvo \n" +"PO-Revision-Date: 2018-12-06 09:05+0000\n" +"Last-Translator: lodopidolo\n" "Language-Team: Spanish (http://www.transifex.com/openstf/stf/language/es/)\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" @@ -16,9 +18,9 @@ msgstr "" "Plural-Forms: nplurals=2; plural=(n != 1);\n" #: app/components/stf/device/device-info-filter/index.js:119 -#: app/components/stf/device/device-info-filter/index.js:52 -#: app/components/stf/device/device-info-filter/index.js:61 -#: app/components/stf/device/device-info-filter/index.js:71 +#: app/components/stf/device/device-info-filter/index.js:54 +#: app/components/stf/device/device-info-filter/index.js:63 +#: app/components/stf/device/device-info-filter/index.js:73 msgid "-" msgstr "" @@ -34,21 +36,25 @@ msgstr "Ya hay un paquete instalado con el mismo nombre" msgid "" "A previously installed package of the same name has a different signature " "than the new package (and the old package's data was not removed)." -msgstr "" +msgstr "Se ha instalado un paquete previamente con el mismo nombre pero con una firma diferente a la del nuevo paquete (y el paquete antiguo no ha sido eliminado)." #: app/components/stf/install/install-error-filter.js:50 msgid "A secure container mount point couldn't be accessed on external media." -msgstr "" +msgstr "Un punto de montaje de contenedor seguro no puede ser accedido desde un medio externo." #: app/control-panes/info/info.html:1 #: app/device-list/column/device-column-service.js:178 msgid "ABI" msgstr "" -#: app/components/stf/device/device-info-filter/index.js:58 +#: app/components/stf/device/device-info-filter/index.js:60 msgid "AC" msgstr "" +#: app/settings/keys/adb-keys/adb-keys.html:1 +msgid "ADB Keys" +msgstr "" + #: app/settings/keys/access-tokens/access-tokens.html:1 msgid "Access Tokens" msgstr "Tokens de acceso" @@ -69,10 +75,6 @@ msgstr "Acciones" msgid "Activity" msgstr "Actividad" -#: app/settings/keys/adb-keys/adb-keys.html:1 -msgid "ADB Keys" -msgstr "" - #: app/control-panes/resources/resources.html:1 msgid "Add" msgstr "Añadir" @@ -88,7 +90,7 @@ msgstr "Añadir Llave" #: app/components/stf/common-ui/modals/add-adb-key-modal/add-adb-key-modal.html:1 msgid "Add the following ADB Key to STF?" -msgstr "" +msgstr "¿Añadir las siguientes llaves ADB a STF?" #: app/layout/layout-controller.js:7 msgid "Admin mode has been disabled." @@ -113,7 +115,7 @@ msgstr "Modo avión" #: app/control-panes/automation/store-account/store-account.html:1 #: app/control-panes/dashboard/apps/apps.html:1 msgid "App Store" -msgstr "" +msgstr "Tienda de aplicaciones" #: app/control-panes/dashboard/install/install.html:1 msgid "App Upload" @@ -123,15 +125,19 @@ msgstr "Subir aplicación" msgid "Apps" msgstr "Aplicaciones" -#: app/control-panes/advanced/maintenance/maintenance-controller.js:9 +#: app/control-panes/advanced/maintenance/maintenance-controller.js:10 msgid "Are you sure you want to reboot this device?" msgstr "¿Estás seguro de querer reiniciar este dispositivo?" +#: app/components/stf/device/device-info-filter/index.js:30 +msgid "Automating" +msgstr "Automatizando" + #: app/control-panes/control-panes-controller.js:14 msgid "Automation" msgstr "Automatización" -#: app/components/stf/device/device-info-filter/index.js:28 +#: app/components/stf/device/device-info-filter/index.js:29 msgid "Available" msgstr "Disponible" @@ -144,27 +150,27 @@ msgstr "Atrás" msgid "Battery" msgstr "Batería" -#: app/device-list/column/device-column-service.js:202 +#: app/device-list/column/device-column-service.js:208 msgid "Battery Health" -msgstr "" +msgstr "Salud de la batería" -#: app/device-list/column/device-column-service.js:226 +#: app/device-list/column/device-column-service.js:232 msgid "Battery Level" msgstr "Nivel de batería" -#: app/device-list/column/device-column-service.js:210 +#: app/device-list/column/device-column-service.js:216 msgid "Battery Source" -msgstr "" +msgstr "Fuente de batería" -#: app/device-list/column/device-column-service.js:218 +#: app/device-list/column/device-column-service.js:224 msgid "Battery Status" msgstr "Estado de la batería" -#: app/device-list/column/device-column-service.js:239 +#: app/device-list/column/device-column-service.js:245 msgid "Battery Temp" -msgstr "" +msgstr "Temperatura de batería" -#: app/components/stf/device/device-info-filter/index.js:89 +#: app/components/stf/device/device-info-filter/index.js:91 msgid "Bluetooth" msgstr "Bluetooth" @@ -173,7 +179,7 @@ msgid "Browser" msgstr "Navegador" #: app/components/stf/device/device-info-filter/index.js:12 -#: app/components/stf/device/device-info-filter/index.js:27 +#: app/components/stf/device/device-info-filter/index.js:28 msgid "Busy" msgstr "En uso" @@ -181,6 +187,11 @@ msgstr "En uso" msgid "Busy Devices" msgstr "Dispositivos en uso" +#: app/control-panes/info/info.html:1 +#: app/control-panes/performance/cpu/cpu.html:1 +msgid "CPU" +msgstr "CPU" + #: app/control-panes/advanced/input/input.html:1 msgid "Camera" msgstr "Cámara" @@ -202,7 +213,7 @@ msgstr "" msgid "Category" msgstr "Categoría" -#: app/components/stf/device/device-info-filter/index.js:67 +#: app/components/stf/device/device-info-filter/index.js:69 msgid "Charging" msgstr "Cargando" @@ -221,11 +232,11 @@ msgstr "Limpiar" msgid "Clipboard" msgstr "Portapapeles" -#: app/components/stf/device/device-info-filter/index.js:46 +#: app/components/stf/device/device-info-filter/index.js:48 msgid "Cold" msgstr "Frío" -#: app/components/stf/device/device-info-filter/index.js:21 +#: app/components/stf/device/device-info-filter/index.js:22 #: app/components/stf/device/device-info-filter/index.js:6 #: app/control-panes/info/info.html:1 msgid "Connected" @@ -247,10 +258,9 @@ msgstr "Cookies" msgid "Cores" msgstr "Núcleos" -#: app/control-panes/info/info.html:1 -#: app/control-panes/performance/cpu/cpu.html:1 -msgid "CPU" -msgstr "CPU" +#: app/control-panes/device-control/device-control.html:1 +msgid "Current rotation:" +msgstr "Rotación actual" #: app/device-list/device-list.html:1 msgid "Customize" @@ -288,9 +298,9 @@ msgstr "Datos" msgid "Date" msgstr "Fecha" -#: app/components/stf/device/device-info-filter/index.js:48 +#: app/components/stf/device/device-info-filter/index.js:50 msgid "Dead" -msgstr "" +msgstr "Muerto" #: app/control-panes/resources/resources.html:1 msgid "Delete" @@ -315,34 +325,34 @@ msgstr "Desarrollador" msgid "Device" msgstr "Dispositivo" -#: app/device-list/details/device-list-details-directive.js:39 -#: app/device-list/icons/device-list-icons-directive.js:123 -msgid "Device cannot get kicked from the group" -msgstr "" - -#: app/components/stf/device/device-info-filter/index.js:38 -msgid "Device is not present anymore for some reason." -msgstr "" - -#: app/components/stf/device/device-info-filter/index.js:39 -msgid "Device is present but offline." -msgstr "" - #: app/control-panes/info/info.html:1 msgid "Device Photo" -msgstr "" +msgstr "Foto de dispositivo" #: app/control-panes/automation/device-settings/device-settings.html:1 msgid "Device Settings" msgstr "Configuración de Dispositivo" +#: app/device-list/details/device-list-details-directive.js:38 +#: app/device-list/icons/device-list-icons-directive.js:123 +msgid "Device cannot get kicked from the group" +msgstr "El dispositivo no puede ser expulsado del grupo" + +#: app/components/stf/device/device-info-filter/index.js:40 +msgid "Device is not present anymore for some reason." +msgstr "Por algún motivo el dispositivo ya no está presente" + +#: app/components/stf/device/device-info-filter/index.js:41 +msgid "Device is present but offline." +msgstr "El dispositivo está presente pero no disponible" + #: app/components/stf/common-ui/modals/fatal-message/fatal-message.html:1 msgid "Device was disconnected" msgstr "El dispositivo se ha desconectado" -#: app/components/stf/device/device-info-filter/index.js:37 +#: app/components/stf/device/device-info-filter/index.js:39 msgid "Device was kicked by automatic timeout." -msgstr "" +msgstr "El dispositivo fue expulsado por un exceso de tiempo automático" #: app/device-list/device-list.html:1 app/menu/menu.html:1 msgid "Devices" @@ -352,36 +362,40 @@ msgstr "Dispositivos" msgid "Disable WiFi" msgstr "Deshabilitar WIFI" -#: app/components/stf/device/device-info-filter/index.js:68 +#: app/components/stf/device/device-info-filter/index.js:70 msgid "Discharging" -msgstr "" +msgstr "Descargando" #: app/components/stf/common-ui/modals/socket-disconnected/socket-disconnected.html:1 -#: app/components/stf/device/device-info-filter/index.js:20 +#: app/components/stf/device/device-info-filter/index.js:21 #: app/components/stf/device/device-info-filter/index.js:5 msgid "Disconnected" msgstr "Desconectado" #: app/control-panes/info/info.html:1 msgid "Display" -msgstr "" +msgstr "Pantalla" + +#: app/control-panes/resources/resources.html:1 +msgid "Domain" +msgstr "Dominio" #: app/control-panes/dashboard/install/install.html:7 msgid "Drop file to upload" -msgstr "" +msgstr "Suelta aquí el fichero a subir" -#: app/components/stf/device/device-info-filter/index.js:90 +#: app/components/stf/device/device-info-filter/index.js:92 msgid "Dummy" msgstr "" -#: app/settings/notifications/notifications.html:1 -msgid "Enable notifications" -msgstr "Habilitar notificaciones" - #: app/control-panes/automation/device-settings/device-settings.html:1 msgid "Enable WiFi" msgstr "Habilitar WIFI" +#: app/settings/notifications/notifications.html:1 +msgid "Enable notifications" +msgstr "Habilitar notificaciones" + #: app/control-panes/info/info.html:1 msgid "Encrypted" msgstr "Encriptado" @@ -392,18 +406,22 @@ msgstr "Error" #: app/components/stf/control/control-service.js:129 msgid "Error while getting data" -msgstr "" +msgstr "Error obteniendo datos" #: app/components/stf/socket/socket-state/socket-state-directive.js:35 msgid "Error while reconnecting" -msgstr "" +msgstr "Error al reconectar" -#: app/components/stf/device/device-info-filter/index.js:91 +#: app/components/stf/device/device-info-filter/index.js:93 msgid "Ethernet" msgstr "Ethernet" #: app/control-panes/dashboard/shell/shell.html:1 msgid "Executes remote shell commands" +msgstr "Ejecuta comandos de terminal remota" + +#: app/control-panes/info/info.html:1 +msgid "FPS" msgstr "" #: app/components/stf/upload/upload-error-filter.js:5 @@ -412,7 +430,7 @@ msgstr "Fallo al descargar el fichero" #: app/control-panes/advanced/input/input.html:1 msgid "Fast Forward" -msgstr "" +msgstr "Avance rápido" #: app/control-panes/control-panes-controller.js:26 msgid "File Explorer" @@ -420,7 +438,7 @@ msgstr "Explorador de fichero" #: app/components/stf/common-ui/filter-button/filter-button.html:1 msgid "Filter" -msgstr "" +msgstr "Filtro" #: app/control-panes/info/info.html:1 msgid "Find Device" @@ -428,19 +446,15 @@ msgstr "Encontrar dispositivo" #: app/components/stf/common-ui/modals/add-adb-key-modal/add-adb-key-modal.html:1 msgid "Fingerprint" -msgstr "" - -#: app/control-panes/info/info.html:1 -msgid "FPS" -msgstr "" +msgstr "Huella" #: app/control-panes/info/info.html:1 msgid "Frequency" -msgstr "" +msgstr "Frecuencia" -#: app/components/stf/device/device-info-filter/index.js:69 +#: app/components/stf/device/device-info-filter/index.js:71 msgid "Full" -msgstr "" +msgstr "Lleno" #: app/settings/settings-controller.js:5 msgid "General" @@ -448,11 +462,11 @@ msgstr "General" #: app/components/stf/tokens/generate-access-token/generate-access-token.html:1 msgid "Generate Access Token" -msgstr "" +msgstr "Genera testimonio de acceso" #: app/control-panes/advanced/vnc/vnc.html:1 msgid "Generate Login for VNC" -msgstr "" +msgstr "Genera inicio de sesión para VNC" #: app/components/stf/tokens/generate-access-token/generate-access-token.html:1 msgid "Generate New Token" @@ -461,28 +475,28 @@ msgstr "Generar nuevo token" #: app/control-panes/logs/logs.html:1 #: app/control-panes/resources/resources.html:1 msgid "Get" -msgstr "" +msgstr "Obtener" #: app/control-panes/dashboard/clipboard/clipboard.html:1 msgid "Get clipboard contents" -msgstr "" +msgstr "Obtener contenido del portapapeles" #: app/control-panes/dashboard/navigation/navigation.html:1 msgid "Go Back" -msgstr "" +msgstr "Ir atrás" #: app/control-panes/dashboard/navigation/navigation.html:1 msgid "Go Forward" -msgstr "" +msgstr "Ir adelante" #: app/components/stf/common-ui/modals/fatal-message/fatal-message.html:1 #: app/control-panes/control-panes-hotkeys-controller.js:89 msgid "Go to Device List" msgstr "Ir a la lista de dispositivos" -#: app/components/stf/device/device-info-filter/index.js:47 +#: app/components/stf/device/device-info-filter/index.js:49 msgid "Good" -msgstr "" +msgstr "Bueno" #: app/control-panes/info/info.html:1 msgid "Hardware" @@ -490,7 +504,7 @@ msgstr "Hardware" #: app/control-panes/info/info.html:1 msgid "Health" -msgstr "" +msgstr "Salud" #: app/control-panes/info/info.html:1 msgid "Height" @@ -511,28 +525,32 @@ msgstr "Home" #: app/control-panes/advanced/port-forwarding/port-forwarding.html:1 msgid "Host" -msgstr "" +msgstr "Terminal" #: app/control-panes/advanced/port-forwarding/port-forwarding.html:1 msgid "Hostname" -msgstr "" +msgstr "Nombre de terminal" #: app/control-panes/info/info.html:1 msgid "ICCID" -msgstr "" +msgstr "ICCID" #: app/control-panes/info/info.html:1 msgid "ID" -msgstr "" +msgstr "ID" #: app/control-panes/info/info.html:1 msgid "IMEI" msgstr "IMEI" +#: app/control-panes/info/info.html:1 +msgid "IMSI" +msgstr "IMSI" + #: auth/ldap/scripts/signin/signin.html:1 #: auth/mock/scripts/signin/signin.html:1 msgid "Incorrect login details" -msgstr "" +msgstr "Datos de inicio de sesión incorrectos" #: app/control-panes/control-panes-controller.js:32 msgid "Info" @@ -544,7 +562,7 @@ msgstr "Inspeccionar dispositivo" #: app/control-panes/inspect/inspect.html:1 msgid "Inspecting is currently only supported in WebView" -msgstr "" +msgstr "La inspección sólo está soportada para WebView actualmente" #: app/control-panes/inspect/inspect.html:1 msgid "Inspector" @@ -572,11 +590,11 @@ msgstr "Instalando aplicación..." #: app/components/stf/keys/add-adb-key/add-adb-key.html:1 msgid "Key" -msgstr "" +msgstr "Llave" #: app/settings/settings-controller.js:10 msgid "Keys" -msgstr "" +msgstr "Llaves" #: app/control-panes/device-control/device-control.html:1 msgid "Landscape" @@ -588,11 +606,11 @@ msgstr "Idioma" #: app/control-panes/dashboard/install/activities/activities.html:1 msgid "Launch Activity" -msgstr "" +msgstr "Iniciar actividad" #: app/control-panes/dashboard/install/install.html:7 msgid "Launching activity..." -msgstr "" +msgstr "Iniciando actividad" #: app/control-panes/info/info.html:1 app/control-panes/logs/logs.html:1 msgid "Level" @@ -602,9 +620,9 @@ msgstr "Nivel" msgid "Local Settings" msgstr "" -#: app/device-list/column/device-column-service.js:250 +#: app/device-list/column/device-column-service.js:256 msgid "Location" -msgstr "" +msgstr "Posición" #: app/control-panes/automation/device-settings/device-settings.html:7 msgid "Lock Rotation" @@ -612,7 +630,7 @@ msgstr "Bloquear rotación" #: app/control-panes/control-panes-controller.js:50 msgid "Logs" -msgstr "" +msgstr "Trazas" #: app/control-panes/advanced/maintenance/maintenance.html:1 msgid "Maintenance" @@ -620,8 +638,8 @@ msgstr "Mantenimiento" #: app/settings/keys/access-tokens/access-tokens.html:1 msgid "" -"Make sure to copy your access token now. You won't be able to see it again!" -msgstr "Asegúrate de copiar tu token de acceso ahora. ¡No podrás volver a verlo más!" +"Make sure to copy your access token now. You won't be able to see it again." +msgstr "Asegúrate de copiar el testigo de acceso ahora. Si lo pierde no se podrá recuperar." #: app/control-panes/dashboard/apps/apps.html:1 msgid "Manage Apps" @@ -638,7 +656,7 @@ msgstr "" #: app/control-panes/advanced/input/input.html:1 msgid "Media" -msgstr "" +msgstr "Medio" #: app/control-panes/info/info.html:1 msgid "Memory" @@ -648,23 +666,23 @@ msgstr "Memoria" msgid "Menu" msgstr "Menú" -#: app/components/stf/device/device-info-filter/index.js:92 +#: app/components/stf/device/device-info-filter/index.js:94 msgid "Mobile" msgstr "Móvil" -#: app/components/stf/device/device-info-filter/index.js:93 +#: app/components/stf/device/device-info-filter/index.js:95 msgid "Mobile DUN" msgstr "" -#: app/components/stf/device/device-info-filter/index.js:94 +#: app/components/stf/device/device-info-filter/index.js:96 msgid "Mobile High Priority" msgstr "" -#: app/components/stf/device/device-info-filter/index.js:95 +#: app/components/stf/device/device-info-filter/index.js:97 msgid "Mobile MMS" msgstr "" -#: app/components/stf/device/device-info-filter/index.js:96 +#: app/components/stf/device/device-info-filter/index.js:98 msgid "Mobile SUPL" msgstr "" @@ -673,20 +691,21 @@ msgstr "" msgid "Model" msgstr "Modelo" +#: app/settings/keys/adb-keys/adb-keys.html:1 +msgid "More about ADB Keys" +msgstr "Más sobre llaves ADB" + #: app/settings/keys/access-tokens/access-tokens.html:1 msgid "More about Access Tokens" msgstr "Más sobre Tokens de acceso" -#: app/settings/keys/adb-keys/adb-keys.html:1 -msgid "More about ADB Keys" -msgstr "" - #: app/control-panes/advanced/input/input.html:1 msgid "Mute" msgstr "Silencio" #: app/control-panes/explorer/explorer.html:1 #: app/control-panes/info/info.html:1 +#: app/control-panes/resources/resources.html:1 msgid "Name" msgstr "Nombre" @@ -707,18 +726,22 @@ msgstr "Red" msgid "Next" msgstr "Siguiente" -#: app/components/stf/device/device-info-filter/index.js:116 +#: app/components/stf/device/device-info-filter/index.js:117 msgid "No" msgstr "No" +#: app/settings/keys/adb-keys/adb-keys.html:1 +msgid "No ADB keys" +msgstr "No hay llaves ADB" + +#: app/control-panes/advanced/port-forwarding/port-forwarding.html:1 +msgid "No Ports Forwarded" +msgstr "No hay puertos redirigidos" + #: app/settings/keys/access-tokens/access-tokens.html:1 msgid "No access tokens" msgstr "Sin tokens de acceso" -#: app/settings/keys/adb-keys/adb-keys.html:1 -msgid "No ADB keys" -msgstr "" - #: app/components/stf/control/control-service.js:126 msgid "No clipboard data" msgstr "No hay datos en el portapapeles" @@ -739,10 +762,6 @@ msgstr "No hay dispositivos conectados" msgid "No photo available" msgstr "No hay imagen disponible" -#: app/control-panes/advanced/port-forwarding/port-forwarding.html:1 -msgid "No Ports Forwarded" -msgstr "" - #: app/control-panes/screenshots/screenshots.html:5 msgid "No screenshots taken" msgstr "No hay capturas de pantalla" @@ -751,11 +770,11 @@ msgstr "No hay capturas de pantalla" msgid "Normal Mode" msgstr "Modo normal" -#: app/components/stf/device/device-info-filter/index.js:70 +#: app/components/stf/device/device-info-filter/index.js:72 msgid "Not Charging" msgstr "No se está cargando" -#: app/device-list/column/device-column-service.js:256 +#: app/device-list/column/device-column-service.js:262 msgid "Notes" msgstr "Notas" @@ -771,7 +790,12 @@ msgstr "Notificaciones" msgid "Number" msgstr "Número" -#: app/components/stf/device/device-info-filter/index.js:22 +#: app/control-panes/info/info.html:1 +#: app/device-list/column/device-column-service.js:55 +msgid "OS" +msgstr "" + +#: app/components/stf/device/device-info-filter/index.js:23 #: app/components/stf/device/device-info-filter/index.js:7 msgid "Offline" msgstr "Offline" @@ -789,18 +813,17 @@ msgstr "Abrir" msgid "Orientation" msgstr "Orientación" -#: app/control-panes/info/info.html:1 -#: app/device-list/column/device-column-service.js:55 -msgid "OS" -msgstr "" - -#: app/components/stf/device/device-info-filter/index.js:49 +#: app/components/stf/device/device-info-filter/index.js:51 msgid "Over Voltage" -msgstr "" +msgstr "Exceso de voltaje" -#: app/components/stf/device/device-info-filter/index.js:50 +#: app/components/stf/device/device-info-filter/index.js:52 msgid "Overheat" -msgstr "" +msgstr "Exceso de temperatura" + +#: app/control-panes/logs/logs.html:1 +msgid "PID" +msgstr "PID" #: app/control-panes/dashboard/install/activities/activities.html:1 msgid "Package" @@ -811,6 +834,10 @@ msgstr "Paquete" msgid "Password" msgstr "Contraseña" +#: app/control-panes/resources/resources.html:1 +msgid "Path" +msgstr "" + #: app/control-panes/explorer/explorer.html:1 msgid "Permissions" msgstr "Permisos" @@ -819,25 +846,25 @@ msgstr "Permisos" msgid "Phone" msgstr "Teléfono" -#: app/device-list/column/device-column-service.js:196 +#: app/device-list/column/device-column-service.js:202 msgid "Phone ICCID" -msgstr "" +msgstr "ICCID del teléfono" #: app/device-list/column/device-column-service.js:190 msgid "Phone IMEI" msgstr "IMEI del teléfono" +#: app/device-list/column/device-column-service.js:196 +msgid "Phone IMSI" +msgstr "IMSI del teléfono" + #: app/control-panes/info/info.html:1 msgid "Physical Device" msgstr "Dispositivo físico" -#: app/control-panes/logs/logs.html:1 -msgid "PID" -msgstr "" - #: app/control-panes/info/info.html:1 msgid "Place" -msgstr "" +msgstr "Lugar" #: app/control-panes/info/info.html:1 msgid "Platform" @@ -851,14 +878,22 @@ msgstr "Inicio/Pausa" msgid "Please enter a valid email" msgstr "Por favor, introduce un email válido" -#: auth/mock/scripts/signin/signin.html:1 -msgid "Please enter your email" -msgstr "Por favor, introduce tu email" - #: auth/ldap/scripts/signin/signin.html:1 msgid "Please enter your LDAP username" msgstr "Por favor, introduce tu usuario de LDAP" +#: app/control-panes/automation/store-account/store-account.html:1 +msgid "Please enter your Store password" +msgstr "Por favor, introduce tu contraseña" + +#: app/control-panes/automation/store-account/store-account.html:1 +msgid "Please enter your Store username" +msgstr "Por favor, introduce to nombre de usuario" + +#: auth/mock/scripts/signin/signin.html:1 +msgid "Please enter your email" +msgstr "Por favor, introduce tu email" + #: auth/mock/scripts/signin/signin.html:1 msgid "Please enter your name" msgstr "Por favor, introduce tu nombre" @@ -867,14 +902,6 @@ msgstr "Por favor, introduce tu nombre" msgid "Please enter your password" msgstr "Por favor, introduce tu contraseña" -#: app/control-panes/automation/store-account/store-account.html:1 -msgid "Please enter your Store password" -msgstr "" - -#: app/control-panes/automation/store-account/store-account.html:1 -msgid "Please enter your Store username" -msgstr "" - #: app/control-panes/advanced/port-forwarding/port-forwarding.html:1 #: app/control-panes/advanced/vnc/vnc.html:1 msgid "Port" @@ -882,7 +909,7 @@ msgstr "Puerto" #: app/control-panes/advanced/port-forwarding/port-forwarding.html:1 msgid "Port Forwarding" -msgstr "" +msgstr "Puerto de reenvío" #: app/control-panes/device-control/device-control.html:1 msgid "Portrait" @@ -896,7 +923,7 @@ msgstr "" msgid "Power Source" msgstr "" -#: app/components/stf/device/device-info-filter/index.js:24 +#: app/components/stf/device/device-info-filter/index.js:25 #: app/components/stf/device/device-info-filter/index.js:9 msgid "Preparing" msgstr "Preparando" @@ -934,14 +961,18 @@ msgstr "" msgid "RAM" msgstr "RAM" +#: app/control-panes/info/info.html:1 +msgid "ROM" +msgstr "ROM" + #: app/components/stf/device/device-info-filter/index.js:10 -#: app/components/stf/device/device-info-filter/index.js:25 +#: app/components/stf/device/device-info-filter/index.js:26 msgid "Ready" msgstr "Listo" #: app/components/stf/socket/socket-state/socket-state-directive.js:39 msgid "Reconnected successfully." -msgstr "" +msgstr "Reconectado con éxito" #: app/components/stf/common-ui/refresh-page/refresh-page.html:1 msgid "Refresh" @@ -971,14 +1002,14 @@ msgstr "Eliminar" msgid "Reset" msgstr "Reiniciar" -#: app/control-panes/dashboard/navigation/navigation.html:1 -msgid "Reset all browser settings" -msgstr "Restablecer todos los ajustes del navegador" - #: app/settings/general/local/local-settings.html:1 msgid "Reset Settings" msgstr "Restablecer ajustes" +#: app/control-panes/dashboard/navigation/navigation.html:1 +msgid "Reset all browser settings" +msgstr "Restablecer todos los ajustes del navegador" + #: app/control-panes/advanced/maintenance/maintenance.html:1 msgid "Restart Device" msgstr "Reiniciar dispositivo" @@ -999,10 +1030,6 @@ msgstr "" msgid "Roaming" msgstr "" -#: app/control-panes/info/info.html:1 -msgid "ROM" -msgstr "ROM" - #: app/components/stf/device-context-menu/device-context-menu.html:1 #: app/control-panes/control-panes-hotkeys-controller.js:92 msgid "Rotate Left" @@ -1035,6 +1062,19 @@ msgstr "" msgid "Run this command to copy the key to your clipboard" msgstr "Ejecuta este comando para copiar la clave al portapapeles" +#: app/control-panes/info/info.html:1 +msgid "SD Card Mounted" +msgstr "" + +#: app/control-panes/info/info.html:1 +#: app/device-list/column/device-column-service.js:171 +msgid "SDK" +msgstr "SDK" + +#: app/control-panes/info/info.html:1 +msgid "SIM" +msgstr "SIM" + #: app/components/stf/device-context-menu/device-context-menu.html:1 msgid "Save ScreenShot" msgstr "Guardar captura de pantalla" @@ -1055,19 +1095,14 @@ msgstr "Captura de pantalla" msgid "Screenshots" msgstr "Capturas de Pantalla" -#: app/control-panes/info/info.html:1 -msgid "SD Card Mounted" -msgstr "" - -#: app/control-panes/info/info.html:1 -#: app/device-list/column/device-column-service.js:171 -msgid "SDK" -msgstr "SDK" - #: app/control-panes/advanced/input/input.html:1 msgid "Search" msgstr "Buscar" +#: app/control-panes/resources/resources.html:1 +msgid "Secure" +msgstr "" + #: app/control-panes/control-panes-hotkeys-controller.js:91 msgid "Selects Next IME" msgstr "" @@ -1118,10 +1153,6 @@ msgstr "Desconectar" msgid "Silent Mode" msgstr "Modo silencio" -#: app/control-panes/info/info.html:1 -msgid "SIM" -msgstr "SIM" - #: app/control-panes/explorer/explorer.html:1 #: app/control-panes/info/info.html:1 msgid "Size" @@ -1131,7 +1162,7 @@ msgstr "Tamaño" msgid "Socket connection was lost" msgstr "Se perdió la conexión con el socket" -#: app/components/stf/device/device-info-filter/index.js:36 +#: app/components/stf/device/device-info-filter/index.js:38 msgid "Someone stole your device." msgstr "Alguien robó tu dispositivo" @@ -1153,8 +1184,13 @@ msgstr "Estado" msgid "Stop" msgstr "Parar" +#: app/components/stf/device/device-info-filter/index.js:14 +msgid "Stop Automation" +msgstr "" + #: app/components/stf/device-context-menu/device-context-menu.html:1 #: app/components/stf/device/device-info-filter/index.js:11 +#: app/control-panes/device-control/device-control.html:1 msgid "Stop Using" msgstr "" @@ -1170,6 +1206,10 @@ msgstr "Subtipo" msgid "Switch Charset" msgstr "" +#: app/control-panes/logs/logs.html:1 +msgid "TID" +msgstr "" + #: app/control-panes/logs/logs.html:1 msgid "Tag" msgstr "Etiqueta" @@ -1190,11 +1230,15 @@ msgstr "Temperatura" msgid "Text" msgstr "Texto" +#: app/components/stf/install/install-error-filter.js:22 +msgid "The URI passed in is invalid." +msgstr "" + #: app/components/stf/screen/screen.html:1 msgid "The current view is marked secure and cannot be viewed remotely." msgstr "La vista actual está marcada como segura y no puede ser vista de forma remota" -#: app/control-panes/advanced/maintenance/maintenance-controller.js:10 +#: app/control-panes/advanced/maintenance/maintenance-controller.js:11 msgid "The device will be unavailable for a moment." msgstr "El dispositivo no estará disponible durante unos instantes" @@ -1306,6 +1350,12 @@ msgstr "" msgid "The parser did not find any certificates in the .apk." msgstr "" +#: app/components/stf/install/install-error-filter.js:76 +msgid "" +"The parser encountered a CertificateEncodingException in one of the files in" +" the .apk." +msgstr "" + #: app/components/stf/install/install-error-filter.js:78 msgid "The parser encountered a bad or missing package name in the manifest." msgstr "" @@ -1314,12 +1364,6 @@ msgstr "" msgid "The parser encountered a bad shared user id name in the manifest." msgstr "" -#: app/components/stf/install/install-error-filter.js:76 -msgid "" -"The parser encountered a CertificateEncodingException in one of the files in" -" the .apk." -msgstr "" - #: app/components/stf/install/install-error-filter.js:70 msgid "The parser encountered an unexpected exception." msgstr "" @@ -1362,14 +1406,6 @@ msgid "" " installing apps." msgstr "" -#: app/components/stf/install/install-error-filter.js:22 -msgid "The URI passed in is invalid." -msgstr "" - -#: app/control-panes/logs/logs.html:1 -msgid "TID" -msgstr "" - #: app/control-panes/logs/logs.html:1 msgid "Time" msgstr "" @@ -1390,11 +1426,6 @@ msgstr "" msgid "Total Devices" msgstr "Dispositivos Totales" -#: app/components/stf/common-ui/modals/fatal-message/fatal-message.html:1 -#: app/control-panes/resources/resources.html:1 -msgid "translate" -msgstr "traducir" - #: app/components/stf/common-ui/modals/fatal-message/fatal-message.html:1 #: app/components/stf/common-ui/modals/socket-disconnected/socket-disconnected.html:1 msgid "Try to reconnect" @@ -1404,7 +1435,11 @@ msgstr "Volver a conectar" msgid "Type" msgstr "Tipo" -#: app/components/stf/device/device-info-filter/index.js:23 +#: app/components/stf/device/device-info-filter/index.js:61 +msgid "USB" +msgstr "USB" + +#: app/components/stf/device/device-info-filter/index.js:24 #: app/components/stf/device/device-info-filter/index.js:8 msgid "Unauthorized" msgstr "No autorizado" @@ -1413,12 +1448,12 @@ msgstr "No autorizado" msgid "Uninstall" msgstr "Desinstalar" -#: app/components/stf/device/device-info-filter/index.js:14 -#: app/components/stf/device/device-info-filter/index.js:29 +#: app/components/stf/device/device-info-filter/index.js:15 +#: app/components/stf/device/device-info-filter/index.js:31 msgid "Unknown" msgstr "Desconocido" -#: app/components/stf/device/device-info-filter/index.js:40 +#: app/components/stf/device/device-info-filter/index.js:42 msgid "Unknown reason." msgstr "Razón desconocida." @@ -1426,18 +1461,18 @@ msgstr "Razón desconocida." msgid "Unlock Rotation" msgstr "Desbloquear rotación" -#: app/components/stf/device/device-info-filter/index.js:51 +#: app/components/stf/device/device-info-filter/index.js:53 msgid "Unspecified Failure" msgstr "Fallo no especificado" -#: app/components/stf/upload/upload-error-filter.js:7 -msgid "Upload failed" -msgstr "Subida fallida" - #: app/control-panes/dashboard/install/install.html:5 msgid "Upload From Link" msgstr "Subir desde enlace" +#: app/components/stf/upload/upload-error-filter.js:7 +msgid "Upload failed" +msgstr "Subida fallida" + #: app/components/stf/upload/upload-error-filter.js:8 msgid "Upload unknown error" msgstr "Error de subida desconocido" @@ -1454,10 +1489,6 @@ msgstr "Subiendo..." msgid "Usable Devices" msgstr "" -#: app/components/stf/device/device-info-filter/index.js:59 -msgid "USB" -msgstr "USB" - #: app/control-panes/advanced/usb/usb.html:1 msgid "Usb speed" msgstr "Velocidad de USB" @@ -1466,7 +1497,7 @@ msgstr "Velocidad de USB" msgid "Use" msgstr "Uso" -#: app/device-list/column/device-column-service.js:262 +#: app/device-list/column/device-column-service.js:268 msgid "User" msgstr "Usuario" @@ -1474,7 +1505,7 @@ msgstr "Usuario" msgid "Username" msgstr "Nombre de usuario" -#: app/components/stf/device/device-info-filter/index.js:26 +#: app/components/stf/device/device-info-filter/index.js:27 msgid "Using" msgstr "En uso" @@ -1482,6 +1513,14 @@ msgstr "En uso" msgid "Using Fallback" msgstr "" +#: app/control-panes/advanced/vnc/vnc.html:1 +msgid "VNC" +msgstr "" + +#: app/control-panes/resources/resources.html:1 +msgid "Value" +msgstr "" + #: app/control-panes/info/info.html:1 msgid "Version" msgstr "Versión" @@ -1494,10 +1533,6 @@ msgstr "" msgid "Vibrate Mode" msgstr "Modo vibración" -#: app/control-panes/advanced/vnc/vnc.html:1 -msgid "VNC" -msgstr "" - #: app/control-panes/info/info.html:1 msgid "Voltage" msgstr "" @@ -1522,22 +1557,22 @@ msgstr "Atención:" msgid "Web" msgstr "Web" -#: app/control-panes/info/info.html:1 -msgid "Width" -msgstr "Ancho" - -#: app/components/stf/device/device-info-filter/index.js:105 -#: app/components/stf/device/device-info-filter/index.js:97 +#: app/components/stf/device/device-info-filter/index.js:107 +#: app/components/stf/device/device-info-filter/index.js:99 #: app/control-panes/automation/device-settings/device-settings.html:1 #: app/control-panes/dashboard/apps/apps.html:1 msgid "WiFi" msgstr "WIFI" -#: app/components/stf/device/device-info-filter/index.js:98 +#: app/components/stf/device/device-info-filter/index.js:100 msgid "WiMAX" msgstr "" -#: app/components/stf/device/device-info-filter/index.js:60 +#: app/control-panes/info/info.html:1 +msgid "Width" +msgstr "Ancho" + +#: app/components/stf/device/device-info-filter/index.js:62 msgid "Wireless" msgstr "" @@ -1549,10 +1584,15 @@ msgstr "" msgid "Y DPI" msgstr "" -#: app/components/stf/device/device-info-filter/index.js:113 +#: app/components/stf/device/device-info-filter/index.js:115 msgid "Yes" msgstr "Sí" -#: app/components/stf/device/device-info-filter/index.js:35 +#: app/components/stf/device/device-info-filter/index.js:37 msgid "You (or someone else) kicked the device." msgstr "" + +#: app/components/stf/common-ui/modals/fatal-message/fatal-message.html:1 +#: app/control-panes/resources/resources.html:1 +msgid "translate" +msgstr "traducir" diff --git a/res/common/lang/po/stf.fr.po b/res/common/lang/po/stf.fr.po index 94e473f9..da41454e 100644 --- a/res/common/lang/po/stf.fr.po +++ b/res/common/lang/po/stf.fr.po @@ -1,21 +1,22 @@ # # Translators: +# ctest 06 , 2018 # Guillaume Chertier , 2016 msgid "" msgstr "" "Project-Id-Version: STF\n" -"PO-Revision-Date: 2016-04-04 09:12+0000\n" -"Last-Translator: Guillaume Chertier \n" +"PO-Revision-Date: 2018-04-26 09:06+0000\n" +"Last-Translator: ctest 06 \n" "Language-Team: French (http://www.transifex.com/openstf/stf/language/fr/)\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Language: fr\n" "Plural-Forms: nplurals=2; plural=(n > 1);\n" -#: app/components/stf/device/device-info-filter/index.js:117 -#: app/components/stf/device/device-info-filter/index.js:52 -#: app/components/stf/device/device-info-filter/index.js:61 -#: app/components/stf/device/device-info-filter/index.js:71 +#: app/components/stf/device/device-info-filter/index.js:119 +#: app/components/stf/device/device-info-filter/index.js:54 +#: app/components/stf/device/device-info-filter/index.js:63 +#: app/components/stf/device/device-info-filter/index.js:73 msgid "-" msgstr "-" @@ -42,10 +43,14 @@ msgstr "Un conteneur sécurisé équipé ne peut pas être accessible sur un sup msgid "ABI" msgstr "IBP" -#: app/components/stf/device/device-info-filter/index.js:58 +#: app/components/stf/device/device-info-filter/index.js:60 msgid "AC" msgstr "AC" +#: app/settings/keys/adb-keys/adb-keys.html:1 +msgid "ADB Keys" +msgstr "Clefs ADB" + #: app/settings/keys/access-tokens/access-tokens.html:1 msgid "Access Tokens" msgstr "Jetons d'Accès" @@ -66,10 +71,6 @@ msgstr "Actions" msgid "Activity" msgstr "Activité" -#: app/settings/keys/adb-keys/adb-keys.html:1 -msgid "ADB Keys" -msgstr "Clefs ADB" - #: app/control-panes/resources/resources.html:1 msgid "Add" msgstr "Ajouter" @@ -124,11 +125,15 @@ msgstr "Applications" msgid "Are you sure you want to reboot this device?" msgstr "Est vous sûr de vouloir redémarrer ce terminal?" +#: app/components/stf/device/device-info-filter/index.js:30 +msgid "Automating" +msgstr "En cours d'automatisation" + #: app/control-panes/control-panes-controller.js:14 msgid "Automation" msgstr "Automatisation" -#: app/components/stf/device/device-info-filter/index.js:28 +#: app/components/stf/device/device-info-filter/index.js:29 msgid "Available" msgstr "Disponible" @@ -141,27 +146,27 @@ msgstr "Précédent" msgid "Battery" msgstr "Batterie" -#: app/device-list/column/device-column-service.js:202 +#: app/device-list/column/device-column-service.js:208 msgid "Battery Health" msgstr "Santé de la Batterie" -#: app/device-list/column/device-column-service.js:226 +#: app/device-list/column/device-column-service.js:232 msgid "Battery Level" msgstr "Niveau de la Batterie" -#: app/device-list/column/device-column-service.js:210 +#: app/device-list/column/device-column-service.js:216 msgid "Battery Source" msgstr "Source de la Batterie" -#: app/device-list/column/device-column-service.js:218 +#: app/device-list/column/device-column-service.js:224 msgid "Battery Status" msgstr "Statut de la Batterie" -#: app/device-list/column/device-column-service.js:239 +#: app/device-list/column/device-column-service.js:245 msgid "Battery Temp" msgstr "Température de la Batterie" -#: app/components/stf/device/device-info-filter/index.js:89 +#: app/components/stf/device/device-info-filter/index.js:91 msgid "Bluetooth" msgstr "Bluetooth" @@ -170,7 +175,7 @@ msgid "Browser" msgstr "Navigateur" #: app/components/stf/device/device-info-filter/index.js:12 -#: app/components/stf/device/device-info-filter/index.js:27 +#: app/components/stf/device/device-info-filter/index.js:28 msgid "Busy" msgstr "Occupé" @@ -178,6 +183,11 @@ msgstr "Occupé" msgid "Busy Devices" msgstr "Terminaux Occupés" +#: app/control-panes/info/info.html:1 +#: app/control-panes/performance/cpu/cpu.html:1 +msgid "CPU" +msgstr "CPU" + #: app/control-panes/advanced/input/input.html:1 msgid "Camera" msgstr "Caméra" @@ -199,7 +209,7 @@ msgstr "Opérateur" msgid "Category" msgstr "Catégorie" -#: app/components/stf/device/device-info-filter/index.js:67 +#: app/components/stf/device/device-info-filter/index.js:69 msgid "Charging" msgstr "Chargement" @@ -218,11 +228,11 @@ msgstr "Nettoyer" msgid "Clipboard" msgstr "Presse-papier" -#: app/components/stf/device/device-info-filter/index.js:46 +#: app/components/stf/device/device-info-filter/index.js:48 msgid "Cold" msgstr "Froid" -#: app/components/stf/device/device-info-filter/index.js:21 +#: app/components/stf/device/device-info-filter/index.js:22 #: app/components/stf/device/device-info-filter/index.js:6 #: app/control-panes/info/info.html:1 msgid "Connected" @@ -244,11 +254,6 @@ msgstr "Cookies" msgid "Cores" msgstr "Coeurs" -#: app/control-panes/info/info.html:1 -#: app/control-panes/performance/cpu/cpu.html:1 -msgid "CPU" -msgstr "CPU" - #: app/control-panes/device-control/device-control.html:1 msgid "Current rotation:" msgstr "Rotation actuelle" @@ -289,7 +294,7 @@ msgstr "Données" msgid "Date" msgstr "Date" -#: app/components/stf/device/device-info-filter/index.js:48 +#: app/components/stf/device/device-info-filter/index.js:50 msgid "Dead" msgstr "Mort" @@ -316,19 +321,6 @@ msgstr "Développeur" msgid "Device" msgstr "Terminal" -#: app/device-list/details/device-list-details-directive.js:38 -#: app/device-list/icons/device-list-icons-directive.js:122 -msgid "Device cannot get kicked from the group" -msgstr "Le Terminal ne peut pas être exclu du groupe" - -#: app/components/stf/device/device-info-filter/index.js:38 -msgid "Device is not present anymore for some reason." -msgstr "Le Terminal n'est plus présent pour certaines raisons" - -#: app/components/stf/device/device-info-filter/index.js:39 -msgid "Device is present but offline." -msgstr "Le Terminal est présent mais Hors-Ligne" - #: app/control-panes/info/info.html:1 msgid "Device Photo" msgstr "Photos du Terminal" @@ -337,11 +329,24 @@ msgstr "Photos du Terminal" msgid "Device Settings" msgstr "Paramètres du Terminal" +#: app/device-list/details/device-list-details-directive.js:38 +#: app/device-list/icons/device-list-icons-directive.js:123 +msgid "Device cannot get kicked from the group" +msgstr "Le Terminal ne peut pas être exclu du groupe" + +#: app/components/stf/device/device-info-filter/index.js:40 +msgid "Device is not present anymore for some reason." +msgstr "Le Terminal n'est plus présent pour certaines raisons" + +#: app/components/stf/device/device-info-filter/index.js:41 +msgid "Device is present but offline." +msgstr "Le Terminal est présent mais Hors-Ligne" + #: app/components/stf/common-ui/modals/fatal-message/fatal-message.html:1 msgid "Device was disconnected" msgstr "Le Terminal était déconnecté" -#: app/components/stf/device/device-info-filter/index.js:37 +#: app/components/stf/device/device-info-filter/index.js:39 msgid "Device was kicked by automatic timeout." msgstr "Le Terminal a été exclu par le Timeout automatique" @@ -353,12 +358,12 @@ msgstr "Terminaux" msgid "Disable WiFi" msgstr "Désactiver le Wifi" -#: app/components/stf/device/device-info-filter/index.js:68 +#: app/components/stf/device/device-info-filter/index.js:70 msgid "Discharging" msgstr "En Décharge" #: app/components/stf/common-ui/modals/socket-disconnected/socket-disconnected.html:1 -#: app/components/stf/device/device-info-filter/index.js:20 +#: app/components/stf/device/device-info-filter/index.js:21 #: app/components/stf/device/device-info-filter/index.js:5 msgid "Disconnected" msgstr "Déconnecté" @@ -367,22 +372,26 @@ msgstr "Déconnecté" msgid "Display" msgstr "écran" +#: app/control-panes/resources/resources.html:1 +msgid "Domain" +msgstr "Domaine" + #: app/control-panes/dashboard/install/install.html:7 msgid "Drop file to upload" msgstr "Déposer le fichier à téléverser" -#: app/components/stf/device/device-info-filter/index.js:90 +#: app/components/stf/device/device-info-filter/index.js:92 msgid "Dummy" msgstr "Mannequin" -#: app/settings/notifications/notifications.html:1 -msgid "Enable notifications" -msgstr "Activer les notifications" - #: app/control-panes/automation/device-settings/device-settings.html:1 msgid "Enable WiFi" msgstr "Activer le Wifi" +#: app/settings/notifications/notifications.html:1 +msgid "Enable notifications" +msgstr "Activer les notifications" + #: app/control-panes/info/info.html:1 msgid "Encrypted" msgstr "Crypté" @@ -399,7 +408,7 @@ msgstr "Erreur lors de l'obtention de données" msgid "Error while reconnecting" msgstr "Erreur lors de la reconnexion" -#: app/components/stf/device/device-info-filter/index.js:91 +#: app/components/stf/device/device-info-filter/index.js:93 msgid "Ethernet" msgstr "Ethernet" @@ -407,6 +416,10 @@ msgstr "Ethernet" msgid "Executes remote shell commands" msgstr "Exécute des commandes Shell à distance" +#: app/control-panes/info/info.html:1 +msgid "FPS" +msgstr "FPS" + #: app/components/stf/upload/upload-error-filter.js:5 msgid "Failed to download file" msgstr "Impossible de télécharger le fichier" @@ -431,15 +444,11 @@ msgstr "Trouver un Terminal" msgid "Fingerprint" msgstr "Empreinte Digitale" -#: app/control-panes/info/info.html:1 -msgid "FPS" -msgstr "FPS" - #: app/control-panes/info/info.html:1 msgid "Frequency" msgstr "Fréquence" -#: app/components/stf/device/device-info-filter/index.js:69 +#: app/components/stf/device/device-info-filter/index.js:71 msgid "Full" msgstr "Rempli" @@ -481,7 +490,7 @@ msgstr "Avancer" msgid "Go to Device List" msgstr "Aller à la Liste des Terminaux" -#: app/components/stf/device/device-info-filter/index.js:47 +#: app/components/stf/device/device-info-filter/index.js:49 msgid "Good" msgstr "Bien" @@ -530,6 +539,10 @@ msgstr "ID" msgid "IMEI" msgstr "IMEI" +#: app/control-panes/info/info.html:1 +msgid "IMSI" +msgstr "IMSI" + #: auth/ldap/scripts/signin/signin.html:1 #: auth/mock/scripts/signin/signin.html:1 msgid "Incorrect login details" @@ -603,7 +616,7 @@ msgstr "Niveau" msgid "Local Settings" msgstr "Paramètres locaux" -#: app/device-list/column/device-column-service.js:250 +#: app/device-list/column/device-column-service.js:256 msgid "Location" msgstr "Localisation" @@ -649,23 +662,23 @@ msgstr "Mémoire" msgid "Menu" msgstr "Menu" -#: app/components/stf/device/device-info-filter/index.js:92 +#: app/components/stf/device/device-info-filter/index.js:94 msgid "Mobile" msgstr "Mobile" -#: app/components/stf/device/device-info-filter/index.js:93 +#: app/components/stf/device/device-info-filter/index.js:95 msgid "Mobile DUN" msgstr "Réseau Commuté" -#: app/components/stf/device/device-info-filter/index.js:94 +#: app/components/stf/device/device-info-filter/index.js:96 msgid "Mobile High Priority" msgstr "Mobile en Priorité Haute" -#: app/components/stf/device/device-info-filter/index.js:95 +#: app/components/stf/device/device-info-filter/index.js:97 msgid "Mobile MMS" msgstr "MMS" -#: app/components/stf/device/device-info-filter/index.js:96 +#: app/components/stf/device/device-info-filter/index.js:98 msgid "Mobile SUPL" msgstr "SUPL" @@ -674,20 +687,21 @@ msgstr "SUPL" msgid "Model" msgstr "Modèle" -#: app/settings/keys/access-tokens/access-tokens.html:1 -msgid "More about Access Tokens" -msgstr "En savoir plus sur les Jetons d'Accès" - #: app/settings/keys/adb-keys/adb-keys.html:1 msgid "More about ADB Keys" msgstr "En savoir plus sur les Clefs ADB" +#: app/settings/keys/access-tokens/access-tokens.html:1 +msgid "More about Access Tokens" +msgstr "En savoir plus sur les Jetons d'Accès" + #: app/control-panes/advanced/input/input.html:1 msgid "Mute" msgstr "Muet" #: app/control-panes/explorer/explorer.html:1 #: app/control-panes/info/info.html:1 +#: app/control-panes/resources/resources.html:1 msgid "Name" msgstr "Nom" @@ -708,18 +722,22 @@ msgstr "Réseau" msgid "Next" msgstr "Suivant" -#: app/components/stf/device/device-info-filter/index.js:115 +#: app/components/stf/device/device-info-filter/index.js:117 msgid "No" msgstr "Non" -#: app/settings/keys/access-tokens/access-tokens.html:1 -msgid "No access tokens" -msgstr "Pas d'accès aux jetons" - #: app/settings/keys/adb-keys/adb-keys.html:1 msgid "No ADB keys" msgstr "Pas de clefs ADB" +#: app/control-panes/advanced/port-forwarding/port-forwarding.html:1 +msgid "No Ports Forwarded" +msgstr "Pas de ports redirigés" + +#: app/settings/keys/access-tokens/access-tokens.html:1 +msgid "No access tokens" +msgstr "Pas d'accès aux jetons" + #: app/components/stf/control/control-service.js:126 msgid "No clipboard data" msgstr "Pas de données dans le Presse-Papier" @@ -740,10 +758,6 @@ msgstr "Pas de terminaux connectés" msgid "No photo available" msgstr "Pas de photos disponibles" -#: app/control-panes/advanced/port-forwarding/port-forwarding.html:1 -msgid "No Ports Forwarded" -msgstr "Pas de ports redirigés" - #: app/control-panes/screenshots/screenshots.html:5 msgid "No screenshots taken" msgstr "Pas de captures d'écran prises" @@ -752,11 +766,11 @@ msgstr "Pas de captures d'écran prises" msgid "Normal Mode" msgstr "Mode Normal" -#: app/components/stf/device/device-info-filter/index.js:70 +#: app/components/stf/device/device-info-filter/index.js:72 msgid "Not Charging" msgstr "Pas en charge" -#: app/device-list/column/device-column-service.js:256 +#: app/device-list/column/device-column-service.js:262 msgid "Notes" msgstr "Notes" @@ -772,7 +786,12 @@ msgstr "Notifications" msgid "Number" msgstr "Nombre" -#: app/components/stf/device/device-info-filter/index.js:22 +#: app/control-panes/info/info.html:1 +#: app/device-list/column/device-column-service.js:55 +msgid "OS" +msgstr "OS" + +#: app/components/stf/device/device-info-filter/index.js:23 #: app/components/stf/device/device-info-filter/index.js:7 msgid "Offline" msgstr "Hors Ligne" @@ -790,19 +809,18 @@ msgstr "Ouvrir" msgid "Orientation" msgstr "Orientation" -#: app/control-panes/info/info.html:1 -#: app/device-list/column/device-column-service.js:55 -msgid "OS" -msgstr "OS" - -#: app/components/stf/device/device-info-filter/index.js:49 +#: app/components/stf/device/device-info-filter/index.js:51 msgid "Over Voltage" msgstr "Surtension" -#: app/components/stf/device/device-info-filter/index.js:50 +#: app/components/stf/device/device-info-filter/index.js:52 msgid "Overheat" msgstr "Surchauffe" +#: app/control-panes/logs/logs.html:1 +msgid "PID" +msgstr "PID" + #: app/control-panes/dashboard/install/activities/activities.html:1 msgid "Package" msgstr "Paquet" @@ -812,6 +830,10 @@ msgstr "Paquet" msgid "Password" msgstr "Mot de Passe" +#: app/control-panes/resources/resources.html:1 +msgid "Path" +msgstr "Chemin" + #: app/control-panes/explorer/explorer.html:1 msgid "Permissions" msgstr "Permissions" @@ -820,7 +842,7 @@ msgstr "Permissions" msgid "Phone" msgstr "Téléphone" -#: app/device-list/column/device-column-service.js:196 +#: app/device-list/column/device-column-service.js:202 msgid "Phone ICCID" msgstr "ICCID du Téléphone" @@ -828,14 +850,14 @@ msgstr "ICCID du Téléphone" msgid "Phone IMEI" msgstr "IMEI du Téléphone" +#: app/device-list/column/device-column-service.js:196 +msgid "Phone IMSI" +msgstr "IMSI du Téléphone" + #: app/control-panes/info/info.html:1 msgid "Physical Device" msgstr "Terminal Physique" -#: app/control-panes/logs/logs.html:1 -msgid "PID" -msgstr "PID" - #: app/control-panes/info/info.html:1 msgid "Place" msgstr "Place" @@ -852,22 +874,10 @@ msgstr "Jouer/Pause" msgid "Please enter a valid email" msgstr "S'il vous plaît entrez un e-mail valide" -#: auth/mock/scripts/signin/signin.html:1 -msgid "Please enter your email" -msgstr "S'il vous plaît entrez vôtre e-mail" - #: auth/ldap/scripts/signin/signin.html:1 msgid "Please enter your LDAP username" msgstr "S'il vous plaît entrez vôtre compte LDAP" -#: auth/mock/scripts/signin/signin.html:1 -msgid "Please enter your name" -msgstr "S'il vous plaît entrez vôtre nom" - -#: auth/ldap/scripts/signin/signin.html:1 -msgid "Please enter your password" -msgstr "S'il vous plaît entrez vôtre mot de passe" - #: app/control-panes/automation/store-account/store-account.html:1 msgid "Please enter your Store password" msgstr "S'il vous plaît entrez vôtre mot de passe du Store" @@ -876,6 +886,18 @@ msgstr "S'il vous plaît entrez vôtre mot de passe du Store" msgid "Please enter your Store username" msgstr "S'il vous plaît entrez vôtre identifiant du Store" +#: auth/mock/scripts/signin/signin.html:1 +msgid "Please enter your email" +msgstr "S'il vous plaît entrez vôtre e-mail" + +#: auth/mock/scripts/signin/signin.html:1 +msgid "Please enter your name" +msgstr "S'il vous plaît entrez vôtre nom" + +#: auth/ldap/scripts/signin/signin.html:1 +msgid "Please enter your password" +msgstr "S'il vous plaît entrez vôtre mot de passe" + #: app/control-panes/advanced/port-forwarding/port-forwarding.html:1 #: app/control-panes/advanced/vnc/vnc.html:1 msgid "Port" @@ -897,7 +919,7 @@ msgstr "Alimentation" msgid "Power Source" msgstr "Source d'Alimentation" -#: app/components/stf/device/device-info-filter/index.js:24 +#: app/components/stf/device/device-info-filter/index.js:25 #: app/components/stf/device/device-info-filter/index.js:9 msgid "Preparing" msgstr "En Préparation" @@ -935,8 +957,12 @@ msgstr "En cours de téléversement des Applications ...." msgid "RAM" msgstr "RAM" +#: app/control-panes/info/info.html:1 +msgid "ROM" +msgstr "ROM" + #: app/components/stf/device/device-info-filter/index.js:10 -#: app/components/stf/device/device-info-filter/index.js:25 +#: app/components/stf/device/device-info-filter/index.js:26 msgid "Ready" msgstr "Prêt" @@ -972,14 +998,14 @@ msgstr "Enlever" msgid "Reset" msgstr "Réinitialiser" -#: app/control-panes/dashboard/navigation/navigation.html:1 -msgid "Reset all browser settings" -msgstr "Réinitialiser tous les paramètres des navigateurs" - #: app/settings/general/local/local-settings.html:1 msgid "Reset Settings" msgstr "Réinitialiser les paramètres" +#: app/control-panes/dashboard/navigation/navigation.html:1 +msgid "Reset all browser settings" +msgstr "Réinitialiser tous les paramètres des navigateurs" + #: app/control-panes/advanced/maintenance/maintenance.html:1 msgid "Restart Device" msgstr "Redémarrer le Terminal" @@ -1000,10 +1026,6 @@ msgstr "Rembobiner" msgid "Roaming" msgstr "Roaming" -#: app/control-panes/info/info.html:1 -msgid "ROM" -msgstr "ROM" - #: app/components/stf/device-context-menu/device-context-menu.html:1 #: app/control-panes/control-panes-hotkeys-controller.js:92 msgid "Rotate Left" @@ -1036,6 +1058,19 @@ msgstr "Exécutez la commande suivante sur la ligne de commande pour déboguer l msgid "Run this command to copy the key to your clipboard" msgstr "Exécutez cette commande pour copier la clef de votre presse-papier" +#: app/control-panes/info/info.html:1 +msgid "SD Card Mounted" +msgstr "Carte SD Monté" + +#: app/control-panes/info/info.html:1 +#: app/device-list/column/device-column-service.js:171 +msgid "SDK" +msgstr "SDK" + +#: app/control-panes/info/info.html:1 +msgid "SIM" +msgstr "SIM" + #: app/components/stf/device-context-menu/device-context-menu.html:1 msgid "Save ScreenShot" msgstr "Sauver la capture d'écran" @@ -1056,19 +1091,14 @@ msgstr "Capture d'écran" msgid "Screenshots" msgstr "Captures d'écran" -#: app/control-panes/info/info.html:1 -msgid "SD Card Mounted" -msgstr "Carte SD Monté" - -#: app/control-panes/info/info.html:1 -#: app/device-list/column/device-column-service.js:171 -msgid "SDK" -msgstr "SDK" - #: app/control-panes/advanced/input/input.html:1 msgid "Search" msgstr "Rechercher" +#: app/control-panes/resources/resources.html:1 +msgid "Secure" +msgstr "Protéger" + #: app/control-panes/control-panes-hotkeys-controller.js:91 msgid "Selects Next IME" msgstr "Sélectionner le prochain IME" @@ -1119,10 +1149,6 @@ msgstr "Se déconnecter" msgid "Silent Mode" msgstr "Mode Silencieux" -#: app/control-panes/info/info.html:1 -msgid "SIM" -msgstr "SIM" - #: app/control-panes/explorer/explorer.html:1 #: app/control-panes/info/info.html:1 msgid "Size" @@ -1132,7 +1158,7 @@ msgstr "Taille" msgid "Socket connection was lost" msgstr "La connexion au Socket a été perdu" -#: app/components/stf/device/device-info-filter/index.js:36 +#: app/components/stf/device/device-info-filter/index.js:38 msgid "Someone stole your device." msgstr "Quelqu'un a volé votre terminal." @@ -1154,6 +1180,10 @@ msgstr "Statut" msgid "Stop" msgstr "Arrêter" +#: app/components/stf/device/device-info-filter/index.js:14 +msgid "Stop Automation" +msgstr "Arrêter l'automatisation" + #: app/components/stf/device-context-menu/device-context-menu.html:1 #: app/components/stf/device/device-info-filter/index.js:11 #: app/control-panes/device-control/device-control.html:1 @@ -1172,6 +1202,10 @@ msgstr "Sous Type" msgid "Switch Charset" msgstr "Permuter le Charset" +#: app/control-panes/logs/logs.html:1 +msgid "TID" +msgstr "TID" + #: app/control-panes/logs/logs.html:1 msgid "Tag" msgstr "étiquette" @@ -1192,6 +1226,10 @@ msgstr "Température" msgid "Text" msgstr "Texte" +#: app/components/stf/install/install-error-filter.js:22 +msgid "The URI passed in is invalid." +msgstr "L'URI transmise n'est pas invalide." + #: app/components/stf/screen/screen.html:1 msgid "The current view is marked secure and cannot be viewed remotely." msgstr "La vue actuelle est marqué sécurisé et ne peut être consulté à distance." @@ -1308,6 +1346,12 @@ msgstr "L'analyseur n'a pas trouvé toutes les tags actionnables (de l'instrumen msgid "The parser did not find any certificates in the .apk." msgstr "L'analyseur n'a pas trouvé de certificat dans le fichier .apk." +#: app/components/stf/install/install-error-filter.js:76 +msgid "" +"The parser encountered a CertificateEncodingException in one of the files in" +" the .apk." +msgstr "L'analyseur a rencontré une CertificateEncodingException dans l'un des fichiers de l'apk." + #: app/components/stf/install/install-error-filter.js:78 msgid "The parser encountered a bad or missing package name in the manifest." msgstr "L'analyseur a rencontré un mauvais ou manquant nom du paquet dans le manifest." @@ -1316,12 +1360,6 @@ msgstr "L'analyseur a rencontré un mauvais ou manquant nom du paquet dans le ma msgid "The parser encountered a bad shared user id name in the manifest." msgstr "L'analyseur a rencontré un mauvais nom d'utilisateur partagé dans le manifest." -#: app/components/stf/install/install-error-filter.js:76 -msgid "" -"The parser encountered a CertificateEncodingException in one of the files in" -" the .apk." -msgstr "L'analyseur a rencontré une CertificateEncodingException dans l'un des fichiers de l'apk." - #: app/components/stf/install/install-error-filter.js:70 msgid "The parser encountered an unexpected exception." msgstr "L'analyseur a rencontré une exception inattendue." @@ -1364,14 +1402,6 @@ msgid "" " installing apps." msgstr "Le système n'a pas réussi à installer le paquet parce que l'utilisateur est limité à partir de l'installation d'applications." -#: app/components/stf/install/install-error-filter.js:22 -msgid "The URI passed in is invalid." -msgstr "L'URI transmise n'est pas invalide." - -#: app/control-panes/logs/logs.html:1 -msgid "TID" -msgstr "TID" - #: app/control-panes/logs/logs.html:1 msgid "Time" msgstr "Temps" @@ -1392,11 +1422,6 @@ msgstr "Basculer de Web/Natif" msgid "Total Devices" msgstr "Nombre total de Terminaux" -#: app/components/stf/common-ui/modals/fatal-message/fatal-message.html:1 -#: app/control-panes/resources/resources.html:1 -msgid "translate" -msgstr "Traduire" - #: app/components/stf/common-ui/modals/fatal-message/fatal-message.html:1 #: app/components/stf/common-ui/modals/socket-disconnected/socket-disconnected.html:1 msgid "Try to reconnect" @@ -1406,7 +1431,11 @@ msgstr "Essayer de se reconnecter" msgid "Type" msgstr "Type" -#: app/components/stf/device/device-info-filter/index.js:23 +#: app/components/stf/device/device-info-filter/index.js:61 +msgid "USB" +msgstr "USB" + +#: app/components/stf/device/device-info-filter/index.js:24 #: app/components/stf/device/device-info-filter/index.js:8 msgid "Unauthorized" msgstr "Non Autorisé" @@ -1415,12 +1444,12 @@ msgstr "Non Autorisé" msgid "Uninstall" msgstr "Désinstaller" -#: app/components/stf/device/device-info-filter/index.js:14 -#: app/components/stf/device/device-info-filter/index.js:29 +#: app/components/stf/device/device-info-filter/index.js:15 +#: app/components/stf/device/device-info-filter/index.js:31 msgid "Unknown" msgstr "Inconnu" -#: app/components/stf/device/device-info-filter/index.js:40 +#: app/components/stf/device/device-info-filter/index.js:42 msgid "Unknown reason." msgstr "Raison inconnue." @@ -1428,18 +1457,18 @@ msgstr "Raison inconnue." msgid "Unlock Rotation" msgstr "Débloquer la Rotation" -#: app/components/stf/device/device-info-filter/index.js:51 +#: app/components/stf/device/device-info-filter/index.js:53 msgid "Unspecified Failure" msgstr "Défaillance non spécifiée" -#: app/components/stf/upload/upload-error-filter.js:7 -msgid "Upload failed" -msgstr "Téléversement raté" - #: app/control-panes/dashboard/install/install.html:5 msgid "Upload From Link" msgstr "Téléverser depuis le Lien" +#: app/components/stf/upload/upload-error-filter.js:7 +msgid "Upload failed" +msgstr "Téléversement raté" + #: app/components/stf/upload/upload-error-filter.js:8 msgid "Upload unknown error" msgstr "Erreur inconnue lors du Téléversement" @@ -1456,10 +1485,6 @@ msgstr "En cours de téléversement ..." msgid "Usable Devices" msgstr "Terminaux utilisables" -#: app/components/stf/device/device-info-filter/index.js:59 -msgid "USB" -msgstr "USB" - #: app/control-panes/advanced/usb/usb.html:1 msgid "Usb speed" msgstr "Vitesse USB" @@ -1468,7 +1493,7 @@ msgstr "Vitesse USB" msgid "Use" msgstr "Utiliser" -#: app/device-list/column/device-column-service.js:262 +#: app/device-list/column/device-column-service.js:268 msgid "User" msgstr "Utilisateur" @@ -1476,7 +1501,7 @@ msgstr "Utilisateur" msgid "Username" msgstr "Nom de l'utilisateur" -#: app/components/stf/device/device-info-filter/index.js:26 +#: app/components/stf/device/device-info-filter/index.js:27 msgid "Using" msgstr "En Utilisation" @@ -1484,6 +1509,14 @@ msgstr "En Utilisation" msgid "Using Fallback" msgstr "Reprise de l'Utilisation" +#: app/control-panes/advanced/vnc/vnc.html:1 +msgid "VNC" +msgstr "VNC" + +#: app/control-panes/resources/resources.html:1 +msgid "Value" +msgstr "Valeur" + #: app/control-panes/info/info.html:1 msgid "Version" msgstr "Version" @@ -1496,10 +1529,6 @@ msgstr "Version de la mise à jour" msgid "Vibrate Mode" msgstr "Mode Vibration" -#: app/control-panes/advanced/vnc/vnc.html:1 -msgid "VNC" -msgstr "VNC" - #: app/control-panes/info/info.html:1 msgid "Voltage" msgstr "Tension" @@ -1524,22 +1553,22 @@ msgstr "Avertissement:" msgid "Web" msgstr "Web" -#: app/control-panes/info/info.html:1 -msgid "Width" -msgstr "Largeur" - -#: app/components/stf/device/device-info-filter/index.js:105 -#: app/components/stf/device/device-info-filter/index.js:97 +#: app/components/stf/device/device-info-filter/index.js:107 +#: app/components/stf/device/device-info-filter/index.js:99 #: app/control-panes/automation/device-settings/device-settings.html:1 #: app/control-panes/dashboard/apps/apps.html:1 msgid "WiFi" msgstr "Wifi" -#: app/components/stf/device/device-info-filter/index.js:98 +#: app/components/stf/device/device-info-filter/index.js:100 msgid "WiMAX" msgstr "WiMax" -#: app/components/stf/device/device-info-filter/index.js:60 +#: app/control-panes/info/info.html:1 +msgid "Width" +msgstr "Largeur" + +#: app/components/stf/device/device-info-filter/index.js:62 msgid "Wireless" msgstr "Sans Fil" @@ -1551,10 +1580,15 @@ msgstr "X DPI" msgid "Y DPI" msgstr "Y DPI" -#: app/components/stf/device/device-info-filter/index.js:113 +#: app/components/stf/device/device-info-filter/index.js:115 msgid "Yes" msgstr "Oui" -#: app/components/stf/device/device-info-filter/index.js:35 +#: app/components/stf/device/device-info-filter/index.js:37 msgid "You (or someone else) kicked the device." msgstr "Vous (ou quelqu'un d'autre) a exclu le Terminal." + +#: app/components/stf/common-ui/modals/fatal-message/fatal-message.html:1 +#: app/control-panes/resources/resources.html:1 +msgid "translate" +msgstr "Traduire" diff --git a/res/common/lang/po/stf.pot b/res/common/lang/po/stf.pot index ab17d879..5848c05b 100644 --- a/res/common/lang/po/stf.pot +++ b/res/common/lang/po/stf.pot @@ -4,10 +4,10 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Project-Id-Version: \n" -#: app/components/stf/device/device-info-filter/index.js:117 -#: app/components/stf/device/device-info-filter/index.js:52 -#: app/components/stf/device/device-info-filter/index.js:61 -#: app/components/stf/device/device-info-filter/index.js:71 +#: app/components/stf/device/device-info-filter/index.js:119 +#: app/components/stf/device/device-info-filter/index.js:54 +#: app/components/stf/device/device-info-filter/index.js:63 +#: app/components/stf/device/device-info-filter/index.js:73 msgid "-" msgstr "" @@ -28,14 +28,18 @@ msgid "A secure container mount point couldn't be accessed on external media." msgstr "" #: app/control-panes/info/info.html:1 -#: app/device-list/column/device-column-service.js:178 +#: app/device-list/column/device-column-service.js:193 msgid "ABI" msgstr "" -#: app/components/stf/device/device-info-filter/index.js:58 +#: app/components/stf/device/device-info-filter/index.js:60 msgid "AC" msgstr "" +#: app/settings/keys/adb-keys/adb-keys.html:1 +msgid "ADB Keys" +msgstr "" + #: app/settings/keys/access-tokens/access-tokens.html:1 msgid "Access Tokens" msgstr "" @@ -56,10 +60,6 @@ msgstr "" msgid "Activity" msgstr "" -#: app/settings/keys/adb-keys/adb-keys.html:1 -msgid "ADB Keys" -msgstr "" - #: app/control-panes/resources/resources.html:1 msgid "Add" msgstr "" @@ -106,6 +106,10 @@ msgstr "" msgid "App Upload" msgstr "" +#: app/control-panes/device-control/device-control.html:1 +msgid "App switch" +msgstr "" + #: app/control-panes/dashboard/apps/apps.html:1 msgid "Apps" msgstr "" @@ -114,11 +118,15 @@ msgstr "" msgid "Are you sure you want to reboot this device?" msgstr "" +#: app/components/stf/device/device-info-filter/index.js:30 +msgid "Automating" +msgstr "" + #: app/control-panes/control-panes-controller.js:14 msgid "Automation" msgstr "" -#: app/components/stf/device/device-info-filter/index.js:28 +#: app/components/stf/device/device-info-filter/index.js:29 msgid "Available" msgstr "" @@ -131,36 +139,36 @@ msgstr "" msgid "Battery" msgstr "" -#: app/device-list/column/device-column-service.js:202 +#: app/device-list/column/device-column-service.js:235 msgid "Battery Health" msgstr "" -#: app/device-list/column/device-column-service.js:226 +#: app/device-list/column/device-column-service.js:259 msgid "Battery Level" msgstr "" -#: app/device-list/column/device-column-service.js:210 +#: app/device-list/column/device-column-service.js:243 msgid "Battery Source" msgstr "" -#: app/device-list/column/device-column-service.js:218 +#: app/device-list/column/device-column-service.js:251 msgid "Battery Status" msgstr "" -#: app/device-list/column/device-column-service.js:239 +#: app/device-list/column/device-column-service.js:270 msgid "Battery Temp" msgstr "" -#: app/components/stf/device/device-info-filter/index.js:89 +#: app/components/stf/device/device-info-filter/index.js:91 msgid "Bluetooth" msgstr "" -#: app/device-list/column/device-column-service.js:153 +#: app/device-list/column/device-column-service.js:165 msgid "Browser" msgstr "" #: app/components/stf/device/device-info-filter/index.js:12 -#: app/components/stf/device/device-info-filter/index.js:27 +#: app/components/stf/device/device-info-filter/index.js:28 msgid "Busy" msgstr "" @@ -168,6 +176,15 @@ msgstr "" msgid "Busy Devices" msgstr "" +#: app/control-panes/info/info.html:1 +#: app/control-panes/performance/cpu/cpu.html:1 +msgid "CPU" +msgstr "" + +#: app/device-list/column/device-column-service.js:199 +msgid "CPU Platform" +msgstr "" + #: app/control-panes/advanced/input/input.html:1 msgid "Camera" msgstr "" @@ -189,7 +206,7 @@ msgstr "" msgid "Category" msgstr "" -#: app/components/stf/device/device-info-filter/index.js:67 +#: app/components/stf/device/device-info-filter/index.js:69 msgid "Charging" msgstr "" @@ -208,11 +225,11 @@ msgstr "" msgid "Clipboard" msgstr "" -#: app/components/stf/device/device-info-filter/index.js:46 +#: app/components/stf/device/device-info-filter/index.js:48 msgid "Cold" msgstr "" -#: app/components/stf/device/device-info-filter/index.js:21 +#: app/components/stf/device/device-info-filter/index.js:22 #: app/components/stf/device/device-info-filter/index.js:6 #: app/control-panes/info/info.html:1 msgid "Connected" @@ -234,11 +251,6 @@ msgstr "" msgid "Cores" msgstr "" -#: app/control-panes/info/info.html:1 -#: app/control-panes/performance/cpu/cpu.html:1 -msgid "CPU" -msgstr "" - #: app/control-panes/device-control/device-control.html:1 msgid "Current rotation:" msgstr "" @@ -279,7 +291,7 @@ msgstr "" msgid "Date" msgstr "" -#: app/components/stf/device/device-info-filter/index.js:48 +#: app/components/stf/device/device-info-filter/index.js:50 msgid "Dead" msgstr "" @@ -306,19 +318,6 @@ msgstr "" msgid "Device" msgstr "" -#: app/device-list/details/device-list-details-directive.js:38 -#: app/device-list/icons/device-list-icons-directive.js:122 -msgid "Device cannot get kicked from the group" -msgstr "" - -#: app/components/stf/device/device-info-filter/index.js:38 -msgid "Device is not present anymore for some reason." -msgstr "" - -#: app/components/stf/device/device-info-filter/index.js:39 -msgid "Device is present but offline." -msgstr "" - #: app/control-panes/info/info.html:1 msgid "Device Photo" msgstr "" @@ -327,11 +326,24 @@ msgstr "" msgid "Device Settings" msgstr "" +#: app/device-list/details/device-list-details-directive.js:38 +#: app/device-list/icons/device-list-icons-directive.js:123 +msgid "Device cannot get kicked from the group" +msgstr "" + +#: app/components/stf/device/device-info-filter/index.js:40 +msgid "Device is not present anymore for some reason." +msgstr "" + +#: app/components/stf/device/device-info-filter/index.js:41 +msgid "Device is present but offline." +msgstr "" + #: app/components/stf/common-ui/modals/fatal-message/fatal-message.html:1 msgid "Device was disconnected" msgstr "" -#: app/components/stf/device/device-info-filter/index.js:37 +#: app/components/stf/device/device-info-filter/index.js:39 msgid "Device was kicked by automatic timeout." msgstr "" @@ -344,12 +356,12 @@ msgstr "" msgid "Disable WiFi" msgstr "" -#: app/components/stf/device/device-info-filter/index.js:68 +#: app/components/stf/device/device-info-filter/index.js:70 msgid "Discharging" msgstr "" #: app/components/stf/common-ui/modals/socket-disconnected/socket-disconnected.html:1 -#: app/components/stf/device/device-info-filter/index.js:20 +#: app/components/stf/device/device-info-filter/index.js:21 #: app/components/stf/device/device-info-filter/index.js:5 msgid "Disconnected" msgstr "" @@ -358,22 +370,26 @@ msgstr "" msgid "Display" msgstr "" +#: app/control-panes/resources/resources.html:1 +msgid "Domain" +msgstr "" + #: app/control-panes/dashboard/install/install.html:7 msgid "Drop file to upload" msgstr "" -#: app/components/stf/device/device-info-filter/index.js:90 +#: app/components/stf/device/device-info-filter/index.js:92 msgid "Dummy" msgstr "" -#: app/settings/notifications/notifications.html:1 -msgid "Enable notifications" -msgstr "" - #: app/control-panes/automation/device-settings/device-settings.html:1 msgid "Enable WiFi" msgstr "" +#: app/settings/notifications/notifications.html:1 +msgid "Enable notifications" +msgstr "" + #: app/control-panes/info/info.html:1 msgid "Encrypted" msgstr "" @@ -382,7 +398,7 @@ msgstr "" msgid "Error" msgstr "" -#: app/components/stf/control/control-service.js:129 +#: app/components/stf/control/control-service.js:130 msgid "Error while getting data" msgstr "" @@ -390,7 +406,7 @@ msgstr "" msgid "Error while reconnecting" msgstr "" -#: app/components/stf/device/device-info-filter/index.js:91 +#: app/components/stf/device/device-info-filter/index.js:93 msgid "Ethernet" msgstr "" @@ -398,6 +414,10 @@ msgstr "" msgid "Executes remote shell commands" msgstr "" +#: app/control-panes/info/info.html:1 +msgid "FPS" +msgstr "" + #: app/components/stf/upload/upload-error-filter.js:5 msgid "Failed to download file" msgstr "" @@ -422,15 +442,11 @@ msgstr "" msgid "Fingerprint" msgstr "" -#: app/control-panes/info/info.html:1 -msgid "FPS" -msgstr "" - #: app/control-panes/info/info.html:1 msgid "Frequency" msgstr "" -#: app/components/stf/device/device-info-filter/index.js:69 +#: app/components/stf/device/device-info-filter/index.js:71 msgid "Full" msgstr "" @@ -468,11 +484,11 @@ msgid "Go Forward" msgstr "" #: app/components/stf/common-ui/modals/fatal-message/fatal-message.html:1 -#: app/control-panes/control-panes-hotkeys-controller.js:89 +#: app/control-panes/control-panes-hotkeys-controller.js:92 msgid "Go to Device List" msgstr "" -#: app/components/stf/device/device-info-filter/index.js:47 +#: app/components/stf/device/device-info-filter/index.js:49 msgid "Good" msgstr "" @@ -521,6 +537,10 @@ msgstr "" msgid "IMEI" msgstr "" +#: app/control-panes/info/info.html:1 +msgid "IMSI" +msgstr "" + #: auth/ldap/scripts/signin/signin.html:1 #: auth/mock/scripts/signin/signin.html:1 msgid "Incorrect login details" @@ -595,7 +615,7 @@ msgstr "" msgid "Local Settings" msgstr "" -#: app/device-list/column/device-column-service.js:250 +#: app/device-list/column/device-column-service.js:279 msgid "Location" msgstr "" @@ -624,7 +644,7 @@ msgid "Manner Mode" msgstr "" #: app/control-panes/info/info.html:1 -#: app/device-list/column/device-column-service.js:165 +#: app/device-list/column/device-column-service.js:177 msgid "Manufacturer" msgstr "" @@ -640,23 +660,23 @@ msgstr "" msgid "Menu" msgstr "" -#: app/components/stf/device/device-info-filter/index.js:92 +#: app/components/stf/device/device-info-filter/index.js:94 msgid "Mobile" msgstr "" -#: app/components/stf/device/device-info-filter/index.js:93 +#: app/components/stf/device/device-info-filter/index.js:95 msgid "Mobile DUN" msgstr "" -#: app/components/stf/device/device-info-filter/index.js:94 +#: app/components/stf/device/device-info-filter/index.js:96 msgid "Mobile High Priority" msgstr "" -#: app/components/stf/device/device-info-filter/index.js:95 +#: app/components/stf/device/device-info-filter/index.js:97 msgid "Mobile MMS" msgstr "" -#: app/components/stf/device/device-info-filter/index.js:96 +#: app/components/stf/device/device-info-filter/index.js:98 msgid "Mobile SUPL" msgstr "" @@ -665,20 +685,21 @@ msgstr "" msgid "Model" msgstr "" -#: app/settings/keys/access-tokens/access-tokens.html:1 -msgid "More about Access Tokens" -msgstr "" - #: app/settings/keys/adb-keys/adb-keys.html:1 msgid "More about ADB Keys" msgstr "" +#: app/settings/keys/access-tokens/access-tokens.html:1 +msgid "More about Access Tokens" +msgstr "" + #: app/control-panes/advanced/input/input.html:1 msgid "Mute" msgstr "" #: app/control-panes/explorer/explorer.html:1 #: app/control-panes/info/info.html:1 +#: app/control-panes/resources/resources.html:1 msgid "Name" msgstr "" @@ -699,19 +720,23 @@ msgstr "" msgid "Next" msgstr "" -#: app/components/stf/device/device-info-filter/index.js:115 +#: app/components/stf/device/device-info-filter/index.js:117 msgid "No" msgstr "" -#: app/settings/keys/access-tokens/access-tokens.html:1 -msgid "No access tokens" -msgstr "" - #: app/settings/keys/adb-keys/adb-keys.html:1 msgid "No ADB keys" msgstr "" -#: app/components/stf/control/control-service.js:126 +#: app/control-panes/advanced/port-forwarding/port-forwarding.html:1 +msgid "No Ports Forwarded" +msgstr "" + +#: app/settings/keys/access-tokens/access-tokens.html:1 +msgid "No access tokens" +msgstr "" + +#: app/components/stf/control/control-service.js:127 msgid "No clipboard data" msgstr "" @@ -731,10 +756,6 @@ msgstr "" msgid "No photo available" msgstr "" -#: app/control-panes/advanced/port-forwarding/port-forwarding.html:1 -msgid "No Ports Forwarded" -msgstr "" - #: app/control-panes/screenshots/screenshots.html:5 msgid "No screenshots taken" msgstr "" @@ -743,11 +764,11 @@ msgstr "" msgid "Normal Mode" msgstr "" -#: app/components/stf/device/device-info-filter/index.js:70 +#: app/components/stf/device/device-info-filter/index.js:72 msgid "Not Charging" msgstr "" -#: app/device-list/column/device-column-service.js:256 +#: app/device-list/column/device-column-service.js:285 msgid "Notes" msgstr "" @@ -763,7 +784,12 @@ msgstr "" msgid "Number" msgstr "" -#: app/components/stf/device/device-info-filter/index.js:22 +#: app/control-panes/info/info.html:1 +#: app/device-list/column/device-column-service.js:55 +msgid "OS" +msgstr "" + +#: app/components/stf/device/device-info-filter/index.js:23 #: app/components/stf/device/device-info-filter/index.js:7 msgid "Offline" msgstr "" @@ -777,23 +803,26 @@ msgstr "" msgid "Open" msgstr "" +#: app/device-list/column/device-column-service.js:205 +msgid "OpenGL ES version" +msgstr "" + #: app/control-panes/info/info.html:1 msgid "Orientation" msgstr "" -#: app/control-panes/info/info.html:1 -#: app/device-list/column/device-column-service.js:55 -msgid "OS" -msgstr "" - -#: app/components/stf/device/device-info-filter/index.js:49 +#: app/components/stf/device/device-info-filter/index.js:51 msgid "Over Voltage" msgstr "" -#: app/components/stf/device/device-info-filter/index.js:50 +#: app/components/stf/device/device-info-filter/index.js:52 msgid "Overheat" msgstr "" +#: app/control-panes/logs/logs.html:1 +msgid "PID" +msgstr "" + #: app/control-panes/dashboard/install/activities/activities.html:1 msgid "Package" msgstr "" @@ -803,30 +832,34 @@ msgstr "" msgid "Password" msgstr "" +#: app/control-panes/resources/resources.html:1 +msgid "Path" +msgstr "" + #: app/control-panes/explorer/explorer.html:1 msgid "Permissions" msgstr "" -#: app/device-list/column/device-column-service.js:184 +#: app/device-list/column/device-column-service.js:211 msgid "Phone" msgstr "" -#: app/device-list/column/device-column-service.js:196 +#: app/device-list/column/device-column-service.js:229 msgid "Phone ICCID" msgstr "" -#: app/device-list/column/device-column-service.js:190 +#: app/device-list/column/device-column-service.js:217 msgid "Phone IMEI" msgstr "" +#: app/device-list/column/device-column-service.js:223 +msgid "Phone IMSI" +msgstr "" + #: app/control-panes/info/info.html:1 msgid "Physical Device" msgstr "" -#: app/control-panes/logs/logs.html:1 -msgid "PID" -msgstr "" - #: app/control-panes/info/info.html:1 msgid "Place" msgstr "" @@ -843,22 +876,10 @@ msgstr "" msgid "Please enter a valid email" msgstr "" -#: auth/mock/scripts/signin/signin.html:1 -msgid "Please enter your email" -msgstr "" - #: auth/ldap/scripts/signin/signin.html:1 msgid "Please enter your LDAP username" msgstr "" -#: auth/mock/scripts/signin/signin.html:1 -msgid "Please enter your name" -msgstr "" - -#: auth/ldap/scripts/signin/signin.html:1 -msgid "Please enter your password" -msgstr "" - #: app/control-panes/automation/store-account/store-account.html:1 msgid "Please enter your Store password" msgstr "" @@ -867,6 +888,18 @@ msgstr "" msgid "Please enter your Store username" msgstr "" +#: auth/mock/scripts/signin/signin.html:1 +msgid "Please enter your email" +msgstr "" + +#: auth/mock/scripts/signin/signin.html:1 +msgid "Please enter your name" +msgstr "" + +#: auth/ldap/scripts/signin/signin.html:1 +msgid "Please enter your password" +msgstr "" + #: app/control-panes/advanced/port-forwarding/port-forwarding.html:1 #: app/control-panes/advanced/vnc/vnc.html:1 msgid "Port" @@ -888,20 +921,20 @@ msgstr "" msgid "Power Source" msgstr "" -#: app/components/stf/device/device-info-filter/index.js:24 +#: app/components/stf/device/device-info-filter/index.js:25 #: app/components/stf/device/device-info-filter/index.js:9 msgid "Preparing" msgstr "" -#: app/control-panes/control-panes-hotkeys-controller.js:104 +#: app/control-panes/control-panes-hotkeys-controller.js:107 msgid "Press Back button" msgstr "" -#: app/control-panes/control-panes-hotkeys-controller.js:103 +#: app/control-panes/control-panes-hotkeys-controller.js:106 msgid "Press Home button" msgstr "" -#: app/control-panes/control-panes-hotkeys-controller.js:102 +#: app/control-panes/control-panes-hotkeys-controller.js:105 msgid "Press Menu button" msgstr "" @@ -926,11 +959,19 @@ msgstr "" msgid "RAM" msgstr "" +#: app/control-panes/info/info.html:1 +msgid "ROM" +msgstr "" + #: app/components/stf/device/device-info-filter/index.js:10 -#: app/components/stf/device/device-info-filter/index.js:25 +#: app/components/stf/device/device-info-filter/index.js:26 msgid "Ready" msgstr "" +#: app/components/stf/device-context-menu/device-context-menu.html:1 +msgid "Recents" +msgstr "" + #: app/components/stf/socket/socket-state/socket-state-directive.js:39 msgid "Reconnected successfully." msgstr "" @@ -963,14 +1004,14 @@ msgstr "" msgid "Reset" msgstr "" -#: app/control-panes/dashboard/navigation/navigation.html:1 -msgid "Reset all browser settings" -msgstr "" - #: app/settings/general/local/local-settings.html:1 msgid "Reset Settings" msgstr "" +#: app/control-panes/dashboard/navigation/navigation.html:1 +msgid "Reset all browser settings" +msgstr "" + #: app/control-panes/advanced/maintenance/maintenance.html:1 msgid "Restart Device" msgstr "" @@ -991,17 +1032,13 @@ msgstr "" msgid "Roaming" msgstr "" -#: app/control-panes/info/info.html:1 -msgid "ROM" -msgstr "" - #: app/components/stf/device-context-menu/device-context-menu.html:1 -#: app/control-panes/control-panes-hotkeys-controller.js:92 +#: app/control-panes/control-panes-hotkeys-controller.js:95 msgid "Rotate Left" msgstr "" #: app/components/stf/device-context-menu/device-context-menu.html:1 -#: app/control-panes/control-panes-hotkeys-controller.js:93 +#: app/control-panes/control-panes-hotkeys-controller.js:96 msgid "Rotate Right" msgstr "" @@ -1025,6 +1062,19 @@ msgstr "" msgid "Run this command to copy the key to your clipboard" msgstr "" +#: app/control-panes/info/info.html:1 +msgid "SD Card Mounted" +msgstr "" + +#: app/control-panes/info/info.html:1 +#: app/device-list/column/device-column-service.js:183 +msgid "SDK" +msgstr "" + +#: app/control-panes/info/info.html:1 +msgid "SIM" +msgstr "" + #: app/components/stf/device-context-menu/device-context-menu.html:1 msgid "Save ScreenShot" msgstr "" @@ -1033,7 +1083,7 @@ msgstr "" msgid "Save..." msgstr "" -#: app/device-list/column/device-column-service.js:135 +#: app/device-list/column/device-column-service.js:147 msgid "Screen" msgstr "" @@ -1045,25 +1095,20 @@ msgstr "" msgid "Screenshots" msgstr "" -#: app/control-panes/info/info.html:1 -msgid "SD Card Mounted" -msgstr "" - -#: app/control-panes/info/info.html:1 -#: app/device-list/column/device-column-service.js:171 -msgid "SDK" -msgstr "" - #: app/control-panes/advanced/input/input.html:1 msgid "Search" msgstr "" -#: app/control-panes/control-panes-hotkeys-controller.js:91 +#: app/control-panes/resources/resources.html:1 +msgid "Secure" +msgstr "" + +#: app/control-panes/control-panes-hotkeys-controller.js:94 msgid "Selects Next IME" msgstr "" #: app/control-panes/info/info.html:1 -#: app/device-list/column/device-column-service.js:159 +#: app/device-list/column/device-column-service.js:171 msgid "Serial" msgstr "" @@ -1109,10 +1154,6 @@ msgstr "" msgid "Silent Mode" msgstr "" -#: app/control-panes/info/info.html:1 -msgid "SIM" -msgstr "" - #: app/control-panes/explorer/explorer.html:1 #: app/control-panes/info/info.html:1 msgid "Size" @@ -1122,7 +1163,7 @@ msgstr "" msgid "Socket connection was lost" msgstr "" -#: app/components/stf/device/device-info-filter/index.js:36 +#: app/components/stf/device/device-info-filter/index.js:38 msgid "Someone stole your device." msgstr "" @@ -1144,6 +1185,10 @@ msgstr "" msgid "Stop" msgstr "" +#: app/components/stf/device/device-info-filter/index.js:14 +msgid "Stop Automation" +msgstr "" + #: app/components/stf/device-context-menu/device-context-menu.html:1 #: app/components/stf/device/device-info-filter/index.js:11 #: app/control-panes/device-control/device-control.html:1 @@ -1162,6 +1207,10 @@ msgstr "" msgid "Switch Charset" msgstr "" +#: app/control-panes/logs/logs.html:1 +msgid "TID" +msgstr "" + #: app/control-panes/logs/logs.html:1 msgid "Tag" msgstr "" @@ -1182,6 +1231,10 @@ msgstr "" msgid "Text" msgstr "" +#: app/components/stf/install/install-error-filter.js:22 +msgid "The URI passed in is invalid." +msgstr "" + #: app/components/stf/screen/screen.html:1 msgid "The current view is marked secure and cannot be viewed remotely." msgstr "" @@ -1274,6 +1327,10 @@ msgstr "" msgid "The parser did not find any certificates in the .apk." msgstr "" +#: app/components/stf/install/install-error-filter.js:76 +msgid "The parser encountered a CertificateEncodingException in one of the files in the .apk." +msgstr "" + #: app/components/stf/install/install-error-filter.js:78 msgid "The parser encountered a bad or missing package name in the manifest." msgstr "" @@ -1282,10 +1339,6 @@ msgstr "" msgid "The parser encountered a bad shared user id name in the manifest." msgstr "" -#: app/components/stf/install/install-error-filter.js:76 -msgid "The parser encountered a CertificateEncodingException in one of the files in the .apk." -msgstr "" - #: app/components/stf/install/install-error-filter.js:70 msgid "The parser encountered an unexpected exception." msgstr "" @@ -1322,14 +1375,6 @@ msgstr "" msgid "The system failed to install the package because the user is restricted from installing apps." msgstr "" -#: app/components/stf/install/install-error-filter.js:22 -msgid "The URI passed in is invalid." -msgstr "" - -#: app/control-panes/logs/logs.html:1 -msgid "TID" -msgstr "" - #: app/control-panes/logs/logs.html:1 msgid "Time" msgstr "" @@ -1342,7 +1387,7 @@ msgstr "" msgid "Title" msgstr "" -#: app/control-panes/control-panes-hotkeys-controller.js:107 +#: app/control-panes/control-panes-hotkeys-controller.js:110 msgid "Toggle Web/Native" msgstr "" @@ -1350,11 +1395,6 @@ msgstr "" msgid "Total Devices" msgstr "" -#: app/components/stf/common-ui/modals/fatal-message/fatal-message.html:1 -#: app/control-panes/resources/resources.html:1 -msgid "translate" -msgstr "" - #: app/components/stf/common-ui/modals/fatal-message/fatal-message.html:1 #: app/components/stf/common-ui/modals/socket-disconnected/socket-disconnected.html:1 msgid "Try to reconnect" @@ -1364,7 +1404,11 @@ msgstr "" msgid "Type" msgstr "" -#: app/components/stf/device/device-info-filter/index.js:23 +#: app/components/stf/device/device-info-filter/index.js:61 +msgid "USB" +msgstr "" + +#: app/components/stf/device/device-info-filter/index.js:24 #: app/components/stf/device/device-info-filter/index.js:8 msgid "Unauthorized" msgstr "" @@ -1373,12 +1417,12 @@ msgstr "" msgid "Uninstall" msgstr "" -#: app/components/stf/device/device-info-filter/index.js:14 -#: app/components/stf/device/device-info-filter/index.js:29 +#: app/components/stf/device/device-info-filter/index.js:15 +#: app/components/stf/device/device-info-filter/index.js:31 msgid "Unknown" msgstr "" -#: app/components/stf/device/device-info-filter/index.js:40 +#: app/components/stf/device/device-info-filter/index.js:42 msgid "Unknown reason." msgstr "" @@ -1386,18 +1430,18 @@ msgstr "" msgid "Unlock Rotation" msgstr "" -#: app/components/stf/device/device-info-filter/index.js:51 +#: app/components/stf/device/device-info-filter/index.js:53 msgid "Unspecified Failure" msgstr "" -#: app/components/stf/upload/upload-error-filter.js:7 -msgid "Upload failed" -msgstr "" - #: app/control-panes/dashboard/install/install.html:5 msgid "Upload From Link" msgstr "" +#: app/components/stf/upload/upload-error-filter.js:7 +msgid "Upload failed" +msgstr "" + #: app/components/stf/upload/upload-error-filter.js:8 msgid "Upload unknown error" msgstr "" @@ -1414,10 +1458,6 @@ msgstr "" msgid "Usable Devices" msgstr "" -#: app/components/stf/device/device-info-filter/index.js:59 -msgid "USB" -msgstr "" - #: app/control-panes/advanced/usb/usb.html:1 msgid "Usb speed" msgstr "" @@ -1426,7 +1466,7 @@ msgstr "" msgid "Use" msgstr "" -#: app/device-list/column/device-column-service.js:262 +#: app/device-list/column/device-column-service.js:291 msgid "User" msgstr "" @@ -1434,7 +1474,7 @@ msgstr "" msgid "Username" msgstr "" -#: app/components/stf/device/device-info-filter/index.js:26 +#: app/components/stf/device/device-info-filter/index.js:27 msgid "Using" msgstr "" @@ -1442,6 +1482,14 @@ msgstr "" msgid "Using Fallback" msgstr "" +#: app/control-panes/advanced/vnc/vnc.html:1 +msgid "VNC" +msgstr "" + +#: app/control-panes/resources/resources.html:1 +msgid "Value" +msgstr "" + #: app/control-panes/info/info.html:1 msgid "Version" msgstr "" @@ -1454,10 +1502,6 @@ msgstr "" msgid "Vibrate Mode" msgstr "" -#: app/control-panes/advanced/vnc/vnc.html:1 -msgid "VNC" -msgstr "" - #: app/control-panes/info/info.html:1 msgid "Voltage" msgstr "" @@ -1482,22 +1526,22 @@ msgstr "" msgid "Web" msgstr "" -#: app/control-panes/info/info.html:1 -msgid "Width" -msgstr "" - -#: app/components/stf/device/device-info-filter/index.js:105 -#: app/components/stf/device/device-info-filter/index.js:97 +#: app/components/stf/device/device-info-filter/index.js:107 +#: app/components/stf/device/device-info-filter/index.js:99 #: app/control-panes/automation/device-settings/device-settings.html:1 #: app/control-panes/dashboard/apps/apps.html:1 msgid "WiFi" msgstr "" -#: app/components/stf/device/device-info-filter/index.js:98 +#: app/components/stf/device/device-info-filter/index.js:100 msgid "WiMAX" msgstr "" -#: app/components/stf/device/device-info-filter/index.js:60 +#: app/control-panes/info/info.html:1 +msgid "Width" +msgstr "" + +#: app/components/stf/device/device-info-filter/index.js:62 msgid "Wireless" msgstr "" @@ -1509,10 +1553,15 @@ msgstr "" msgid "Y DPI" msgstr "" -#: app/components/stf/device/device-info-filter/index.js:113 +#: app/components/stf/device/device-info-filter/index.js:115 msgid "Yes" msgstr "" -#: app/components/stf/device/device-info-filter/index.js:35 +#: app/components/stf/device/device-info-filter/index.js:37 msgid "You (or someone else) kicked the device." msgstr "" + +#: app/components/stf/common-ui/modals/fatal-message/fatal-message.html:1 +#: app/control-panes/resources/resources.html:1 +msgid "translate" +msgstr "" diff --git a/res/common/lang/po/stf.pt_BR.po b/res/common/lang/po/stf.pt_BR.po new file mode 100644 index 00000000..f47abb6c --- /dev/null +++ b/res/common/lang/po/stf.pt_BR.po @@ -0,0 +1,1596 @@ +# +# Translators: +# Joao Pereira , 2017 +# John Voloski , 2017 +# Luiz Esmiralha , 2019 +# Luiz Lohn , 2017 +msgid "" +msgstr "" +"Project-Id-Version: STF\n" +"PO-Revision-Date: 2019-04-21 10:14+0000\n" +"Last-Translator: Luiz Esmiralha \n" +"Language-Team: Portuguese (Brazil) (http://www.transifex.com/openstf/stf/language/pt_BR/)\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: pt_BR\n" +"Plural-Forms: nplurals=2; plural=(n > 1);\n" + +#: app/components/stf/device/device-info-filter/index.js:119 +#: app/components/stf/device/device-info-filter/index.js:54 +#: app/components/stf/device/device-info-filter/index.js:63 +#: app/components/stf/device/device-info-filter/index.js:73 +msgid "-" +msgstr "-" + +#: app/components/stf/common-ui/modals/version-update/version-update.html:1 +msgid "A new version of STF is available" +msgstr "Uma nova versão do STF está disponível" + +#: app/components/stf/install/install-error-filter.js:26 +msgid "A package is already installed with the same name." +msgstr "Já existe um pacote instalado com este nome." + +#: app/components/stf/install/install-error-filter.js:30 +msgid "" +"A previously installed package of the same name has a different signature " +"than the new package (and the old package's data was not removed)." +msgstr "Um pacote instalado anteriormente com o mesmo nome tem uma assinatura diferente do novo pacote (e os dados do pacote antigo não foram removidos)." + +#: app/components/stf/install/install-error-filter.js:50 +msgid "A secure container mount point couldn't be accessed on external media." +msgstr "Não foi possível acessar um ponto de montagem de um contêiner seguro em uma mídia externa." + +#: app/control-panes/info/info.html:1 +#: app/device-list/column/device-column-service.js:178 +msgid "ABI" +msgstr "ABI" + +#: app/components/stf/device/device-info-filter/index.js:60 +msgid "AC" +msgstr "ACI" + +#: app/settings/keys/adb-keys/adb-keys.html:1 +msgid "ADB Keys" +msgstr "Chaves ADB" + +#: app/settings/keys/access-tokens/access-tokens.html:1 +msgid "Access Tokens" +msgstr "Tokens de Acesso" + +#: app/control-panes/automation/store-account/store-account.html:1 +msgid "Account" +msgstr "Conta" + +#: app/control-panes/dashboard/install/activities/activities.html:1 +msgid "Action" +msgstr "Ação" + +#: app/control-panes/automation/store-account/store-account.html:1 +msgid "Actions" +msgstr "Ações" + +#: app/control-panes/dashboard/install/activities/activities.html:1 +msgid "Activity" +msgstr "Atividades" + +#: app/control-panes/resources/resources.html:1 +msgid "Add" +msgstr "Adicionar" + +#: app/components/stf/keys/add-adb-key/add-adb-key.html:1 +msgid "Add ADB Key" +msgstr "Adicionar chave ADB" + +#: app/components/stf/common-ui/modals/add-adb-key-modal/add-adb-key-modal.html:1 +#: app/components/stf/keys/add-adb-key/add-adb-key.html:1 +msgid "Add Key" +msgstr "Adicionar chave" + +#: app/components/stf/common-ui/modals/add-adb-key-modal/add-adb-key-modal.html:1 +msgid "Add the following ADB Key to STF?" +msgstr "Adicionar esta chave ADB no STF?" + +#: app/layout/layout-controller.js:7 +msgid "Admin mode has been disabled." +msgstr "Modo administrador foi desabilitado" + +#: app/layout/layout-controller.js:6 +msgid "Admin mode has been enabled." +msgstr "Modo administrador foi habilitado" + +#: app/control-panes/control-panes-controller.js:20 +msgid "Advanced" +msgstr "Avançado" + +#: app/control-panes/advanced/input/input.html:1 +msgid "Advanced Input" +msgstr "Entrada Avançada" + +#: app/control-panes/info/info.html:1 +msgid "Airplane Mode" +msgstr "Modo Avião" + +#: app/control-panes/automation/store-account/store-account.html:1 +#: app/control-panes/dashboard/apps/apps.html:1 +msgid "App Store" +msgstr "App Store" + +#: app/control-panes/dashboard/install/install.html:1 +msgid "App Upload" +msgstr "Instalar Aplicativo" + +#: app/control-panes/dashboard/apps/apps.html:1 +msgid "Apps" +msgstr "Aplicativos" + +#: app/control-panes/advanced/maintenance/maintenance-controller.js:10 +msgid "Are you sure you want to reboot this device?" +msgstr "Você tem certeza que deseja reiniciar o dispositivo?" + +#: app/components/stf/device/device-info-filter/index.js:30 +msgid "Automating" +msgstr "Automatizando" + +#: app/control-panes/control-panes-controller.js:14 +msgid "Automation" +msgstr "Automação" + +#: app/components/stf/device/device-info-filter/index.js:29 +msgid "Available" +msgstr "Disponível" + +#: app/components/stf/device-context-menu/device-context-menu.html:1 +#: app/control-panes/device-control/device-control.html:1 +msgid "Back" +msgstr "Voltar" + +#: app/control-panes/info/info.html:1 +msgid "Battery" +msgstr "Bateria" + +#: app/device-list/column/device-column-service.js:208 +msgid "Battery Health" +msgstr "Saúde da Bateria" + +#: app/device-list/column/device-column-service.js:232 +msgid "Battery Level" +msgstr "Nível da Bateria" + +#: app/device-list/column/device-column-service.js:216 +msgid "Battery Source" +msgstr "Fonte da Bateria" + +#: app/device-list/column/device-column-service.js:224 +msgid "Battery Status" +msgstr "Estado da Bateria" + +#: app/device-list/column/device-column-service.js:245 +msgid "Battery Temp" +msgstr "Temperatura da Bateria" + +#: app/components/stf/device/device-info-filter/index.js:91 +msgid "Bluetooth" +msgstr "Bluetooth" + +#: app/device-list/column/device-column-service.js:153 +msgid "Browser" +msgstr "Navegador" + +#: app/components/stf/device/device-info-filter/index.js:12 +#: app/components/stf/device/device-info-filter/index.js:28 +msgid "Busy" +msgstr "Ocupado" + +#: app/device-list/stats/device-list-stats.html:1 +msgid "Busy Devices" +msgstr "Dispositivos Ocupados" + +#: app/control-panes/info/info.html:1 +#: app/control-panes/performance/cpu/cpu.html:1 +msgid "CPU" +msgstr "CPU" + +#: app/control-panes/advanced/input/input.html:1 +msgid "Camera" +msgstr "Câmera" + +#: app/components/stf/common-ui/modals/add-adb-key-modal/add-adb-key-modal.html:1 +msgid "Cancel" +msgstr "Cancelar" + +#: app/components/stf/upload/upload-error-filter.js:6 +msgid "Cannot access specified URL" +msgstr "Não pode acessar a URL inserida" + +#: app/control-panes/info/info.html:1 +#: app/device-list/column/device-column-service.js:43 +msgid "Carrier" +msgstr "Operadora" + +#: app/control-panes/dashboard/install/activities/activities.html:1 +msgid "Category" +msgstr "Categoria" + +#: app/components/stf/device/device-info-filter/index.js:69 +msgid "Charging" +msgstr "Carregando" + +#: auth/ldap/scripts/signin/signin.html:1 +#: auth/mock/scripts/signin/signin.html:1 +msgid "Check errors below" +msgstr "Verifique os erros abaixo" + +#: app/components/stf/common-ui/clear-button/clear-button.html:1 +#: app/control-panes/advanced/run-js/run-js.html:1 +#: app/control-panes/logs/logs.html:1 +msgid "Clear" +msgstr "Limpar" + +#: app/control-panes/dashboard/clipboard/clipboard.html:1 +msgid "Clipboard" +msgstr "Área de Transferência" + +#: app/components/stf/device/device-info-filter/index.js:48 +msgid "Cold" +msgstr "Frio" + +#: app/components/stf/device/device-info-filter/index.js:22 +#: app/components/stf/device/device-info-filter/index.js:6 +#: app/control-panes/info/info.html:1 +msgid "Connected" +msgstr "Conectado" + +#: app/components/stf/socket/socket-state/socket-state-directive.js:20 +msgid "Connected successfully." +msgstr "Conectado com sucesso." + +#: app/menu/menu.html:1 +msgid "Control" +msgstr "Controlar" + +#: app/control-panes/resources/resources.html:1 +msgid "Cookies" +msgstr "Cookies" + +#: app/control-panes/info/info.html:1 +msgid "Cores" +msgstr "Núcleos" + +#: app/control-panes/device-control/device-control.html:1 +msgid "Current rotation:" +msgstr "Rotação atual" + +#: app/device-list/device-list.html:1 +msgid "Customize" +msgstr "Customizar" + +#: app/control-panes/advanced/input/input.html:12 +msgid "D-pad Center" +msgstr "D-pad Centralizado" + +#: app/control-panes/advanced/input/input.html:20 +msgid "D-pad Down" +msgstr "D-pad abaixo" + +#: app/control-panes/advanced/input/input.html:9 +msgid "D-pad Left" +msgstr "D-pad Esquerda" + +#: app/control-panes/advanced/input/input.html:15 +msgid "D-pad Right" +msgstr "D-pad Direita" + +#: app/control-panes/advanced/input/input.html:4 +msgid "D-pad Up" +msgstr "D-pad Acima" + +#: app/control-panes/control-panes-controller.js:41 +msgid "Dashboard" +msgstr "Painel de Controle" + +#: app/control-panes/dashboard/install/activities/activities.html:1 +msgid "Data" +msgstr "Dados" + +#: app/control-panes/explorer/explorer.html:1 +msgid "Date" +msgstr "Data" + +#: app/components/stf/device/device-info-filter/index.js:50 +msgid "Dead" +msgstr "Parado" + +#: app/control-panes/resources/resources.html:1 +msgid "Delete" +msgstr "Deletar" + +#: app/control-panes/info/info.html:1 +msgid "Density" +msgstr "Densidade" + +#: app/device-list/device-list.html:1 +msgid "Details" +msgstr "Detalhes" + +#: app/control-panes/dashboard/apps/apps.html:1 +msgid "Developer" +msgstr "Desenvolvedor" + +#: app/components/stf/common-ui/modals/add-adb-key-modal/add-adb-key-modal.html:1 +#: app/components/stf/keys/add-adb-key/add-adb-key.html:1 +#: app/control-panes/advanced/port-forwarding/port-forwarding.html:1 +#: app/control-panes/inspect/inspect.html:1 +msgid "Device" +msgstr "Dispositivo" + +#: app/control-panes/info/info.html:1 +msgid "Device Photo" +msgstr "Foto do Dispositivo" + +#: app/control-panes/automation/device-settings/device-settings.html:1 +msgid "Device Settings" +msgstr "Configurações do Dispositivo" + +#: app/device-list/details/device-list-details-directive.js:38 +#: app/device-list/icons/device-list-icons-directive.js:123 +msgid "Device cannot get kicked from the group" +msgstr "O dispositivo não pode ser removido do grupo" + +#: app/components/stf/device/device-info-filter/index.js:40 +msgid "Device is not present anymore for some reason." +msgstr "O dispositivo não está mais disponível por algum motivo." + +#: app/components/stf/device/device-info-filter/index.js:41 +msgid "Device is present but offline." +msgstr "Dispositivo presenta mas está indisponível" + +#: app/components/stf/common-ui/modals/fatal-message/fatal-message.html:1 +msgid "Device was disconnected" +msgstr "Dispositivo desconectado" + +#: app/components/stf/device/device-info-filter/index.js:39 +msgid "Device was kicked by automatic timeout." +msgstr "Dispositivo foi removido por tempo limite automático." + +#: app/device-list/device-list.html:1 app/menu/menu.html:1 +msgid "Devices" +msgstr "Dispositivos" + +#: app/control-panes/automation/device-settings/device-settings.html:1 +msgid "Disable WiFi" +msgstr "Desabilitar WiFi" + +#: app/components/stf/device/device-info-filter/index.js:70 +msgid "Discharging" +msgstr "Descarregando" + +#: app/components/stf/common-ui/modals/socket-disconnected/socket-disconnected.html:1 +#: app/components/stf/device/device-info-filter/index.js:21 +#: app/components/stf/device/device-info-filter/index.js:5 +msgid "Disconnected" +msgstr "Disconectado" + +#: app/control-panes/info/info.html:1 +msgid "Display" +msgstr "Exibição" + +#: app/control-panes/resources/resources.html:1 +msgid "Domain" +msgstr "Domínio" + +#: app/control-panes/dashboard/install/install.html:7 +msgid "Drop file to upload" +msgstr "Arrastar arquivo para instalar" + +#: app/components/stf/device/device-info-filter/index.js:92 +msgid "Dummy" +msgstr "Modelo" + +#: app/control-panes/automation/device-settings/device-settings.html:1 +msgid "Enable WiFi" +msgstr "Ativar Wifi" + +#: app/settings/notifications/notifications.html:1 +msgid "Enable notifications" +msgstr "Habilitar notificações" + +#: app/control-panes/info/info.html:1 +msgid "Encrypted" +msgstr "Encriptar" + +#: app/components/stf/socket/socket-state/socket-state-directive.js:31 +msgid "Error" +msgstr "Erro" + +#: app/components/stf/control/control-service.js:129 +msgid "Error while getting data" +msgstr "Erro ao pegar os dados" + +#: app/components/stf/socket/socket-state/socket-state-directive.js:35 +msgid "Error while reconnecting" +msgstr "Erro ao reconectar" + +#: app/components/stf/device/device-info-filter/index.js:93 +msgid "Ethernet" +msgstr "Ethernet" + +#: app/control-panes/dashboard/shell/shell.html:1 +msgid "Executes remote shell commands" +msgstr "Executar comandos shell remotos" + +#: app/control-panes/info/info.html:1 +msgid "FPS" +msgstr "FPS" + +#: app/components/stf/upload/upload-error-filter.js:5 +msgid "Failed to download file" +msgstr "Falha ao baixar arquivo" + +#: app/control-panes/advanced/input/input.html:1 +msgid "Fast Forward" +msgstr "Avanço Rápido " + +#: app/control-panes/control-panes-controller.js:26 +msgid "File Explorer" +msgstr "Explorar Arquivo" + +#: app/components/stf/common-ui/filter-button/filter-button.html:1 +msgid "Filter" +msgstr "Filtrar" + +#: app/control-panes/info/info.html:1 +msgid "Find Device" +msgstr "Encontrar Dispositivo" + +#: app/components/stf/common-ui/modals/add-adb-key-modal/add-adb-key-modal.html:1 +msgid "Fingerprint" +msgstr "Impressão Digital" + +#: app/control-panes/info/info.html:1 +msgid "Frequency" +msgstr "Frequencia" + +#: app/components/stf/device/device-info-filter/index.js:71 +msgid "Full" +msgstr "Completo" + +#: app/settings/settings-controller.js:5 +msgid "General" +msgstr "Geral" + +#: app/components/stf/tokens/generate-access-token/generate-access-token.html:1 +msgid "Generate Access Token" +msgstr "Gerar Token de Acesso" + +#: app/control-panes/advanced/vnc/vnc.html:1 +msgid "Generate Login for VNC" +msgstr "Gerar acesso por VNC" + +#: app/components/stf/tokens/generate-access-token/generate-access-token.html:1 +msgid "Generate New Token" +msgstr "Gerar Novo Token" + +#: app/control-panes/logs/logs.html:1 +#: app/control-panes/resources/resources.html:1 +msgid "Get" +msgstr "Obter" + +#: app/control-panes/dashboard/clipboard/clipboard.html:1 +msgid "Get clipboard contents" +msgstr "Obter conteúdo da área de transferência" + +#: app/control-panes/dashboard/navigation/navigation.html:1 +msgid "Go Back" +msgstr "Voltar" + +#: app/control-panes/dashboard/navigation/navigation.html:1 +msgid "Go Forward" +msgstr "Avançar" + +#: app/components/stf/common-ui/modals/fatal-message/fatal-message.html:1 +#: app/control-panes/control-panes-hotkeys-controller.js:89 +msgid "Go to Device List" +msgstr "Ir para Lista de Dispositivos" + +#: app/components/stf/device/device-info-filter/index.js:49 +msgid "Good" +msgstr "Bom" + +#: app/control-panes/info/info.html:1 +msgid "Hardware" +msgstr "Hardware" + +#: app/control-panes/info/info.html:1 +msgid "Health" +msgstr "Saúde" + +#: app/control-panes/info/info.html:1 +msgid "Height" +msgstr "Altura" + +#: app/menu/menu.html:1 +msgid "Help" +msgstr "Ajuda" + +#: app/control-panes/device-control/device-control.html:1 +msgid "Hide Screen" +msgstr "Ocultar Tela" + +#: app/components/stf/device-context-menu/device-context-menu.html:1 +#: app/control-panes/device-control/device-control.html:1 +msgid "Home" +msgstr "Início" + +#: app/control-panes/advanced/port-forwarding/port-forwarding.html:1 +msgid "Host" +msgstr "Host" + +#: app/control-panes/advanced/port-forwarding/port-forwarding.html:1 +msgid "Hostname" +msgstr "Nome do Host" + +#: app/control-panes/info/info.html:1 +msgid "ICCID" +msgstr "ICCID" + +#: app/control-panes/info/info.html:1 +msgid "ID" +msgstr "ID" + +#: app/control-panes/info/info.html:1 +msgid "IMEI" +msgstr "IMEI" + +#: app/control-panes/info/info.html:1 +msgid "IMSI" +msgstr "IMSI" + +#: auth/ldap/scripts/signin/signin.html:1 +#: auth/mock/scripts/signin/signin.html:1 +msgid "Incorrect login details" +msgstr "Informações de acesso incorretas" + +#: app/control-panes/control-panes-controller.js:32 +msgid "Info" +msgstr "Informações" + +#: app/control-panes/inspect/inspect.html:1 +msgid "Inspect Device" +msgstr "Inspecionar Dispositivo" + +#: app/control-panes/inspect/inspect.html:1 +msgid "Inspecting is currently only supported in WebView" +msgstr "Atualmente a inspeção só é suportada no WebView" + +#: app/control-panes/inspect/inspect.html:1 +msgid "Inspector" +msgstr "Inspetor" + +#: app/components/stf/install/install-error-filter.js:13 +msgid "Installation canceled by user." +msgstr "Instalação cancelada pelo usuário." + +#: app/components/stf/install/install-error-filter.js:9 +msgid "Installation failed due to an unknown error." +msgstr "A instalação falhou devido a um erro desconhecido." + +#: app/components/stf/install/install-error-filter.js:7 +msgid "Installation succeeded." +msgstr "Instalado com sucesso." + +#: app/components/stf/install/install-error-filter.js:11 +msgid "Installation timed out." +msgstr "Timeout durante instalacão." + +#: app/control-panes/dashboard/install/install.html:7 +msgid "Installing app..." +msgstr "Instalando aplicativo..." + +#: app/components/stf/keys/add-adb-key/add-adb-key.html:1 +msgid "Key" +msgstr "Chave" + +#: app/settings/settings-controller.js:10 +msgid "Keys" +msgstr "Chaves" + +#: app/control-panes/device-control/device-control.html:1 +msgid "Landscape" +msgstr "Paisagem" + +#: app/settings/general/language/language.html:1 +msgid "Language" +msgstr "Idioma" + +#: app/control-panes/dashboard/install/activities/activities.html:1 +msgid "Launch Activity" +msgstr "Abrir Activity" + +#: app/control-panes/dashboard/install/install.html:7 +msgid "Launching activity..." +msgstr "Abrindo activity..." + +#: app/control-panes/info/info.html:1 app/control-panes/logs/logs.html:1 +msgid "Level" +msgstr "Nível" + +#: app/settings/general/local/local-settings.html:1 +msgid "Local Settings" +msgstr "Configurações Locais" + +#: app/device-list/column/device-column-service.js:256 +msgid "Location" +msgstr "Localização" + +#: app/control-panes/automation/device-settings/device-settings.html:7 +msgid "Lock Rotation" +msgstr "Desabilitar Rotação da Tela" + +#: app/control-panes/control-panes-controller.js:50 +msgid "Logs" +msgstr "Logs" + +#: app/control-panes/advanced/maintenance/maintenance.html:1 +msgid "Maintenance" +msgstr "Manutenção" + +#: app/settings/keys/access-tokens/access-tokens.html:1 +msgid "" +"Make sure to copy your access token now. You won't be able to see it again." +msgstr "Certifique-se de copiar o seu token de acesso agora. Você não será capaz de vê-lo novamente." + +#: app/control-panes/dashboard/apps/apps.html:1 +msgid "Manage Apps" +msgstr "Gerenciar Aplicativos" + +#: app/control-panes/automation/device-settings/device-settings.html:1 +msgid "Manner Mode" +msgstr "Manner Mode" + +#: app/control-panes/info/info.html:1 +#: app/device-list/column/device-column-service.js:165 +msgid "Manufacturer" +msgstr "Fabricante" + +#: app/control-panes/advanced/input/input.html:1 +msgid "Media" +msgstr "Mídia" + +#: app/control-panes/info/info.html:1 +msgid "Memory" +msgstr "Memória" + +#: app/control-panes/device-control/device-control.html:1 +msgid "Menu" +msgstr "Menu" + +#: app/components/stf/device/device-info-filter/index.js:94 +msgid "Mobile" +msgstr "Dispositivo" + +#: app/components/stf/device/device-info-filter/index.js:95 +msgid "Mobile DUN" +msgstr "DUN do Dispositivo" + +#: app/components/stf/device/device-info-filter/index.js:96 +msgid "Mobile High Priority" +msgstr "Dispositivo com Prioridade Alta" + +#: app/components/stf/device/device-info-filter/index.js:97 +msgid "Mobile MMS" +msgstr "MMS do Dispositivo" + +#: app/components/stf/device/device-info-filter/index.js:98 +msgid "Mobile SUPL" +msgstr "SUPL do Dispositivo" + +#: app/control-panes/info/info.html:1 +#: app/device-list/column/device-column-service.js:31 +msgid "Model" +msgstr "Modelo" + +#: app/settings/keys/adb-keys/adb-keys.html:1 +msgid "More about ADB Keys" +msgstr "Mais sobre Chaves ADB" + +#: app/settings/keys/access-tokens/access-tokens.html:1 +msgid "More about Access Tokens" +msgstr "Mais sobre Token de Acesso" + +#: app/control-panes/advanced/input/input.html:1 +msgid "Mute" +msgstr "Mudo" + +#: app/control-panes/explorer/explorer.html:1 +#: app/control-panes/info/info.html:1 +#: app/control-panes/resources/resources.html:1 +msgid "Name" +msgstr "Nome" + +#: app/menu/menu.html:1 +msgid "Native" +msgstr "Nativo" + +#: app/control-panes/dashboard/navigation/navigation.html:1 +msgid "Navigation" +msgstr "Navegação" + +#: app/control-panes/info/info.html:1 +#: app/device-list/column/device-column-service.js:129 +msgid "Network" +msgstr "Rede" + +#: app/control-panes/advanced/input/input.html:1 +msgid "Next" +msgstr "Próximo" + +#: app/components/stf/device/device-info-filter/index.js:117 +msgid "No" +msgstr "Não" + +#: app/settings/keys/adb-keys/adb-keys.html:1 +msgid "No ADB keys" +msgstr "Nenhuma chave ADB" + +#: app/control-panes/advanced/port-forwarding/port-forwarding.html:1 +msgid "No Ports Forwarded" +msgstr "Sem portas" + +#: app/settings/keys/access-tokens/access-tokens.html:1 +msgid "No access tokens" +msgstr "Nenhum token de acesso" + +#: app/components/stf/control/control-service.js:126 +msgid "No clipboard data" +msgstr "Nenhum dado na área de transferencia" + +#: app/control-panes/resources/resources.html:1 +msgid "No cookies to show" +msgstr "Sem cookies para mostrar" + +#: app/components/stf/screen/screen.html:1 +msgid "No device screen" +msgstr "Nenhuma tela de dispositivo" + +#: app/device-list/empty/device-list-empty.html:1 +msgid "No devices connected" +msgstr "Nenhum dispositivo conectado" + +#: app/components/stf/common-ui/modals/lightbox-image/lightbox-image.html:1 +msgid "No photo available" +msgstr "Nenhuma foto disponível " + +#: app/control-panes/screenshots/screenshots.html:5 +msgid "No screenshots taken" +msgstr "Nenhuma captura de tela" + +#: app/control-panes/automation/device-settings/device-settings.html:1 +msgid "Normal Mode" +msgstr "Modo Normal" + +#: app/components/stf/device/device-info-filter/index.js:72 +msgid "Not Charging" +msgstr "Nada Carregando" + +#: app/device-list/column/device-column-service.js:262 +msgid "Notes" +msgstr "Notas" + +#: app/control-panes/inspect/inspect.html:1 +msgid "Nothing to inspect" +msgstr "Nada para inspecionar" + +#: app/settings/notifications/notifications.html:1 +msgid "Notifications" +msgstr "Notificações" + +#: app/control-panes/info/info.html:1 +msgid "Number" +msgstr "Número" + +#: app/control-panes/info/info.html:1 +#: app/device-list/column/device-column-service.js:55 +msgid "OS" +msgstr "SO" + +#: app/components/stf/device/device-info-filter/index.js:23 +#: app/components/stf/device/device-info-filter/index.js:7 +msgid "Offline" +msgstr "Indisponível" + +#: app/components/stf/common-ui/error-message/error-message.html:1 +#: app/control-panes/dashboard/install/install.html:7 +msgid "Oops!" +msgstr "Oops!" + +#: app/control-panes/dashboard/navigation/navigation.html:1 +msgid "Open" +msgstr "Aberto" + +#: app/control-panes/info/info.html:1 +msgid "Orientation" +msgstr "Orientação" + +#: app/components/stf/device/device-info-filter/index.js:51 +msgid "Over Voltage" +msgstr "Tensão excessiva" + +#: app/components/stf/device/device-info-filter/index.js:52 +msgid "Overheat" +msgstr "Superaquecimento" + +#: app/control-panes/logs/logs.html:1 +msgid "PID" +msgstr "PID" + +#: app/control-panes/dashboard/install/activities/activities.html:1 +msgid "Package" +msgstr "Pacote" + +#: app/control-panes/advanced/vnc/vnc.html:1 +#: app/control-panes/automation/store-account/store-account.html:1 +msgid "Password" +msgstr "Senha" + +#: app/control-panes/resources/resources.html:1 +msgid "Path" +msgstr "Caminho" + +#: app/control-panes/explorer/explorer.html:1 +msgid "Permissions" +msgstr "Permissões" + +#: app/device-list/column/device-column-service.js:184 +msgid "Phone" +msgstr "Telefone" + +#: app/device-list/column/device-column-service.js:202 +msgid "Phone ICCID" +msgstr "ICCID do Dispositivo" + +#: app/device-list/column/device-column-service.js:190 +msgid "Phone IMEI" +msgstr "IMEI do Dispositivo" + +#: app/device-list/column/device-column-service.js:196 +msgid "Phone IMSI" +msgstr "IMSI do Dispositivo" + +#: app/control-panes/info/info.html:1 +msgid "Physical Device" +msgstr "Dispositivo Físico" + +#: app/control-panes/info/info.html:1 +msgid "Place" +msgstr "Lugar" + +#: app/control-panes/info/info.html:1 +msgid "Platform" +msgstr "Plataforma" + +#: app/control-panes/advanced/input/input.html:1 +msgid "Play/Pause" +msgstr "Play/Pause" + +#: auth/mock/scripts/signin/signin.html:1 +msgid "Please enter a valid email" +msgstr "Por Favor, insira um e-mail válido" + +#: auth/ldap/scripts/signin/signin.html:1 +msgid "Please enter your LDAP username" +msgstr "Por Favor entre com seu usuário LDAP" + +#: app/control-panes/automation/store-account/store-account.html:1 +msgid "Please enter your Store password" +msgstr "Por Favor entre com sua senha da Loja" + +#: app/control-panes/automation/store-account/store-account.html:1 +msgid "Please enter your Store username" +msgstr "Por Favor entre com seu usuário da Loja" + +#: auth/mock/scripts/signin/signin.html:1 +msgid "Please enter your email" +msgstr "Por Favor entre com seu e-mail" + +#: auth/mock/scripts/signin/signin.html:1 +msgid "Please enter your name" +msgstr "Por Favor entre com seu nome" + +#: auth/ldap/scripts/signin/signin.html:1 +msgid "Please enter your password" +msgstr "Por Favor entre com sua senha" + +#: app/control-panes/advanced/port-forwarding/port-forwarding.html:1 +#: app/control-panes/advanced/vnc/vnc.html:1 +msgid "Port" +msgstr "Porta" + +#: app/control-panes/advanced/port-forwarding/port-forwarding.html:1 +msgid "Port Forwarding" +msgstr "Porta de envio" + +#: app/control-panes/device-control/device-control.html:1 +msgid "Portrait" +msgstr "Retrato" + +#: app/control-panes/advanced/input/input.html:1 +msgid "Power" +msgstr "Ligar" + +#: app/control-panes/info/info.html:1 +msgid "Power Source" +msgstr "Fonte de energia" + +#: app/components/stf/device/device-info-filter/index.js:25 +#: app/components/stf/device/device-info-filter/index.js:9 +msgid "Preparing" +msgstr "Preparando" + +#: app/control-panes/control-panes-hotkeys-controller.js:104 +msgid "Press Back button" +msgstr "Pressionar botão Voltar" + +#: app/control-panes/control-panes-hotkeys-controller.js:103 +msgid "Press Home button" +msgstr "Pressionar botão Início" + +#: app/control-panes/control-panes-hotkeys-controller.js:102 +msgid "Press Menu button" +msgstr "Pressionar botão Menu" + +#: app/control-panes/advanced/input/input.html:1 +msgid "Previous" +msgstr "Anterior" + +#: app/control-panes/dashboard/install/install.html:7 +msgid "Processing..." +msgstr "Processando..." + +#: app/control-panes/info/info.html:1 +#: app/device-list/column/device-column-service.js:37 +msgid "Product" +msgstr "Produto" + +#: app/control-panes/dashboard/install/install.html:7 +msgid "Pushing app..." +msgstr "Publicando aplicativo..." + +#: app/control-panes/info/info.html:1 +msgid "RAM" +msgstr "RAM" + +#: app/control-panes/info/info.html:1 +msgid "ROM" +msgstr "ROM" + +#: app/components/stf/device/device-info-filter/index.js:10 +#: app/components/stf/device/device-info-filter/index.js:26 +msgid "Ready" +msgstr "Pronto" + +#: app/components/stf/socket/socket-state/socket-state-directive.js:39 +msgid "Reconnected successfully." +msgstr "Reconectado com sucesso." + +#: app/components/stf/common-ui/refresh-page/refresh-page.html:1 +msgid "Refresh" +msgstr "Atualizar" + +#: app/control-panes/info/info.html:1 +#: app/device-list/column/device-column-service.js:49 +msgid "Released" +msgstr "Liberado" + +#: app/components/stf/common-ui/modals/version-update/version-update.html:1 +#: app/control-panes/dashboard/navigation/navigation.html:1 +msgid "Reload" +msgstr "Recaregar" + +#: app/control-panes/dashboard/remote-debug/remote-debug.html:1 +msgid "Remote debug" +msgstr "Dupurar remotamente" + +#: app/settings/keys/access-tokens/access-tokens.html:1 +#: app/settings/keys/adb-keys/adb-keys.html:1 +msgid "Remove" +msgstr "Remover" + +#: app/control-panes/dashboard/navigation/navigation.html:1 +#: app/device-list/device-list.html:1 +msgid "Reset" +msgstr "Resetar" + +#: app/settings/general/local/local-settings.html:1 +msgid "Reset Settings" +msgstr "Limpar Configurações" + +#: app/control-panes/dashboard/navigation/navigation.html:1 +msgid "Reset all browser settings" +msgstr "Restar todas as configurações do navegador" + +#: app/control-panes/advanced/maintenance/maintenance.html:1 +msgid "Restart Device" +msgstr "Reiniciar Dipositivo" + +#: app/components/stf/screen/screen.html:1 +msgid "Retrieving the device screen has timed out." +msgstr "Recuperar a tela do dispositivo que expirou." + +#: app/components/stf/screen/screen.html:1 +msgid "Retry" +msgstr "Tentar novamente" + +#: app/control-panes/advanced/input/input.html:1 +msgid "Rewind" +msgstr "Rebobinar" + +#: app/control-panes/info/info.html:1 +msgid "Roaming" +msgstr "Roaming" + +#: app/components/stf/device-context-menu/device-context-menu.html:1 +#: app/control-panes/control-panes-hotkeys-controller.js:92 +msgid "Rotate Left" +msgstr "Rotar para Esquerda" + +#: app/components/stf/device-context-menu/device-context-menu.html:1 +#: app/control-panes/control-panes-hotkeys-controller.js:93 +msgid "Rotate Right" +msgstr "Rodar para Direita" + +#: app/control-panes/advanced/run-js/run-js.html:1 +msgid "Run" +msgstr "Rodar" + +#: app/control-panes/advanced/run-js/run-js.html:1 +msgid "Run JavaScript" +msgstr "Rodar JavaScript" + +#: app/control-panes/dashboard/remote-debug/remote-debug-controller.js:31 +msgid "" +"Run the following on your command line to debug the device from your Browser" +msgstr "Executar a seguinte linha de comando para depurar o navegador do seu dispositivo" + +#: app/control-panes/dashboard/remote-debug/remote-debug-controller.js:28 +msgid "" +"Run the following on your command line to debug the device from your IDE" +msgstr "Executar a seguinte linha de comando para depurar o IDE do seu dispositivo" + +#: app/components/stf/keys/add-adb-key/add-adb-key.html:1 +msgid "Run this command to copy the key to your clipboard" +msgstr "Executar este comando para copiar a chave para a área de transferência" + +#: app/control-panes/info/info.html:1 +msgid "SD Card Mounted" +msgstr "Catão SD Montado" + +#: app/control-panes/info/info.html:1 +#: app/device-list/column/device-column-service.js:171 +msgid "SDK" +msgstr "DSK" + +#: app/control-panes/info/info.html:1 +msgid "SIM" +msgstr "Cartão SIM" + +#: app/components/stf/device-context-menu/device-context-menu.html:1 +msgid "Save ScreenShot" +msgstr "Salvar Captura da Tela" + +#: app/control-panes/advanced/run-js/run-js.html:1 +msgid "Save..." +msgstr "Salvar..." + +#: app/device-list/column/device-column-service.js:135 +msgid "Screen" +msgstr "Tela" + +#: app/control-panes/screenshots/screenshots.html:1 +msgid "Screenshot" +msgstr "Captura da Tela" + +#: app/control-panes/control-panes-controller.js:8 +msgid "Screenshots" +msgstr "Capturas das Telas" + +#: app/control-panes/advanced/input/input.html:1 +msgid "Search" +msgstr "Buscar" + +#: app/control-panes/resources/resources.html:1 +msgid "Secure" +msgstr "Seguro" + +#: app/control-panes/control-panes-hotkeys-controller.js:91 +msgid "Selects Next IME" +msgstr "Selecionar Próximo IME" + +#: app/control-panes/info/info.html:1 +#: app/device-list/column/device-column-service.js:159 +msgid "Serial" +msgstr "Serial" + +#: app/control-panes/advanced/vnc/vnc.html:1 +msgid "Server" +msgstr "Servidor" + +#: auth/ldap/scripts/signin/signin.html:1 +#: auth/mock/scripts/signin/signin.html:1 +msgid "Server error. Check log output." +msgstr "Servidor com erro. Verifique o log de saída" + +#: app/control-panes/resources/resources.html:1 +msgid "Set" +msgstr "Inserir" + +#: app/control-panes/resources/resources.html:1 +msgid "Set Cookie" +msgstr "Inserir Cookie" + +#: app/control-panes/dashboard/apps/apps.html:1 app/menu/menu.html:1 +msgid "Settings" +msgstr "Configurações" + +#: app/control-panes/dashboard/shell/shell.html:1 +msgid "Shell" +msgstr "Shell" + +#: app/control-panes/device-control/device-control.html:1 +msgid "Show Screen" +msgstr "Mostrar Tela" + +#: app/control-panes/automation/store-account/store-account.html:1 +msgid "Sign In" +msgstr "Entrar" + +#: app/control-panes/automation/store-account/store-account.html:1 +msgid "Sign Out" +msgstr "Sair" + +#: app/control-panes/automation/device-settings/device-settings.html:1 +msgid "Silent Mode" +msgstr "Modo Silencioso" + +#: app/control-panes/explorer/explorer.html:1 +#: app/control-panes/info/info.html:1 +msgid "Size" +msgstr "Tamanho" + +#: app/components/stf/socket/socket-state/socket-state-directive.js:26 +msgid "Socket connection was lost" +msgstr "Conexão Socket foi perdida" + +#: app/components/stf/device/device-info-filter/index.js:38 +msgid "Someone stole your device." +msgstr "Alguém roubou seu dispositivo." + +#: app/control-panes/advanced/input/input.html:1 +msgid "Special Keys" +msgstr "Chaves Especiais" + +#: app/control-panes/logs/logs.html:1 +msgid "Start/Stop Logging" +msgstr "Iniciar/Pausar Entrada" + +#: app/control-panes/info/info.html:1 +#: app/device-list/column/device-column-service.js:25 +msgid "Status" +msgstr "Estado" + +#: app/control-panes/advanced/input/input.html:1 +#: app/control-panes/logs/logs.html:1 +msgid "Stop" +msgstr "Parar" + +#: app/components/stf/device/device-info-filter/index.js:14 +msgid "Stop Automation" +msgstr "Parar Automação" + +#: app/components/stf/device-context-menu/device-context-menu.html:1 +#: app/components/stf/device/device-info-filter/index.js:11 +#: app/control-panes/device-control/device-control.html:1 +msgid "Stop Using" +msgstr "Parar de Usar" + +#: app/control-panes/automation/store-account/store-account.html:1 +msgid "Store Account" +msgstr "Conta da Loja" + +#: app/control-panes/info/info.html:1 +msgid "Sub Type" +msgstr "Sub Tipo" + +#: app/control-panes/advanced/input/input.html:1 +msgid "Switch Charset" +msgstr "Switch Charset" + +#: app/control-panes/logs/logs.html:1 +msgid "TID" +msgstr "TID" + +#: app/control-panes/logs/logs.html:1 +msgid "Tag" +msgstr "Tag" + +#: app/control-panes/screenshots/screenshots.html:1 +msgid "Take Pageshot (Needs WebView running)" +msgstr "Capturar a Página (Necessita que o WebView seja executado)" + +#: app/control-panes/screenshots/screenshots.html:1 +msgid "Take Screenshot" +msgstr "Captura Tela" + +#: app/control-panes/info/info.html:1 +msgid "Temperature" +msgstr "Temperatura" + +#: app/control-panes/logs/logs.html:1 +msgid "Text" +msgstr "Texto" + +#: app/components/stf/install/install-error-filter.js:22 +msgid "The URI passed in is invalid." +msgstr "URI informada é invalida." + +#: app/components/stf/screen/screen.html:1 +msgid "The current view is marked secure and cannot be viewed remotely." +msgstr "A visualização atual foi marcada como segura e não pode ser visualizada remotamente." + +#: app/control-panes/advanced/maintenance/maintenance-controller.js:11 +msgid "The device will be unavailable for a moment." +msgstr "Este dispositivo estará indisponível por algum momento." + +#: app/components/stf/install/install-error-filter.js:34 +msgid "The existing package could not be deleted." +msgstr "O pacote existente não pode ser deletado." + +#: app/components/stf/install/install-error-filter.js:58 +msgid "" +"The new package couldn't be installed because the verification did not " +"succeed." +msgstr "O novo pacote não pode ser instalado porque o arquivo verificado não está correto." + +#: app/components/stf/install/install-error-filter.js:56 +msgid "" +"The new package couldn't be installed because the verification timed out." +msgstr "O novo pacote não pode ser instalado porque o tempo de verificação expirou." + +#: app/components/stf/install/install-error-filter.js:54 +msgid "" +"The new package couldn't be installed in the specified install location " +"because the media is not available." +msgstr "O novo pacote não pode ser instalado no local específico porque a mídia não está disponível." + +#: app/components/stf/install/install-error-filter.js:52 +msgid "" +"The new package couldn't be installed in the specified install location." +msgstr "O novo pacote não pode ser instalado no local específico." + +#: app/components/stf/install/install-error-filter.js:40 +msgid "" +"The new package failed because it contains a content provider with thesame " +"authority as a provider already installed in the system." +msgstr "O novo pacote falhou porque ele contém um provedor de conteúdo com a mesma autoridade como um provedor já instalado no sistema." + +#: app/components/stf/install/install-error-filter.js:44 +msgid "" +"The new package failed because it has specified that it is a test-only " +"package and the caller has not supplied the INSTALL_ALLOW_TEST flag." +msgstr "O novo pacote falhou porque ele especificou que ele é um pacote test-only e a função que chama não forneceu o sinalizador INSTALL_ALLOW_TEST." + +#: app/components/stf/install/install-error-filter.js:42 +msgid "" +"The new package failed because the current SDK version is newer than that " +"required by the package." +msgstr "O novo pacote falhou porque a versão atual do SDK é mais recente do que a exigida pelo pacote." + +#: app/components/stf/install/install-error-filter.js:38 +msgid "" +"The new package failed because the current SDK version is older than that " +"required by the package." +msgstr "The new package failed because the current SDK version is older than that required by the package." + +#: app/components/stf/install/install-error-filter.js:36 +msgid "" +"The new package failed while optimizing and validating its dex files, either" +" because there was not enough storage or the validation failed." +msgstr "O novo pacote falhou ao otimizar e validar seus arquivos dex, porque não havia armazenamento suficiente ou a validação falhou." + +#: app/components/stf/install/install-error-filter.js:64 +msgid "" +"The new package has an older version code than the currently installed " +"package." +msgstr "O novo pacote falhou para otimizar e validar os seus arquivos dex, porque não existe uma exploração suficiente ou uma validação falhou." + +#: app/components/stf/install/install-error-filter.js:62 +msgid "The new package is assigned a different UID than it previously held." +msgstr "O novo pacote é atribuído um UID diferente do que anteriormente realizada." + +#: app/components/stf/install/install-error-filter.js:48 +msgid "The new package uses a feature that is not available." +msgstr "O novo pacote usa um recurso que não está disponível." + +#: app/components/stf/install/install-error-filter.js:32 +msgid "The new package uses a shared library that is not available." +msgstr "O novo pacote usa uma biblioteca compartilhada que não está disponível." + +#: app/components/stf/install/install-error-filter.js:20 +msgid "The package archive file is invalid." +msgstr "Arquivo no pacote é inválido." + +#: app/components/stf/install/install-error-filter.js:46 +msgid "" +"The package being installed contains native code, but none that is " +"compatible with the device's CPU_ABI." +msgstr "O pacote que está sendo instalado contém código nativo, mas nenhum compatível com o CPU_ABI do dispositivo." + +#: app/components/stf/install/install-error-filter.js:60 +msgid "The package changed from what the calling program expected." +msgstr "O pacote mudou do que o programa esperava." + +#: app/components/stf/install/install-error-filter.js:18 +msgid "The package is already installed." +msgstr "Pacote já instalado." + +#: app/components/stf/install/install-error-filter.js:24 +msgid "" +"The package manager service found that the device didn't have enough storage" +" space to install the app." +msgstr "O serviço gerenciador de pacotes descobriu que o dispositivo não tinha espaço de armazenamento suficiente para instalar o aplicativo." + +#: app/components/stf/install/install-error-filter.js:84 +msgid "" +"The parser did not find any actionable tags (instrumentation or application)" +" in the manifest." +msgstr "A análise não encontrou nenhum marcador acionável (instrumentação ou aplicação) no manifesto." + +#: app/components/stf/install/install-error-filter.js:72 +msgid "The parser did not find any certificates in the .apk." +msgstr "A análise não encontrou nenhum certificado no .apk." + +#: app/components/stf/install/install-error-filter.js:76 +msgid "" +"The parser encountered a CertificateEncodingException in one of the files in" +" the .apk." +msgstr "A Análise encontrou o CertificateEncodingException em um dos arquivos no .apk." + +#: app/components/stf/install/install-error-filter.js:78 +msgid "The parser encountered a bad or missing package name in the manifest." +msgstr "A análise encontrou um nome de pacote incorreto ou ausente no manifesto." + +#: app/components/stf/install/install-error-filter.js:80 +msgid "The parser encountered a bad shared user id name in the manifest." +msgstr "A análise encontrou um nome de ID de usuário compartilhado incorreto no manifesto." + +#: app/components/stf/install/install-error-filter.js:70 +msgid "The parser encountered an unexpected exception." +msgstr "A análise encontrou uma exceção não esperada. " + +#: app/components/stf/install/install-error-filter.js:82 +msgid "The parser encountered some structural problem in the manifest." +msgstr "A análise encontrou algum problema na estrutura do manifesto." + +#: app/components/stf/install/install-error-filter.js:74 +msgid "The parser found inconsistent certificates on the files in the .apk." +msgstr "A análise encontrou uma inconsistência no certificado presente nos arquivos do .apk." + +#: app/components/stf/install/install-error-filter.js:66 +msgid "" +"The parser was given a path that is not a file, or does not end with the " +"expected '.apk' extension." +msgstr "A análise encontrou: foi dado um caminho que não é um arquivo, ou não termina com a extensão '.apk' esperado." + +#: app/components/stf/install/install-error-filter.js:68 +msgid "The parser was unable to retrieve the AndroidManifest.xml file." +msgstr "Não foi possível analizar o arquivo AndroidManifest.xml." + +#: app/components/stf/install/install-error-filter.js:28 +msgid "The requested shared user does not exist." +msgstr "O usuário compartilhado solicitado não existe." + +#: app/components/stf/install/install-error-filter.js:90 +msgid "" +"The system failed to install the package because its packaged native code " +"did not match any of the ABIs supported by the system." +msgstr "O sistema falhou ao instalar o pacote porque seu código nativo não correspondia a nenhuma das ABIs suportadas pelo sistema." + +#: app/components/stf/install/install-error-filter.js:86 +msgid "The system failed to install the package because of system issues." +msgstr "O sistema falhou ao instalar o pacote devido a problemas do sistema." + +#: app/components/stf/install/install-error-filter.js:88 +msgid "" +"The system failed to install the package because the user is restricted from" +" installing apps." +msgstr "O sistema falhou ao instalar o pacote porque o usuário não é autorizado a instalar aplicativos." + +#: app/control-panes/logs/logs.html:1 +msgid "Time" +msgstr "Horário" + +#: app/components/stf/keys/add-adb-key/add-adb-key.html:1 +msgid "Tip:" +msgstr "Dica:" + +#: app/components/stf/tokens/generate-access-token/generate-access-token.html:1 +msgid "Title" +msgstr "Título" + +#: app/control-panes/control-panes-hotkeys-controller.js:107 +msgid "Toggle Web/Native" +msgstr "Alterar entre Web e Nativo" + +#: app/device-list/stats/device-list-stats.html:1 +msgid "Total Devices" +msgstr "Total de Dispositivos" + +#: app/components/stf/common-ui/modals/fatal-message/fatal-message.html:1 +#: app/components/stf/common-ui/modals/socket-disconnected/socket-disconnected.html:1 +msgid "Try to reconnect" +msgstr "Tentar reconectar" + +#: app/control-panes/info/info.html:1 +msgid "Type" +msgstr "Tipo" + +#: app/components/stf/device/device-info-filter/index.js:61 +msgid "USB" +msgstr "USB" + +#: app/components/stf/device/device-info-filter/index.js:24 +#: app/components/stf/device/device-info-filter/index.js:8 +msgid "Unauthorized" +msgstr "Não Autorizado" + +#: app/control-panes/dashboard/install/install.html:7 +msgid "Uninstall" +msgstr "Desinstalar" + +#: app/components/stf/device/device-info-filter/index.js:15 +#: app/components/stf/device/device-info-filter/index.js:31 +msgid "Unknown" +msgstr "Desconhecido" + +#: app/components/stf/device/device-info-filter/index.js:42 +msgid "Unknown reason." +msgstr "Razão desconhecida." + +#: app/control-panes/automation/device-settings/device-settings.html:6 +msgid "Unlock Rotation" +msgstr "Desabilitar Rotação" + +#: app/components/stf/device/device-info-filter/index.js:53 +msgid "Unspecified Failure" +msgstr "Falha não especificada" + +#: app/control-panes/dashboard/install/install.html:5 +msgid "Upload From Link" +msgstr "Fazer envio por Link" + +#: app/components/stf/upload/upload-error-filter.js:7 +msgid "Upload failed" +msgstr "Envio falhou" + +#: app/components/stf/upload/upload-error-filter.js:8 +msgid "Upload unknown error" +msgstr "Envio com erro desconhecido" + +#: app/components/stf/upload/upload-error-filter.js:4 +msgid "Uploaded file is not valid" +msgstr "Arquivo enviado não é válido" + +#: app/control-panes/dashboard/install/install.html:7 +msgid "Uploading..." +msgstr "Enviado..." + +#: app/device-list/stats/device-list-stats.html:1 +msgid "Usable Devices" +msgstr "Dispositivos Utilizáveis" + +#: app/control-panes/advanced/usb/usb.html:1 +msgid "Usb speed" +msgstr "Velocidade do USB" + +#: app/components/stf/device/device-info-filter/index.js:13 +msgid "Use" +msgstr "Usar" + +#: app/device-list/column/device-column-service.js:268 +msgid "User" +msgstr "Usuário" + +#: app/control-panes/automation/store-account/store-account.html:1 +msgid "Username" +msgstr "Usuário" + +#: app/components/stf/device/device-info-filter/index.js:27 +msgid "Using" +msgstr "Usando" + +#: app/control-panes/info/info.html:1 +msgid "Using Fallback" +msgstr "Retornar verssão" + +#: app/control-panes/advanced/vnc/vnc.html:1 +msgid "VNC" +msgstr "VNC" + +#: app/control-panes/resources/resources.html:1 +msgid "Value" +msgstr "Valor" + +#: app/control-panes/info/info.html:1 +msgid "Version" +msgstr "Versão" + +#: app/components/stf/common-ui/modals/version-update/version-update.html:1 +msgid "Version Update" +msgstr "Atualização da Versão" + +#: app/control-panes/automation/device-settings/device-settings.html:1 +msgid "Vibrate Mode" +msgstr "Modo vibrar" + +#: app/control-panes/info/info.html:1 +msgid "Voltage" +msgstr "Voltage" + +#: app/control-panes/advanced/input/input.html:1 +msgid "Volume" +msgstr "Volume" + +#: app/control-panes/advanced/input/input.html:1 +msgid "Volume Down" +msgstr "Baixar Volume" + +#: app/control-panes/advanced/input/input.html:1 +msgid "Volume Up" +msgstr "Aumentar Volume" + +#: app/settings/keys/access-tokens/access-tokens.html:1 +msgid "Warning:" +msgstr "Atenção:" + +#: app/menu/menu.html:1 +msgid "Web" +msgstr "Web" + +#: app/components/stf/device/device-info-filter/index.js:107 +#: app/components/stf/device/device-info-filter/index.js:99 +#: app/control-panes/automation/device-settings/device-settings.html:1 +#: app/control-panes/dashboard/apps/apps.html:1 +msgid "WiFi" +msgstr "WiFi" + +#: app/components/stf/device/device-info-filter/index.js:100 +msgid "WiMAX" +msgstr "WiMAX" + +#: app/control-panes/info/info.html:1 +msgid "Width" +msgstr "Largura" + +#: app/components/stf/device/device-info-filter/index.js:62 +msgid "Wireless" +msgstr "Wireless" + +#: app/control-panes/info/info.html:1 +msgid "X DPI" +msgstr "DPI X" + +#: app/control-panes/info/info.html:1 +msgid "Y DPI" +msgstr "DPI Y" + +#: app/components/stf/device/device-info-filter/index.js:115 +msgid "Yes" +msgstr "Sim" + +#: app/components/stf/device/device-info-filter/index.js:37 +msgid "You (or someone else) kicked the device." +msgstr "Você removeu o dispositivo." + +#: app/components/stf/common-ui/modals/fatal-message/fatal-message.html:1 +#: app/control-panes/resources/resources.html:1 +msgid "translate" +msgstr "traduzir" diff --git a/res/common/lang/translations/stf.es.json b/res/common/lang/translations/stf.es.json index b4348a7a..7e701ab5 100644 --- a/res/common/lang/translations/stf.es.json +++ b/res/common/lang/translations/stf.es.json @@ -1 +1 @@ -{"es":{"A new version of STF is available":"Una nueva versión de STF está disponible","A package is already installed with the same name.":"Ya hay un paquete instalado con el mismo nombre","Access Tokens":"Tokens de acceso","Account":"Cuenta","Action":"Acción","Actions":"Acciones","Activity":"Actividad","Add":"Añadir","Add ADB Key":"Añadir Llave de ADB","Add Key":"Añadir Llave","Admin mode has been disabled.":"El modo administrador se ha desactivado","Admin mode has been enabled.":"El modo administrador se ha activado","Advanced":"Avanzado","Airplane Mode":"Modo avión","App Upload":"Subir aplicación","Apps":"Aplicaciones","Are you sure you want to reboot this device?":"¿Estás seguro de querer reiniciar este dispositivo?","Automation":"Automatización","Available":"Disponible","Back":"Atrás","Battery":"Batería","Battery Level":"Nivel de batería","Battery Status":"Estado de la batería","Bluetooth":"Bluetooth","Browser":"Navegador","Busy":"En uso","Busy Devices":"Dispositivos en uso","Camera":"Cámara","Cancel":"Cancelar","Cannot access specified URL":"No se puedo accecer a la URL especificada","Category":"Categoría","Charging":"Cargando","Check errors below":"Comprueba los siguientes errores","Clear":"Limpiar","Clipboard":"Portapapeles","Cold":"Frío","Connected":"Conectado","Connected successfully.":"Conectado con éxito","Control":"Control","Cookies":"Cookies","Cores":"Núcleos","CPU":"CPU","Customize":"Personalizar","Dashboard":"Tablero","Data":"Datos","Date":"Fecha","Delete":"Borrar","Density":"Densidad","Details":"Detalles","Developer":"Desarrollador","Device":"Dispositivo","Device Settings":"Configuración de Dispositivo","Device was disconnected":"El dispositivo se ha desconectado","Devices":"Dispositivos","Disable WiFi":"Deshabilitar WIFI","Disconnected":"Desconectado","Enable notifications":"Habilitar notificaciones","Enable WiFi":"Habilitar WIFI","Encrypted":"Encriptado","Error":"Error","Ethernet":"Ethernet","Failed to download file":"Fallo al descargar el fichero","File Explorer":"Explorador de fichero","File Name":"Nombre del archivo","Find Device":"Encontrar dispositivo","General":"General","Generate New Token":"Generar nuevo token","Go to Device List":"Ir a la lista de dispositivos","Hardware":"Hardware","Height":"Ancho","Help":"Ayuda","Hide Screen":"Ocultar pantalla","Home":"Home","IMEI":"IMEI","Info":"Información","Inspect Device":"Inspeccionar dispositivo","Inspector":"Inspector","Installation canceled by user.":"Instalación cancelada por el usuario","Installation failed due to an unknown error.":"La instalación falló debido a un error desconocido","Installation succeeded.":"Instalado con éxito","Installation timed out.":"La instalación superó el tiempo de espera","Installing app...":"Instalando aplicación...","Language":"Idioma","Level":"Nivel","Lock Rotation":"Bloquear rotación","Maintenance":"Mantenimiento","Make sure to copy your access token now. You won't be able to see it again!":"Asegúrate de copiar tu token de acceso ahora. ¡No podrás volver a verlo más!","Manage Apps":"Gestionar aplicaciones","Memory":"Memoria","Menu":"Menú","Mobile":"Móvil","Model":"Modelo","More about Access Tokens":"Más sobre Tokens de acceso","Mute":"Silencio","Name":"Nombre","Native":"Nativo","Navigation":"Navegación","Network":"Red","Next":"Siguiente","No":"No","No access tokens":"Sin tokens de acceso","No clipboard data":"No hay datos en el portapapeles","No cookies to show":"No hay cookies que mostrar","No devices connected":"No hay dispositivos conectados","No photo available":"No hay imagen disponible","No screenshots taken":"No hay capturas de pantalla","Normal Mode":"Modo normal","Not Charging":"No se está cargando","Notes":"Notas","Nothing to inspect":"No hay nada que inspeccionar","Notifications":"Notificaciones","Number":"Número","Offline":"Offline","Oops!":"¡Ups!","Open":"Abrir","Orientation":"Orientación","Package":"Paquete","Password":"Contraseña","Permissions":"Permisos","Phone":"Teléfono","Phone IMEI":"IMEI del teléfono","Physical Device":"Dispositivo físico","Platform":"Plataforma","Play/Pause":"Inicio/Pausa","Please enter a valid email":"Por favor, introduce un email válido","Please enter your email":"Por favor, introduce tu email","Please enter your LDAP username":"Por favor, introduce tu usuario de LDAP","Please enter your name":"Por favor, introduce tu nombre","Please enter your password":"Por favor, introduce tu contraseña","Port":"Puerto","Preparing":"Preparando","Press Back button":"Pulsa el botón Volver","Press Home button":"Pulsa el botón Home","Press Menu button":"Pulsa el botón Menú","Previous":"Anterior","Processing...":"Procesando...","Product":"Producto","RAM":"RAM","Ready":"Listo","Refresh":"Actualizar","Reload":"Recargar","Remote debug":"Conexión remota","Remove":"Eliminar","Reset":"Reiniciar","Reset all browser settings":"Restablecer todos los ajustes del navegador","Reset Settings":"Restablecer ajustes","Restart Device":"Reiniciar dispositivo","Retry":"Reintentar","ROM":"ROM","Rotate Left":"Rotar a la izquierda","Rotate Right":"Rotar a la derecha","Run":"Ejecutar","Run JavaScript":"Ejecutar JavaScript","Run this command to copy the key to your clipboard":"Ejecuta este comando para copiar la clave al portapapeles","Sample of log format":"Muestra de formato de registro","Save Logs":"Guardar los registros","Save ScreenShot":"Guardar captura de pantalla","Save...":"Guardar","Screen":"Pantalla","Screenshot":"Captura de pantalla","Screenshots":"Capturas de Pantalla","SDK":"SDK","Search":"Buscar","Serial":"Serie","Server":"Servidor","Settings":"Configuración","Shell":"Línea de Comandos","Show Screen":"Mostar pantalla","Sign In":"Acceder","Sign Out":"Desconectar","Silent Mode":"Modo silencio","SIM":"SIM","Size":"Tamaño","Socket connection was lost":"Se perdió la conexión con el socket","Someone stole your device.":"Alguien robó tu dispositivo","Special Keys":"Teclas especiales","Status":"Estado","Stop":"Parar","Sub Type":"Subtipo","Tag":"Etiqueta","Take Screenshot":"Capturar pantalla","Temperature":"Temperatura","Text":"Texto","The current view is marked secure and cannot be viewed remotely.":"La vista actual está marcada como segura y no puede ser vista de forma remota","The device will be unavailable for a moment.":"El dispositivo no estará disponible durante unos instantes","The existing package could not be deleted.":"El paquete no se pudo eliminar","The new package couldn't be installed because the verification did not succeed.":"El nuevo paquete no se pudo instalar porque no se pudo verificar","The new package couldn't be installed because the verification timed out.":"El nuevo paquete no se pudo instalar porque se excedió el tiempo de espera al verificarlo","The new package couldn't be installed in the specified install location.":"El nuevo paquete no se pudo instalar en el sitio especificado para su instalación","The new package failed because the current SDK version is newer than that required by the package.":"El nuevo paquete falló porque la versión actual del SDK es más reciente que la que requiere el paquete","The new package failed because the current SDK version is older than that required by the package.":"El nuevo paquete falló porque la versión actual del SDK es más antigua que la que requiere el paquete","The new package has an older version code than the currently installed package.":"El nuevo paquete tiene una versión de código más antigua que el paquete instalado actualmente.","The new package uses a feature that is not available.":"El nuevo paquete utiliza una característica que no está disponible.","The package archive file is invalid.":"El archivo del paquete no es válido","The package being installed contains native code, but none that is compatible with the device's CPU_ABI.":"El paquete que se está instalando contiene código nativo que no es compatible con el CPU_ABI del dispositivo.","The package is already installed.":"El paquete ya está instalado","Tip:":"Truco:","Title":"Título","Total Devices":"Dispositivos Totales","translate":"traducir","Try to reconnect":"Volver a conectar","Type":"Tipo","Unauthorized":"No autorizado","Uninstall":"Desinstalar","Unknown":"Desconocido","Unknown reason.":"Razón desconocida.","Unlock Rotation":"Desbloquear rotación","Unspecified Failure":"Fallo no especificado","Upload failed":"Subida fallida","Upload From Link":"Subir desde enlace","Upload unknown error":"Error de subida desconocido","Uploaded file is not valid":"El fichero de subida no es válido","Uploading...":"Subiendo...","USB":"USB","Usb speed":"Velocidad de USB","Use":"Uso","User":"Usuario","Username":"Nombre de usuario","Using":"En uso","Version":"Versión","Vibrate Mode":"Modo vibración","Volume":"Volumen","Volume Down":"Bajar volumen","Volume Up":"Subir volumen","Warning:":"Atención:","Web":"Web","Width":"Ancho","WiFi":"WIFI","Yes":"Sí"}} \ No newline at end of file +{"es":{"A new version of STF is available":"Una nueva versión de STF está disponible","A package is already installed with the same name.":"Ya hay un paquete instalado con el mismo nombre","A previously installed package of the same name has a different signature than the new package (and the old package's data was not removed).":"Se ha instalado un paquete previamente con el mismo nombre pero con una firma diferente a la del nuevo paquete (y el paquete antiguo no ha sido eliminado).","A secure container mount point couldn't be accessed on external media.":"Un punto de montaje de contenedor seguro no puede ser accedido desde un medio externo.","Access Tokens":"Tokens de acceso","Account":"Cuenta","Action":"Acción","Actions":"Acciones","Activity":"Actividad","Add":"Añadir","Add ADB Key":"Añadir Llave de ADB","Add Key":"Añadir Llave","Add the following ADB Key to STF?":"¿Añadir las siguientes llaves ADB a STF?","Admin mode has been disabled.":"El modo administrador se ha desactivado","Admin mode has been enabled.":"El modo administrador se ha activado","Advanced":"Avanzado","Airplane Mode":"Modo avión","App Store":"Tienda de aplicaciones","App Upload":"Subir aplicación","Apps":"Aplicaciones","Are you sure you want to reboot this device?":"¿Estás seguro de querer reiniciar este dispositivo?","Automating":"Automatizando","Automation":"Automatización","Available":"Disponible","Back":"Atrás","Battery":"Batería","Battery Health":"Salud de la batería","Battery Level":"Nivel de batería","Battery Source":"Fuente de batería","Battery Status":"Estado de la batería","Battery Temp":"Temperatura de batería","Bluetooth":"Bluetooth","Browser":"Navegador","Busy":"En uso","Busy Devices":"Dispositivos en uso","CPU":"CPU","Camera":"Cámara","Cancel":"Cancelar","Cannot access specified URL":"No se puedo accecer a la URL especificada","Category":"Categoría","Charging":"Cargando","Check errors below":"Comprueba los siguientes errores","Clear":"Limpiar","Clipboard":"Portapapeles","Cold":"Frío","Connected":"Conectado","Connected successfully.":"Conectado con éxito","Control":"Control","Cookies":"Cookies","Cores":"Núcleos","Current rotation:":"Rotación actual","Customize":"Personalizar","Dashboard":"Tablero","Data":"Datos","Date":"Fecha","Dead":"Muerto","Delete":"Borrar","Density":"Densidad","Details":"Detalles","Developer":"Desarrollador","Device":"Dispositivo","Device Photo":"Foto de dispositivo","Device Settings":"Configuración de Dispositivo","Device cannot get kicked from the group":"El dispositivo no puede ser expulsado del grupo","Device is not present anymore for some reason.":"Por algún motivo el dispositivo ya no está presente","Device is present but offline.":"El dispositivo está presente pero no disponible","Device was disconnected":"El dispositivo se ha desconectado","Device was kicked by automatic timeout.":"El dispositivo fue expulsado por un exceso de tiempo automático","Devices":"Dispositivos","Disable WiFi":"Deshabilitar WIFI","Discharging":"Descargando","Disconnected":"Desconectado","Display":"Pantalla","Domain":"Dominio","Drop file to upload":"Suelta aquí el fichero a subir","Enable WiFi":"Habilitar WIFI","Enable notifications":"Habilitar notificaciones","Encrypted":"Encriptado","Error":"Error","Error while getting data":"Error obteniendo datos","Error while reconnecting":"Error al reconectar","Ethernet":"Ethernet","Executes remote shell commands":"Ejecuta comandos de terminal remota","Failed to download file":"Fallo al descargar el fichero","Fast Forward":"Avance rápido","File Explorer":"Explorador de fichero","Filter":"Filtro","Find Device":"Encontrar dispositivo","Fingerprint":"Huella","Frequency":"Frecuencia","Full":"Lleno","General":"General","Generate Access Token":"Genera testimonio de acceso","Generate Login for VNC":"Genera inicio de sesión para VNC","Generate New Token":"Generar nuevo token","Get":"Obtener","Get clipboard contents":"Obtener contenido del portapapeles","Go Back":"Ir atrás","Go Forward":"Ir adelante","Go to Device List":"Ir a la lista de dispositivos","Good":"Bueno","Hardware":"Hardware","Health":"Salud","Height":"Ancho","Help":"Ayuda","Hide Screen":"Ocultar pantalla","Home":"Home","Host":"Terminal","Hostname":"Nombre de terminal","ICCID":"ICCID","ID":"ID","IMEI":"IMEI","IMSI":"IMSI","Incorrect login details":"Datos de inicio de sesión incorrectos","Info":"Información","Inspect Device":"Inspeccionar dispositivo","Inspecting is currently only supported in WebView":"La inspección sólo está soportada para WebView actualmente","Inspector":"Inspector","Installation canceled by user.":"Instalación cancelada por el usuario","Installation failed due to an unknown error.":"La instalación falló debido a un error desconocido","Installation succeeded.":"Instalado con éxito","Installation timed out.":"La instalación superó el tiempo de espera","Installing app...":"Instalando aplicación...","Key":"Llave","Keys":"Llaves","Language":"Idioma","Launch Activity":"Iniciar actividad","Launching activity...":"Iniciando actividad","Level":"Nivel","Location":"Posición","Lock Rotation":"Bloquear rotación","Logs":"Trazas","Maintenance":"Mantenimiento","Make sure to copy your access token now. You won't be able to see it again.":"Asegúrate de copiar el testigo de acceso ahora. Si lo pierde no se podrá recuperar.","Manage Apps":"Gestionar aplicaciones","Media":"Medio","Memory":"Memoria","Menu":"Menú","Mobile":"Móvil","Model":"Modelo","More about ADB Keys":"Más sobre llaves ADB","More about Access Tokens":"Más sobre Tokens de acceso","Mute":"Silencio","Name":"Nombre","Native":"Nativo","Navigation":"Navegación","Network":"Red","Next":"Siguiente","No":"No","No ADB keys":"No hay llaves ADB","No Ports Forwarded":"No hay puertos redirigidos","No access tokens":"Sin tokens de acceso","No clipboard data":"No hay datos en el portapapeles","No cookies to show":"No hay cookies que mostrar","No devices connected":"No hay dispositivos conectados","No photo available":"No hay imagen disponible","No screenshots taken":"No hay capturas de pantalla","Normal Mode":"Modo normal","Not Charging":"No se está cargando","Notes":"Notas","Nothing to inspect":"No hay nada que inspeccionar","Notifications":"Notificaciones","Number":"Número","Offline":"Offline","Oops!":"¡Ups!","Open":"Abrir","Orientation":"Orientación","Over Voltage":"Exceso de voltaje","Overheat":"Exceso de temperatura","PID":"PID","Package":"Paquete","Password":"Contraseña","Permissions":"Permisos","Phone":"Teléfono","Phone ICCID":"ICCID del teléfono","Phone IMEI":"IMEI del teléfono","Phone IMSI":"IMSI del teléfono","Physical Device":"Dispositivo físico","Place":"Lugar","Platform":"Plataforma","Play/Pause":"Inicio/Pausa","Please enter a valid email":"Por favor, introduce un email válido","Please enter your LDAP username":"Por favor, introduce tu usuario de LDAP","Please enter your Store password":"Por favor, introduce tu contraseña","Please enter your Store username":"Por favor, introduce to nombre de usuario","Please enter your email":"Por favor, introduce tu email","Please enter your name":"Por favor, introduce tu nombre","Please enter your password":"Por favor, introduce tu contraseña","Port":"Puerto","Port Forwarding":"Puerto de reenvío","Preparing":"Preparando","Press Back button":"Pulsa el botón Volver","Press Home button":"Pulsa el botón Home","Press Menu button":"Pulsa el botón Menú","Previous":"Anterior","Processing...":"Procesando...","Product":"Producto","RAM":"RAM","ROM":"ROM","Ready":"Listo","Reconnected successfully.":"Reconectado con éxito","Refresh":"Actualizar","Reload":"Recargar","Remote debug":"Conexión remota","Remove":"Eliminar","Reset":"Reiniciar","Reset Settings":"Restablecer ajustes","Reset all browser settings":"Restablecer todos los ajustes del navegador","Restart Device":"Reiniciar dispositivo","Retry":"Reintentar","Rotate Left":"Rotar a la izquierda","Rotate Right":"Rotar a la derecha","Run":"Ejecutar","Run JavaScript":"Ejecutar JavaScript","Run this command to copy the key to your clipboard":"Ejecuta este comando para copiar la clave al portapapeles","SDK":"SDK","SIM":"SIM","Save ScreenShot":"Guardar captura de pantalla","Save...":"Guardar","Screen":"Pantalla","Screenshot":"Captura de pantalla","Screenshots":"Capturas de Pantalla","Search":"Buscar","Serial":"Serie","Server":"Servidor","Settings":"Configuración","Shell":"Línea de Comandos","Show Screen":"Mostar pantalla","Sign In":"Acceder","Sign Out":"Desconectar","Silent Mode":"Modo silencio","Size":"Tamaño","Socket connection was lost":"Se perdió la conexión con el socket","Someone stole your device.":"Alguien robó tu dispositivo","Special Keys":"Teclas especiales","Status":"Estado","Stop":"Parar","Sub Type":"Subtipo","Tag":"Etiqueta","Take Screenshot":"Capturar pantalla","Temperature":"Temperatura","Text":"Texto","The current view is marked secure and cannot be viewed remotely.":"La vista actual está marcada como segura y no puede ser vista de forma remota","The device will be unavailable for a moment.":"El dispositivo no estará disponible durante unos instantes","The existing package could not be deleted.":"El paquete no se pudo eliminar","The new package couldn't be installed because the verification did not succeed.":"El nuevo paquete no se pudo instalar porque no se pudo verificar","The new package couldn't be installed because the verification timed out.":"El nuevo paquete no se pudo instalar porque se excedió el tiempo de espera al verificarlo","The new package couldn't be installed in the specified install location.":"El nuevo paquete no se pudo instalar en el sitio especificado para su instalación","The new package failed because the current SDK version is newer than that required by the package.":"El nuevo paquete falló porque la versión actual del SDK es más reciente que la que requiere el paquete","The new package failed because the current SDK version is older than that required by the package.":"El nuevo paquete falló porque la versión actual del SDK es más antigua que la que requiere el paquete","The new package has an older version code than the currently installed package.":"El nuevo paquete tiene una versión de código más antigua que el paquete instalado actualmente.","The new package uses a feature that is not available.":"El nuevo paquete utiliza una característica que no está disponible.","The package archive file is invalid.":"El archivo del paquete no es válido","The package being installed contains native code, but none that is compatible with the device's CPU_ABI.":"El paquete que se está instalando contiene código nativo que no es compatible con el CPU_ABI del dispositivo.","The package is already installed.":"El paquete ya está instalado","Tip:":"Truco:","Title":"Título","Total Devices":"Dispositivos Totales","Try to reconnect":"Volver a conectar","Type":"Tipo","USB":"USB","Unauthorized":"No autorizado","Uninstall":"Desinstalar","Unknown":"Desconocido","Unknown reason.":"Razón desconocida.","Unlock Rotation":"Desbloquear rotación","Unspecified Failure":"Fallo no especificado","Upload From Link":"Subir desde enlace","Upload failed":"Subida fallida","Upload unknown error":"Error de subida desconocido","Uploaded file is not valid":"El fichero de subida no es válido","Uploading...":"Subiendo...","Usb speed":"Velocidad de USB","Use":"Uso","User":"Usuario","Username":"Nombre de usuario","Using":"En uso","Version":"Versión","Vibrate Mode":"Modo vibración","Volume":"Volumen","Volume Down":"Bajar volumen","Volume Up":"Subir volumen","Warning:":"Atención:","Web":"Web","WiFi":"WIFI","Width":"Ancho","Yes":"Sí","translate":"traducir"}} diff --git a/res/common/lang/translations/stf.fr.json b/res/common/lang/translations/stf.fr.json index 34ea1c53..558b13d2 100644 --- a/res/common/lang/translations/stf.fr.json +++ b/res/common/lang/translations/stf.fr.json @@ -1 +1 @@ -{"fr":{"-":"-","A new version of STF is available":"Une nouvelle version de STF est disponible","A package is already installed with the same name.":"Un paquet est déjà installé avec le même nom","A previously installed package of the same name has a different signature than the new package (and the old package's data was not removed).":"Un paquet précédemment installée du même nom a une signature différente de celle du nouveau paquet (et les données de l'ancien paquet n'a pas été supprimée).","A secure container mount point couldn't be accessed on external media.":"Un conteneur sécurisé équipé ne peut pas être accessible sur un support externe.","ABI":"IBP","AC":"AC","Access Tokens":"Jetons d'Accès","Account":"Compte","Action":"Action","Actions":"Actions","Activity":"Activité","ADB Keys":"Clefs ADB","Add":"Ajouter","Add ADB Key":"Ajouter une Clef ADB","Add Key":"Ajouter une Clef","Add the following ADB Key to STF?":"Ajouter la Clef ADB suivante dans STF?","Admin mode has been disabled.":"Le Mode Administrateur a été désactivé","Admin mode has been enabled.":"Le Mode Administrateur a été activé","Advanced":"Avancé","Advanced Input":"Entrée Avancé","Airplane Mode":"Mode Avion","App Store":"App Store","App Upload":"Téléverser une Application","Apps":"Applications","Are you sure you want to reboot this device?":"Est vous sûr de vouloir redémarrer ce terminal?","Automation":"Automatisation","Available":"Disponible","Back":"Précédent","Battery":"Batterie","Battery Health":"Santé de la Batterie","Battery Level":"Niveau de la Batterie","Battery Source":"Source de la Batterie","Battery Status":"Statut de la Batterie","Battery Temp":"Température de la Batterie","Bluetooth":"Bluetooth","Browser":"Navigateur","Busy":"Occupé","Busy Devices":"Terminaux Occupés","Camera":"Caméra","Cancel":"Annuler","Cannot access specified URL":"Impossible d’accéder à l'URL spécifiée","Carrier":"Opérateur","Category":"Catégorie","Charging":"Chargement","Check errors below":"Vérifier les erreurs ci-dessous","Clear":"Nettoyer","Clipboard":"Presse-papier","Cold":"Froid","Connected":"Connecté","Connected successfully.":"Connexion réussie","Control":"Contrôle","Cookies":"Cookies","Cores":"Coeurs","CPU":"CPU","Current rotation:":"Rotation actuelle","Customize":"Personnaliser","D-pad Center":"D-pad Centre","D-pad Down":"D-pad Bas","D-pad Left":"D-pad Gauche","D-pad Right":"D-pad Droite","D-pad Up":"D-pad Haut","Dashboard":"Tableau","Data":"Données","Date":"Date","Dead":"Mort","Delete":"Supprimer","Density":"Densité","Details":"Détails","Developer":"Développeur","Device":"Terminal","Device cannot get kicked from the group":"Le Terminal ne peut pas être exclu du groupe","Device is not present anymore for some reason.":"Le Terminal n'est plus présent pour certaines raisons","Device is present but offline.":"Le Terminal est présent mais Hors-Ligne","Device Photo":"Photos du Terminal","Device Settings":"Paramètres du Terminal","Device was disconnected":"Le Terminal était déconnecté","Device was kicked by automatic timeout.":"Le Terminal a été exclu par le Timeout automatique","Devices":"Terminaux","Disable WiFi":"Désactiver le Wifi","Discharging":"En Décharge","Disconnected":"Déconnecté","Display":"écran","Drop file to upload":"Déposer le fichier à téléverser","Dummy":"Mannequin","Enable notifications":"Activer les notifications","Enable WiFi":"Activer le Wifi","Encrypted":"Crypté","Error":"Erreur","Error while getting data":"Erreur lors de l'obtention de données","Error while reconnecting":"Erreur lors de la reconnexion","Ethernet":"Ethernet","Executes remote shell commands":"Exécute des commandes Shell à distance","Failed to download file":"Impossible de télécharger le fichier","Fast Forward":"Avance Rapide","File Explorer":"Explorateur de Fichiers","File Name":"Nom de fichier","Filter":"Filtrer","Find Device":"Trouver un Terminal","Fingerprint":"Empreinte Digitale","FPS":"FPS","Frequency":"Fréquence","Full":"Rempli","General":"Général","Generate Access Token":"Générer un Jeton d'Accès","Generate Login for VNC":"Générer un identifiant pour VNC","Generate New Token":"Générer un Nouveau Jeton","Get":"Obtenir","Get clipboard contents":"Obtenir le contenu du Presse-Papier","Go Back":"Retour","Go Forward":"Avancer","Go to Device List":"Aller à la Liste des Terminaux","Good":"Bien","Hardware":"Matériel","Health":"Santé","Height":"Taille","Help":"Aide","Hide Screen":"Cacher l'écran","Home":"Accueil","Host":"Hôte","Hostname":"Nom de l'Hôte","ICCID":"ICCID","ID":"ID","IMEI":"IMEI","Incorrect login details":"Informations de connexion incorrectes","Info":"Informations","Inspect Device":"Inspecter le Terminal","Inspecting is currently only supported in WebView":"L'inspection est actuellement pris en charge uniquement dans WebView","Inspector":"Inspecteur","Installation canceled by user.":"Installation annulée par l'utilisateur","Installation failed due to an unknown error.":"Installation échouée due à une erreur inconnue","Installation succeeded.":"Installation réussie","Installation timed out.":"L'installation a expirée.","Installing app...":"En cours d'installation de l'application","Key":"Clef","Keys":"Clefs","Landscape":"Paysage","Language":"Langage","Launch Activity":"Lancer l'Activité","Launching activity...":"En cours de lancement de l'activité ...","Level":"Niveau","Local Settings":"Paramètres locaux","Location":"Localisation","Lock Rotation":"Bloquer la Rotation","Logs":"Logs","Maintenance":"Maintenance","Make sure to copy your access token now. You won't be able to see it again.":"Assurez-vous de copier votre jeton d'accès maintenant. Vous ne serez pas en mesure de le voir à nouveau.","Manage Apps":"Gérer les Applications","Manner Mode":"Mode Silencieux","Manufacturer":"Fabricant","Media":"Médias","Memory":"Mémoire","Menu":"Menu","Mobile":"Mobile","Mobile DUN":"Réseau Commuté","Mobile High Priority":"Mobile en Priorité Haute","Mobile MMS":"MMS","Mobile SUPL":"SUPL","Model":"Modèle","More about Access Tokens":"En savoir plus sur les Jetons d'Accès","More about ADB Keys":"En savoir plus sur les Clefs ADB","Mute":"Muet","Name":"Nom","Native":"Natif","Navigation":"Navigation","Network":"Réseau","Next":"Suivant","No":"Non","No access tokens":"Pas d'accès aux jetons","No ADB keys":"Pas de clefs ADB","No clipboard data":"Pas de données dans le Presse-Papier","No cookies to show":"Pas de cookies à afficher","No device screen":"Pas d'écran de terminal","No devices connected":"Pas de terminaux connectés","No photo available":"Pas de photos disponibles","No Ports Forwarded":"Pas de ports redirigés","No screenshots taken":"Pas de captures d'écran prises","Normal Mode":"Mode Normal","Not Charging":"Pas en charge","Notes":"Notes","Nothing to inspect":"Rien à inspecter","Notifications":"Notifications","Number":"Nombre","Offline":"Hors Ligne","Oops!":"Oups!","Open":"Ouvrir","Orientation":"Orientation","OS":"OS","Over Voltage":"Surtension","Overheat":"Surchauffe","Package":"Paquet","Password":"Mot de Passe","Permissions":"Permissions","Phone":"Téléphone","Phone ICCID":"ICCID du Téléphone","Phone IMEI":"IMEI du Téléphone","Physical Device":"Terminal Physique","PID":"PID","Place":"Place","Platform":"Plateforme","Play/Pause":"Jouer/Pause","Please enter a valid email":"S'il vous plaît entrez un e-mail valide","Please enter your email":"S'il vous plaît entrez vôtre e-mail","Please enter your LDAP username":"S'il vous plaît entrez vôtre compte LDAP","Please enter your name":"S'il vous plaît entrez vôtre nom","Please enter your password":"S'il vous plaît entrez vôtre mot de passe","Please enter your Store password":"S'il vous plaît entrez vôtre mot de passe du Store","Please enter your Store username":"S'il vous plaît entrez vôtre identifiant du Store","Port":"Port","Port Forwarding":"Redirection de Ports","Portrait":"Portrait","Power":"Alimentation","Power Source":"Source d'Alimentation","Preparing":"En Préparation","Press Back button":"Appuyer sur le bouton Retour","Press Home button":"Appuyer sur le bouton Accueil","Press Menu button":"Appuyer sur le bouton Menu","Previous":"Précédent","Processing...":"En Traitement ....","Product":"Produit","Pushing app...":"En cours de téléversement des Applications ....","RAM":"RAM","Ready":"Prêt","Reconnected successfully.":"Reconnexions réussis","Refresh":"Rafraîchir","Released":"Versionée","Reload":"Recharger","Remote debug":"Débogage à distance","Remove":"Enlever","Reset":"Réinitialiser","Reset all browser settings":"Réinitialiser tous les paramètres des navigateurs","Reset Settings":"Réinitialiser les paramètres","Restart Device":"Redémarrer le Terminal","Retrieving the device screen has timed out.":"La récupération de l'écran de l'appareil a expiré.","Retry":"Recommencer","Rewind":"Rembobiner","Roaming":"Roaming","ROM":"ROM","Rotate Left":"Tourner vers la gauche","Rotate Right":"Tourner vers la droite","Run":"Exécuter","Run JavaScript":"Exécuter JavaScript","Run the following on your command line to debug the device from your Browser":"Exécutez la commande suivante sur la ligne de commande pour déboguer le périphérique de votre navigateur","Run the following on your command line to debug the device from your IDE":"Exécutez la commande suivante sur la ligne de commande pour déboguer le périphérique de votre IDE","Run this command to copy the key to your clipboard":"Exécutez cette commande pour copier la clef de votre presse-papier","Sample of log format":"Échantillon de format de logs","Save Logs":"Sauvegarder les logs","Save ScreenShot":"Sauver la capture d'écran","Save...":"Sauvegarde ...","Screen":"écran","Screenshot":"Capture d'écran","Screenshots":"Captures d'écran","SD Card Mounted":"Carte SD Monté","SDK":"SDK","Search":"Rechercher","Selects Next IME":"Sélectionner le prochain IME","Serial":"Sériel","Server":"Serveur","Server error. Check log output.":"Erreur Serveur. Vérifier les logs de sortie.","Set":"Paramétrer","Set Cookie":"Paramétrer le Cookie","Settings":"Paramètres","Shell":"Shell","Show Screen":"Afficher l'écran","Sign In":"S'enregistrer","Sign Out":"Se déconnecter","Silent Mode":"Mode Silencieux","SIM":"SIM","Size":"Taille","Socket connection was lost":"La connexion au Socket a été perdu","Someone stole your device.":"Quelqu'un a volé votre terminal.","Special Keys":"Clefs spéciales","Start/Stop Logging":"Démarrer/Arrêter les logs","Status":"Statut","Stop":"Arrêter","Stop Using":"Cesser d'utiliser","Store Account":"Compte du Store","Sub Type":"Sous Type","Switch Charset":"Permuter le Charset","Tag":"étiquette","Take Pageshot (Needs WebView running)":"Prendre une Prise de vue de la page (besoin de WebView)","Take Screenshot":"Prendre une Capture d'écran","Temperature":"Température","Text":"Texte","The current view is marked secure and cannot be viewed remotely.":"La vue actuelle est marqué sécurisé et ne peut être consulté à distance.","The device will be unavailable for a moment.":"Le terminal ne sera pas disponible pour un moment","The existing package could not be deleted.":"Le package existant ne peut pas être supprimé.","The new package couldn't be installed because the verification did not succeed.":"Le nouveau paquet n'a pas pu être installé car la vérification n'a pas réussi.","The new package couldn't be installed because the verification timed out.":"Le nouveau paquet n'a pas pu être installé car la vérification a expiré.","The new package couldn't be installed in the specified install location because the media is not available.":"Le nouveau paquet n'a pas pu être installé à l'emplacement spécifié pour l'installation parce que les médias ne sont pas disponibles.","The new package couldn't be installed in the specified install location.":"Le nouveau paquet n'a pas pu être installé à l'emplacement spécifié pour installation.","The new package failed because it contains a content provider with thesame authority as a provider already installed in the system.":"Le nouveau paquet a échoué car it contient un fournisseur de contenu avec la même autorité en tant que fournisseur déjà installé dans le système.","The new package failed because it has specified that it is a test-only package and the caller has not supplied the INSTALL_ALLOW_TEST flag.":"Le nouveau paquet a échoué car il a précisé qu'il est un paquet de test uniquement et l'appelant n'a pas fourni le drapeau INSTALL_ALLOW_TEST.","The new package failed because the current SDK version is newer than that required by the package.":"Le nouveau paquet a échoué parce que la version actuelle du SDK est plus récente que celle requise par le paquet.","The new package failed because the current SDK version is older than that required by the package.":"Le nouveau paquet a échoué parce que la version actuelle du SDK est plus ancienne que celle requise par le paquet.","The new package failed while optimizing and validating its dex files, either because there was not enough storage or the validation failed.":"Le nouveau paquet a échoué lors de l'optimisation et l'évaluation de ses fichiers dex, soit parce qu'il n'y avait pas assez de stockage ou la validation a échoué.","The new package has an older version code than the currently installed package.":"Le nouveau paquet a un code de version plus ancien que le paquet actuellement installé.","The new package is assigned a different UID than it previously held.":"Le nouveau paquet est affecté un ID différent qu'il détenait auparavant.","The new package uses a feature that is not available.":"Le nouveau paquet utilise une fonctionnalité qui n'est pas disponible.","The new package uses a shared library that is not available.":"Le nouveau paquet utilise une bibliothèque partagée qui n'est pas disponible.","The package archive file is invalid.":"Le fichier d'archive de paquet est invalide.","The package being installed contains native code, but none that is compatible with the device's CPU_ABI.":"Le package installé contient du code natif, mais aucun qui soit compatible avec le CPU_ABI du terminal.","The package changed from what the calling program expected.":"Le paquet a changé de ce que le programme appelant avait prévu.","The package is already installed.":"Le paquet est déjà installé","The package manager service found that the device didn't have enough storage space to install the app.":"Le service de gestionnaire de paquets a constaté que le terminal ne dispose pas de suffisamment d'espace de stockage pour installer l'application.","The parser did not find any actionable tags (instrumentation or application) in the manifest.":"L'analyseur n'a pas trouvé toutes les tags actionnables (de l'instrumentation et des applications) dans le manifest.","The parser did not find any certificates in the .apk.":"L'analyseur n'a pas trouvé de certificat dans le fichier .apk.","The parser encountered a bad or missing package name in the manifest.":"L'analyseur a rencontré un mauvais ou manquant nom du paquet dans le manifest.","The parser encountered a bad shared user id name in the manifest.":"L'analyseur a rencontré un mauvais nom d'utilisateur partagé dans le manifest.","The parser encountered a CertificateEncodingException in one of the files in the .apk.":"L'analyseur a rencontré une CertificateEncodingException dans l'un des fichiers de l'apk.","The parser encountered an unexpected exception.":"L'analyseur a rencontré une exception inattendue.","The parser encountered some structural problem in the manifest.":"L'analyseur a rencontré un problème structurel dans le manifest.","The parser found inconsistent certificates on the files in the .apk.":"L'analyseur a trouvé des certificats contradictoires sur les fichiers de l'apk.","The parser was given a path that is not a file, or does not end with the expected '.apk' extension.":"L'analyseur a donné un chemin qui n'est pas un fichier, ou ne se termine pas avec le \".apk\" extension attendue.","The parser was unable to retrieve the AndroidManifest.xml file.":"L'analyseur n'a pas pu extraire le fichier AndroidManifest.xml.","The requested shared user does not exist.":"L'utilisateur requêté partagé n'existe pas.","The system failed to install the package because its packaged native code did not match any of the ABIs supported by the system.":"Le système n'a pas réussi à installer le paquet parce que son code natif emballé ne correspond à aucune ABI supporté par le système.","The system failed to install the package because of system issues.":"Le système n'a pas réussi à installer le paquet en raison de problèmes du système.","The system failed to install the package because the user is restricted from installing apps.":"Le système n'a pas réussi à installer le paquet parce que l'utilisateur est limité à partir de l'installation d'applications.","The URI passed in is invalid.":"L'URI transmise n'est pas invalide.","TID":"TID","Time":"Temps","Tip:":"Astuce:","Title":"Titre","Toggle Web/Native":"Basculer de Web/Natif","Total Devices":"Nombre total de Terminaux","translate":"Traduire","Try to reconnect":"Essayer de se reconnecter","Type":"Type","Unauthorized":"Non Autorisé","Uninstall":"Désinstaller","Unknown":"Inconnu","Unknown reason.":"Raison inconnue.","Unlock Rotation":"Débloquer la Rotation","Unspecified Failure":"Défaillance non spécifiée","Upload failed":"Téléversement raté","Upload From Link":"Téléverser depuis le Lien","Upload unknown error":"Erreur inconnue lors du Téléversement","Uploaded file is not valid":"Le fichier téléversé n'est pas valide","Uploading...":"En cours de téléversement ...","Usable Devices":"Terminaux utilisables","USB":"USB","Usb speed":"Vitesse USB","Use":"Utiliser","User":"Utilisateur","Username":"Nom de l'utilisateur","Using":"En Utilisation","Using Fallback":"Reprise de l'Utilisation","Version":"Version","Version Update":"Version de la mise à jour","Vibrate Mode":"Mode Vibration","VNC":"VNC","Voltage":"Tension","Volume":"Volume","Volume Down":"Baisser le Volume","Volume Up":"Augmenter le Volume","Warning:":"Avertissement:","Web":"Web","Width":"Largeur","WiFi":"Wifi","WiMAX":"WiMax","Wireless":"Sans Fil","X DPI":"X DPI","Y DPI":"Y DPI","Yes":"Oui","You (or someone else) kicked the device.":"Vous (ou quelqu'un d'autre) a exclu le Terminal."}} \ No newline at end of file +{"fr":{"-":"-","A new version of STF is available":"Une nouvelle version de STF est disponible","A package is already installed with the same name.":"Un paquet est déjà installé avec le même nom","A previously installed package of the same name has a different signature than the new package (and the old package's data was not removed).":"Un paquet précédemment installée du même nom a une signature différente de celle du nouveau paquet (et les données de l'ancien paquet n'a pas été supprimée).","A secure container mount point couldn't be accessed on external media.":"Un conteneur sécurisé équipé ne peut pas être accessible sur un support externe.","ABI":"IBP","AC":"AC","ADB Keys":"Clefs ADB","Access Tokens":"Jetons d'Accès","Account":"Compte","Action":"Action","Actions":"Actions","Activity":"Activité","Add":"Ajouter","Add ADB Key":"Ajouter une Clef ADB","Add Key":"Ajouter une Clef","Add the following ADB Key to STF?":"Ajouter la Clef ADB suivante dans STF?","Admin mode has been disabled.":"Le Mode Administrateur a été désactivé","Admin mode has been enabled.":"Le Mode Administrateur a été activé","Advanced":"Avancé","Advanced Input":"Entrée Avancé","Airplane Mode":"Mode Avion","App Store":"App Store","App Upload":"Téléverser une Application","Apps":"Applications","Are you sure you want to reboot this device?":"Est vous sûr de vouloir redémarrer ce terminal?","Automating":"En cours d'automatisation","Automation":"Automatisation","Available":"Disponible","Back":"Précédent","Battery":"Batterie","Battery Health":"Santé de la Batterie","Battery Level":"Niveau de la Batterie","Battery Source":"Source de la Batterie","Battery Status":"Statut de la Batterie","Battery Temp":"Température de la Batterie","Bluetooth":"Bluetooth","Browser":"Navigateur","Busy":"Occupé","Busy Devices":"Terminaux Occupés","CPU":"CPU","Camera":"Caméra","Cancel":"Annuler","Cannot access specified URL":"Impossible d’accéder à l'URL spécifiée","Carrier":"Opérateur","Category":"Catégorie","Charging":"Chargement","Check errors below":"Vérifier les erreurs ci-dessous","Clear":"Nettoyer","Clipboard":"Presse-papier","Cold":"Froid","Connected":"Connecté","Connected successfully.":"Connexion réussie","Control":"Contrôle","Cookies":"Cookies","Cores":"Coeurs","Current rotation:":"Rotation actuelle","Customize":"Personnaliser","D-pad Center":"D-pad Centre","D-pad Down":"D-pad Bas","D-pad Left":"D-pad Gauche","D-pad Right":"D-pad Droite","D-pad Up":"D-pad Haut","Dashboard":"Tableau","Data":"Données","Date":"Date","Dead":"Mort","Delete":"Supprimer","Density":"Densité","Details":"Détails","Developer":"Développeur","Device":"Terminal","Device Photo":"Photos du Terminal","Device Settings":"Paramètres du Terminal","Device cannot get kicked from the group":"Le Terminal ne peut pas être exclu du groupe","Device is not present anymore for some reason.":"Le Terminal n'est plus présent pour certaines raisons","Device is present but offline.":"Le Terminal est présent mais Hors-Ligne","Device was disconnected":"Le Terminal était déconnecté","Device was kicked by automatic timeout.":"Le Terminal a été exclu par le Timeout automatique","Devices":"Terminaux","Disable WiFi":"Désactiver le Wifi","Discharging":"En Décharge","Disconnected":"Déconnecté","Display":"écran","Domain":"Domaine","Drop file to upload":"Déposer le fichier à téléverser","Dummy":"Mannequin","Enable WiFi":"Activer le Wifi","Enable notifications":"Activer les notifications","Encrypted":"Crypté","Error":"Erreur","Error while getting data":"Erreur lors de l'obtention de données","Error while reconnecting":"Erreur lors de la reconnexion","Ethernet":"Ethernet","Executes remote shell commands":"Exécute des commandes Shell à distance","FPS":"FPS","Failed to download file":"Impossible de télécharger le fichier","Fast Forward":"Avance Rapide","File Explorer":"Explorateur de Fichiers","Filter":"Filtrer","Find Device":"Trouver un Terminal","Fingerprint":"Empreinte Digitale","Frequency":"Fréquence","Full":"Rempli","General":"Général","Generate Access Token":"Générer un Jeton d'Accès","Generate Login for VNC":"Générer un identifiant pour VNC","Generate New Token":"Générer un Nouveau Jeton","Get":"Obtenir","Get clipboard contents":"Obtenir le contenu du Presse-Papier","Go Back":"Retour","Go Forward":"Avancer","Go to Device List":"Aller à la Liste des Terminaux","Good":"Bien","Hardware":"Matériel","Health":"Santé","Height":"Taille","Help":"Aide","Hide Screen":"Cacher l'écran","Home":"Accueil","Host":"Hôte","Hostname":"Nom de l'Hôte","ICCID":"ICCID","ID":"ID","IMEI":"IMEI","IMSI":"IMSI","Incorrect login details":"Informations de connexion incorrectes","Info":"Informations","Inspect Device":"Inspecter le Terminal","Inspecting is currently only supported in WebView":"L'inspection est actuellement pris en charge uniquement dans WebView","Inspector":"Inspecteur","Installation canceled by user.":"Installation annulée par l'utilisateur","Installation failed due to an unknown error.":"Installation échouée due à une erreur inconnue","Installation succeeded.":"Installation réussie","Installation timed out.":"L'installation a expirée.","Installing app...":"En cours d'installation de l'application","Key":"Clef","Keys":"Clefs","Landscape":"Paysage","Language":"Langage","Launch Activity":"Lancer l'Activité","Launching activity...":"En cours de lancement de l'activité ...","Level":"Niveau","Local Settings":"Paramètres locaux","Location":"Localisation","Lock Rotation":"Bloquer la Rotation","Logs":"Logs","Maintenance":"Maintenance","Make sure to copy your access token now. You won't be able to see it again.":"Assurez-vous de copier votre jeton d'accès maintenant. Vous ne serez pas en mesure de le voir à nouveau.","Manage Apps":"Gérer les Applications","Manner Mode":"Mode Silencieux","Manufacturer":"Fabricant","Media":"Médias","Memory":"Mémoire","Menu":"Menu","Mobile":"Mobile","Mobile DUN":"Réseau Commuté","Mobile High Priority":"Mobile en Priorité Haute","Mobile MMS":"MMS","Mobile SUPL":"SUPL","Model":"Modèle","More about ADB Keys":"En savoir plus sur les Clefs ADB","More about Access Tokens":"En savoir plus sur les Jetons d'Accès","Mute":"Muet","Name":"Nom","Native":"Natif","Navigation":"Navigation","Network":"Réseau","Next":"Suivant","No":"Non","No ADB keys":"Pas de clefs ADB","No Ports Forwarded":"Pas de ports redirigés","No access tokens":"Pas d'accès aux jetons","No clipboard data":"Pas de données dans le Presse-Papier","No cookies to show":"Pas de cookies à afficher","No device screen":"Pas d'écran de terminal","No devices connected":"Pas de terminaux connectés","No photo available":"Pas de photos disponibles","No screenshots taken":"Pas de captures d'écran prises","Normal Mode":"Mode Normal","Not Charging":"Pas en charge","Notes":"Notes","Nothing to inspect":"Rien à inspecter","Notifications":"Notifications","Number":"Nombre","OS":"OS","Offline":"Hors Ligne","Oops!":"Oups!","Open":"Ouvrir","Orientation":"Orientation","Over Voltage":"Surtension","Overheat":"Surchauffe","PID":"PID","Package":"Paquet","Password":"Mot de Passe","Path":"Chemin","Permissions":"Permissions","Phone":"Téléphone","Phone ICCID":"ICCID du Téléphone","Phone IMEI":"IMEI du Téléphone","Phone IMSI":"IMSI du Téléphone","Physical Device":"Terminal Physique","Place":"Place","Platform":"Plateforme","Play/Pause":"Jouer/Pause","Please enter a valid email":"S'il vous plaît entrez un e-mail valide","Please enter your LDAP username":"S'il vous plaît entrez vôtre compte LDAP","Please enter your Store password":"S'il vous plaît entrez vôtre mot de passe du Store","Please enter your Store username":"S'il vous plaît entrez vôtre identifiant du Store","Please enter your email":"S'il vous plaît entrez vôtre e-mail","Please enter your name":"S'il vous plaît entrez vôtre nom","Please enter your password":"S'il vous plaît entrez vôtre mot de passe","Port":"Port","Port Forwarding":"Redirection de Ports","Portrait":"Portrait","Power":"Alimentation","Power Source":"Source d'Alimentation","Preparing":"En Préparation","Press Back button":"Appuyer sur le bouton Retour","Press Home button":"Appuyer sur le bouton Accueil","Press Menu button":"Appuyer sur le bouton Menu","Previous":"Précédent","Processing...":"En Traitement ....","Product":"Produit","Pushing app...":"En cours de téléversement des Applications ....","RAM":"RAM","ROM":"ROM","Ready":"Prêt","Reconnected successfully.":"Reconnexions réussis","Refresh":"Rafraîchir","Released":"Versionée","Reload":"Recharger","Remote debug":"Débogage à distance","Remove":"Enlever","Reset":"Réinitialiser","Reset Settings":"Réinitialiser les paramètres","Reset all browser settings":"Réinitialiser tous les paramètres des navigateurs","Restart Device":"Redémarrer le Terminal","Retrieving the device screen has timed out.":"La récupération de l'écran de l'appareil a expiré.","Retry":"Recommencer","Rewind":"Rembobiner","Roaming":"Roaming","Rotate Left":"Tourner vers la gauche","Rotate Right":"Tourner vers la droite","Run":"Exécuter","Run JavaScript":"Exécuter JavaScript","Run the following on your command line to debug the device from your Browser":"Exécutez la commande suivante sur la ligne de commande pour déboguer le périphérique de votre navigateur","Run the following on your command line to debug the device from your IDE":"Exécutez la commande suivante sur la ligne de commande pour déboguer le périphérique de votre IDE","Run this command to copy the key to your clipboard":"Exécutez cette commande pour copier la clef de votre presse-papier","SD Card Mounted":"Carte SD Monté","SDK":"SDK","SIM":"SIM","Save ScreenShot":"Sauver la capture d'écran","Save...":"Sauvegarde ...","Screen":"écran","Screenshot":"Capture d'écran","Screenshots":"Captures d'écran","Search":"Rechercher","Secure":"Protéger","Selects Next IME":"Sélectionner le prochain IME","Serial":"Sériel","Server":"Serveur","Server error. Check log output.":"Erreur Serveur. Vérifier les logs de sortie.","Set":"Paramétrer","Set Cookie":"Paramétrer le Cookie","Settings":"Paramètres","Shell":"Shell","Show Screen":"Afficher l'écran","Sign In":"S'enregistrer","Sign Out":"Se déconnecter","Silent Mode":"Mode Silencieux","Size":"Taille","Socket connection was lost":"La connexion au Socket a été perdu","Someone stole your device.":"Quelqu'un a volé votre terminal.","Special Keys":"Clefs spéciales","Start/Stop Logging":"Démarrer/Arrêter les logs","Status":"Statut","Stop":"Arrêter","Stop Automation":"Arrêter l'automatisation","Stop Using":"Cesser d'utiliser","Store Account":"Compte du Store","Sub Type":"Sous Type","Switch Charset":"Permuter le Charset","TID":"TID","Tag":"étiquette","Take Pageshot (Needs WebView running)":"Prendre une Prise de vue de la page (besoin de WebView)","Take Screenshot":"Prendre une Capture d'écran","Temperature":"Température","Text":"Texte","The URI passed in is invalid.":"L'URI transmise n'est pas invalide.","The current view is marked secure and cannot be viewed remotely.":"La vue actuelle est marqué sécurisé et ne peut être consulté à distance.","The device will be unavailable for a moment.":"Le terminal ne sera pas disponible pour un moment","The existing package could not be deleted.":"Le package existant ne peut pas être supprimé.","The new package couldn't be installed because the verification did not succeed.":"Le nouveau paquet n'a pas pu être installé car la vérification n'a pas réussi.","The new package couldn't be installed because the verification timed out.":"Le nouveau paquet n'a pas pu être installé car la vérification a expiré.","The new package couldn't be installed in the specified install location because the media is not available.":"Le nouveau paquet n'a pas pu être installé à l'emplacement spécifié pour l'installation parce que les médias ne sont pas disponibles.","The new package couldn't be installed in the specified install location.":"Le nouveau paquet n'a pas pu être installé à l'emplacement spécifié pour installation.","The new package failed because it contains a content provider with thesame authority as a provider already installed in the system.":"Le nouveau paquet a échoué car it contient un fournisseur de contenu avec la même autorité en tant que fournisseur déjà installé dans le système.","The new package failed because it has specified that it is a test-only package and the caller has not supplied the INSTALL_ALLOW_TEST flag.":"Le nouveau paquet a échoué car il a précisé qu'il est un paquet de test uniquement et l'appelant n'a pas fourni le drapeau INSTALL_ALLOW_TEST.","The new package failed because the current SDK version is newer than that required by the package.":"Le nouveau paquet a échoué parce que la version actuelle du SDK est plus récente que celle requise par le paquet.","The new package failed because the current SDK version is older than that required by the package.":"Le nouveau paquet a échoué parce que la version actuelle du SDK est plus ancienne que celle requise par le paquet.","The new package failed while optimizing and validating its dex files, either because there was not enough storage or the validation failed.":"Le nouveau paquet a échoué lors de l'optimisation et l'évaluation de ses fichiers dex, soit parce qu'il n'y avait pas assez de stockage ou la validation a échoué.","The new package has an older version code than the currently installed package.":"Le nouveau paquet a un code de version plus ancien que le paquet actuellement installé.","The new package is assigned a different UID than it previously held.":"Le nouveau paquet est affecté un ID différent qu'il détenait auparavant.","The new package uses a feature that is not available.":"Le nouveau paquet utilise une fonctionnalité qui n'est pas disponible.","The new package uses a shared library that is not available.":"Le nouveau paquet utilise une bibliothèque partagée qui n'est pas disponible.","The package archive file is invalid.":"Le fichier d'archive de paquet est invalide.","The package being installed contains native code, but none that is compatible with the device's CPU_ABI.":"Le package installé contient du code natif, mais aucun qui soit compatible avec le CPU_ABI du terminal.","The package changed from what the calling program expected.":"Le paquet a changé de ce que le programme appelant avait prévu.","The package is already installed.":"Le paquet est déjà installé","The package manager service found that the device didn't have enough storage space to install the app.":"Le service de gestionnaire de paquets a constaté que le terminal ne dispose pas de suffisamment d'espace de stockage pour installer l'application.","The parser did not find any actionable tags (instrumentation or application) in the manifest.":"L'analyseur n'a pas trouvé toutes les tags actionnables (de l'instrumentation et des applications) dans le manifest.","The parser did not find any certificates in the .apk.":"L'analyseur n'a pas trouvé de certificat dans le fichier .apk.","The parser encountered a CertificateEncodingException in one of the files in the .apk.":"L'analyseur a rencontré une CertificateEncodingException dans l'un des fichiers de l'apk.","The parser encountered a bad or missing package name in the manifest.":"L'analyseur a rencontré un mauvais ou manquant nom du paquet dans le manifest.","The parser encountered a bad shared user id name in the manifest.":"L'analyseur a rencontré un mauvais nom d'utilisateur partagé dans le manifest.","The parser encountered an unexpected exception.":"L'analyseur a rencontré une exception inattendue.","The parser encountered some structural problem in the manifest.":"L'analyseur a rencontré un problème structurel dans le manifest.","The parser found inconsistent certificates on the files in the .apk.":"L'analyseur a trouvé des certificats contradictoires sur les fichiers de l'apk.","The parser was given a path that is not a file, or does not end with the expected '.apk' extension.":"L'analyseur a donné un chemin qui n'est pas un fichier, ou ne se termine pas avec le \".apk\" extension attendue.","The parser was unable to retrieve the AndroidManifest.xml file.":"L'analyseur n'a pas pu extraire le fichier AndroidManifest.xml.","The requested shared user does not exist.":"L'utilisateur requêté partagé n'existe pas.","The system failed to install the package because its packaged native code did not match any of the ABIs supported by the system.":"Le système n'a pas réussi à installer le paquet parce que son code natif emballé ne correspond à aucune ABI supporté par le système.","The system failed to install the package because of system issues.":"Le système n'a pas réussi à installer le paquet en raison de problèmes du système.","The system failed to install the package because the user is restricted from installing apps.":"Le système n'a pas réussi à installer le paquet parce que l'utilisateur est limité à partir de l'installation d'applications.","Time":"Temps","Tip:":"Astuce:","Title":"Titre","Toggle Web/Native":"Basculer de Web/Natif","Total Devices":"Nombre total de Terminaux","Try to reconnect":"Essayer de se reconnecter","Type":"Type","USB":"USB","Unauthorized":"Non Autorisé","Uninstall":"Désinstaller","Unknown":"Inconnu","Unknown reason.":"Raison inconnue.","Unlock Rotation":"Débloquer la Rotation","Unspecified Failure":"Défaillance non spécifiée","Upload From Link":"Téléverser depuis le Lien","Upload failed":"Téléversement raté","Upload unknown error":"Erreur inconnue lors du Téléversement","Uploaded file is not valid":"Le fichier téléversé n'est pas valide","Uploading...":"En cours de téléversement ...","Usable Devices":"Terminaux utilisables","Usb speed":"Vitesse USB","Use":"Utiliser","User":"Utilisateur","Username":"Nom de l'utilisateur","Using":"En Utilisation","Using Fallback":"Reprise de l'Utilisation","VNC":"VNC","Value":"Valeur","Version":"Version","Version Update":"Version de la mise à jour","Vibrate Mode":"Mode Vibration","Voltage":"Tension","Volume":"Volume","Volume Down":"Baisser le Volume","Volume Up":"Augmenter le Volume","Warning:":"Avertissement:","Web":"Web","WiFi":"Wifi","WiMAX":"WiMax","Width":"Largeur","Wireless":"Sans Fil","X DPI":"X DPI","Y DPI":"Y DPI","Yes":"Oui","You (or someone else) kicked the device.":"Vous (ou quelqu'un d'autre) a exclu le Terminal.","translate":"Traduire"}} diff --git a/res/common/lang/translations/stf.pt_BR.json b/res/common/lang/translations/stf.pt_BR.json new file mode 100644 index 00000000..34550dac --- /dev/null +++ b/res/common/lang/translations/stf.pt_BR.json @@ -0,0 +1 @@ +{"pt_BR":{"-":"-","A new version of STF is available":"Uma nova versão do STF está disponível","A package is already installed with the same name.":"Já existe um pacote instalado com este nome.","A previously installed package of the same name has a different signature than the new package (and the old package's data was not removed).":"Um pacote instalado anteriormente com o mesmo nome tem uma assinatura diferente do novo pacote (e os dados do pacote antigo não foram removidos).","A secure container mount point couldn't be accessed on external media.":"Não foi possível acessar um ponto de montagem de um contêiner seguro em uma mídia externa.","ABI":"ABI","AC":"ACI","ADB Keys":"Chaves ADB","Access Tokens":"Tokens de Acesso","Account":"Conta","Action":"Ação","Actions":"Ações","Activity":"Atividades","Add":"Adicionar","Add ADB Key":"Adicionar chave ADB","Add Key":"Adicionar chave","Add the following ADB Key to STF?":"Adicionar esta chave ADB no STF?","Admin mode has been disabled.":"Modo administrador foi desabilitado","Admin mode has been enabled.":"Modo administrador foi habilitado","Advanced":"Avançado","Advanced Input":"Entrada Avançada","Airplane Mode":"Modo Avião","App Store":"App Store","App Upload":"Instalar Aplicativo","Apps":"Aplicativos","Are you sure you want to reboot this device?":"Você tem certeza que deseja reiniciar o dispositivo?","Automating":"Automatizando","Automation":"Automação","Available":"Disponível","Back":"Voltar","Battery":"Bateria","Battery Health":"Saúde da Bateria","Battery Level":"Nível da Bateria","Battery Source":"Fonte da Bateria","Battery Status":"Estado da Bateria","Battery Temp":"Temperatura da Bateria","Bluetooth":"Bluetooth","Browser":"Navegador","Busy":"Ocupado","Busy Devices":"Devices Ocupados","CPU":"CPU","Camera":"Câmera","Cancel":"Cancelar","Cannot access specified URL":"Não pode acessar a URL inserida","Carrier":"Operadora","Category":"Categoria","Charging":"Carregando","Check errors below":"Verifique os erros abaixo","Clear":"Limpar","Clipboard":"Área de Transferência","Cold":"Frio","Connected":"Conectado","Connected successfully.":"Conectado com sucesso.","Control":"Controlar","Cookies":"Cookies","Cores":"Núcleos","Current rotation:":"Rotação atual","Customize":"Customizar","D-pad Center":"D-pad Centralizado","D-pad Down":"D-pad abaixo","D-pad Left":"D-pad Esquerda","D-pad Right":"D-pad Direita","D-pad Up":"D-pad Acima","Dashboard":"Painel de Controle","Data":"Dados","Date":"Data","Dead":"Parado","Delete":"Deletar","Density":"Densidade","Details":"Detalhes","Developer":"Desenvolvedor","Device":"Dispositivo","Device Photo":"Foto do Dispositivo","Device Settings":"Configurações do Dispositivo","Device cannot get kicked from the group":"O dispositivo não pode ser removido do grupo","Device is not present anymore for some reason.":"O dispositivo não está mais disponível por algum motivo.","Device is present but offline.":"Dispositivo presenta mas está indisponível","Device was disconnected":"Dispositivo desconectado","Device was kicked by automatic timeout.":"Dispositivo foi removido por tempo limite automático.","Devices":"Dispositivos","Disable WiFi":"Desabilitar WiFi","Discharging":"Descarregando","Disconnected":"Disconectado","Display":"Exibição","Domain":"Domínio","Drop file to upload":"Arrastar arquivo para instalar","Dummy":"Modelo","Enable WiFi":"Ativar Wifi","Enable notifications":"Habilitar notificações","Encrypted":"Encriptar","Error":"Erro","Error while getting data":"Erro ao pegar os dados","Error while reconnecting":"Erro ao reconectar","Ethernet":"Ethernet","Executes remote shell commands":"Executar comandos shell remotos","FPS":"FPS","Failed to download file":"Falha ao baixar arquivo","Fast Forward":"Avanço Rápido ","File Explorer":"Explorar Arquivo","Filter":"Filtrar","Find Device":"Encontrar Dispositivo","Fingerprint":"Impressão Digital","Frequency":"Frequencia","Full":"Completo","General":"Geral","Generate Access Token":"Gerar Token de Acesso","Generate Login for VNC":"Gerar acesso por VNC","Generate New Token":"Gerar Novo Token","Get":"Obter","Get clipboard contents":"Obter conteúdo da área de transferência","Go Back":"Voltar","Go Forward":"Avançar","Go to Device List":"Ir para Lista de Dispositivos","Good":"Bom","Hardware":"Hardware","Health":"Saúde","Height":"Altura","Help":"Ajuda","Hide Screen":"Ocultar Tela","Home":"Início","Host":"Host","Hostname":"Nome do Host","ICCID":"ICCID","ID":"ID","IMEI":"IMEI","IMSI":"IMSI","Incorrect login details":"Informações de acesso incorretas","Info":"Informações","Inspect Device":"Inspecionar Dispositivo","Inspecting is currently only supported in WebView":"Atualmente a inspeção só é suportada no WebView","Inspector":"Inspetor","Installation canceled by user.":"Instalação cancelada pelo usuário.","Installation failed due to an unknown error.":"A instalação falhou devido a um erro desconhecido.","Installation succeeded.":"Instalado com sucesso.","Installation timed out.":"Timeout durante instalacão.","Installing app...":"Instalando aplicativo...","Key":"Chave","Keys":"Chaves","Landscape":"Paisagem","Language":"Idioma","Launch Activity":"Abrir Activity","Launching activity...":"Abrindo activity...","Level":"Nível","Local Settings":"Configurações Locais","Location":"Localização","Lock Rotation":"Desabilitar Rotação da Tela","Logs":"Logs","Maintenance":"Manutenção","Make sure to copy your access token now. You won't be able to see it again.":"Certifique-se de copiar o seu token de acesso agora. Você não será capaz de vê-lo novamente.","Manage Apps":"Gerenciar Aplicativos","Manner Mode":"Manner Mode","Manufacturer":"Fabricante","Media":"Mídia","Memory":"Memória","Menu":"Menu","Mobile":"Dispositivo","Mobile DUN":"DUN do Dispositivo","Mobile High Priority":"Dispositivo com Prioridade Alta","Mobile MMS":"MMS do Dispositivo","Mobile SUPL":"SUPL do Dispositivo","Model":"Modelo","More about ADB Keys":"Mais sobre Chaves ADB","More about Access Tokens":"Mais sobre Token de Acesso","Mute":"Mudo","Name":"Nome","Native":"Nativo","Navigation":"Navegação","Network":"Rede","Next":"Próximo","No":"Não","No ADB keys":"Nenhuma chave ADB","No Ports Forwarded":"Sem portas","No access tokens":"Nenhum token de acesso","No clipboard data":"Nenhum dado na área de transferencia","No cookies to show":"Sem cookies para mostrar","No device screen":"Nenhuma tela de dispositivo","No devices connected":"Nenhum device conectado","No photo available":"Nenhuma foto disponível ","No screenshots taken":"Nenhuma captura de tela","Normal Mode":"Modo Normal","Not Charging":"Nada Carregando","Notes":"Notas","Nothing to inspect":"Nada para inspecionar","Notifications":"Notificações","Number":"Número","OS":"SO","Offline":"Indisponível","Oops!":"Oops!","Open":"Aberto","Orientation":"Orientação","Over Voltage":"Tensão excessiva","Overheat":"Superaquecimento","PID":"PID","Package":"Pacote","Password":"Senha","Path":"Caminho","Permissions":"Permissões","Phone":"Telefone","Phone ICCID":"ICCID do Dispositivo","Phone IMEI":"IMEI do Dispositivo","Phone IMSI":"IMSI do Dispositivo","Physical Device":"Dispositivo Físico","Place":"Lugar","Platform":"Plataforma","Play/Pause":"Play/Pause","Please enter a valid email":"Por Favor, insira um e-mail válido","Please enter your LDAP username":"Por Favor entre com seu usuário LDAP","Please enter your Store password":"Por Favor entre com sua senha da Loja","Please enter your Store username":"Por Favor entre com seu usuário da Loja","Please enter your email":"Por Favor entre com seu e-mail","Please enter your name":"Por Favor entre com seu nome","Please enter your password":"Por Favor entre com sua senha","Port":"Porta","Port Forwarding":"Porta de envio","Portrait":"Retrato","Power":"Ligar","Power Source":"Fonte de energia","Preparing":"Preparando","Press Back button":"Pressionar botão Voltar","Press Home button":"Pressionar botão Início","Press Menu button":"Pressionar botão Menu","Previous":"Anterior","Processing...":"Processando...","Product":"Produto","Pushing app...":"Publicando aplicativo...","RAM":"RAM","ROM":"ROM","Ready":"Pronto","Reconnected successfully.":"Reconectado com sucesso.","Refresh":"Atualizar","Released":"Liberado","Reload":"Recaregar","Remote debug":"Dupurar remotamente","Remove":"Remover","Reset":"Resetar","Reset Settings":"Limpar Configurações","Reset all browser settings":"Restar todas as configurações do navegador","Restart Device":"Reiniciar Dipositivo","Retrieving the device screen has timed out.":"Recuperar a tela do dispositivo que expirou.","Retry":"Tentar novamente","Rewind":"Rebobinar","Roaming":"Roaming","Rotate Left":"Rotar para Esquerda","Rotate Right":"Rodar para Direita","Run":"Rodar","Run JavaScript":"Rodar JavaScript","Run the following on your command line to debug the device from your Browser":"Executar a seguinte linha de comando para depurar o navegador do seu dispositivo","Run the following on your command line to debug the device from your IDE":"Executar a seguinte linha de comando para depurar o IDE do seu dispositivo","Run this command to copy the key to your clipboard":"Executar este comando para copiar a chave para a área de transferência","SD Card Mounted":"Catão SD Montado","SDK":"DSK","SIM":"Cartão SIM","Save ScreenShot":"Salvar Captura da Tela","Save...":"Salvar...","Screen":"Tela","Screenshot":"Captura da Tela","Screenshots":"Capturas das Telas","Search":"Buscar","Secure":"Seguro","Selects Next IME":"Selecionar Próximo IME","Serial":"Serial","Server":"Servidor","Server error. Check log output.":"Servidor com erro. Verifique o log de saída","Set":"Inserir","Set Cookie":"Inserir Cookie","Settings":"Configurações","Shell":"Shell","Show Screen":"Mostrar Tela","Sign In":"Entrar","Sign Out":"Sair","Silent Mode":"Modo Silencioso","Size":"Tamanho","Socket connection was lost":"Conexão Socket foi perdida","Someone stole your device.":"Alguém roubou seu dispositivo.","Special Keys":"Chaves Especiais","Start/Stop Logging":"Iniciar/Pausar Entrada","Status":"Estado","Stop":"Parar","Stop Automation":"Parar Automação","Stop Using":"Parar de Usar","Store Account":"Conta da Loja","Sub Type":"Sub Tipo","Switch Charset":"Switch Charset","TID":"TID","Tag":"Tag","Take Pageshot (Needs WebView running)":"Capturar a Página (Necessita que o WebView seja executado)","Take Screenshot":"Captura Tela","Temperature":"Temperatura","Text":"Texto","The URI passed in is invalid.":"URI informada é invalida.","The current view is marked secure and cannot be viewed remotely.":"A visualização atual foi marcada como segura e não pode ser visualizada remotamente.","The device will be unavailable for a moment.":"Este dispositivo estará indisponível por algum momento.","The existing package could not be deleted.":"O pacote existente não pode ser deletado.","The new package couldn't be installed because the verification did not succeed.":"O novo pacote não pode ser instalado porque o arquivo verificado não está correto.","The new package couldn't be installed because the verification timed out.":"O novo pacote não pode ser instalado porque o tempo de verificação expirou.","The new package couldn't be installed in the specified install location because the media is not available.":"O novo pacote não pode ser instalado no local específico porque a mídia não está disponível.","The new package couldn't be installed in the specified install location.":"O novo pacote não pode ser instalado no local específico.","The new package failed because it contains a content provider with thesame authority as a provider already installed in the system.":"O novo pacote falhou porque ele contém um provedor de conteúdo com a mesma autoridade como um provedor já instalado no sistema.","The new package failed because it has specified that it is a test-only package and the caller has not supplied the INSTALL_ALLOW_TEST flag.":"O novo pacote falhou porque ele especificou que ele é um pacote test-only e a função que chama não forneceu o sinalizador INSTALL_ALLOW_TEST.","The new package failed because the current SDK version is newer than that required by the package.":"O novo pacote falhou porque a versão atual do SDK é mais recente do que a exigida pelo pacote.","The new package failed because the current SDK version is older than that required by the package.":"The new package failed because the current SDK version is older than that required by the package.","The new package failed while optimizing and validating its dex files, either because there was not enough storage or the validation failed.":"O novo pacote falhou ao otimizar e validar seus arquivos dex, porque não havia armazenamento suficiente ou a validação falhou.","The new package has an older version code than the currently installed package.":"O novo pacote falhou para otimizar e validar os seus arquivos dex, porque não existe uma exploração suficiente ou uma validação falhou.","The new package is assigned a different UID than it previously held.":"O novo pacote é atribuído um UID diferente do que anteriormente realizada.","The new package uses a feature that is not available.":"O novo pacote usa um recurso que não está disponível.","The new package uses a shared library that is not available.":"O novo pacote usa uma biblioteca compartilhada que não está disponível.","The package archive file is invalid.":"Arquivo no pacote é inválido.","The package being installed contains native code, but none that is compatible with the device's CPU_ABI.":"O pacote que está sendo instalado contém código nativo, mas nenhum compatível com o CPU_ABI do dispositivo.","The package changed from what the calling program expected.":"O pacote mudou do que o programa esperava.","The package is already installed.":"Pacote já instalado.","The package manager service found that the device didn't have enough storage space to install the app.":"O serviço gerenciador de pacotes descobriu que o dispositivo não tinha espaço de armazenamento suficiente para instalar o aplicativo.","The parser did not find any actionable tags (instrumentation or application) in the manifest.":"A análise não encontrou nenhum marcador acionável (instrumentação ou aplicação) no manifesto.","The parser did not find any certificates in the .apk.":"A análise não encontrou nenhum certificado no .apk.","The parser encountered a CertificateEncodingException in one of the files in the .apk.":"A Análise encontrou o CertificateEncodingException em um dos arquivos no .apk.","The parser encountered a bad or missing package name in the manifest.":"A análise encontrou um nome de pacote incorreto ou ausente no manifesto.","The parser encountered a bad shared user id name in the manifest.":"A análise encontrou um nome de ID de usuário compartilhado incorreto no manifesto.","The parser encountered an unexpected exception.":"A análise encontrou uma exceção não esperada. ","The parser encountered some structural problem in the manifest.":"A análise encontrou algum problema na estrutura do manifesto.","The parser found inconsistent certificates on the files in the .apk.":"A análise encontrou uma inconsistência no certificado presente nos arquivos do .apk.","The parser was given a path that is not a file, or does not end with the expected '.apk' extension.":"A análise encontrou: foi dado um caminho que não é um arquivo, ou não termina com a extensão '.apk' esperado.","The parser was unable to retrieve the AndroidManifest.xml file.":"Não foi possível analizar o arquivo AndroidManifest.xml.","The requested shared user does not exist.":"O usuário compartilhado solicitado não existe.","The system failed to install the package because its packaged native code did not match any of the ABIs supported by the system.":"O sistema falhou ao instalar o pacote porque seu código nativo não correspondia a nenhuma das ABIs suportadas pelo sistema.","The system failed to install the package because of system issues.":"O sistema falhou ao instalar o pacote devido a problemas do sistema.","The system failed to install the package because the user is restricted from installing apps.":"O sistema falhou ao instalar o pacote porque o usuário não é autorizado a instalar aplicativos.","Time":"Horário","Tip:":"Dica:","Title":"Título","Toggle Web/Native":"Alterar entre Web e Nativo","Total Devices":"Total de Dispositivos","Try to reconnect":"Tentar reconectar","Type":"Tipo","USB":"USB","Unauthorized":"Não Autorizado","Uninstall":"Desinstalar","Unknown":"Desconhecido","Unknown reason.":"Razão desconhecida.","Unlock Rotation":"Desabilitar Rotação","Unspecified Failure":"Falha não especificada","Upload From Link":"Fazer envio por Link","Upload failed":"Envio falhou","Upload unknown error":"Envio com erro desconhecido","Uploaded file is not valid":"Arquivo enviado não é válido","Uploading...":"Enviado...","Usable Devices":"Dispositivos Utilizáveis","Usb speed":"Velocidade do USB","Use":"Usar","User":"Usuário","Username":"Usuário","Using":"Usando","Using Fallback":"Retornar verssão","VNC":"VNC","Value":"Valor","Version":"Versão","Version Update":"Atualização da Versão","Vibrate Mode":"Modo vibrar","Voltage":"Voltage","Volume":"Volume","Volume Down":"Baixar Volume","Volume Up":"Aumentar Volume","Warning:":"Atenção:","Web":"Web","WiFi":"WiFi","WiMAX":"WiMAX","Width":"Largura","Wireless":"Wireless","X DPI":"DPI X","Y DPI":"DPI Y","Yes":"Sim","You (or someone else) kicked the device.":"Você removeu o dispositivo.","translate":"traduzir"}} \ No newline at end of file diff --git a/res/test/e2e/control/control-spec.js b/res/test/e2e/control/control-spec.js index 460cbede..3af88c1a 100644 --- a/res/test/e2e/control/control-spec.js +++ b/res/test/e2e/control/control-spec.js @@ -1,19 +1,27 @@ describe('Control Page', function() { var DeviceListPage = require('../devices') var deviceListPage = new DeviceListPage() + var localhost = browser.baseUrl var ControlPage = function() { this.get = function() { - browser.get(protractor.getInstance().baseUrl + 'control') + browser.get(localhost + 'control') } - this.kickDeviceButton = element.all(by.css('.kick-device')).first() + + this.kickDeviceButton = element.all(by.css('.kick-device')) + this.devicesDropDown = element(by.css('.device-name-text')) + + this.openDevicesDropDown = function() { + return this.devicesDropDown.click() + } + + this.getFirstKickDeviceButton = function() { + return this.kickDeviceButton.first() + } + this.kickDevice = function() { this.openDevicesDropDown() - this.kickDeviceButton.click() - } - this.devicesDropDown = element(by.css('.device-name-text')) - this.openDevicesDropDown = function() { - this.devicesDropDown.click() + this.getFirstKickDeviceButton().click() } } @@ -26,8 +34,8 @@ describe('Control Page', function() { browser.sleep(500) - browser.getLocationAbsUrl().then(function(newUrl) { - expect(newUrl).toMatch(protractor.getInstance().baseUrl + 'control') + browser.getCurrentUrl().then(function(newUrl) { + expect(newUrl).toContain(localhost + 'control/') }) }) @@ -135,10 +143,10 @@ describe('Control Page', function() { it('should stop controlling an usable device', function() { controlPage.kickDevice() - waitUrl(/devices/) + browser.wait(waitUrl(/devices/), 5000) - browser.getLocationAbsUrl().then(function(newUrl) { - expect(newUrl).toBe(protractor.getInstance().baseUrl + 'devices') + browser.getCurrentUrl().then(function(newUrl) { + expect(newUrl).toBe(localhost + 'devices') }) }) diff --git a/res/test/e2e/devices/devices-spec.js b/res/test/e2e/devices/devices-spec.js index 30ce7966..aad40971 100644 --- a/res/test/e2e/devices/devices-spec.js +++ b/res/test/e2e/devices/devices-spec.js @@ -4,10 +4,16 @@ describe('Device Page', function() { var DeviceListPage = require('./') var deviceListPage = new DeviceListPage() + var LoginPage = require('../login') + var loginPage = new LoginPage() + + var WidgetContainerPage = require('../widget-container') + var widgetContainerObj = new WidgetContainerPage() + it('should go to Devices List page', function() { deviceListPage.get() - browser.getLocationAbsUrl().then(function(newUrl) { - expect(newUrl).toBe(protractor.getInstance().baseUrl + 'devices') + browser.getCurrentUrl().then(function(newUrl) { + expect(newUrl).toBe(browser.baseUrl + 'devices') }) }) @@ -20,6 +26,18 @@ describe('Device Page', function() { expect(deviceListPage.searchInput.getAttribute('value')).toBe('state: "available"') }) + it('should not display used device if filter is set to - state using', function() { + deviceListPage.get() + deviceListPage.filterUsingDevices() + deviceListPage.getNumberOfFilteredOutDevices().then(function(amount) { + var filteredOut = amount + deviceListPage.numberOfDevices().then(function(amount) { + var notFiltered = amount + expect(notFiltered - filteredOut).toBe(0) + }) + }) + }) + it('should have more than 1 device available', function() { expect(deviceListPage.devicesUsable.count()).toBeGreaterThan(0) }) @@ -28,6 +46,66 @@ describe('Device Page', function() { expect(deviceListPage.availableDevice().getAttribute('class')).toMatch('state-available') }) + it('should be able to unassign used device', function() { + deviceListPage.get() + deviceListPage.controlAvailableDevice() + deviceListPage.get() + deviceListPage.unassignDevice() + browser.getCurrentUrl().then(function(newUrl) { + expect(newUrl).toBe(browser.baseUrl + 'devices') + }) + }) + + it('should be able to reuse assign device', function() { + // Test for issue #1076 + + deviceListPage.get() + deviceListPage.controlAvailableDevice() + deviceListPage.get() + deviceListPage.selectAssignedDevice() + browser.getCurrentUrl().then(function(newUrl) { + expect(newUrl).toContain(browser.baseUrl + 'control/') + }) + }) + + it('should one device be marked as busy as is used by another user', function() { + deviceListPage.get() + deviceListPage.controlAvailableDevice() + + loginPage.doFreshLogin('tester', 'test_user2@login.com') + deviceListPage.get() + expect(deviceListPage.getNumberOfBusyDevices()).toBe(1) + }) + + it('should not be able to pick up device marked as busy', function() { + deviceListPage.get() + deviceListPage.controlAvailableDevice() + + loginPage.doFreshLogin('tester', 'test_user2@login.com') + deviceListPage.get() + deviceListPage.selectBusyDevice() + browser.getCurrentUrl().then(function(newUrl) { + expect(newUrl).toContain(browser.baseUrl + 'devices') + }) + }) + + afterEach(function() { + // Relogin to test account if don't use standard test account + deviceListPage.get() + widgetContainerObj.getUserNameFromWidget().then(function(userName) { + if (userName.toLowerCase() !== loginPage.getUserName().toLowerCase()) { + loginPage.doFreshLogin() + } + }) + + // Unassign element if is assigned + deviceListPage.get() + deviceListPage.deviceStopUsingBtn.count().then(function(elements) { + if (elements > 0) { + deviceListPage.unassignDevice() + } + }) + }) }) describe('List View', function() { diff --git a/res/test/e2e/devices/index.js b/res/test/e2e/devices/index.js index 92529a95..2884b5e0 100644 --- a/res/test/e2e/devices/index.js +++ b/res/test/e2e/devices/index.js @@ -1,22 +1,70 @@ module.exports = function DeviceListPage() { + this.get = function() { - // TODO: Let's get rid off the login first - browser.get(protractor.getInstance().baseUrl + 'devices') + browser.get(browser.baseUrl + 'devices') + browser.wait(waitUrl(/devices/), 5000) } + this.devices = element(by.model('tracker.devices')) + this.deviceStopUsingBtn = element.all(by.css('.state-using')) this.devicesByCss = element.all(by.css('ul.devices-icon-view > li')) this.devicesUsable = element.all(by.css('.state-available')) + this.devicesBusy = element.all(by.css('.state-busy')) this.searchInput = element(by.model('search.deviceFilter')) + this.devicesFilteredOut = element.all(by.xpath('//*[contains(@class, "filter-out")]')) + this.filterAvailableDevices = function() { return this.searchInput.sendKeys('state: "available"') } - this.numberOfDevices = function() { - return this.devicesByCss.count() + + this.filterUsingDevices = function() { + return this.searchInput.sendKeys('state: "using"') } + + this.numberOfDevices = function() { + return this.devicesByCss.count().then(function(amount) { + return amount + }) + } + + this.getNumberOfFilteredOutDevices = function() { + return this.devicesFilteredOut.count().then(function(amount) { + return amount + }) + } + + this.getNumberOfBusyDevices = function() { + return this.devicesBusy.count().then(function(amount) { + return amount + }) + } + this.availableDevice = function() { return this.devicesUsable.first() } + this.controlAvailableDevice = function() { - return this.availableDevice().click() + this.availableDevice().click() + browser.wait(waitUrl(/control/), 5000) + } + + this.assignedDevice = function() { + return this.deviceStopUsingBtn.first() + } + + this.getFirstBusyDevice = function() { + return this.devicesBusy.first() + } + + this.unassignDevice = function() { + return this.assignedDevice().click() + } + + this.selectAssignedDevice = function() { + return this.assignedDevice().element(by.xpath('..')).click() + } + + this.selectBusyDevice = function() { + return this.getFirstBusyDevice().element(by.xpath('..')).click() } } diff --git a/res/test/e2e/helpers/browser-logs.js b/res/test/e2e/helpers/browser-logs.js index 989e8b60..6765a8fa 100644 --- a/res/test/e2e/helpers/browser-logs.js +++ b/res/test/e2e/helpers/browser-logs.js @@ -15,7 +15,7 @@ module.exports = function BrowserLogs(opts) { } browser.getCapabilities().then(function(cap) { - var browserName = ' ' + cap.caps_.browserName + ' log ' + var browserName = ' ' + cap.browserName + ' log ' var browserStyled = chalk.bgBlue.white.bold(browserName) + ' ' browser.manage().logs().get('browser').then(function(browserLogs) { diff --git a/res/test/e2e/login/index.js b/res/test/e2e/login/index.js index cb6389cb..c05c92f0 100644 --- a/res/test/e2e/login/index.js +++ b/res/test/e2e/login/index.js @@ -1,5 +1,5 @@ module.exports = function LoginPage() { - this.login = protractor.getInstance().params.login + this.login = browser.params.login this.get = function() { return browser.get(this.login.url) @@ -17,22 +17,37 @@ module.exports = function LoginPage() { this.setName = function(username) { return this.username.sendKeys(username) } + this.setEmail = function(email) { return this.email.sendKeys(email) } + this.setPassword = function(password) { return this.password.sendKeys(password) } + this.submit = function() { return this.username.submit() } - this.doLogin = function() { + + this.getUserName = function() { + return this.login.username + } + + this.doLogin = function(userName, email, password) { + var EC = protractor.ExpectedConditions + var timeout = 15000 + var loginName = (typeof userName !== 'undefined') ? userName : this.login.username + var loginEmail = (typeof email !== 'undefined') ? email : this.login.email + var loginPassword = (typeof password !== 'undefined') ? email : this.login.password + this.get() - this.setName(this.login.username) + browser.wait(EC.presenceOf(element(by.css('[value="Log In"]'))), timeout) + this.setName(loginName) if (this.login.method === 'ldap') { - this.setPassword(this.login.password) + this.setPassword(loginPassword) } else { - this.setEmail(this.login.email) + this.setEmail(loginEmail) } this.submit() @@ -43,6 +58,17 @@ module.exports = function LoginPage() { }) }) } + + this.doFreshLogin = function(userName, email, password) { + // Clean up cookies + browser.executeScript('window.localStorage.clear();') + browser.executeScript('window.sessionStorage.clear();') + browser.driver.manage().deleteAllCookies() + + // Procced again through login process + this.doLogin(userName, email, password) + } + this.cleanUp = function() { this.username = null this.password = null diff --git a/res/test/e2e/login/login-spec.js b/res/test/e2e/login/login-spec.js index 1a89644c..d1ec56b4 100644 --- a/res/test/e2e/login/login-spec.js +++ b/res/test/e2e/login/login-spec.js @@ -2,14 +2,20 @@ describe('Login Page', function() { var LoginPage = require('./') var loginPage = new LoginPage() + beforeEach(function() { + browser.executeScript('window.localStorage.clear();') + browser.executeScript('window.sessionStorage.clear();') + browser.driver.manage().deleteAllCookies() + }) + it('should have an url to login', function() { expect(loginPage.login.url).toMatch('http') }) it('should login with method: "' + loginPage.login.method + '"', function() { loginPage.doLogin().then(function() { - browser.getLocationAbsUrl().then(function(newUrl) { - expect(newUrl).toBe(protractor.getInstance().baseUrl + 'devices') + browser.getCurrentUrl().then(function(newUrl) { + expect(newUrl).toBe(browser.baseUrl + 'devices') }) }) }) diff --git a/res/test/e2e/widget-container/index.js b/res/test/e2e/widget-container/index.js new file mode 100644 index 00000000..3235b343 --- /dev/null +++ b/res/test/e2e/widget-container/index.js @@ -0,0 +1,18 @@ +module.exports = function WidgetContainerPage() { + + this.get = function() { + browser.get(browser.baseUrl + 'devices') + browser.wait(waitUrl(/devices/), 5000) + } + + this.userName = element(by.binding('currentUser.name')) + this.amountOfAssignedToUserDevices = element(by.xpath('//*[@class="number color-orange"]/span')) + + this.getUserNameFromWidget = function() { + return this.userName.getText() + } + + this.getAmountOfAssignedToUserDevices = function() { + return this.amountOfAssignedToUserDevices.getText() + } +} diff --git a/res/test/e2e/widget-container/widget-container-spec.js b/res/test/e2e/widget-container/widget-container-spec.js new file mode 100644 index 00000000..24e514d6 --- /dev/null +++ b/res/test/e2e/widget-container/widget-container-spec.js @@ -0,0 +1,36 @@ +describe('Widget Container Page', function() { + + var DeviceListPage = require('../devices') + var deviceListPage = new DeviceListPage() + + var WidgetContainerPage = require('./') + var widgetContainerObj = new WidgetContainerPage() + + var LoginPage = require('../login') + var loginPage = new LoginPage() + + it('should display amount of devices used by the user', function() { + deviceListPage.get() + deviceListPage.controlAvailableDevice() + deviceListPage.get() + widgetContainerObj.getAmountOfAssignedToUserDevices().then(function(amount) { + expect(amount).toBe('1') + }) + }) + + it('should display user name after login on widget', function() { + widgetContainerObj.getUserNameFromWidget().then(function(userName) { + expect(userName.toLowerCase()).toBe(loginPage.getUserName().toLowerCase()) + }) + }) + + afterEach(function() { + // Unassign element if is assigned + deviceListPage.get() + deviceListPage.deviceStopUsingBtn.count().then(function(elements) { + if (elements > 0) { + deviceListPage.unassignDevice() + } + }) + }) +}) diff --git a/res/test/protractor-multi.conf.js b/res/test/protractor-multi.conf.js index fbe7ea24..2826f90d 100644 --- a/res/test/protractor-multi.conf.js +++ b/res/test/protractor-multi.conf.js @@ -1,6 +1,5 @@ var config = require('./protractor.conf').config //var LoginPage = require('./e2e/login') -//var HtmlReporter = require('protractor-html-screenshot-reporter') //var WaitUrl = require('./e2e/helpers/wait-url') config.chromeOnly = false diff --git a/res/test/protractor.conf.js b/res/test/protractor.conf.js index 9c2610cb..af51561a 100644 --- a/res/test/protractor.conf.js +++ b/res/test/protractor.conf.js @@ -2,8 +2,12 @@ var LoginPage = require('./e2e/login') var BrowserLogs = require('./e2e/helpers/browser-logs') //var FailFast = require('./e2e/helpers/fail-fast') -var HtmlReporter = require('protractor-html-screenshot-reporter') +var jasmineReporters = require('jasmine-reporters') var WaitUrl = require('./e2e/helpers/wait-url') +var HTMLReport = require('protractor-html-reporter-2') + +var reportsDirectory = './test-results/reports-protractor' +var dashboardReportDirectory = reportsDirectory + '/dashboardReport' module.exports.config = { baseUrl: process.env.STF_URL || 'http://localhost:7100/#!/', @@ -17,7 +21,7 @@ module.exports.config = { params: { login: { url: process.env.STF_LOGINURL || process.env.STF_URL || - 'http://localhost:7120', + 'http://localhost:7100', username: process.env.STF_USERNAME || 'test_user', email: process.env.STF_EMAIL || 'test_user@login.local', password: process.env.STF_PASSWORD, @@ -34,7 +38,7 @@ module.exports.config = { capabilities: { browserName: 'chrome', chromeOptions: { - args: ['--test-type'] // Prevent security warning bug in ChromeDriver + args: ['--test-type --no-sandbox'] // Prevent security warning bug in ChromeDriver } }, chromeOnly: true, @@ -45,16 +49,60 @@ module.exports.config = { this.waitUrl = WaitUrl - jasmine.getEnv().addReporter(new HtmlReporter({ - baseDirectory: './res/test/test_out/screenshots' + jasmine.getEnv().addReporter(new jasmineReporters.JUnitXmlReporter({ + consolidateAll: true, + savePath: reportsDirectory + '/xml', + filePrefix: 'xmlOutput' })) + var fs = require('fs-extra') + if (!fs.existsSync(dashboardReportDirectory)) { + fs.mkdirs(dashboardReportDirectory) + } + + jasmine.getEnv().addReporter({ + specDone: function(result) { + if (result.status === 'failed') { + browser.getCapabilities().then(function(caps) { + var browserName = caps.get('browserName') + + browser.takeScreenshot().then(function(png) { + var stream = fs.createWriteStream(dashboardReportDirectory + '/' + + browserName + '-' + result.fullName + '.png') + stream.write(new Buffer(png, 'base64')) + stream.end() + }) + }) + } + } + }) + afterEach(function() { BrowserLogs({expectNoLogs: true}) //FailFast() }) }, onComplete: function() { + var browserName, browserVersion, platform, testConfig + var capsPromise = browser.getCapabilities() + capsPromise.then(function(caps) { + browserName = caps.get('browserName') + browserVersion = caps.get('version') + platform = caps.get('platform') + + testConfig = { + reportTitle: 'Protractor Test Execution Report', + outputPath: dashboardReportDirectory, + outputFilename: 'index', + screenshotPath: '.', + testBrowser: browserName, + browserVersion: browserVersion, + modifiedSuiteName: false, + screenshotsOnlyOnFailure: true, + testPlatform: platform + } + new HTMLReport().from(reportsDirectory + '/xml/xmlOutput.xml', testConfig) + }) } } diff --git a/vendor/STFService/STFService.apk b/vendor/STFService/STFService.apk index 0ca4ee05..72458018 100644 Binary files a/vendor/STFService/STFService.apk and b/vendor/STFService/STFService.apk differ diff --git a/yarn.lock b/yarn.lock index 3dfc04cd..a3a16ae0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -35,6 +35,11 @@ version "2.53.37" resolved "https://registry.yarnpkg.com/@types/selenium-webdriver/-/selenium-webdriver-2.53.37.tgz#34f743c20e53ae7100ede90870fde554df2447f8" +"@types/selenium-webdriver@^3.0.0": + version "3.0.16" + resolved "https://registry.yarnpkg.com/@types/selenium-webdriver/-/selenium-webdriver-3.0.16.tgz#50a4755f8e33edacd9c406729e9b930d2451902a" + integrity sha512-lMC2G0ItF2xv4UCiwbJGbnJlIuUixHrioOhNGHSCsYCJ8l4t9hMCUimCytvFv7qy6AfSzRxhRHoGa+UqaqwyeA== + "JSV@>= 4.0.x": version "4.0.2" resolved "https://registry.yarnpkg.com/JSV/-/JSV-4.0.2.tgz#d077f6825571f82132f9dffaed587b4029feff57" @@ -50,6 +55,14 @@ accepts@1.3.3, accepts@~1.3.3: mime-types "~2.1.11" negotiator "0.6.1" +accepts@~1.3.7: + version "1.3.7" + resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.7.tgz#531bc726517a3b2b41f850021c6cc15eaab507cd" + integrity sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA== + dependencies: + mime-types "~2.1.24" + negotiator "0.6.2" + acorn-globals@^1.0.3: version "1.0.9" resolved "https://registry.yarnpkg.com/acorn-globals/-/acorn-globals-1.0.9.tgz#55bb5e98691507b74579d0513413217c380c54cf" @@ -126,6 +139,11 @@ adm-zip@0.4.7, adm-zip@^0.4.7: version "0.4.7" resolved "https://registry.yarnpkg.com/adm-zip/-/adm-zip-0.4.7.tgz#8606c2cbf1c426ce8c8ec00174447fd49b6eafc1" +adm-zip@^0.4.9: + version "0.4.13" + resolved "https://registry.yarnpkg.com/adm-zip/-/adm-zip-0.4.13.tgz#597e2f8cc3672151e1307d3e95cddbc75672314a" + integrity sha512-fERNJX8sOXfel6qCBCMPvZLzENBEhZTzKqg6vrOW5pvoEaQuJhRU4ndTAh6lHOxn1I6jnz2NHra56ZODM751uw== + after@0.8.1: version "0.8.1" resolved "https://registry.yarnpkg.com/after/-/after-0.8.1.tgz#ab5d4fb883f596816d3515f8f791c0af486dd627" @@ -141,10 +159,27 @@ agent-base@2: extend "~3.0.0" semver "~5.0.1" +agent-base@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-4.3.0.tgz#8165f01c436009bccad0b1d122f05ed770efc6ee" + integrity sha512-salcGninV0nPrwpGNn4VTXBb1SOuXQBiqbrNXoeizJsHrsL6ERFM2Ne3JUSBWRE6aeNJI2ROP/WEEIDUiDe3cg== + dependencies: + es6-promisify "^5.0.0" + +ajv-errors@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/ajv-errors/-/ajv-errors-1.0.1.tgz#f35986aceb91afadec4102fbd85014950cefa64d" + integrity sha512-DCRfO/4nQ+89p/RK43i8Ezd41EqdGIU4ld7nGF8OQ14oc/we5rEntLCUa7+jrn3nn83BosfwZA0wb4pon2o8iQ== + ajv-keywords@^1.0.0: version "1.5.1" resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-1.5.1.tgz#314dd0a4b3368fad3dfcdc54ede6171b886daf3c" +ajv-keywords@^3.1.0: + version "3.4.1" + resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.4.1.tgz#ef916e271c64ac12171fd8384eaae6b2345854da" + integrity sha512-RO1ibKvd27e6FEShVFfPALuHI3WjSVNeK5FIsmme/LYRNxjKuNj+Dt7bucLa6NdSv3JcVTyMlm9kGR84z1XpaQ== + ajv@^4.7.0, ajv@^4.9.1: version "4.11.8" resolved "https://registry.yarnpkg.com/ajv/-/ajv-4.11.8.tgz#82ffb02b29e662ae53bdc20af15947706739c536" @@ -152,6 +187,16 @@ ajv@^4.7.0, ajv@^4.9.1: co "^4.6.0" json-stable-stringify "^1.0.1" +ajv@^6.1.0, ajv@^6.5.5: + version "6.11.0" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.11.0.tgz#c3607cbc8ae392d8a5a536f25b21f8e5f3f87fe9" + integrity sha512-nCprB/0syFYy9fVYU1ox1l2KN8S9I+tziH8D4zdZuLT3N6RMlGSGt5FSTpAiHB/Whv8Qs1cWHma1aMKZyaHRKA== + dependencies: + fast-deep-equal "^3.1.1" + fast-json-stable-stringify "^2.0.0" + json-schema-traverse "^0.4.1" + uri-js "^4.2.2" + align-text@^0.1.1, align-text@^0.1.3: version "0.1.4" resolved "https://registry.yarnpkg.com/align-text/-/align-text-0.1.4.tgz#0cd90a561093f35d0a99256c22b7069433fad117" @@ -174,6 +219,11 @@ amdefine@>=0.0.4: version "1.0.1" resolved "https://registry.yarnpkg.com/amdefine/-/amdefine-1.0.1.tgz#4a5282ac164729e93619bcfd3ad151f817ce91f5" +android-device-list@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/android-device-list/-/android-device-list-1.2.1.tgz#d81f59076bae7453f26792e54dc1568ccb6ba47c" + integrity sha512-ttKIAkeNdJB49aQaxHnCKgIIn+xDDOYgWA/SRsx5RwH2/943Y2it87KBqBDIr1ZsTHj2kcKNC0Nns/sCYLYvFw== + angular-gettext-tools@^2.2.0: version "2.3.5" resolved "https://registry.yarnpkg.com/angular-gettext-tools/-/angular-gettext-tools-2.3.5.tgz#9a97a6a283bcc21c14c42aa24802fc1d84034f83" @@ -186,14 +236,29 @@ angular-gettext-tools@^2.2.0: typescript "~2.0.3" typescript-eslint-parser "^1.0.2" +ansi-colors@^3.0.0: + version "3.2.4" + resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-3.2.4.tgz#e3a3da4bfbae6c86a9c285625de124a234026fbf" + integrity sha512-hHUXGagefjN2iRrID63xckIvotOXOojhQKWIPUZ4mNUZ9nLZW+7FMNoE1lOkEhNWYsx/7ysGIuJYCiMAA9FnrA== + ansi-escapes@^1.1.0: version "1.4.0" resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-1.4.0.tgz#d3a8a83b319aa67793662b13e761c7911422306e" +ansi-html@0.0.7: + version "0.0.7" + resolved "https://registry.yarnpkg.com/ansi-html/-/ansi-html-0.0.7.tgz#813584021962a9e9e6fd039f940d12f56ca7859e" + integrity sha1-gTWEAhliqenm/QOflA0S9WynhZ4= + ansi-regex@^2.0.0: version "2.1.1" resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df" +ansi-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998" + integrity sha1-7QMXwyIGT3lGbAKWa922Bas32Zg= + ansi-styles@^2.2.1: version "2.2.1" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe" @@ -209,6 +274,14 @@ anymatch@^1.3.0: arrify "^1.0.0" micromatch "^2.1.5" +anymatch@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-2.0.0.tgz#bcb24b4f37934d9aa7ac17b4adaf89e7c76ef2eb" + integrity sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw== + dependencies: + micromatch "^3.1.4" + normalize-path "^2.1.1" + append-field@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/append-field/-/append-field-0.1.0.tgz#6ddc58fa083c7bc545d3c5995b2830cc2366d44a" @@ -245,10 +318,25 @@ arr-diff@^2.0.0: dependencies: arr-flatten "^1.0.1" +arr-diff@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-4.0.0.tgz#d6461074febfec71e7e15235761a329a5dc7c520" + integrity sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA= + arr-flatten@^1.0.1: version "1.0.3" resolved "https://registry.yarnpkg.com/arr-flatten/-/arr-flatten-1.0.3.tgz#a274ed85ac08849b6bd7847c4580745dc51adfb1" +arr-flatten@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/arr-flatten/-/arr-flatten-1.1.0.tgz#36048bbff4e7b47e136644316c99669ea5ae91f1" + integrity sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg== + +arr-union@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/arr-union/-/arr-union-3.1.0.tgz#e39b09aea9def866a8f206e288af63919bae39c4" + integrity sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ= + array-differ@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/array-differ/-/array-differ-1.0.0.tgz#eff52e3758249d33be402b8bb8e564bb2b5d4031" @@ -261,6 +349,11 @@ array-flatten@1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" +array-flatten@^2.1.0: + version "2.1.2" + resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-2.1.2.tgz#24ef80a28c1a893617e2149b0c6d0d788293b099" + integrity sha512-hNfzcOV8W4NdualtqBFPyVO+54DSJuZGY9qT4pRroB6S9e3iiido2ISIC5h9R2sPJ8H3FHCIiEnsv1lPXO3KtQ== + array-parallel@~0.1.3: version "0.1.3" resolved "https://registry.yarnpkg.com/array-parallel/-/array-parallel-0.1.3.tgz#8f785308926ed5aa478c47e64d1b334b6c0c947d" @@ -287,6 +380,11 @@ array-unique@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.2.1.tgz#a1d97ccafcbc2625cc70fadceb36a50c58b01a53" +array-unique@^0.3.2: + version "0.3.2" + resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.3.2.tgz#a894b75d4bc4f6cd679ef3244a9fd8f46ae2d428" + integrity sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg= + arraybuffer.slice@0.0.6: version "0.0.6" resolved "https://registry.yarnpkg.com/arraybuffer.slice/-/arraybuffer.slice-0.0.6.tgz#f33b2159f0532a3f3107a272c0ccfbd1ad2979ca" @@ -344,6 +442,11 @@ assertion-error@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/assertion-error/-/assertion-error-1.0.2.tgz#13ca515d86206da0bac66e834dd397d87581094c" +assign-symbols@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/assign-symbols/-/assign-symbols-1.0.0.tgz#59667f41fadd4f20ccbc2bb96b8d4f7f78ec0367" + integrity sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c= + ast-traverse@~0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/ast-traverse/-/ast-traverse-0.1.1.tgz#69cf2b8386f19dcda1bb1e05d68fe359d8897de6" @@ -360,10 +463,20 @@ async-each@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.1.tgz#19d386a1d9edc6e7c1c85d388aedbcc56d33602d" +async-each@^1.0.1: + version "1.0.3" + resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.3.tgz#b727dbf87d7651602f06f4d4ac387f47d91b0cbf" + integrity sha512-z/WhQ5FPySLdvREByI2vZiTWwCnF0moMJ1hK9YQwDTHKh6I7/uSckMetoRGb5UBZPC1z0jlw+n/XCgjeH7y1AQ== + async-foreach@^0.1.3: version "0.1.3" resolved "https://registry.yarnpkg.com/async-foreach/-/async-foreach-0.1.3.tgz#36121f845c0578172de419a97dbeb1d16ec34542" +async-limiter@~1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.1.tgz#dd379e94f0db8310b08291f9d64c3209766617fd" + integrity sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ== + async@^0.9.0, async@~0.9.0: version "0.9.2" resolved "https://registry.yarnpkg.com/async/-/async-0.9.2.tgz#aea74d5e61c1f899613bf64bda66d4c78f2fd17d" @@ -378,7 +491,7 @@ async@^2.0.1: dependencies: lodash "^4.14.0" -async@^2.5.0: +async@^2.5.0, async@^2.6.2: version "2.6.3" resolved "https://registry.yarnpkg.com/async/-/async-2.6.3.tgz#d72625e2344a3656e3a3ad4fa749fa83299d82ff" integrity sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg== @@ -397,6 +510,11 @@ asynckit@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" +atob@^2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9" + integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg== + autoprefixer@^6.3.1: version "6.7.7" resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-6.7.7.tgz#1dbd1c835658e35ce3f9984099db00585c782014" @@ -426,10 +544,20 @@ aws-sign2@~0.6.0: version "0.6.0" resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.6.0.tgz#14342dd38dbcc94d0e5b87d763cd63612c0e794f" +aws-sign2@~0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8" + integrity sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg= + aws4@^1.2.1: version "1.6.0" resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.6.0.tgz#83ef5ca860b2b32e4a0deedee8c771b9db57471e" +aws4@^1.8.0: + version "1.9.1" + resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.9.1.tgz#7e33d8f7d449b3f673cd72deb9abdc552dbe528e" + integrity sha512-wMHVg2EOHaMRxbzgFJ9gtjOOCrI80OHLG14rxi28XwOW8ux6IiEbRCGGGqCtdAIg4FQCbW20k9RsT4y3gJlFug== + babel-code-frame@^6.16.0: version "6.22.0" resolved "https://registry.yarnpkg.com/babel-code-frame/-/babel-code-frame-6.22.0.tgz#027620bee567a88c32561574e7fd0801d33118e4" @@ -498,6 +626,19 @@ base64url@2.0.0, base64url@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/base64url/-/base64url-2.0.0.tgz#eac16e03ea1438eff9423d69baa36262ed1f70bb" +base@^0.11.1: + version "0.11.2" + resolved "https://registry.yarnpkg.com/base/-/base-0.11.2.tgz#7bde5ced145b6d551a90db87f83c558b4eb48a8f" + integrity sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg== + dependencies: + cache-base "^1.0.1" + class-utils "^0.3.5" + component-emitter "^1.2.1" + define-property "^1.0.0" + isobject "^3.0.1" + mixin-deep "^1.2.0" + pascalcase "^0.1.1" + basic-auth@^1.0.3: version "1.1.0" resolved "https://registry.yarnpkg.com/basic-auth/-/basic-auth-1.1.0.tgz#45221ee429f7ee1e5035be3f51533f1cdfd29884" @@ -542,6 +683,13 @@ bindings@^1.2.1, bindings@~1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/bindings/-/bindings-1.2.1.tgz#14ad6113812d2d37d72e67b4cacb4bb726505f11" +bindings@^1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/bindings/-/bindings-1.5.0.tgz#10353c9e945334bc0511a6d90b38fbc7c9c504df" + integrity sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ== + dependencies: + file-uri-to-path "1.0.0" + blob@0.0.4: version "0.0.4" resolved "https://registry.yarnpkg.com/blob/-/blob-0.0.4.tgz#bcf13052ca54463f30f9fc7e95b9a47630a94921" @@ -552,6 +700,13 @@ block-stream@*: dependencies: inherits "~2.0.0" +blocking-proxy@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/blocking-proxy/-/blocking-proxy-1.0.1.tgz#81d6fd1fe13a4c0d6957df7f91b75e98dac40cb2" + integrity sha512-KE8NFMZr3mN2E0HcvCgRtX7DjhiIQrwle+nSVJVC/yqFb9+xznHl2ZcoBp2L9qzkI4t4cBFJ1efXF8Dwi132RA== + dependencies: + minimist "^1.2.0" + bluebird@3.4.x: version "3.4.7" resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.4.7.tgz#f72d760be09b7f76d08ed8fae98b289a8d05fab3" @@ -592,6 +747,22 @@ body-parser@1.18.2: raw-body "2.3.2" type-is "~1.6.15" +body-parser@1.19.0: + version "1.19.0" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.19.0.tgz#96b2709e57c9c4e09a6fd66a8fd979844f69f08a" + integrity sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw== + dependencies: + bytes "3.1.0" + content-type "~1.0.4" + debug "2.6.9" + depd "~1.1.2" + http-errors "1.7.2" + iconv-lite "0.4.24" + on-finished "~2.3.0" + qs "6.7.0" + raw-body "2.4.0" + type-is "~1.6.17" + body-parser@^1.13.3, body-parser@^1.14.1, body-parser@^1.16.1: version "1.17.2" resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.17.2.tgz#f8892abc8f9e627d42aedafbca66bf5ab99104ee" @@ -607,6 +778,18 @@ body-parser@^1.13.3, body-parser@^1.14.1, body-parser@^1.16.1: raw-body "~2.2.0" type-is "~1.6.15" +bonjour@^3.5.0: + version "3.5.0" + resolved "https://registry.yarnpkg.com/bonjour/-/bonjour-3.5.0.tgz#8e890a183d8ee9a2393b3844c691a42bcf7bc9f5" + integrity sha1-jokKGD2O6aI5OzhExpGkK897yfU= + dependencies: + array-flatten "^2.1.0" + deep-equal "^1.0.1" + dns-equal "^1.0.0" + dns-txt "^2.0.2" + multicast-dns "^6.0.1" + multicast-dns-service-types "^1.1.0" + boolbase@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e" @@ -643,6 +826,22 @@ braces@^1.8.2: preserve "^0.2.0" repeat-element "^1.1.2" +braces@^2.3.1, braces@^2.3.2: + version "2.3.2" + resolved "https://registry.yarnpkg.com/braces/-/braces-2.3.2.tgz#5979fd3f14cd531565e5fa2df1abfff1dfaee729" + integrity sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w== + dependencies: + arr-flatten "^1.1.0" + array-unique "^0.3.2" + extend-shallow "^2.0.1" + fill-range "^4.0.0" + isobject "^3.0.1" + repeat-element "^1.1.2" + snapdragon "^0.8.1" + snapdragon-node "^2.0.1" + split-string "^3.0.2" + to-regex "^3.0.1" + breakable@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/breakable/-/breakable-1.0.0.tgz#784a797915a38ead27bad456b5572cb4bbaa78c1" @@ -715,6 +914,13 @@ browserslist@^1.3.6, browserslist@^1.5.2, browserslist@^1.7.6: caniuse-db "^1.0.30000639" electron-to-chromium "^1.2.7" +browserstack@^1.5.1: + version "1.5.3" + resolved "https://registry.yarnpkg.com/browserstack/-/browserstack-1.5.3.tgz#93ab48799a12ef99dbd074dd595410ddb196a7ac" + integrity sha512-AO+mECXsW4QcqC9bxwM29O7qWa7bJT94uBFzeb5brylIQwawuEziwq20dPYbins95GlWzOawgyDNdjYAo32EKg== + dependencies: + https-proxy-agent "^2.2.1" + buffer-crc32@~0.2.3: version "0.2.13" resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242" @@ -723,6 +929,11 @@ buffer-equal-constant-time@1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819" +buffer-indexof@^1.0.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/buffer-indexof/-/buffer-indexof-1.1.1.tgz#52fabcc6a606d1a00302802648ef68f639da268c" + integrity sha512-4/rOEg86jivtPTeOUUT61jJO1Ya1TrR/OkqCSZDyq84WJh3LuuiphBYJN+fm5xufIk4XAFcEwte/8WzC8If/1g== + buffer-xor@^1.0.2: version "1.0.3" resolved "https://registry.yarnpkg.com/buffer-xor/-/buffer-xor-1.0.3.tgz#26e61ed1422fb70dd42e6e36729ed51d855fe8d9" @@ -803,6 +1014,26 @@ bytes@3.0.0: resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048" integrity sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg= +bytes@3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.0.tgz#f6cf7933a360e0588fa9fde85651cdc7f805d1f6" + integrity sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg== + +cache-base@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/cache-base/-/cache-base-1.0.1.tgz#0a7f46416831c8b662ee36fe4e7c59d76f666ab2" + integrity sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ== + dependencies: + collection-visit "^1.0.0" + component-emitter "^1.2.1" + get-value "^2.0.6" + has-value "^1.0.0" + isobject "^3.0.1" + set-value "^2.0.0" + to-object-path "^0.3.0" + union-value "^1.0.0" + unset-value "^1.0.0" + caller-path@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/caller-path/-/caller-path-0.1.0.tgz#94085ef63581ecd3daa92444a8fe94e82577751f" @@ -940,6 +1171,30 @@ chokidar@^1.0.0, chokidar@^1.4.1: optionalDependencies: fsevents "^1.0.0" +chokidar@^2.0.0: + version "2.1.8" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.1.8.tgz#804b3a7b6a99358c3c5c61e71d8728f041cff917" + integrity sha512-ZmZUazfOzf0Nve7duiCKD23PFSCs4JPoYyccjUFF3aQkQadqBhfzhjkwBH2mNOG9cTBwhamM37EIsIkZw3nRgg== + dependencies: + anymatch "^2.0.0" + async-each "^1.0.1" + braces "^2.3.2" + glob-parent "^3.1.0" + inherits "^2.0.3" + is-binary-path "^1.0.0" + is-glob "^4.0.0" + normalize-path "^3.0.0" + path-is-absolute "^1.0.0" + readdirp "^2.2.1" + upath "^1.1.1" + optionalDependencies: + fsevents "^1.2.7" + +chownr@^1.1.1: + version "1.1.3" + resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.3.tgz#42d837d5239688d55f303003a508230fa6727142" + integrity sha512-i70fVHhmV3DtTl6nqvZOnIjbY0Pe4kAUjwHj8z0zAdgBtYrJyYwLKCCuRBQ5ppkyL0AkN7HKRnETdmdp1zqNXw== + cipher-base@^1.0.0, cipher-base@^1.0.1, cipher-base@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/cipher-base/-/cipher-base-1.0.3.tgz#eeabf194419ce900da3018c207d212f2a6df0a07" @@ -956,6 +1211,16 @@ clap@^1.0.9: dependencies: chalk "^1.1.3" +class-utils@^0.3.5: + version "0.3.6" + resolved "https://registry.yarnpkg.com/class-utils/-/class-utils-0.3.6.tgz#f93369ae8b9a7ce02fd41faad0ca83033190c463" + integrity sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg== + dependencies: + arr-union "^3.1.0" + define-property "^0.2.5" + isobject "^3.0.0" + static-extend "^0.1.1" + clean-css@4.1.x: version "4.1.4" resolved "https://registry.yarnpkg.com/clean-css/-/clean-css-4.1.4.tgz#eec8811db27457e0078d8ca921fa81b72fa82bf4" @@ -995,6 +1260,15 @@ cliui@^3.2.0: strip-ansi "^3.0.1" wrap-ansi "^2.0.0" +cliui@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-4.1.0.tgz#348422dbe82d800b3022eef4f6ac10bf2e4d1b49" + integrity sha512-4FG+RSG9DL7uEwRUZXZn3SS34DiDPfzP0VOiEwtUWlE+AR2EIg+hSyvrIgUUfhdgR/UkAeW2QHgeP+hWrXs7jQ== + dependencies: + string-width "^2.1.1" + strip-ansi "^4.0.0" + wrap-ansi "^2.0.0" + clone-stats@^0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/clone-stats/-/clone-stats-0.0.1.tgz#b88f94a82cf38b8791d58046ea4029ad88ca99d1" @@ -1021,6 +1295,14 @@ code-point-at@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77" +collection-visit@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/collection-visit/-/collection-visit-1.0.0.tgz#4bc0373c164bc3291b4d368c829cf1a80a59dca0" + integrity sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA= + dependencies: + map-visit "^1.0.0" + object-visit "^1.0.0" + color-convert@^1.3.0: version "1.9.0" resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.0.tgz#1accf97dd739b983bf994d56fec8f95853641b7a" @@ -1077,7 +1359,7 @@ combined-stream@^1.0.5, combined-stream@~1.0.5: dependencies: delayed-stream "~1.0.0" -combined-stream@^1.0.6: +combined-stream@^1.0.6, combined-stream@~1.0.6: version "1.0.8" resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== @@ -1140,6 +1422,11 @@ component-emitter@1.2.1, component-emitter@^1.2.0: version "1.2.1" resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.2.1.tgz#137918d6d78283f7df7a6b7c5a63e140e69425e6" +component-emitter@^1.2.1: + version "1.3.0" + resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.0.tgz#16e4070fba8ae29b679f2215853ee181ab2eabc0" + integrity sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg== + component-inherit@0.0.3: version "0.0.3" resolved "https://registry.yarnpkg.com/component-inherit/-/component-inherit-0.0.3.tgz#645fc4adf58b72b649d5cae65135619db26ff143" @@ -1232,9 +1519,12 @@ constants-browserify@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/constants-browserify/-/constants-browserify-1.0.0.tgz#c20b96d8c617748aaf1c16021760cd27fcb8cb75" -content-disposition@0.5.2: - version "0.5.2" - resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.2.tgz#0cf68bb9ddf5f2be7961c3a85178cb85dba78cb4" +content-disposition@0.5.3: + version "0.5.3" + resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.3.tgz#e130caf7e7279087c5616c2007d0485698984fbd" + integrity sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g== + dependencies: + safe-buffer "5.1.2" content-type@~1.0.2: version "1.0.2" @@ -1266,6 +1556,11 @@ cookie@0.3.1: version "0.3.1" resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.3.1.tgz#e7e0a1f9ef43b4c8ba925c5c5a96e806d16873bb" +cookie@0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.0.tgz#beb437e7022b3b6d49019d088665303ebe9c14ba" + integrity sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg== + cookiejar@^2.0.6: version "2.1.1" resolved "https://registry.yarnpkg.com/cookiejar/-/cookiejar-2.1.1.tgz#41ad57b1b555951ec171412a81942b1e8200d34a" @@ -1282,6 +1577,11 @@ cookies@0.7.0: depd "~1.1.0" keygrip "~1.0.1" +copy-descriptor@^0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d" + integrity sha1-Z29us8OZl8LuGsOpJP1hJHSPV40= + core-js@^2.2.0: version "2.4.1" resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.4.1.tgz#4de911e667b0eae9124e34254b53aea6fc618d3e" @@ -1343,6 +1643,17 @@ cross-spawn@^4.0.0: lru-cache "^4.0.1" which "^1.2.9" +cross-spawn@^6.0.0: + version "6.0.5" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4" + integrity sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ== + dependencies: + nice-try "^1.0.4" + path-key "^2.0.1" + semver "^5.5.0" + shebang-command "^1.2.0" + which "^1.2.9" + cryptiles@2.x.x: version "2.0.5" resolved "https://registry.yarnpkg.com/cryptiles/-/cryptiles-2.0.5.tgz#3bdfecdc608147c1c67202fa291e7dca59eaa3b8" @@ -1556,9 +1867,10 @@ debug@0.7.4, debug@~0.7.4: version "0.7.4" resolved "https://registry.yarnpkg.com/debug/-/debug-0.7.4.tgz#06e1ea8082c2cb14e39806e22e2f6f757f92af39" -debug@2, debug@2.6.8, debug@^2.1.1, debug@^2.1.2, debug@^2.1.3, debug@^2.2.0, debug@^2.6.6, debug@~2.6.3, debug@~2.6.4, debug@~2.6.6: - version "2.6.8" - resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.8.tgz#e731531ca2ede27d188222427da17821d68ff4fc" +debug@2, debug@2.6.9, debug@^2.1.1, debug@^2.1.2, debug@^2.1.3, debug@^2.2.0, debug@^2.3.3, debug@~2.6.3, debug@~2.6.4, debug@~2.6.6: + version "2.6.9" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" + integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== dependencies: ms "2.0.0" @@ -1580,44 +1892,116 @@ debug@2.6.7: dependencies: ms "2.0.0" -debug@2.6.9: - version "2.6.9" - resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" - integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== +debug@2.6.8: + version "2.6.8" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.8.tgz#e731531ca2ede27d188222427da17821d68ff4fc" dependencies: ms "2.0.0" -debug@^3.1.0: +debug@^3.1.0, debug@^3.1.1, debug@^3.2.5, debug@^3.2.6: version "3.2.6" resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b" integrity sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ== dependencies: ms "^2.1.1" +debug@^4.1.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791" + integrity sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw== + dependencies: + ms "^2.1.1" + decamelize@^1.0.0, decamelize@^1.1.1, decamelize@^1.1.2: version "1.2.0" resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" +decamelize@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-2.0.0.tgz#656d7bbc8094c4c788ea53c5840908c9c7d063c7" + integrity sha512-Ikpp5scV3MSYxY39ymh45ZLEecsTdv/Xj2CaQfI8RLMuwi7XvjX9H/fhraiSuU+C5w5NTDu4ZU72xNiZnurBPg== + dependencies: + xregexp "4.0.0" + +decode-uri-component@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545" + integrity sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU= + deep-eql@^0.1.3: version "0.1.3" resolved "https://registry.yarnpkg.com/deep-eql/-/deep-eql-0.1.3.tgz#ef558acab8de25206cd713906d74e56930eb69f2" dependencies: type-detect "0.1.1" +deep-equal@^1.0.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.1.1.tgz#b5c98c942ceffaf7cb051e24e1434a25a2e6076a" + integrity sha512-yd9c5AdiqVcR+JjcwUQb9DkhJc8ngNr0MahEBGvDiJw8puWab2yZlh+nkasOnZP+EGTAP6rRp2JzJhJZzvNF8g== + dependencies: + is-arguments "^1.0.4" + is-date-object "^1.0.1" + is-regex "^1.0.4" + object-is "^1.0.1" + object-keys "^1.1.1" + regexp.prototype.flags "^1.2.0" + deep-extend@^0.4.0, deep-extend@~0.4.0: version "0.4.2" resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.4.2.tgz#48b699c27e334bf89f10892be432f6e4c7d34a7f" +deep-extend@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac" + integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA== + deep-is@~0.1.3: version "0.1.3" resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34" +default-gateway@^2.6.0: + version "2.7.2" + resolved "https://registry.yarnpkg.com/default-gateway/-/default-gateway-2.7.2.tgz#b7ef339e5e024b045467af403d50348db4642d0f" + integrity sha512-lAc4i9QJR0YHSDFdzeBQKfZ1SRDG3hsJNEkrpcZa8QhBfidLAilT60BDEIVUUGqosFp425KOgB3uYqcnQrWafQ== + dependencies: + execa "^0.10.0" + ip-regex "^2.1.0" + defaults@^1.0.0: version "1.0.3" resolved "https://registry.yarnpkg.com/defaults/-/defaults-1.0.3.tgz#c656051e9817d9ff08ed881477f3fe4019f3ef7d" dependencies: clone "^1.0.2" +define-properties@^1.1.2, define-properties@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.3.tgz#cf88da6cbee26fe6db7094f61d870cbd84cee9f1" + integrity sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ== + dependencies: + object-keys "^1.0.12" + +define-property@^0.2.5: + version "0.2.5" + resolved "https://registry.yarnpkg.com/define-property/-/define-property-0.2.5.tgz#c35b1ef918ec3c990f9a5bc57be04aacec5c8116" + integrity sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY= + dependencies: + is-descriptor "^0.1.0" + +define-property@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/define-property/-/define-property-1.0.0.tgz#769ebaaf3f4a63aad3af9e8d304c9bbe79bfb0e6" + integrity sha1-dp66rz9KY6rTr56NMEybvnm/sOY= + dependencies: + is-descriptor "^1.0.0" + +define-property@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/define-property/-/define-property-2.0.2.tgz#d459689e8d654ba77e02a817f8710d702cb16e9d" + integrity sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ== + dependencies: + is-descriptor "^1.0.2" + isobject "^3.0.1" + defined@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/defined/-/defined-1.0.0.tgz#c98d9bcef75674188e110969151199e39b1fa693" @@ -1649,6 +2033,18 @@ del@^2.0.1, del@^2.0.2, del@^2.2.0: pinkie-promise "^2.0.0" rimraf "^2.2.8" +del@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/del/-/del-3.0.0.tgz#53ecf699ffcbcb39637691ab13baf160819766e5" + integrity sha1-U+z2mf/LyzljdpGrE7rxYIGXZuU= + dependencies: + globby "^6.1.0" + is-path-cwd "^1.0.0" + is-path-in-cwd "^1.0.0" + p-map "^1.1.1" + pify "^3.0.0" + rimraf "^2.2.8" + delayed-stream@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" @@ -1698,6 +2094,16 @@ detect-file@^0.1.0: dependencies: fs-exists-sync "^0.1.0" +detect-libc@^1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b" + integrity sha1-+hN8S9aY7fVc1c0CrFWfkaTEups= + +detect-node@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.0.4.tgz#014ee8f8f669c5c58023da64b8179c083a28c46c" + integrity sha512-ZIzRpLJrOj7jjP2miAtgqIfmzbxa4ZOr5jJc601zklsfEx9oTzmmj2nVpIPRpNlRTIh8lc1kyViIY7BWSGNmKw== + detective@^4.3.1: version "4.5.0" resolved "https://registry.yarnpkg.com/detective/-/detective-4.5.0.tgz#6e5a8c6b26e6c7a254b1c6b6d7490d98ec91edd1" @@ -1728,6 +2134,26 @@ discontinuous-range@1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/discontinuous-range/-/discontinuous-range-1.0.0.tgz#e38331f0844bba49b9a9cb71c771585aab1bc65a" +dns-equal@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/dns-equal/-/dns-equal-1.0.0.tgz#b39e7f1da6eb0a75ba9c17324b34753c47e0654d" + integrity sha1-s55/HabrCnW6nBcySzR1PEfgZU0= + +dns-packet@^1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/dns-packet/-/dns-packet-1.3.1.tgz#12aa426981075be500b910eedcd0b47dd7deda5a" + integrity sha512-0UxfQkMhYAUaZI+xrNZOz/as5KgDU0M/fQ9b6SpkyLbk3GEswDi6PADJVaYJradtRVsRIlF1zLyOodbcTCDzUg== + dependencies: + ip "^1.1.0" + safe-buffer "^5.0.1" + +dns-txt@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/dns-txt/-/dns-txt-2.0.2.tgz#b91d806f5d27188e4ab3e7d107d881a1cc4642b6" + integrity sha1-uR2Ab10nGI5Ks+fRB9iBocxGQrY= + dependencies: + buffer-indexof "^1.0.0" + doctrine@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-2.0.0.tgz#c73d8d2909d22291e1a007a395804da8b665fe63" @@ -1854,6 +2280,13 @@ encodeurl@~1.0.2: resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" integrity sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k= +end-of-stream@^1.1.0: + version "1.4.4" + resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" + integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q== + dependencies: + once "^1.4.0" + end-of-stream@~0.1.5: version "0.1.5" resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-0.1.5.tgz#8e177206c3c80837d85632e8b9359dfe8b2f6eaf" @@ -1999,6 +2432,32 @@ error-ex@^1.2.0: dependencies: is-arrayish "^0.2.1" +es-abstract@^1.17.0-next.1: + version "1.17.2" + resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.17.2.tgz#965b10af56597b631da15872c17a405e86c1fd46" + integrity sha512-YoKuru3Lyoy7yVTBSH2j7UxTqe/je3dWAruC0sHvZX1GNd5zX8SSLvQqEgO9b3Ex8IW+goFI9arEEsFIbulhOw== + dependencies: + es-to-primitive "^1.2.1" + function-bind "^1.1.1" + has "^1.0.3" + has-symbols "^1.0.1" + is-callable "^1.1.5" + is-regex "^1.0.5" + object-inspect "^1.7.0" + object-keys "^1.1.1" + object.assign "^4.1.0" + string.prototype.trimleft "^2.1.1" + string.prototype.trimright "^2.1.1" + +es-to-primitive@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.1.tgz#e55cd4c9cdc188bcefb03b366c736323fc5c898a" + integrity sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA== + dependencies: + is-callable "^1.1.4" + is-date-object "^1.0.1" + is-symbol "^1.0.2" + es5-ext@^0.10.14, es5-ext@^0.10.9, es5-ext@~0.10.14: version "0.10.23" resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.23.tgz#7578b51be974207a5487821b56538c224e4e7b38" @@ -2025,10 +2484,22 @@ es6-map@^0.1.3: es6-symbol "~3.1.1" event-emitter "~0.3.5" +es6-promise@^4.0.3: + version "4.2.8" + resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.2.8.tgz#4eb21594c972bc40553d276e510539143db53e0a" + integrity sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w== + es6-promise@~4.0.3: version "4.0.5" resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.0.5.tgz#7882f30adde5b240ccfa7f7d78c548330951ae42" +es6-promisify@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/es6-promisify/-/es6-promisify-5.0.0.tgz#5109d62f3e56ea967c4b63505aef08291c8a5203" + integrity sha1-UQnWLz5W6pZ8S2NQWu8IKRyKUgM= + dependencies: + es6-promise "^4.0.3" + es6-set@~0.1.5: version "0.1.5" resolved "https://registry.yarnpkg.com/es6-set/-/es6-set-0.1.5.tgz#d2b3ec5d4d800ced818db538d28974db0a73ccb1" @@ -2208,11 +2679,12 @@ events@^1.0.0: version "1.1.1" resolved "https://registry.yarnpkg.com/events/-/events-1.1.1.tgz#9ebdb7635ad099c70dcc4c2a1f5004288e8bd924" -eventsource@0.1.6: - version "0.1.6" - resolved "https://registry.yarnpkg.com/eventsource/-/eventsource-0.1.6.tgz#0acede849ed7dd1ccc32c811bb11b944d4f29232" +eventsource@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/eventsource/-/eventsource-1.0.7.tgz#8fbc72c93fcd34088090bc0a4e64f4b5cee6d8d0" + integrity sha512-4Ln17+vVT0k8aWq+t/bF5arcS3EpT9gYtW66EPacdj/mAFevznsnyoHLPy2BA8gbIQeIHoPsvwmfBftfcG//BQ== dependencies: - original ">=0.0.5" + original "^1.0.0" evp_bytestokey@^1.0.0: version "1.0.0" @@ -2220,6 +2692,19 @@ evp_bytestokey@^1.0.0: dependencies: create-hash "^1.1.1" +execa@^0.10.0: + version "0.10.0" + resolved "https://registry.yarnpkg.com/execa/-/execa-0.10.0.tgz#ff456a8f53f90f8eccc71a96d11bdfc7f082cb50" + integrity sha512-7XOMnz8Ynx1gGo/3hyV9loYNPWM94jG3+3T3Y8tsfSstFmETmENCMU/A/zj8Lyaj1lkgEepKepvd6240tBRvlw== + dependencies: + cross-spawn "^6.0.0" + get-stream "^3.0.0" + is-stream "^1.1.0" + npm-run-path "^2.0.0" + p-finally "^1.0.0" + signal-exit "^3.0.0" + strip-eof "^1.0.0" + execa@^0.5.0: version "0.5.1" resolved "https://registry.yarnpkg.com/execa/-/execa-0.5.1.tgz#de3fb85cb8d6e91c85bcbceb164581785cb57b36" @@ -2232,6 +2717,19 @@ execa@^0.5.0: signal-exit "^3.0.0" strip-eof "^1.0.0" +execa@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/execa/-/execa-1.0.0.tgz#c6236a5bb4df6d6f15e88e7f017798216749ddd8" + integrity sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA== + dependencies: + cross-spawn "^6.0.0" + get-stream "^4.0.0" + is-stream "^1.1.0" + npm-run-path "^2.0.0" + p-finally "^1.0.0" + signal-exit "^3.0.0" + strip-eof "^1.0.0" + exit-hook@^1.0.0: version "1.1.1" resolved "https://registry.yarnpkg.com/exit-hook/-/exit-hook-1.1.1.tgz#f05ca233b48c05d54fff07765df8507e95c02ff8" @@ -2254,6 +2752,19 @@ expand-brackets@^0.1.4: dependencies: is-posix-bracket "^0.1.0" +expand-brackets@^2.1.4: + version "2.1.4" + resolved "https://registry.yarnpkg.com/expand-brackets/-/expand-brackets-2.1.4.tgz#b77735e315ce30f6b6eff0f83b04151a22449622" + integrity sha1-t3c14xXOMPa27/D4OwQVGiJEliI= + dependencies: + debug "^2.3.3" + define-property "^0.2.5" + extend-shallow "^2.0.1" + posix-character-classes "^0.1.0" + regex-not "^1.0.0" + snapdragon "^0.8.1" + to-regex "^3.0.1" + expand-range@^0.1.0: version "0.1.1" resolved "https://registry.yarnpkg.com/expand-range/-/expand-range-0.1.1.tgz#4cb8eda0993ca56fa4f41fc42f3cbb4ccadff044" @@ -2288,42 +2799,61 @@ express-validator@^2.20.8: lodash "4.16.x" validator "5.7.x" -express@^4.13.3, express@^4.14.0: - version "4.15.3" - resolved "https://registry.yarnpkg.com/express/-/express-4.15.3.tgz#bab65d0f03aa80c358408972fc700f916944b662" +express@^4.14.0, express@^4.16.2: + version "4.17.1" + resolved "https://registry.yarnpkg.com/express/-/express-4.17.1.tgz#4491fc38605cf51f8629d39c2b5d026f98a4c134" + integrity sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g== dependencies: - accepts "~1.3.3" + accepts "~1.3.7" array-flatten "1.1.1" - content-disposition "0.5.2" - content-type "~1.0.2" - cookie "0.3.1" + body-parser "1.19.0" + content-disposition "0.5.3" + content-type "~1.0.4" + cookie "0.4.0" cookie-signature "1.0.6" - debug "2.6.7" - depd "~1.1.0" - encodeurl "~1.0.1" + debug "2.6.9" + depd "~1.1.2" + encodeurl "~1.0.2" escape-html "~1.0.3" - etag "~1.8.0" - finalhandler "~1.0.3" - fresh "0.5.0" + etag "~1.8.1" + finalhandler "~1.1.2" + fresh "0.5.2" merge-descriptors "1.0.1" methods "~1.1.2" on-finished "~2.3.0" - parseurl "~1.3.1" + parseurl "~1.3.3" path-to-regexp "0.1.7" - proxy-addr "~1.1.4" - qs "6.4.0" - range-parser "~1.2.0" - send "0.15.3" - serve-static "1.12.3" - setprototypeof "1.0.3" - statuses "~1.3.1" - type-is "~1.6.15" - utils-merge "1.0.0" - vary "~1.1.1" + proxy-addr "~2.0.5" + qs "6.7.0" + range-parser "~1.2.1" + safe-buffer "5.1.2" + send "0.17.1" + serve-static "1.14.1" + setprototypeof "1.1.1" + statuses "~1.5.0" + type-is "~1.6.18" + utils-merge "1.0.1" + vary "~1.1.2" -extend@3, extend@^3.0.0, extend@~3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.1.tgz#a755ea7bc1adfcc5a31ce7e762dbaadc5e636444" +extend-shallow@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-2.0.1.tgz#51af7d614ad9a9f610ea1bafbb989d6b1c56890f" + integrity sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8= + dependencies: + is-extendable "^0.1.0" + +extend-shallow@^3.0.0, extend-shallow@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-3.0.2.tgz#26a71aaf073b39fb2127172746131c2704028db8" + integrity sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg= + dependencies: + assign-symbols "^1.0.0" + is-extendable "^1.0.1" + +extend@3, extend@^3.0.0, extend@~3.0.0, extend@~3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" + integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== extglob@^0.3.1: version "0.3.2" @@ -2331,6 +2861,20 @@ extglob@^0.3.1: dependencies: is-extglob "^1.0.0" +extglob@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/extglob/-/extglob-2.0.4.tgz#ad00fe4dc612a9232e8718711dc5cb5ab0285543" + integrity sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw== + dependencies: + array-unique "^0.3.2" + define-property "^1.0.0" + expand-brackets "^2.1.4" + extend-shallow "^2.0.1" + fragment-cache "^0.2.1" + regex-not "^1.0.0" + snapdragon "^0.8.1" + to-regex "^3.0.1" + extract-text-webpack-plugin@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/extract-text-webpack-plugin/-/extract-text-webpack-plugin-1.0.1.tgz#c95bf3cbaac49dc96f1dc6e072549fbb654ccd2c" @@ -2375,6 +2919,16 @@ fancy-log@^1.1.0: chalk "^1.1.1" time-stamp "^1.0.0" +fast-deep-equal@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.1.tgz#545145077c501491e33b15ec408c294376e94ae4" + integrity sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA== + +fast-json-stable-stringify@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" + integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== + fast-levenshtein@~2.0.4: version "2.0.6" resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" @@ -2389,9 +2943,10 @@ faye-websocket@^0.10.0: dependencies: websocket-driver ">=0.5.1" -faye-websocket@~0.11.0: - version "0.11.1" - resolved "https://registry.yarnpkg.com/faye-websocket/-/faye-websocket-0.11.1.tgz#f0efe18c4f56e4f40afc7e06c719fd5ee6188f38" +faye-websocket@~0.11.1: + version "0.11.3" + resolved "https://registry.yarnpkg.com/faye-websocket/-/faye-websocket-0.11.3.tgz#5c0e9a8968e8912c286639fde977a8b209f2508e" + integrity sha512-D2y4bovYpzziGgbHYtGCMjlJM36vAl/y+xUyn1C+FVx8szd1E+86KwVw6XvYSzOP8iMpm1X0I4xJD+QtUb36OA== dependencies: websocket-driver ">=0.5.1" @@ -2421,6 +2976,11 @@ file-loader@^0.9.0: dependencies: loader-utils "~0.2.5" +file-uri-to-path@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz#553a7b8446ff6f684359c445f1e37a05dacc33dd" + integrity sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw== + filename-regex@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/filename-regex/-/filename-regex-2.0.1.tgz#c1c4b9bee3e09725ddb106b75c1e301fe2f18b26" @@ -2435,7 +2995,17 @@ fill-range@^2.1.0: repeat-element "^1.1.2" repeat-string "^1.5.2" -finalhandler@1.0.3, finalhandler@~1.0.3: +fill-range@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-4.0.0.tgz#d544811d428f98eb06a63dc402d2403c328c38f7" + integrity sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc= + dependencies: + extend-shallow "^2.0.1" + is-number "^3.0.0" + repeat-string "^1.6.1" + to-regex-range "^2.1.0" + +finalhandler@1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.0.3.tgz#ef47e77950e999780e86022a560e3217e0d0cc89" dependencies: @@ -2447,6 +3017,19 @@ finalhandler@1.0.3, finalhandler@~1.0.3: statuses "~1.3.1" unpipe "~1.0.0" +finalhandler@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.2.tgz#b7e7d000ffd11938d0fdb053506f6ebabe9f587d" + integrity sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA== + dependencies: + debug "2.6.9" + encodeurl "~1.0.2" + escape-html "~1.0.3" + on-finished "~2.3.0" + parseurl "~1.3.3" + statuses "~1.5.0" + unpipe "~1.0.0" + find-index@^0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/find-index/-/find-index-0.1.1.tgz#675d358b2ca3892d795a1ab47232f8b6e2e0dde4" @@ -2464,6 +3047,13 @@ find-up@^2.0.0: dependencies: locate-path "^2.0.0" +find-up@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-3.0.0.tgz#49169f1d7993430646da61ecc5ae355c21c97b73" + integrity sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg== + dependencies: + locate-path "^3.0.0" + findup-sync@^0.4.2: version "0.4.3" resolved "https://registry.yarnpkg.com/findup-sync/-/findup-sync-0.4.3.tgz#40043929e7bc60adf0b7f4827c4c6e75a0deca12" @@ -2506,7 +3096,7 @@ flatten@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/flatten/-/flatten-1.0.2.tgz#dae46a9d78fbe25292258cc1e780a41d95c03782" -for-in@^1.0.1: +for-in@^1.0.1, for-in@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80" @@ -2537,6 +3127,15 @@ form-data@^2.3.1: combined-stream "^1.0.6" mime-types "^2.1.12" +form-data@~2.3.2: + version "2.3.3" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.3.tgz#dcce52c05f644f298c6a7ab936bd724ceffbf3a6" + integrity sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.6" + mime-types "^2.1.12" + formatio@1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/formatio/-/formatio-1.1.1.tgz#5ed3ccd636551097383465d996199100e86161e9" @@ -2555,6 +3154,18 @@ forwarded@~0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.0.tgz#19ef9874c4ae1c297bcf078fde63a09b66a84363" +forwarded@~0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.2.tgz#98c23dab1175657b8c0573e8ceccd91b0ff18c84" + integrity sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ= + +fragment-cache@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/fragment-cache/-/fragment-cache-0.2.1.tgz#4290fad27f13e89be7f33799c6bc5a0abfff0d19" + integrity sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk= + dependencies: + map-cache "^0.2.2" + fresh@0.5.0: version "0.5.0" resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.0.tgz#f474ca5e6a9246d6fd8e0953cfa9b9c805afa78e" @@ -2578,7 +3189,7 @@ fs-exists-sync@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/fs-exists-sync/-/fs-exists-sync-0.1.0.tgz#982d6893af918e72d08dec9e8673ff2b5a8d6add" -fs-extra@~1.0.0: +fs-extra@^1.0.0, fs-extra@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-1.0.0.tgz#cd3ce5f7e7cb6145883fcae3191e9877f8587950" dependencies: @@ -2586,10 +3197,31 @@ fs-extra@~1.0.0: jsonfile "^2.1.0" klaw "^1.0.0" +fs-extra@^8.1.0: + version "8.1.0" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-8.1.0.tgz#49d43c45a88cd9677668cb7be1b46efdb8d2e1c0" + integrity sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g== + dependencies: + graceful-fs "^4.2.0" + jsonfile "^4.0.0" + universalify "^0.1.0" + +fs-minipass@^1.2.5: + version "1.2.7" + resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-1.2.7.tgz#ccff8570841e7fe4265693da88936c55aed7f7c7" + integrity sha512-GWSSJGFy4e9GUeCcbIkED+bgAoFyj7XF1mV8rma3QW4NIqX9Kyx79N/PF61H5udOV3aY1IaMLs6pGbH71nlCTA== + dependencies: + minipass "^2.6.0" + fs.realpath@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" +fs@0.0.1-security: + version "0.0.1-security" + resolved "https://registry.yarnpkg.com/fs/-/fs-0.0.1-security.tgz#8a7bd37186b6dddf3813f23858b57ecaaf5e41d4" + integrity sha1-invTcYa23d84E/I4WLV+yq9eQdQ= + fsevents@^1.0.0: version "1.1.2" resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.1.2.tgz#3282b713fb3ad80ede0e9fcf4611b5aa6fc033f4" @@ -2597,6 +3229,15 @@ fsevents@^1.0.0: nan "^2.3.0" node-pre-gyp "^0.6.36" +fsevents@^1.2.7: + version "1.2.11" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.2.11.tgz#67bf57f4758f02ede88fb2a1712fef4d15358be3" + integrity sha512-+ux3lx6peh0BpvY0JebGyZoiR4D+oYzdPZMKJwkZ+sFkNJzpL7tXc/wehS49gUAxg3tmMHPHZkA8JU2rhhgDHw== + dependencies: + bindings "^1.5.0" + nan "^2.12.1" + node-pre-gyp "*" + fstream-ignore@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/fstream-ignore/-/fstream-ignore-1.0.5.tgz#9c31dae34767018fe1d249b24dada67d092da105" @@ -2618,6 +3259,11 @@ function-bind@^1.0.2: version "1.1.0" resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.0.tgz#16176714c801798e4e8f2cf7f7529467bb4a5771" +function-bind@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" + integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== + gauge@~2.7.3: version "2.7.4" resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.4.tgz#2c03405c7538c39d7eb37b317022e325fb018bf7" @@ -2668,6 +3314,23 @@ get-stream@^2.2.0: object-assign "^4.0.1" pinkie-promise "^2.0.0" +get-stream@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-3.0.0.tgz#8e943d1358dc37555054ecbe2edb05aa174ede14" + integrity sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ= + +get-stream@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-4.1.0.tgz#c1b255575f3dc21d59bfc79cd3d2b46b1c3a54b5" + integrity sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w== + dependencies: + pump "^3.0.0" + +get-value@^2.0.3, get-value@^2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/get-value/-/get-value-2.0.6.tgz#dc15ca1c672387ca76bd37ac0a395ba2042a2c28" + integrity sha1-3BXKHGcjh8p2vTesCjlbogQqLCg= + getpass@^0.1.1: version "0.1.7" resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa" @@ -2687,6 +3350,14 @@ glob-parent@^2.0.0: dependencies: is-glob "^2.0.0" +glob-parent@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-3.1.0.tgz#9e6af6299d8d3bd2bd40430832bd113df906c5ae" + integrity sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4= + dependencies: + is-glob "^3.1.0" + path-dirname "^1.0.0" + glob-stream@^3.1.5: version "3.1.18" resolved "https://registry.yarnpkg.com/glob-stream/-/glob-stream-3.1.18.tgz#9170a5f12b790306fdfe598f313f8f7954fd143b" @@ -2757,6 +3428,18 @@ glob@^7.0.0, glob@^7.0.3, glob@^7.0.5, glob@^7.1.1, glob@~7.1.1: once "^1.3.0" path-is-absolute "^1.0.0" +glob@^7.0.6, glob@^7.1.3: + version "7.1.6" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6" + integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.4" + once "^1.3.0" + path-is-absolute "^1.0.0" + glob@~3.1.21: version "3.1.21" resolved "https://registry.yarnpkg.com/glob/-/glob-3.1.21.tgz#d29e0a055dea5138f4d07ed40e8982e83c2066cd" @@ -2796,6 +3479,17 @@ globby@^5.0.0: pify "^2.0.0" pinkie-promise "^2.0.0" +globby@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/globby/-/globby-6.1.0.tgz#f5a6d70e8395e21c858fb0489d64df02424d506c" + integrity sha1-9abXDoOV4hyFj7BInWTfAkJNUGw= + dependencies: + array-union "^1.0.1" + glob "^7.0.3" + object-assign "^4.0.1" + pify "^2.0.0" + pinkie-promise "^2.0.0" + globule@^1.0.0: version "1.2.0" resolved "https://registry.yarnpkg.com/globule/-/globule-1.2.0.tgz#1dc49c6822dd9e8a2fa00ba2a295006e8664bd09" @@ -2833,6 +3527,11 @@ graceful-fs@^3.0.0: dependencies: natives "^1.1.0" +graceful-fs@^4.1.11, graceful-fs@^4.2.0: + version "4.2.3" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.3.tgz#4a12ff1b60376ef09862c2093edd908328be8423" + integrity sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ== + graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.1.9: version "4.1.11" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.11.tgz#0e8bdfe4d1ddb8854d64e04ea7c00e2a026e5658" @@ -2982,10 +3681,20 @@ gulplog@^1.0.0: dependencies: glogg "^1.0.0" +handle-thing@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/handle-thing/-/handle-thing-2.0.0.tgz#0e039695ff50c93fc288557d696f3c1dc6776754" + integrity sha512-d4sze1JNC454Wdo2fkuyzCr6aHcbL6PGGuFAz0Li/NcOm1tCHGnWDRmJP85dh9IhQErTc2svWFEX5xHIOo//kQ== + har-schema@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-1.0.5.tgz#d263135f43307c02c602afc8fe95970c0151369e" +har-schema@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92" + integrity sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI= + har-validator@~2.0.6: version "2.0.6" resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-2.0.6.tgz#cdcbc08188265ad119b6a5a7c8ab70eecfb5d27d" @@ -3002,6 +3711,14 @@ har-validator@~4.2.1: ajv "^4.9.1" har-schema "^1.0.5" +har-validator@~5.1.0: + version "5.1.3" + resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.1.3.tgz#1ef89ebd3e4996557675eed9893110dc350fa080" + integrity sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g== + dependencies: + ajv "^6.5.5" + har-schema "^2.0.0" + has-ansi@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91" @@ -3038,22 +3755,70 @@ has-flag@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-1.0.0.tgz#9d9e793165ce017a00f00418c43f942a7b1d11fa" +has-flag@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" + integrity sha1-tdRU3CGZriJWmfNGfloH87lVuv0= + has-gulplog@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/has-gulplog/-/has-gulplog-0.1.0.tgz#6414c82913697da51590397dafb12f22967811ce" dependencies: sparkles "^1.0.0" +has-symbols@^1.0.0, has-symbols@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.1.tgz#9f5214758a44196c406d9bd76cebf81ec2dd31e8" + integrity sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg== + has-unicode@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9" +has-value@^0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/has-value/-/has-value-0.3.1.tgz#7b1f58bada62ca827ec0a2078025654845995e1f" + integrity sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8= + dependencies: + get-value "^2.0.3" + has-values "^0.1.4" + isobject "^2.0.0" + +has-value@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/has-value/-/has-value-1.0.0.tgz#18b281da585b1c5c51def24c930ed29a0be6b177" + integrity sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc= + dependencies: + get-value "^2.0.6" + has-values "^1.0.0" + isobject "^3.0.0" + +has-values@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/has-values/-/has-values-0.1.4.tgz#6d61de95d91dfca9b9a02089ad384bff8f62b771" + integrity sha1-bWHeldkd/Km5oCCJrThL/49it3E= + +has-values@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/has-values/-/has-values-1.0.0.tgz#95b0b63fec2146619a6fe57fe75628d5a39efe4f" + integrity sha1-lbC2P+whRmGab+V/51Yo1aOe/k8= + dependencies: + is-number "^3.0.0" + kind-of "^4.0.0" + has@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/has/-/has-1.0.1.tgz#8461733f538b0837c9361e39a9ab9e9704dc2f28" dependencies: function-bind "^1.0.2" +has@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" + integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw== + dependencies: + function-bind "^1.1.1" + hash-base@^2.0.0: version "2.0.2" resolved "https://registry.yarnpkg.com/hash-base/-/hash-base-2.0.2.tgz#66ea1d856db4e8a5470cadf6fce23ae5244ef2e1" @@ -3119,10 +3884,25 @@ hosted-git-info@^2.1.4: version "2.4.2" resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.4.2.tgz#0076b9f46a270506ddbaaea56496897460612a67" +hpack.js@^2.1.6: + version "2.1.6" + resolved "https://registry.yarnpkg.com/hpack.js/-/hpack.js-2.1.6.tgz#87774c0949e513f42e84575b3c45681fade2a0b2" + integrity sha1-h3dMCUnlE/QuhFdbPEVoH63ioLI= + dependencies: + inherits "^2.0.1" + obuf "^1.0.0" + readable-stream "^2.0.1" + wbuf "^1.1.0" + html-comment-regex@^1.1.0: version "1.1.1" resolved "https://registry.yarnpkg.com/html-comment-regex/-/html-comment-regex-1.1.1.tgz#668b93776eaae55ebde8f3ad464b307a4963625e" +html-entities@^1.2.0: + version "1.2.1" + resolved "https://registry.yarnpkg.com/html-entities/-/html-entities-1.2.1.tgz#0df29351f0721163515dfb9e5543e5f6eed5162f" + integrity sha1-DfKTUfByEWNRXfueVUPl9u7VFi8= + html-loader@^0.4.0: version "0.4.5" resolved "https://registry.yarnpkg.com/html-loader/-/html-loader-0.4.5.tgz#5fbcd87cd63a5c49a7fce2fe56f425e05729c68c" @@ -3156,6 +3936,11 @@ htmlparser2@~3.8.1: entities "1.0" readable-stream "1.1" +http-deceiver@^1.2.7: + version "1.2.7" + resolved "https://registry.yarnpkg.com/http-deceiver/-/http-deceiver-1.2.7.tgz#fa7168944ab9a519d337cb0bec7284dc3e723d87" + integrity sha1-+nFolEq5pRnTN8sL7HKE3D5yPYc= + http-errors@1.6.2: version "1.6.2" resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.6.2.tgz#0a002cc85707192a7e7946ceedc11155f60ec736" @@ -3166,6 +3951,17 @@ http-errors@1.6.2: setprototypeof "1.0.3" statuses ">= 1.3.1 < 2" +http-errors@1.7.2: + version "1.7.2" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.2.tgz#4f5029cf13239f31036e5b2e55292bcfbcc85c8f" + integrity sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg== + dependencies: + depd "~1.1.2" + inherits "2.0.3" + setprototypeof "1.1.1" + statuses ">= 1.5.0 < 2" + toidentifier "1.0.0" + http-errors@~1.5.0: version "1.5.1" resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.5.1.tgz#788c0d2c1de2c81b9e6e8c01843b6b97eb920750" @@ -3204,14 +4000,15 @@ http-errors@~1.7.2: statuses ">= 1.5.0 < 2" toidentifier "1.0.0" -http-proxy-middleware@~0.17.1: - version "0.17.4" - resolved "https://registry.yarnpkg.com/http-proxy-middleware/-/http-proxy-middleware-0.17.4.tgz#642e8848851d66f09d4f124912846dbaeb41b833" +http-proxy-middleware@~0.18.0: + version "0.18.0" + resolved "https://registry.yarnpkg.com/http-proxy-middleware/-/http-proxy-middleware-0.18.0.tgz#0987e6bb5a5606e5a69168d8f967a87f15dd8aab" + integrity sha512-Fs25KVMPAIIcgjMZkVHJoKg9VcXcC1C8yb9JUgeDvVXY0S/zgVIhMb+qVswDIgtJe2DfckMSY2d6TuTEutlk6Q== dependencies: http-proxy "^1.16.2" - is-glob "^3.1.0" - lodash "^4.17.2" - micromatch "^2.3.11" + is-glob "^4.0.0" + lodash "^4.17.5" + micromatch "^3.1.9" http-proxy@^1.11.2, http-proxy@^1.13.0, http-proxy@^1.16.2: version "1.16.2" @@ -3228,6 +4025,15 @@ http-signature@~1.1.0: jsprim "^1.2.2" sshpk "^1.7.0" +http-signature@~1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1" + integrity sha1-muzZJRFHcvPZW2WmCruPfBj7rOE= + dependencies: + assert-plus "^1.0.0" + jsprim "^1.2.2" + sshpk "^1.7.0" + https-browserify@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-0.0.1.tgz#3f91365cabe60b77ed0ebba24b454e3e09d95a82" @@ -3240,6 +4046,14 @@ https-proxy-agent@^1.0.0: debug "2" extend "3" +https-proxy-agent@^2.2.1: + version "2.2.4" + resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-2.2.4.tgz#4ee7a737abd92678a293d9b34a1af4d0d08c787b" + integrity sha512-OmvfoQ53WLjtA9HeYP9RNrWMJzzAz1JGaSFr1nijg0PVR1JaD/xbJq1mdEIIlxGpXp9eSe/O2LgU9DJmTPd0Eg== + dependencies: + agent-base "^4.3.0" + debug "^3.1.0" + iconv-lite@0.4.15, iconv-lite@^0.4.4, iconv-lite@^0.4.5: version "0.4.15" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.15.tgz#fe265a218ac6a57cfe854927e9d04c19825eddeb" @@ -3249,6 +4063,13 @@ iconv-lite@0.4.19: resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.19.tgz#f7468f60135f5e5dad3399c0a81be9a1603a082b" integrity sha512-oTZqweIP51xaGPI4uPa56/Pri/480R+mo7SeU+YETByQNhDG55ycFyNLIgta9vXhILrxXDmF7ZGhqZIcuN0gJQ== +iconv-lite@0.4.24: + version "0.4.24" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" + integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== + dependencies: + safer-buffer ">= 2.1.2 < 3" + icss-replace-symbols@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/icss-replace-symbols/-/icss-replace-symbols-1.1.0.tgz#06ea6f83679a7749e386cfe1fe812ae5db223ded" @@ -3257,6 +4078,13 @@ ieee754@^1.1.4: version "1.1.8" resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.8.tgz#be33d40ac10ef1926701f6f08a2d86fbfd1ad3e4" +ignore-walk@^3.0.1: + version "3.0.3" + resolved "https://registry.yarnpkg.com/ignore-walk/-/ignore-walk-3.0.3.tgz#017e2447184bfeade7c238e4aefdd1e8f95b1e37" + integrity sha512-m7o6xuOaT1aqheYHKf8W6J5pYH85ZI9w077erOzLje3JsB1gkafkAhHHY19dqjulgIZHFm32Cp5uNZgcQqdJKw== + dependencies: + minimatch "^3.0.4" + ignore@^3.2.0: version "3.3.3" resolved "https://registry.yarnpkg.com/ignore/-/ignore-3.3.3.tgz#432352e57accd87ab3110e82d3fea0e47812156d" @@ -3265,6 +4093,19 @@ image-size@~0.5.0: version "0.5.5" resolved "https://registry.yarnpkg.com/image-size/-/image-size-0.5.5.tgz#09dfd4ab9d20e29eb1c3e80b8990378df9e3cb9c" +immediate@~3.0.5: + version "3.0.6" + resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.0.6.tgz#9db1dbd0faf8de6fbe0f5dd5e56bb606280de69b" + integrity sha1-nbHb0Pr43m++D13V5Wu2BigN5ps= + +import-local@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/import-local/-/import-local-2.0.0.tgz#55070be38a5993cf18ef6db7e961f5bee5c5a09d" + integrity sha512-b6s04m3O+s3CGSbqDIyP4R6aAwAeYlVq9+WUWep6iHa8ETRf9yei1U48C5MmfJmV9AiLYYBKPMq/W+/WRpQmCQ== + dependencies: + pkg-dir "^3.0.0" + resolve-cwd "^2.0.0" + imports-loader@^0.6.5: version "0.6.5" resolved "https://registry.yarnpkg.com/imports-loader/-/imports-loader-0.6.5.tgz#ae74653031d59e37b3c2fb2544ac61aeae3530a6" @@ -3340,6 +4181,14 @@ inquirer@^0.12.0: strip-ansi "^3.0.0" through "^2.3.6" +internal-ip@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/internal-ip/-/internal-ip-3.0.1.tgz#df5c99876e1d2eb2ea2d74f520e3f669a00ece27" + integrity sha512-NXXgESC2nNVtU+pqmC9e6R8B1GpKxzsAQhffvh5AL79qKnodd+L7tnEQmTiUAVngqLalPbSqRA7XGIEL5nCd0Q== + dependencies: + default-gateway "^2.6.0" + ipaddr.js "^1.5.2" + interpret@^0.6.4: version "0.6.6" resolved "https://registry.yarnpkg.com/interpret/-/interpret-0.6.6.tgz#fecd7a18e7ce5ca6abfb953e1f86213a49f1625b" @@ -3352,10 +4201,35 @@ invert-kv@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/invert-kv/-/invert-kv-1.0.0.tgz#104a8e4aaca6d3d8cd157a8ef8bfab2d7a3ffdb6" +invert-kv@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/invert-kv/-/invert-kv-2.0.0.tgz#7393f5afa59ec9ff5f67a27620d11c226e3eec02" + integrity sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA== + +ip-regex@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/ip-regex/-/ip-regex-2.1.0.tgz#fa78bf5d2e6913c911ce9f819ee5146bb6d844e9" + integrity sha1-+ni/XS5pE8kRzp+BnuUUa7bYROk= + +ip@^1.1.0, ip@^1.1.5: + version "1.1.5" + resolved "https://registry.yarnpkg.com/ip/-/ip-1.1.5.tgz#bdded70114290828c0a039e72ef25f5aaec4354a" + integrity sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo= + ipaddr.js@1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.3.0.tgz#1e03a52fdad83a8bbb2b25cbf4998b4cffcd3dec" +ipaddr.js@1.9.0: + version "1.9.0" + resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.0.tgz#37df74e430a0e47550fe54a2defe30d8acd95f65" + integrity sha512-M4Sjn6N/+O6/IXSJseKqHoFc+5FdGJ22sXqnjTpdZweHK64MzEPAyQZyEU3R/KRv2GLoa7nNtg/C2Ev6m7z+eA== + +ipaddr.js@^1.5.2: + version "1.9.1" + resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3" + integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g== + is-absolute-url@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/is-absolute-url/-/is-absolute-url-2.1.0.tgz#50530dfb84fcc9aa7dbe7852e83a37b93b9f2aa6" @@ -3367,6 +4241,25 @@ is-absolute@^0.2.3: is-relative "^0.2.1" is-windows "^0.2.0" +is-accessor-descriptor@^0.1.6: + version "0.1.6" + resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz#a9e12cb3ae8d876727eeef3843f8a0897b5c98d6" + integrity sha1-qeEss66Nh2cn7u84Q/igiXtcmNY= + dependencies: + kind-of "^3.0.2" + +is-accessor-descriptor@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz#169c2f6d3df1f992618072365c9b0ea1f6878656" + integrity sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ== + dependencies: + kind-of "^6.0.0" + +is-arguments@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/is-arguments/-/is-arguments-1.0.4.tgz#3faf966c7cba0ff437fb31f6250082fcf0448cf3" + integrity sha512-xPh0Rmt8NE65sNzvyUmWgI1tz3mKq74lGA0mL8LYZcoIzKOzDh6HmrYm3d18k60nHerC8A9Km8kYu87zfSFnLA== + is-arrayish@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" @@ -3387,6 +4280,48 @@ is-builtin-module@^1.0.0: dependencies: builtin-modules "^1.0.0" +is-callable@^1.1.4, is-callable@^1.1.5: + version "1.1.5" + resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.1.5.tgz#f7e46b596890456db74e7f6e976cb3273d06faab" + integrity sha512-ESKv5sMCJB2jnHTWZ3O5itG+O128Hsus4K4Qh1h2/cgn2vbgnLSVqfV46AeJA9D5EeeLa9w81KUXMtn34zhX+Q== + +is-data-descriptor@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz#0b5ee648388e2c860282e793f1856fec3f301b56" + integrity sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y= + dependencies: + kind-of "^3.0.2" + +is-data-descriptor@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz#d84876321d0e7add03990406abbbbd36ba9268c7" + integrity sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ== + dependencies: + kind-of "^6.0.0" + +is-date-object@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.2.tgz#bda736f2cd8fd06d32844e7743bfa7494c3bfd7e" + integrity sha512-USlDT524woQ08aoZFzh3/Z6ch9Y/EWXEHQ/AaRN0SkKq4t2Jw2R2339tSXmwuVoY7LLlBCbOIlx2myP/L5zk0g== + +is-descriptor@^0.1.0: + version "0.1.6" + resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-0.1.6.tgz#366d8240dde487ca51823b1ab9f07a10a78251ca" + integrity sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg== + dependencies: + is-accessor-descriptor "^0.1.6" + is-data-descriptor "^0.1.4" + kind-of "^5.0.0" + +is-descriptor@^1.0.0, is-descriptor@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-1.0.2.tgz#3b159746a66604b04f8c81524ba365c5f14d86ec" + integrity sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg== + dependencies: + is-accessor-descriptor "^1.0.0" + is-data-descriptor "^1.0.0" + kind-of "^6.0.2" + is-dotfile@^1.0.0: version "1.0.3" resolved "https://registry.yarnpkg.com/is-dotfile/-/is-dotfile-1.0.3.tgz#a6a2f32ffd2dfb04f5ca25ecd0f6b83cf798a1e1" @@ -3411,15 +4346,22 @@ is-expression@^3.0.0: acorn "~4.0.2" object-assign "^4.0.1" -is-extendable@^0.1.1: +is-extendable@^0.1.0, is-extendable@^0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-0.1.1.tgz#62b110e289a471418e3ec36a617d472e301dfc89" +is-extendable@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-1.0.1.tgz#a7470f9e426733d81bd81e1155264e3a3507cab4" + integrity sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA== + dependencies: + is-plain-object "^2.0.4" + is-extglob@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-1.0.0.tgz#ac468177c4943405a092fc8f29760c6ffc6206c0" -is-extglob@^2.1.0: +is-extglob@^2.1.0, is-extglob@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" @@ -3451,6 +4393,13 @@ is-glob@^3.1.0: dependencies: is-extglob "^2.1.0" +is-glob@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.1.tgz#7567dbe9f2f5e2467bc77ab83c4a29482407a5dc" + integrity sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg== + dependencies: + is-extglob "^2.1.1" + is-my-json-valid@^2.10.0, is-my-json-valid@^2.12.4: version "2.16.0" resolved "https://registry.yarnpkg.com/is-my-json-valid/-/is-my-json-valid-2.16.0.tgz#f079dd9bfdae65ee2038aae8acbc86ab109e3693" @@ -3496,6 +4445,13 @@ is-plain-obj@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-1.1.0.tgz#71a50c8429dfca773c92a390a4a03b39fcd51d3e" +is-plain-object@^2.0.3, is-plain-object@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677" + integrity sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og== + dependencies: + isobject "^3.0.1" + is-posix-bracket@^0.1.0: version "0.1.1" resolved "https://registry.yarnpkg.com/is-posix-bracket/-/is-posix-bracket-0.1.1.tgz#3334dc79774368e92f016e6fbc0a88f5cd6e6bc4" @@ -3522,6 +4478,13 @@ is-regex@^1.0.3: dependencies: has "^1.0.1" +is-regex@^1.0.4, is-regex@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.0.5.tgz#39d589a358bf18967f726967120b8fc1aed74eae" + integrity sha512-vlKW17SNq44owv5AQR3Cq0bQPEb8+kF3UKZ2fiZNOWtztYE5i0CzCZxFDwO58qAOWtxdBRVO/V5Qin1wjCqFYQ== + dependencies: + has "^1.0.3" + is-relative@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/is-relative/-/is-relative-0.2.1.tgz#d27f4c7d516d175fb610db84bbeef23c3bc97aa5" @@ -3544,6 +4507,13 @@ is-svg@^2.0.0: dependencies: html-comment-regex "^1.1.0" +is-symbol@^1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.3.tgz#38e1014b9e6329be0de9d24a414fd7441ec61937" + integrity sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ== + dependencies: + has-symbols "^1.0.1" + is-typedarray@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" @@ -3562,6 +4532,16 @@ is-windows@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-0.2.0.tgz#de1aa6d63ea29dd248737b69f1ff8b8002d2108c" +is-windows@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d" + integrity sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA== + +is-wsl@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-1.1.0.tgz#1f16e4aa22b04d1336b66188a66af3c600c3a66d" + integrity sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0= + isarray@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf" @@ -3588,6 +4568,11 @@ isobject@^2.0.0: dependencies: isarray "1.0.0" +isobject@^3.0.0, isobject@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df" + integrity sha1-TkMekrEalzFjaqH5yNHMvP2reN8= + isstream@0.1.x, isstream@~0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" @@ -3611,13 +4596,24 @@ jasmine-core@^2.4.1: version "2.6.3" resolved "https://registry.yarnpkg.com/jasmine-core/-/jasmine-core-2.6.3.tgz#45072950e4a42b1e322fe55c001100a465d77815" +jasmine-core@^3.3: + version "3.5.0" + resolved "https://registry.yarnpkg.com/jasmine-core/-/jasmine-core-3.5.0.tgz#132c23e645af96d85c8bca13c8758b18429fc1e4" + integrity sha512-nCeAiw37MIMA9w9IXso7bRaLl+c/ef3wnxsoSAlYrzS+Ot0zTG6nU8G/cIfGkqpkjX2wNaIW9RFG0TwIFnG6bA== + jasmine-core@~2.4.0: version "2.4.1" resolved "https://registry.yarnpkg.com/jasmine-core/-/jasmine-core-2.4.1.tgz#6f83ab3a0f16951722ce07d206c773d57cc838be" -jasmine-reporters@^2.1.1: - version "2.2.1" - resolved "https://registry.yarnpkg.com/jasmine-reporters/-/jasmine-reporters-2.2.1.tgz#de9a9201367846269e7ca8adff5b44221671fcbd" +jasmine-core@~2.8.0: + version "2.8.0" + resolved "https://registry.yarnpkg.com/jasmine-core/-/jasmine-core-2.8.0.tgz#bcc979ae1f9fd05701e45e52e65d3a5d63f1a24e" + integrity sha1-vMl5rh+f0FcB5F5S5l06XWPxok4= + +jasmine-reporters@^2.3.0, jasmine-reporters@^2.3.2: + version "2.3.2" + resolved "https://registry.yarnpkg.com/jasmine-reporters/-/jasmine-reporters-2.3.2.tgz#898818ffc234eb8b3f635d693de4586f95548d43" + integrity sha512-u/7AT9SkuZsUfFBLLzbErohTGNsEUCKaQbsVYnLFW1gEuL2DzmBL4n8v90uZsqIqlWvWUgian8J6yOt5Fyk/+A== dependencies: mkdirp "^0.5.1" xmldom "^0.1.22" @@ -3630,10 +4626,24 @@ jasmine@2.4.1: glob "^3.2.11" jasmine-core "~2.4.0" +jasmine@2.8.0: + version "2.8.0" + resolved "https://registry.yarnpkg.com/jasmine/-/jasmine-2.8.0.tgz#6b089c0a11576b1f16df11b80146d91d4e8b8a3e" + integrity sha1-awicChFXax8W3xG4AUbZHU6Lij4= + dependencies: + exit "^0.1.2" + glob "^7.0.6" + jasmine-core "~2.8.0" + jasminewd2@0.0.10: version "0.0.10" resolved "https://registry.yarnpkg.com/jasminewd2/-/jasminewd2-0.0.10.tgz#94f48ae2bc946cad643035467b4bb7ea9c1075ef" +jasminewd2@^2.1.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/jasminewd2/-/jasminewd2-2.2.0.tgz#e37cf0b17f199cce23bea71b2039395246b4ec4e" + integrity sha1-43zwsX8ZnM4jvqcbIDk5Uka07E4= + jmespath@0.15.0: version "0.15.0" resolved "https://registry.yarnpkg.com/jmespath/-/jmespath-0.15.0.tgz#a3f222a9aae9f966f5d27c796510e28091764217" @@ -3734,6 +4744,11 @@ json-schema-faker@^0.2.8: faker "~3.1.0" randexp "~0.4.2" +json-schema-traverse@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" + integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== + json-schema@0.2.3: version "0.2.3" resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13" @@ -3766,6 +4781,13 @@ jsonfile@^2.1.0: optionalDependencies: graceful-fs "^4.1.6" +jsonfile@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-4.0.0.tgz#8771aae0799b64076b76640fca058f9c10e33ecb" + integrity sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss= + optionalDependencies: + graceful-fs "^4.1.6" + jsonify@~0.0.0: version "0.0.0" resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.0.tgz#2c74b6ee41d93ca51b7b5aaee8f503631d252a73" @@ -3808,6 +4830,16 @@ jstransformer@1.0.0: is-promise "^2.0.0" promise "^7.0.1" +jszip@^3.1.3: + version "3.2.2" + resolved "https://registry.yarnpkg.com/jszip/-/jszip-3.2.2.tgz#b143816df7e106a9597a94c77493385adca5bd1d" + integrity sha512-NmKajvAFQpbg3taXQXr/ccS2wcucR1AZ+NtyWp2Nq7HHVsXhcJFR8p0Baf32C2yVvBylFWVeKf+WI2AnvlPhpA== + dependencies: + lie "~3.3.0" + pako "~1.0.2" + readable-stream "~2.3.6" + set-immediate-shim "~1.0.1" + jwa@^1.1.4: version "1.1.5" resolved "https://registry.yarnpkg.com/jwa/-/jwa-1.1.5.tgz#a0552ce0220742cd52e153774a32905c30e756e5" @@ -3825,9 +4857,10 @@ jws@^3.1.0: jwa "^1.1.4" safe-buffer "^5.0.1" -karma-chrome-launcher@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/karma-chrome-launcher/-/karma-chrome-launcher-1.0.1.tgz#be5ae7c4264f9a0a2e22e3d984beb325ad92c8cb" +karma-chrome-launcher@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/karma-chrome-launcher/-/karma-chrome-launcher-2.2.0.tgz#cf1b9d07136cc18fe239327d24654c3dbc368acf" + integrity sha512-uf/ZVpAabDBPvdPdveyk1EPgbnloPvFFGgmRhYLTDH7gEB4nZdSBk8yTU47w1g/drLSx5uMOkjKk7IWKfWg/+w== dependencies: fs-access "^1.0.0" which "^1.2.1" @@ -3842,9 +4875,12 @@ karma-ie-launcher@^1.0.0: dependencies: lodash "^4.6.1" -karma-jasmine@^1.0.2: - version "1.1.0" - resolved "https://registry.yarnpkg.com/karma-jasmine/-/karma-jasmine-1.1.0.tgz#22e4c06bf9a182e5294d1f705e3733811b810acf" +karma-jasmine@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/karma-jasmine/-/karma-jasmine-2.0.1.tgz#26e3e31f2faf272dd80ebb0e1898914cc3a19763" + integrity sha512-iuC0hmr9b+SNn1DaUD2QEYtUxkS1J+bSJSn7ejdEexs7P8EYvA1CWkEdrDQ+8jVH3AgWlCNwjYsT1chjcNW9lA== + dependencies: + jasmine-core "^3.3" karma-junit-reporter@^1.1.0: version "1.2.0" @@ -3878,9 +4914,10 @@ karma-webpack@^1.8.0: source-map "^0.1.41" webpack-dev-middleware "^1.0.11" -karma@^1.1.2: - version "1.7.0" - resolved "https://registry.yarnpkg.com/karma/-/karma-1.7.0.tgz#6f7a1a406446fa2e187ec95398698f4cee476269" +karma@^1.7.1: + version "1.7.1" + resolved "https://registry.yarnpkg.com/karma/-/karma-1.7.1.tgz#85cc08e9e0a22d7ce9cca37c4a1be824f6a2b1ae" + integrity sha512-k5pBjHDhmkdaUccnC7gE3mBzZjcxyxYsYVaqiL2G5AqlfLyBO5nw2VdNK+O16cveEPd/gIOWULH7gkiYYwVNHg== dependencies: bluebird "^3.3.0" body-parser "^1.16.1" @@ -3918,7 +4955,12 @@ keygrip@~1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/keygrip/-/keygrip-1.0.1.tgz#b02fa4816eef21a8c4b35ca9e52921ffc89a30e9" -kind-of@^3.0.2: +killable@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/killable/-/killable-1.0.1.tgz#4c8ce441187a061c7474fb87ca08e2a638194892" + integrity sha512-LzqtLKlUwirEUyl/nicirVmNiPvYs7l5n8wOPP7fyJVpUPkvCnW/vuiXGpylGUlnPDnB7311rARzAt3Mhswpjg== + +kind-of@^3.0.2, kind-of@^3.0.3, kind-of@^3.2.0: version "3.2.2" resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64" dependencies: @@ -3930,6 +4972,16 @@ kind-of@^4.0.0: dependencies: is-buffer "^1.1.5" +kind-of@^5.0.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-5.1.0.tgz#729c91e2d857b7a419a1f9aa65685c4c33f5845d" + integrity sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw== + +kind-of@^6.0.0, kind-of@^6.0.2: + version "6.0.3" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd" + integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw== + klaw@^1.0.0: version "1.3.1" resolved "https://registry.yarnpkg.com/klaw/-/klaw-1.3.1.tgz#4088433b46b3b1ba259d78785d8e96f73ba02439" @@ -3946,6 +4998,13 @@ lcid@^1.0.0: dependencies: invert-kv "^1.0.0" +lcid@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/lcid/-/lcid-2.0.0.tgz#6ef5d2df60e52f82eb228a4c373e8d1f397253cf" + integrity sha512-avPEb8P8EGnwXKClwsNUgryVjllcRqtMYa49NTsbQagYuT1DcXnl1915oxWjoyGrXR6zH/Y0Zc96xWsPcoDKeA== + dependencies: + invert-kv "^2.0.0" + ldap-filter@0.2.2: version "0.2.2" resolved "https://registry.yarnpkg.com/ldap-filter/-/ldap-filter-0.2.2.tgz#f2b842be0b86da3352798505b31ebcae590d77d0" @@ -3994,6 +5053,13 @@ levn@^0.3.0, levn@~0.3.0: prelude-ls "~1.1.2" type-check "~0.3.2" +lie@~3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/lie/-/lie-3.3.0.tgz#dcf82dee545f46074daf200c7c1c5a08e0f40f6a" + integrity sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ== + dependencies: + immediate "~3.0.5" + liftoff@^2.1.0: version "2.3.0" resolved "https://registry.yarnpkg.com/liftoff/-/liftoff-2.3.0.tgz#a98f2ff67183d8ba7cfaca10548bd7ff0550b385" @@ -4051,6 +5117,14 @@ locate-path@^2.0.0: p-locate "^2.0.0" path-exists "^3.0.0" +locate-path@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-3.0.0.tgz#dbec3b3ab759758071b58fe59fc41871af21400e" + integrity sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A== + dependencies: + p-locate "^3.0.0" + path-exists "^3.0.0" + lodash._arraypool@~2.4.1: version "2.4.1" resolved "https://registry.yarnpkg.com/lodash._arraypool/-/lodash._arraypool-2.4.1.tgz#e88eecb92e2bb84c9065612fd958a0719cd47f94" @@ -4447,15 +5521,16 @@ lodash@4.16.x: version "4.16.6" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.16.6.tgz#d22c9ac660288f3843e16ba7d2b5d06cca27d777" -lodash@^4.0.0, lodash@^4.0.1, lodash@^4.11.1, lodash@^4.13.1, lodash@^4.14.0, lodash@^4.14.2, lodash@^4.17.2, lodash@^4.2.0, lodash@^4.3.0, lodash@^4.5.0, lodash@^4.6.1, lodash@~4.17.4: - version "4.17.4" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae" - -lodash@^4.17.11, lodash@^4.17.14, lodash@^4.17.4, lodash@^4.17.5: +lodash@^4.0.0, lodash@^4.0.1, lodash@^4.11.1, lodash@^4.13.1, lodash@^4.14.0, lodash@^4.17.11, lodash@^4.17.14, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.2.0, lodash@^4.3.0, lodash@^4.5.0, lodash@^4.6.1, lodash@~4.17.4: version "4.17.15" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548" integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A== +lodash@^4.14.2: + version "4.17.13" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.13.tgz#0bdc3a6adc873d2f4e0c4bac285df91b64fc7b93" + integrity sha512-vm3/XWXfWtRua0FkUyEHBZy8kCPjErNBT9fJx8Zvs+U6zjqPbTUOpkaoum3O5uiA8sm+yNMHXfYkTUHFoMxFNA== + lodash@~1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/lodash/-/lodash-1.0.2.tgz#8f57560c83b59fc270bd3d561b690043430e2551" @@ -4475,6 +5550,11 @@ log4js@^0.6.31: readable-stream "~1.0.2" semver "~4.3.3" +loglevel@^1.4.1: + version "1.6.6" + resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.6.6.tgz#0ee6300cc058db6b3551fa1c4bf73b83bb771312" + integrity sha512-Sgr5lbboAUBo3eXCSPL4/KoVz3ROKquOjcctxmHIt+vol2DrqTQe3SwkKKuYhEiWB5kYa13YyopJ69deJ1irzQ== + lolex@1.3.2: version "1.3.2" resolved "https://registry.yarnpkg.com/lolex/-/lolex-1.3.2.tgz#7c3da62ffcb30f0f5a80a2566ca24e45d8a01f31" @@ -4551,7 +5631,14 @@ machinepack-urls@^4.0.0: lodash "^3.9.2" machine "^9.0.3" -map-cache@^0.2.0: +map-age-cleaner@^0.1.1: + version "0.1.3" + resolved "https://registry.yarnpkg.com/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz#7d583a7306434c055fe474b0f45078e6e1b4b92a" + integrity sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w== + dependencies: + p-defer "^1.0.0" + +map-cache@^0.2.0, map-cache@^0.2.2: version "0.2.2" resolved "https://registry.yarnpkg.com/map-cache/-/map-cache-0.2.2.tgz#c32abd0bd6525d9b051645bb4f26ac5dc98a0dbf" @@ -4563,6 +5650,13 @@ map-stream@^0.1.0, map-stream@~0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/map-stream/-/map-stream-0.1.0.tgz#e56aa94c4c8055a16404a0674b78f215f7c8e194" +map-visit@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/map-visit/-/map-visit-1.0.0.tgz#ecdca8f13144e660f1b5bd41f12f3479d98dfb8f" + integrity sha1-7Nyo8TFE5mDxtb1B8S80edmN+48= + dependencies: + object-visit "^1.0.0" + markdown-serve@^0.3.2: version "0.3.3" resolved "https://registry.yarnpkg.com/markdown-serve/-/markdown-serve-0.3.3.tgz#02328f5b2c60fe0767cd73ab9048861f33196c1b" @@ -4591,6 +5685,15 @@ mem@^1.1.0: dependencies: mimic-fn "^1.0.0" +mem@^4.0.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/mem/-/mem-4.3.0.tgz#461af497bc4ae09608cdb2e60eefb69bff744178" + integrity sha512-qX2bG48pTqYRVmDB37rn/6PT7LcR8T7oAX3bf99u1Tt1nzxYfxkgqDwUwolPlXweM0XzBOBFzSx4kfp7KP1s/w== + dependencies: + map-age-cleaner "^0.1.1" + mimic-fn "^2.0.0" + p-is-promise "^2.0.0" + memory-fs@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/memory-fs/-/memory-fs-0.2.0.tgz#f2bb25368bc121e391c2520de92969caee0a0290" @@ -4632,7 +5735,7 @@ methods@^1.1.1, methods@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" -micromatch@^2.1.5, micromatch@^2.3.11, micromatch@^2.3.7: +micromatch@^2.1.5, micromatch@^2.3.7: version "2.3.11" resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-2.3.11.tgz#86677c97d1720b363431d04d0d15293bd38c1565" dependencies: @@ -4650,6 +5753,25 @@ micromatch@^2.1.5, micromatch@^2.3.11, micromatch@^2.3.7: parse-glob "^3.0.4" regex-cache "^0.4.2" +micromatch@^3.1.10, micromatch@^3.1.4, micromatch@^3.1.9: + version "3.1.10" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.10.tgz#70859bc95c9840952f359a068a3fc49f9ecfac23" + integrity sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg== + dependencies: + arr-diff "^4.0.0" + array-unique "^0.3.2" + braces "^2.3.1" + define-property "^2.0.2" + extend-shallow "^3.0.2" + extglob "^2.0.4" + fragment-cache "^0.2.1" + kind-of "^6.0.2" + nanomatch "^1.2.9" + object.pick "^1.3.0" + regex-not "^1.0.0" + snapdragon "^0.8.1" + to-regex "^3.0.2" + miller-rabin@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/miller-rabin/-/miller-rabin-4.0.0.tgz#4a62fb1d42933c05583982f4c716f6fb9e6c6d3d" @@ -4657,6 +5779,11 @@ miller-rabin@^4.0.0: bn.js "^4.0.0" brorand "^1.0.1" +mime-db@1.43.0: + version "1.43.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.43.0.tgz#0a12e0502650e473d735535050e7c8f4eb4fae58" + integrity sha512-+5dsGEEovYbT8UY9yD7eE4XTc4UwJ1jBYlgaQQF38ENsKR3wj/8q8RFZrF9WIZpB2V1ArTVFUva8sAul1NzRzQ== + "mime-db@>= 1.27.0 < 2": version "1.28.0" resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.28.0.tgz#fedd349be06d2865b7fc57d837c6de4f17d7ac3c" @@ -4671,6 +5798,13 @@ mime-types@^2.1.12, mime-types@~2.1.11, mime-types@~2.1.15, mime-types@~2.1.7: dependencies: mime-db "~1.27.0" +mime-types@~2.1.19, mime-types@~2.1.24: + version "2.1.26" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.26.tgz#9c921fc09b7e149a65dfdc0da4d20997200b0a06" + integrity sha512-01paPWYgLrkqAyrlDorC1uDwl2p3qZT7yl806vW7DvDoxwXi46jsjFbg+WdwotBIk6/MbEhO/dh5aZ5sNj/dWQ== + dependencies: + mime-db "1.43.0" + mime@1.3.4: version "1.3.4" resolved "https://registry.yarnpkg.com/mime/-/mime-1.3.4.tgz#115f9e3b6b3daf2959983cb38f149a2d40eb5d53" @@ -4684,13 +5818,24 @@ mime@1.6.0, mime@^1.4.1: resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== +mime@^2.3.1: + version "2.4.4" + resolved "https://registry.yarnpkg.com/mime/-/mime-2.4.4.tgz#bd7b91135fc6b01cde3e9bae33d659b63d8857e5" + integrity sha512-LRxmNwziLPT828z+4YkNzloCFC2YM4wrB99k+AV5ZbEyfGNWfG8SO1FUXLmLDBSo89NrJZ4DIWeLjy1CHGhMGA== + mimic-fn@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-1.1.0.tgz#e667783d92e89dbd342818b5230b9d62a672ad18" -minicap-prebuilt@^2.3.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/minicap-prebuilt/-/minicap-prebuilt-2.3.0.tgz#a616cf84558a71b98aa70d05bce8be09409dd366" +mimic-fn@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" + integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== + +minicap-prebuilt-beta@^2.4.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/minicap-prebuilt-beta/-/minicap-prebuilt-beta-2.4.0.tgz#fe038a60606f7139074220c3a3d1d83f25976606" + integrity sha512-Zu+SxfyFPsD3CIm0a71kvGS51tyzipkSWnPjHRlbqrLDtXvf8NvO7X0GJAK9a7Zvi7VttNVTiYp8OsSJ0DN4aA== minimalistic-assert@^1.0.0: version "1.0.0" @@ -4734,9 +5879,33 @@ minimist@^1.1.0, minimist@^1.1.3, minimist@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284" -minitouch-prebuilt@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/minitouch-prebuilt/-/minitouch-prebuilt-1.2.0.tgz#e136fb2eb888d6f0283df173d444f1dfb7d9df31" +minipass@^2.6.0, minipass@^2.8.6, minipass@^2.9.0: + version "2.9.0" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-2.9.0.tgz#e713762e7d3e32fed803115cf93e04bca9fcc9a6" + integrity sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg== + dependencies: + safe-buffer "^5.1.2" + yallist "^3.0.0" + +minitouch-prebuilt-beta@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/minitouch-prebuilt-beta/-/minitouch-prebuilt-beta-1.3.0.tgz#34474261bededcb7ade38806e08b01f3ded2bbe5" + integrity sha512-A/L2MbDT7iDv0/FJ9fcUEUA/0BscGatlhij9KCwGFfIePMkzJ9Y64ETimp6ZfWIge2Akpwg7lakZA+hihmU9Kg== + +minizlib@^1.2.1: + version "1.3.3" + resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-1.3.3.tgz#2290de96818a34c29551c8a8d301216bd65a861d" + integrity sha512-6ZYMOEnmVsdCeTJVE0W9ZD+pVnE8h9Hma/iOwwRDsdQoePpoX56/8B6z3P9VNwppJuBKNRuFDRNRqRWexT9G9Q== + dependencies: + minipass "^2.9.0" + +mixin-deep@^1.2.0: + version "1.3.2" + resolved "https://registry.yarnpkg.com/mixin-deep/-/mixin-deep-1.3.2.tgz#1120b43dc359a785dce65b55b82e257ccf479566" + integrity sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA== + dependencies: + for-in "^1.0.2" + is-extendable "^1.0.1" mkdirp@0.5.0: version "0.5.0" @@ -4750,10 +5919,6 @@ mkdirp@0.5.0: dependencies: minimist "0.0.8" -mkdirp@~0.3.5: - version "0.3.5" - resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.3.5.tgz#de3e5f8961c88c787ee1368df849ac4413eca8d7" - moment@^2.10.6: version "2.18.1" resolved "https://registry.yarnpkg.com/moment/-/moment-2.18.1.tgz#c36193dd3ce1c2eed2adb7c802dbbc77a81b1c0f" @@ -4807,6 +5972,19 @@ multer@^1.1.0: type-is "^1.6.4" xtend "^4.0.0" +multicast-dns-service-types@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/multicast-dns-service-types/-/multicast-dns-service-types-1.1.0.tgz#899f11d9686e5e05cb91b35d5f0e63b773cfc901" + integrity sha1-iZ8R2WhuXgXLkbNdXw5jt3PPyQE= + +multicast-dns@^6.0.1: + version "6.2.3" + resolved "https://registry.yarnpkg.com/multicast-dns/-/multicast-dns-6.2.3.tgz#a0ec7bd9055c4282f790c3c82f4e28db3b31b229" + integrity sha512-ji6J5enbMyGRHIAkAOu3WdV8nggqviKCEKtXcOqfphZZtQrmHKycfynJ2V7eVPUA4NhJ6V7Wf4TmGbTwKE9B6g== + dependencies: + dns-packet "^1.3.1" + thunky "^1.0.2" + multipipe@^0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/multipipe/-/multipipe-0.1.2.tgz#2a8f2ddf70eed564dff2d57f1e1a137d9f05078b" @@ -4833,6 +6011,11 @@ my-local-ip@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/my-local-ip/-/my-local-ip-1.0.0.tgz#37585555a4ff1985309edac7c2a045a466be6c32" +nan@^2.12.1: + version "2.14.0" + resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.0.tgz#7818f722027b2459a86f0295d434d1fc2336c52c" + integrity sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg== + nan@^2.2.0, nan@^2.3.0, nan@^2.3.2, nan@^2.3.3, nan@~2.4.0: version "2.4.0" resolved "https://registry.yarnpkg.com/nan/-/nan-2.4.0.tgz#fb3c59d45fe4effe215f0b890f8adf6eb32d2232" @@ -4841,6 +6024,23 @@ nan@~2.3.0: version "2.3.5" resolved "https://registry.yarnpkg.com/nan/-/nan-2.3.5.tgz#822a0dc266290ce4cd3a12282ca3e7e364668a08" +nanomatch@^1.2.9: + version "1.2.13" + resolved "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.13.tgz#b87a8aa4fc0de8fe6be88895b38983ff265bd119" + integrity sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA== + dependencies: + arr-diff "^4.0.0" + array-unique "^0.3.2" + define-property "^2.0.2" + extend-shallow "^3.0.2" + fragment-cache "^0.2.1" + is-windows "^1.0.2" + kind-of "^6.0.2" + object.pick "^1.3.0" + regex-not "^1.0.0" + snapdragon "^0.8.1" + to-regex "^3.0.1" + native-promise-only@^0.8.1: version "0.8.1" resolved "https://registry.yarnpkg.com/native-promise-only/-/native-promise-only-0.8.1.tgz#20a318c30cb45f71fe7adfbf7b21c99c1472ef11" @@ -4870,10 +6070,29 @@ needle@^1.0.0: debug "^2.1.2" iconv-lite "^0.4.4" +needle@^2.2.1: + version "2.4.0" + resolved "https://registry.yarnpkg.com/needle/-/needle-2.4.0.tgz#6833e74975c444642590e15a750288c5f939b57c" + integrity sha512-4Hnwzr3mi5L97hMYeNl8wRW/Onhy4nUKR/lVemJ8gJedxxUyBLm9kkrDColJvoSfwi0jCNhD+xCdOtiGDQiRZg== + dependencies: + debug "^3.2.6" + iconv-lite "^0.4.4" + sax "^1.2.4" + negotiator@0.6.1: version "0.6.1" resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.1.tgz#2b327184e8992101177b28563fb5e7102acd0ca9" +negotiator@0.6.2: + version "0.6.2" + resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.2.tgz#feacf7ccf525a77ae9634436a64883ffeca346fb" + integrity sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw== + +nice-try@^1.0.4: + version "1.0.5" + resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366" + integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ== + no-case@^2.2.0: version "2.3.1" resolved "https://registry.yarnpkg.com/no-case/-/no-case-2.3.1.tgz#7aeba1c73a52184265554b7dc03baf720df80081" @@ -4884,6 +6103,11 @@ node-forge@0.2.24: version "0.2.24" resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.2.24.tgz#fa6f846f42fa93f63a0a30c9fbff7b4e130e0858" +node-forge@0.9.0: + version "0.9.0" + resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.9.0.tgz#d624050edbb44874adca12bb9a52ec63cb782579" + integrity sha512-7ASaDa3pD+lJ3WvXFsxekJQelBKRpne+GOVbLbtHYdd7pFspyeuJHnWfLplGf3SwKGbfs/aYl5V/JCIaHVUKKQ== + node-forge@^0.7.1: version "0.7.6" resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.7.6.tgz#fdf3b418aee1f94f0ef642cd63486c77ca9724ac" @@ -4962,6 +6186,22 @@ node-libs-browser@^1.0.0: util "^0.10.3" vm-browserify "0.0.4" +node-pre-gyp@*: + version "0.14.0" + resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.14.0.tgz#9a0596533b877289bcad4e143982ca3d904ddc83" + integrity sha512-+CvDC7ZttU/sSt9rFjix/P05iS43qHCOOGzcr3Ry99bXG7VX953+vFyEuph/tfqoYu8dttBkE86JSKBO2OzcxA== + dependencies: + detect-libc "^1.0.2" + mkdirp "^0.5.1" + needle "^2.2.1" + nopt "^4.0.1" + npm-packlist "^1.1.6" + npmlog "^4.0.2" + rc "^1.2.7" + rimraf "^2.6.1" + semver "^5.3.0" + tar "^4.4.2" + node-pre-gyp@^0.6.19, node-pre-gyp@^0.6.36: version "0.6.36" resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.6.36.tgz#db604112cb74e0d477554e9b505b17abddfab786" @@ -5026,12 +6266,17 @@ normalize-package-data@^2.3.2, normalize-package-data@^2.3.4: semver "2 || 3 || 4 || 5" validate-npm-package-license "^3.0.1" -normalize-path@^2.0.1: +normalize-path@^2.0.1, normalize-path@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-2.1.1.tgz#1ab28b556e198363a8c1a6f7e6fa20137fe6aed9" dependencies: remove-trailing-separator "^1.0.1" +normalize-path@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" + integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== + normalize-range@^0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/normalize-range/-/normalize-range-0.1.2.tgz#2d10c06bdfd312ea9777695a4d28439456b75942" @@ -5045,6 +6290,26 @@ normalize-url@^1.4.0: query-string "^4.1.0" sort-keys "^1.0.0" +npm-bundled@^1.0.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/npm-bundled/-/npm-bundled-1.1.1.tgz#1edd570865a94cdb1bc8220775e29466c9fb234b" + integrity sha512-gqkfgGePhTpAEgUsGEgcq1rqPXA+tv/aVBlgEzfXwA1yiUJF7xtEt3CtVwOjNYQOVknDk0F20w58Fnm3EtG0fA== + dependencies: + npm-normalize-package-bin "^1.0.1" + +npm-normalize-package-bin@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/npm-normalize-package-bin/-/npm-normalize-package-bin-1.0.1.tgz#6e79a41f23fd235c0623218228da7d9c23b8f6e2" + integrity sha512-EPfafl6JL5/rU+ot6P3gRSCpPDW5VmIzX959Ob1+ySFUuuYHWHekXpwdUZcKP5C+DS4GEtdJluwBjnsNDl+fSA== + +npm-packlist@^1.1.6: + version "1.4.7" + resolved "https://registry.yarnpkg.com/npm-packlist/-/npm-packlist-1.4.7.tgz#9e954365a06b80b18111ea900945af4f88ed4848" + integrity sha512-vAj7dIkp5NhieaGZxBJB8fF4R0078rqsmhJcAfXZ6O7JJhjhPK96n5Ry1oZcfLXgfun0GWTZPOxaEyqv8GBykQ== + dependencies: + ignore-walk "^3.0.1" + npm-bundled "^1.0.1" + npm-run-path@^2.0.0: version "2.0.2" resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f" @@ -5082,6 +6347,11 @@ oauth-sign@~0.8.1: version "0.8.2" resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.8.2.tgz#46a6ab7f0aead8deae9ec0565780b7d4efeb9d43" +oauth-sign@~0.9.0: + version "0.9.0" + resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455" + integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ== + oauth@0.9.x: version "0.9.15" resolved "https://registry.yarnpkg.com/oauth/-/oauth-0.9.15.tgz#bd1fefaf686c96b75475aed5196412ff60cfb9c1" @@ -5102,10 +6372,51 @@ object-component@0.0.3: version "0.0.3" resolved "https://registry.yarnpkg.com/object-component/-/object-component-0.0.3.tgz#f0c69aa50efc95b866c186f400a33769cb2f1291" +object-copy@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/object-copy/-/object-copy-0.1.0.tgz#7e7d858b781bd7c991a41ba975ed3812754e998c" + integrity sha1-fn2Fi3gb18mRpBupde04EnVOmYw= + dependencies: + copy-descriptor "^0.1.0" + define-property "^0.2.5" + kind-of "^3.0.3" + object-hash@~0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/object-hash/-/object-hash-0.3.0.tgz#548208e43b36a44e4da30bad6c56ac53b885e744" +object-inspect@^1.7.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.7.0.tgz#f4f6bd181ad77f006b5ece60bd0b6f398ff74a67" + integrity sha512-a7pEHdh1xKIAgTySUGgLMx/xwDZskN1Ud6egYYN3EdRW4ZMPNEDUTF+hwy2LUC+Bl+SyLXANnwz/jyh/qutKUw== + +object-is@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.0.2.tgz#6b80eb84fe451498f65007982f035a5b445edec4" + integrity sha512-Epah+btZd5wrrfjkJZq1AOB9O6OxUQto45hzFd7lXGrpHPGE0W1k+426yrZV+k6NJOzLNNW/nVsmZdIWsAqoOQ== + +object-keys@^1.0.11, object-keys@^1.0.12, object-keys@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" + integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== + +object-visit@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/object-visit/-/object-visit-1.0.1.tgz#f79c4493af0c5377b59fe39d395e41042dd045bb" + integrity sha1-95xEk68MU3e1n+OdOV5BBC3QRbs= + dependencies: + isobject "^3.0.0" + +object.assign@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.0.tgz#968bf1100d7956bb3ca086f006f846b3bc4008da" + integrity sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w== + dependencies: + define-properties "^1.1.2" + function-bind "^1.1.1" + has-symbols "^1.0.0" + object-keys "^1.0.11" + object.omit@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/object.omit/-/object.omit-2.0.1.tgz#1a9c744829f39dbb858c76ca3579ae2a54ebd1fa" @@ -5113,6 +6424,18 @@ object.omit@^2.0.0: for-own "^0.1.4" is-extendable "^0.1.1" +object.pick@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/object.pick/-/object.pick-1.3.0.tgz#87a10ac4c1694bd2e1cbf53591a66141fb5dd747" + integrity sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c= + dependencies: + isobject "^3.0.1" + +obuf@^1.0.0, obuf@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/obuf/-/obuf-1.1.2.tgz#09bea3343d41859ebd446292d11c9d4db619084e" + integrity sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg== + on-finished@^2.3.0, on-finished@~2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" @@ -5123,7 +6446,7 @@ on-headers@~1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.0.1.tgz#928f5d0f470d49342651ea6794b0857c100693f7" -once@^1.3.0, once@^1.3.3, once@^1.4.0: +once@^1.3.0, once@^1.3.1, once@^1.3.3, once@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" dependencies: @@ -5139,17 +6462,20 @@ onetime@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/onetime/-/onetime-1.1.0.tgz#a1f7838f8314c516f05ecefcbc4ccfe04b4ed789" -open@0.0.5: - version "0.0.5" - resolved "https://registry.yarnpkg.com/open/-/open-0.0.5.tgz#42c3e18ec95466b6bf0dc42f3a2945c3f0cad8fc" - openid@^2.0.1: version "2.0.6" resolved "https://registry.yarnpkg.com/openid/-/openid-2.0.6.tgz#707375e59ab9f73025899727679b20328171c9aa" dependencies: request "^2.61.0" -optimist@^0.6.1, optimist@~0.6.0, optimist@~0.6.1: +opn@^5.1.0: + version "5.5.0" + resolved "https://registry.yarnpkg.com/opn/-/opn-5.5.0.tgz#fc7164fab56d235904c51c3b27da6758ca3b9bfc" + integrity sha512-PqHpggC9bLV0VeWcdKhkpxY+3JTzetLSqTCWL/z/tFIbI6G8JCjondXklT1JinczLz2Xib62sSp0T/gKT4KksA== + dependencies: + is-wsl "^1.1.0" + +optimist@^0.6.1, optimist@~0.6.0: version "0.6.1" resolved "https://registry.yarnpkg.com/optimist/-/optimist-0.6.1.tgz#da3ea74686fa21a19a111c326e90eb15a0196686" dependencies: @@ -5193,11 +6519,12 @@ ordered-read-streams@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/ordered-read-streams/-/ordered-read-streams-0.1.0.tgz#fd565a9af8eb4473ba69b6ed8a34352cb552f126" -original@>=0.0.5: - version "1.0.0" - resolved "https://registry.yarnpkg.com/original/-/original-1.0.0.tgz#9147f93fa1696d04be61e01bd50baeaca656bd3b" +original@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/original/-/original-1.0.2.tgz#e442a61cffe1c5fd20a65f3261c26663b303f25f" + integrity sha512-hyBVl6iqqUOJ8FqRe+l/gS8H+kKYjrEndd5Pm1MfBtsEKA038HkkdbAl/72EAXGyonD/PFsvmVG+EvcIpliMBg== dependencies: - url-parse "1.0.x" + url-parse "^1.4.3" os-browserify@^0.2.0: version "0.2.1" @@ -5221,6 +6548,15 @@ os-locale@^2.0.0: lcid "^1.0.0" mem "^1.1.0" +os-locale@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/os-locale/-/os-locale-3.1.0.tgz#a802a6ee17f24c10483ab9935719cef4ed16bf1a" + integrity sha512-Z8l3R4wYWM40/52Z+S265okfFj8Kt2cC2MKY+xNi3kFs+XGI7WXu/I309QQQYbRW4ijiZ+yxs9pqEhJh0DqW3Q== + dependencies: + execa "^1.0.0" + lcid "^2.0.0" + mem "^4.0.0" + os-tmpdir@^1.0.0, os-tmpdir@~1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" @@ -5232,24 +6568,63 @@ osenv@0, osenv@^0.1.4: os-homedir "^1.0.0" os-tmpdir "^1.0.0" +p-defer@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/p-defer/-/p-defer-1.0.0.tgz#9f6eb182f6c9aa8cd743004a7d4f96b196b0fb0c" + integrity sha1-n26xgvbJqozXQwBKfU+WsZaw+ww= + p-finally@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae" +p-is-promise@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/p-is-promise/-/p-is-promise-2.1.0.tgz#918cebaea248a62cf7ffab8e3bca8c5f882fc42e" + integrity sha512-Y3W0wlRPK8ZMRbNq97l4M5otioeA5lm1z7bkNkxCka8HSPjR0xRWmpCmc9utiaLP9Jb1eD8BgeIxTW4AIF45Pg== + p-limit@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-1.1.0.tgz#b07ff2d9a5d88bec806035895a2bab66a27988bc" +p-limit@^2.0.0: + version "2.2.2" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.2.2.tgz#61279b67721f5287aa1c13a9a7fbbc48c9291b1e" + integrity sha512-WGR+xHecKTr7EbUEhyLSh5Dube9JtdiG78ufaeLxTgpudf/20KqyMioIUZJAezlTIi6evxuoUs9YXc11cU+yzQ== + dependencies: + p-try "^2.0.0" + p-locate@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-2.0.0.tgz#20a0103b222a70c8fd39cc2e580680f3dde5ec43" dependencies: p-limit "^1.1.0" +p-locate@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-3.0.0.tgz#322d69a05c0264b25997d9f40cd8a891ab0064a4" + integrity sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ== + dependencies: + p-limit "^2.0.0" + +p-map@^1.1.1: + version "1.2.0" + resolved "https://registry.yarnpkg.com/p-map/-/p-map-1.2.0.tgz#e4e94f311eabbc8633a1e79908165fca26241b6b" + integrity sha512-r6zKACMNhjPJMTl8KcFH4li//gkrXWfbD6feV8l6doRHlzljFWGJ2AP6iKaCJXyZmAUMOPtvbW7EXkbWO/pLEA== + +p-try@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" + integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== + pako@~0.2.0: version "0.2.9" resolved "https://registry.yarnpkg.com/pako/-/pako-0.2.9.tgz#f3f7522f4ef782348da8161bad9ecfd51bf83a75" +pako@~1.0.2: + version "1.0.10" + resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.10.tgz#4328badb5086a426aa90f541977d4955da5c9732" + integrity sha512-0DTvPVU3ed8+HNXOu5Bs+o//Mbdj9VNQMUOe9oKCwh8l0GNwpTDMKCWbRjgtD291AWnkAgkqA/LOnQS8AmS1tw== + param-case@2.1.x: version "2.1.1" resolved "https://registry.yarnpkg.com/param-case/-/param-case-2.1.1.tgz#df94fd8cf6531ecf75e6bef9a0858fbc72be2247" @@ -5338,6 +6713,11 @@ parseurl@~1.3.3: resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== +pascalcase@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/pascalcase/-/pascalcase-0.1.1.tgz#b363e55e8006ca6fe21784d2db22bd15d7917f14" + integrity sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ= + passport-oauth2@^1.1.2: version "1.4.0" resolved "https://registry.yarnpkg.com/passport-oauth2/-/passport-oauth2-1.4.0.tgz#f62f81583cbe12609be7ce6f160b9395a27b86ad" @@ -5374,6 +6754,11 @@ path-browserify@0.0.0: version "0.0.0" resolved "https://registry.yarnpkg.com/path-browserify/-/path-browserify-0.0.0.tgz#a0b870729aae214005b7d5032ec2cbbb0fb4451a" +path-dirname@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/path-dirname/-/path-dirname-1.0.2.tgz#cc33d24d525e099a5388c0336c6e32b9160609e0" + integrity sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA= + path-exists@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-2.1.0.tgz#0feb6c64f0fc518d9a754dd5efb62c7022761f4b" @@ -5392,7 +6777,7 @@ path-is-inside@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/path-is-inside/-/path-is-inside-1.0.2.tgz#365417dede44430d1c11af61027facf074bdfc53" -path-key@^2.0.0: +path-key@^2.0.0, path-key@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40" @@ -5454,6 +6839,14 @@ path-type@^2.0.0: dependencies: pify "^2.0.0" +path@^0.12.7: + version "0.12.7" + resolved "https://registry.yarnpkg.com/path/-/path-0.12.7.tgz#d4dc2a506c4ce2197eb481ebfcd5b36c0140b10f" + integrity sha1-1NwqUGxM4hl+tIHr/NWzbAFAsQ8= + dependencies: + process "^0.11.1" + util "^0.10.3" + pause-stream@0.0.11: version "0.0.11" resolved "https://registry.yarnpkg.com/pause-stream/-/pause-stream-0.0.11.tgz#fe5a34b0cbce12b5aa6a2b403ee2e73b602f1445" @@ -5486,6 +6879,11 @@ performance-now@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-0.2.0.tgz#33ef30c5c77d4ea21c5a53869d91b56d8f2555e5" +performance-now@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" + integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns= + phantomjs-prebuilt@^2.1.11, phantomjs-prebuilt@^2.1.7: version "2.1.14" resolved "https://registry.yarnpkg.com/phantomjs-prebuilt/-/phantomjs-prebuilt-2.1.14.tgz#d53d311fcfb7d1d08ddb24014558f1188c516da0" @@ -5504,6 +6902,11 @@ pify@^2.0.0: version "2.3.0" resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" +pify@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/pify/-/pify-3.0.0.tgz#e5a4acd2c101fdf3d9a4d07f0dbc4db49dd28176" + integrity sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY= + pinkie-promise@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/pinkie-promise/-/pinkie-promise-2.0.1.tgz#2135d6dfa7a358c069ac9b178776288228450ffa" @@ -5518,6 +6921,13 @@ pipeworks@^1.3.0: version "1.3.1" resolved "https://registry.yarnpkg.com/pipeworks/-/pipeworks-1.3.1.tgz#f8436f8565ed1d97bf3a80632a5397bfd353385f" +pkg-dir@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-3.0.0.tgz#2749020f239ed990881b1f71210d51eb6523bea3" + integrity sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw== + dependencies: + find-up "^3.0.0" + pkginfo@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/pkginfo/-/pkginfo-0.4.0.tgz#349dbb7ffd38081fcadc0853df687f0c7744cd65" @@ -5537,6 +6947,20 @@ pofile@~1.0.0: version "1.0.8" resolved "https://registry.yarnpkg.com/pofile/-/pofile-1.0.8.tgz#09246a1788035404fc4d1ee087fa5e9ea686567d" +portfinder@^1.0.9: + version "1.0.25" + resolved "https://registry.yarnpkg.com/portfinder/-/portfinder-1.0.25.tgz#254fd337ffba869f4b9d37edc298059cb4d35eca" + integrity sha512-6ElJnHBbxVA1XSLgBp7G1FiCkQdlqGzuF7DswL5tcea+E8UpuvPU7beVAjjRwCioTS9ZluNbu+ZyRvgTsmqEBg== + dependencies: + async "^2.6.2" + debug "^3.1.1" + mkdirp "^0.5.1" + +posix-character-classes@^0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab" + integrity sha1-AerA/jta9xoqbAL+q7jB/vfgDqs= + postcss-calc@^5.2.0: version "5.3.1" resolved "https://registry.yarnpkg.com/postcss-calc/-/postcss-calc-5.3.1.tgz#77bae7ca928ad85716e2fda42f261bf7c1d65b5e" @@ -5816,7 +7240,7 @@ process-nextick-args@~2.0.0: resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== -process@^0.11.0, process@~0.11.0: +process@^0.11.0, process@^0.11.1, process@~0.11.0: version "0.11.10" resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182" @@ -5855,14 +7279,19 @@ protobufjs@^3.8.2: ascli "~0.3" bytebuffer "~3 >=3.5" -protractor-html-screenshot-reporter@0.0.21: - version "0.0.21" - resolved "https://registry.yarnpkg.com/protractor-html-screenshot-reporter/-/protractor-html-screenshot-reporter-0.0.21.tgz#0744988b5720ae67ad2b7653eeb4a669e4710833" +protractor-html-reporter-2@1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/protractor-html-reporter-2/-/protractor-html-reporter-2-1.0.4.tgz#ccd8123ae294f243b590f633a8d2859a4c623a8c" + integrity sha512-IlUcRac05bPUWscsWkEYNGNnly35LhNu4rH5/umdrRFiqOkgKdofPjw6sc1cxswTOERErWxQm0tFhl2CBkV1Kw== dependencies: - mkdirp "~0.3.5" - underscore "~1.6.0" + fs "0.0.1-security" + fs-extra "^1.0.0" + jasmine-reporters "^2.3.0" + lodash "^4.17.5" + path "^0.12.7" + xmldoc "^0.5.1" -"protractor@>=4 <5", protractor@^4.0.3: +"protractor@>=4 <5": version "4.0.14" resolved "https://registry.yarnpkg.com/protractor/-/protractor-4.0.14.tgz#efc4a877fac3a182a9dded26cd5869f4762fd172" dependencies: @@ -5882,13 +7311,42 @@ protractor-html-screenshot-reporter@0.0.21: source-map-support "~0.4.0" webdriver-manager "^10.3.0" -proxy-addr@^1.0.10, proxy-addr@~1.1.4: +protractor@^5.4.1: + version "5.4.2" + resolved "https://registry.yarnpkg.com/protractor/-/protractor-5.4.2.tgz#329efe37f48b2141ab9467799be2d4d12eb48c13" + integrity sha512-zlIj64Cr6IOWP7RwxVeD8O4UskLYPoyIcg0HboWJL9T79F1F0VWtKkGTr/9GN6BKL+/Q/GmM7C9kFVCfDbP5sA== + dependencies: + "@types/q" "^0.0.32" + "@types/selenium-webdriver" "^3.0.0" + blocking-proxy "^1.0.0" + browserstack "^1.5.1" + chalk "^1.1.3" + glob "^7.0.3" + jasmine "2.8.0" + jasminewd2 "^2.1.0" + optimist "~0.6.0" + q "1.4.1" + saucelabs "^1.5.0" + selenium-webdriver "3.6.0" + source-map-support "~0.4.0" + webdriver-js-extender "2.1.0" + webdriver-manager "^12.0.6" + +proxy-addr@^1.0.10: version "1.1.4" resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-1.1.4.tgz#27e545f6960a44a627d9b44467e35c1b6b4ce2f3" dependencies: forwarded "~0.1.0" ipaddr.js "1.3.0" +proxy-addr@~2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.5.tgz#34cbd64a2d81f4b1fd21e76f9f06c8a45299ee34" + integrity sha512-t/7RxHXPH6cJtP0pRG6smSr9QJidhB+3kXu0KgXnbGYMgzEnUxRQ4/LDdfOwZEMyIh3/xHb8PX3t+lfL9z+YVQ== + dependencies: + forwarded "~0.1.2" + ipaddr.js "1.9.0" + prr@~0.0.0: version "0.0.0" resolved "https://registry.yarnpkg.com/prr/-/prr-0.0.0.tgz#1a84b85908325501411853d0081ee3fa86e2926a" @@ -5897,6 +7355,11 @@ pseudomap@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3" +psl@^1.1.24: + version "1.7.0" + resolved "https://registry.yarnpkg.com/psl/-/psl-1.7.0.tgz#f1c4c47a8ef97167dea5d6bbf4816d736e884a3c" + integrity sha512-5NsSEDv8zY70ScRnOTn7bK7eanl2MvFrOrS/R6x+dBt5g1ghnj9Zv90kO8GwT8gxcu2ANyFprnFYB85IogIJOQ== + public-encrypt@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/public-encrypt/-/public-encrypt-4.0.0.tgz#39f699f3a46560dd5ebacbca693caf7c65c18cc6" @@ -6000,6 +7463,14 @@ pug-walk@^1.1.3: pug-runtime "^2.0.3" pug-strip-comments "^1.0.2" +pump@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64" + integrity sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww== + dependencies: + end-of-stream "^1.1.0" + once "^1.3.1" + punycode@1.3.2: version "1.3.2" resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.3.2.tgz#9653a036fb7c1ee42342f2325cceefea3926c48d" @@ -6033,6 +7504,11 @@ qs@6.5.1: resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.1.tgz#349cdf6eef89ec45c12d7d5eb3fc0c870343a6d8" integrity sha512-eRzhrN1WSINYCDCbrz796z37LOe3m5tmW7RQf6oBntukAG1nmovJvhnwHHRMAfeoItc1m2Hk02WER2aQ/iqs+A== +qs@6.7.0: + version "6.7.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.7.0.tgz#41dc1a015e3d581f1621776be31afb2876a9b1bc" + integrity sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ== + qs@^6.0.3, qs@^6.5.1: version "6.8.0" resolved "https://registry.yarnpkg.com/qs/-/qs-6.8.0.tgz#87b763f0d37ca54200334cd57bb2ef8f68a1d081" @@ -6042,6 +7518,11 @@ qs@~6.3.0: version "6.3.2" resolved "https://registry.yarnpkg.com/qs/-/qs-6.3.2.tgz#e75bd5f6e268122a2a0e0bda630b2550c166502c" +qs@~6.5.2: + version "6.5.2" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36" + integrity sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA== + query-string@^4.1.0: version "4.3.4" resolved "https://registry.yarnpkg.com/query-string/-/query-string-4.3.4.tgz#bbb693b9ca915c232515b228b1a02b609043dbeb" @@ -6057,13 +7538,10 @@ querystring@0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.0.tgz#b209849203bb25df820da756e747005878521620" -querystringify@0.0.x: - version "0.0.4" - resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-0.0.4.tgz#0cf7f84f9463ff0ae51c4c4b142d95be37724d9c" - -querystringify@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-1.0.0.tgz#6286242112c5b712fa654e526652bf6a13ff05cb" +querystringify@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.1.1.tgz#60e5a5fd64a7f8bfa4d2ab2ed6fdf4c85bad154e" + integrity sha512-w7fLxIRCRT7U8Qu53jQnJyPkYZIaR4n5151KMfcJlO/A9397Wxb1amJvROTK6TOnp7PfoAmg/qXiNHI+08jRfA== randexp@~0.4.2: version "0.4.5" @@ -6108,6 +7586,16 @@ raw-body@2.3.2: iconv-lite "0.4.19" unpipe "1.0.0" +raw-body@2.4.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.4.0.tgz#a1ce6fb9c9bc356ca52e89256ab59059e13d0332" + integrity sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q== + dependencies: + bytes "3.1.0" + http-errors "1.7.2" + iconv-lite "0.4.24" + unpipe "1.0.0" + raw-body@~2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.2.0.tgz#994976cf6a5096a41162840492f0bdc5d6e7fb96" @@ -6129,6 +7617,16 @@ rc@^1.1.7: minimist "^1.2.0" strip-json-comments "~2.0.1" +rc@^1.2.7: + version "1.2.8" + resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed" + integrity sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw== + dependencies: + deep-extend "^0.6.0" + ini "~1.3.0" + minimist "^1.2.0" + strip-json-comments "~2.0.1" + read-pkg-up@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-1.0.1.tgz#9d63c13276c065918d57f002a57f40a1b643fb02" @@ -6202,6 +7700,15 @@ readable-stream@^2.3.5: string_decoder "~1.1.1" util-deprecate "~1.0.1" +readable-stream@^3.0.6: + version "3.5.0" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.5.0.tgz#465d70e6d1087f6162d079cd0b5db7fbebfd1606" + integrity sha512-gSz026xs2LfxBPudDuI41V1lka8cxg64E66SGe78zJlsUofOg/yqwezdIcdfwik6B4h8LFmWPA9ef9X3FiNFLA== + dependencies: + inherits "^2.0.3" + string_decoder "^1.1.1" + util-deprecate "^1.0.1" + readable-stream@~2.0.0: version "2.0.6" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.0.6.tgz#8f90341e68a53ccc928788dacfcd11b36eb9b78e" @@ -6213,6 +7720,19 @@ readable-stream@~2.0.0: string_decoder "~0.10.x" util-deprecate "~1.0.1" +readable-stream@~2.3.6: + version "2.3.7" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57" + integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw== + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.3" + isarray "~1.0.0" + process-nextick-args "~2.0.0" + safe-buffer "~5.1.1" + string_decoder "~1.1.1" + util-deprecate "~1.0.1" + readdirp@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-2.1.0.tgz#4ed0ad060df3073300c48440373f72d1cc642d78" @@ -6222,6 +7742,15 @@ readdirp@^2.0.0: readable-stream "^2.0.2" set-immediate-shim "^1.0.1" +readdirp@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-2.2.1.tgz#0e87622a3325aa33e892285caf8b4e846529a525" + integrity sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ== + dependencies: + graceful-fs "^4.1.11" + micromatch "^3.1.10" + readable-stream "^2.0.2" + readline2@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/readline2/-/readline2-1.0.1.tgz#41059608ffc154757b715d9989d199ffbf372e35" @@ -6302,6 +7831,22 @@ regex-cache@^0.4.2: is-equal-shallow "^0.1.3" is-primitive "^2.0.0" +regex-not@^1.0.0, regex-not@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/regex-not/-/regex-not-1.0.2.tgz#1f4ece27e00b0b65e0247a6810e6a85d83a5752c" + integrity sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A== + dependencies: + extend-shallow "^3.0.2" + safe-regex "^1.1.0" + +regexp.prototype.flags@^1.2.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.3.0.tgz#7aba89b3c13a64509dabcf3ca8d9fbb9bdf5cb75" + integrity sha512-2+Q0C5g951OlYlJz6yu5/M33IcsESLlLfsyIaLJaG4FA2r4yP8MvVMJUUP/fVBkSpbbbZlS5gynbEWLipiiXiQ== + dependencies: + define-properties "^1.1.3" + es-abstract "^1.17.0-next.1" + regexpu-core@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-1.0.0.tgz#86a763f58ee4d7c2f6b102e4764050de7ed90c6b" @@ -6336,7 +7881,7 @@ repeat-string@^0.2.2: version "0.2.2" resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-0.2.2.tgz#c7a8d3236068362059a7e4651fc6884e8b1fb4ae" -repeat-string@^1.5.2: +repeat-string@^1.5.2, repeat-string@^1.6.1: version "1.6.1" resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637" @@ -6383,6 +7928,32 @@ request@2, request@^2.55.0, request@^2.61.0, request@^2.64.0, request@^2.67.0, r tunnel-agent "^0.6.0" uuid "^3.0.0" +request@^2.87.0: + version "2.88.0" + resolved "https://registry.yarnpkg.com/request/-/request-2.88.0.tgz#9c2fca4f7d35b592efe57c7f0a55e81052124fef" + integrity sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg== + dependencies: + aws-sign2 "~0.7.0" + aws4 "^1.8.0" + caseless "~0.12.0" + combined-stream "~1.0.6" + extend "~3.0.2" + forever-agent "~0.6.1" + form-data "~2.3.2" + har-validator "~5.1.0" + http-signature "~1.2.0" + is-typedarray "~1.0.0" + isstream "~0.1.2" + json-stringify-safe "~5.0.1" + mime-types "~2.1.19" + oauth-sign "~0.9.0" + performance-now "^2.1.0" + qs "~6.5.2" + safe-buffer "^5.1.2" + tough-cookie "~2.4.3" + tunnel-agent "^0.6.0" + uuid "^3.3.2" + request@~2.79.0: version "2.79.0" resolved "https://registry.yarnpkg.com/request/-/request-2.79.0.tgz#4dfe5bf6be8b8cdc37fcf93e04b65577722710de" @@ -6423,10 +7994,17 @@ require-uncached@^1.0.2: caller-path "^0.1.0" resolve-from "^1.0.0" -requires-port@1.0.x, requires-port@1.x.x: +requires-port@1.x.x, requires-port@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" +resolve-cwd@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-2.0.0.tgz#00a9f7387556e27038eae232caa372a6a59b665a" + integrity sha1-AKn3OHVW4nA46uIyyqNypqWbZlo= + dependencies: + resolve-from "^3.0.0" + resolve-dir@^0.1.0: version "0.1.1" resolved "https://registry.yarnpkg.com/resolve-dir/-/resolve-dir-0.1.1.tgz#b219259a5602fac5c5c496ad894a6e8cc430261e" @@ -6438,6 +8016,16 @@ resolve-from@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-1.0.1.tgz#26cbfe935d1aeeeabb29bc3fe5aeb01e93d44226" +resolve-from@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-3.0.0.tgz#b22c7af7d9d6881bc8b6e653335eebcb0a188748" + integrity sha1-six699nWiBvItuZTM17rywoYh0g= + +resolve-url@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a" + integrity sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo= + resolve@^1.1.6, resolve@^1.1.7: version "1.3.3" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.3.3.tgz#655907c3469a8680dc2de3a275a8fdd69691f0e5" @@ -6481,6 +8069,13 @@ rimraf@^2.2.8, rimraf@~2.2.6: version "2.2.8" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.2.8.tgz#e439be2aaee327321952730f99a8929e4fc50582" +rimraf@^2.5.4: + version "2.7.1" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec" + integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w== + dependencies: + glob "^7.1.3" + rimraf@~2.4.0: version "2.4.5" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.4.5.tgz#ee710ce5d93a8fdb856fb5ea8ff0e2d75934b2da" @@ -6524,23 +8119,36 @@ rx-lite@^3.1.2: version "3.1.2" resolved "https://registry.yarnpkg.com/rx-lite/-/rx-lite-3.1.2.tgz#19ce502ca572665f3b647b10939f97fd1615f102" -safe-buffer@5.0.1, safe-buffer@^5.0.1, safe-buffer@~5.0.1: +safe-buffer@5.0.1, safe-buffer@~5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.0.1.tgz#d263ca54696cd8a306b5ca6551e92de57918fbe7" -safe-buffer@^5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.0.tgz#fe4c8460397f9eaaaa58e73be46273408a45e223" - -safe-buffer@~5.1.0, safe-buffer@~5.1.1: +safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: version "5.1.2" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== +safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.2, safe-buffer@~5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.0.tgz#b74daec49b1148f88c64b68d49b1e815c1f2f519" + integrity sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg== + safe-json-stringify@~1: version "1.0.4" resolved "https://registry.yarnpkg.com/safe-json-stringify/-/safe-json-stringify-1.0.4.tgz#81a098f447e4bbc3ff3312a243521bc060ef5911" +safe-regex@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/safe-regex/-/safe-regex-1.1.0.tgz#40a3669f3b077d1e943d44629e157dd48023bf2e" + integrity sha1-QKNmnzsHfR6UPURinhV91IAjvy4= + dependencies: + ret "~0.1.10" + +"safer-buffer@>= 2.1.2 < 3": + version "2.1.2" + resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" + integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== + samsam@1.1.2, samsam@~1.1: version "1.1.2" resolved "https://registry.yarnpkg.com/samsam/-/samsam-1.1.2.tgz#bec11fdc83a9fda063401210e40176c3024d1567" @@ -6562,6 +8170,13 @@ sass-loader@^4.0.0: loader-utils "^0.2.15" object-assign "^4.1.0" +saucelabs@^1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/saucelabs/-/saucelabs-1.5.0.tgz#9405a73c360d449b232839919a86c396d379fd9d" + integrity sha512-jlX3FGdWvYf4Q3LFfFWS1QvPg3IGCGWxIc8QBFdPTbpTJnt/v17FHXYVAn7C8sHf1yUXo2c7yIM0isDryfYtHQ== + dependencies: + https-proxy-agent "^2.2.1" + saucelabs@~1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/saucelabs/-/saucelabs-1.3.0.tgz#d240e8009df7fa87306ec4578a69ba3b5c424fee" @@ -6576,6 +8191,25 @@ sax@1.2.1, sax@>=0.6.0, sax@~1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.1.tgz#7b8e656190b228e81a66aea748480d828cd2d37a" +sax@^1.2.4: + version "1.2.4" + resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" + integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw== + +sax@~1.1.1: + version "1.1.6" + resolved "https://registry.yarnpkg.com/sax/-/sax-1.1.6.tgz#5d616be8a5e607d54e114afae55b7eaf2fcc3240" + integrity sha1-XWFr6KXmB9VOEUr65Vt+ry/MMkA= + +schema-utils@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-1.0.0.tgz#0b79a93204d7b600d4b2850d1f66c2a34951c770" + integrity sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g== + dependencies: + ajv "^6.1.0" + ajv-errors "^1.0.0" + ajv-keywords "^3.1.0" + script-loader@^0.7.0: version "0.7.0" resolved "https://registry.yarnpkg.com/script-loader/-/script-loader-0.7.0.tgz#685dc7e7069e0dee7a92674f0ebc5b0f55baa5ec" @@ -6589,6 +8223,11 @@ scss-tokenizer@^0.2.3: js-base64 "^2.1.8" source-map "^0.4.2" +select-hose@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/select-hose/-/select-hose-2.0.0.tgz#625d8658f865af43ec962bfc376a37359a4994ca" + integrity sha1-Yl2GWPhlr0Psliv8N2o3NZpJlMo= + selenium-webdriver@2.53.3: version "2.53.3" resolved "https://registry.yarnpkg.com/selenium-webdriver/-/selenium-webdriver-2.53.3.tgz#d29ff5a957dff1a1b49dc457756e4e4bfbdce085" @@ -6599,9 +8238,27 @@ selenium-webdriver@2.53.3: ws "^1.0.1" xml2js "0.4.4" -"semver@2 || 3 || 4 || 5", semver@^5.0.1, semver@^5.3.0, semver@~5.3.0: - version "5.3.0" - resolved "https://registry.yarnpkg.com/semver/-/semver-5.3.0.tgz#9b2ce5d3de02d17c6012ad326aa6b4d0cf54f94f" +selenium-webdriver@3.6.0, selenium-webdriver@^3.0.1: + version "3.6.0" + resolved "https://registry.yarnpkg.com/selenium-webdriver/-/selenium-webdriver-3.6.0.tgz#2ba87a1662c020b8988c981ae62cb2a01298eafc" + integrity sha512-WH7Aldse+2P5bbFBO4Gle/nuQOdVwpHMTL6raL3uuBj/vPG07k6uzt3aiahu352ONBr5xXh0hDlM3LhtXPOC4Q== + dependencies: + jszip "^3.1.3" + rimraf "^2.5.4" + tmp "0.0.30" + xml2js "^0.4.17" + +selfsigned@^1.9.1: + version "1.10.7" + resolved "https://registry.yarnpkg.com/selfsigned/-/selfsigned-1.10.7.tgz#da5819fd049d5574f28e88a9bcc6dbc6e6f3906b" + integrity sha512-8M3wBCzeWIJnQfl43IKwOmC4H/RAp50S8DF60znzjW5GVqTcSe2vWclt7hmYVPkKPlHWOu5EaWOMZ2Y6W8ZXTA== + dependencies: + node-forge "0.9.0" + +"semver@2 || 3 || 4 || 5", semver@^5.0.1, semver@^5.3.0, semver@^5.5.0, semver@^5.6.0: + version "5.7.1" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" + integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== semver@^4.1.0, semver@~4.3.3: version "4.3.6" @@ -6611,6 +8268,10 @@ semver@~5.0.1: version "5.0.3" resolved "https://registry.yarnpkg.com/semver/-/semver-5.0.3.tgz#77466de589cd5d3c95f138aa78bc569a3cb5d27a" +semver@~5.3.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.3.0.tgz#9b2ce5d3de02d17c6012ad326aa6b4d0cf54f94f" + send@0.15.3: version "0.15.3" resolved "https://registry.yarnpkg.com/send/-/send-0.15.3.tgz#5013f9f99023df50d1bd9892c19e3defd1d53309" @@ -6674,16 +8335,7 @@ serve-index@^1.7.2: mime-types "~2.1.15" parseurl "~1.3.1" -serve-static@1.12.3, serve-static@^1.9.2: - version "1.12.3" - resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.12.3.tgz#9f4ba19e2f3030c547f8af99107838ec38d5b1e2" - dependencies: - encodeurl "~1.0.1" - escape-html "~1.0.3" - parseurl "~1.3.1" - send "0.15.3" - -serve-static@^1.10.0: +serve-static@1.14.1, serve-static@^1.10.0: version "1.14.1" resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.14.1.tgz#666e636dc4f010f7ef29970a88a674320898b2f9" integrity sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg== @@ -6693,14 +8345,33 @@ serve-static@^1.10.0: parseurl "~1.3.3" send "0.17.1" +serve-static@^1.9.2: + version "1.12.3" + resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.12.3.tgz#9f4ba19e2f3030c547f8af99107838ec38d5b1e2" + dependencies: + encodeurl "~1.0.1" + escape-html "~1.0.3" + parseurl "~1.3.1" + send "0.15.3" + set-blocking@^2.0.0, set-blocking@~2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" -set-immediate-shim@^1.0.1: +set-immediate-shim@^1.0.1, set-immediate-shim@~1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz#4b2b1b27eb808a9f8dcc481a58e5e56f599f3f61" +set-value@^2.0.0, set-value@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/set-value/-/set-value-2.0.1.tgz#a18d40530e6f07de4228c7defe4227af8cad005b" + integrity sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw== + dependencies: + extend-shallow "^2.0.1" + is-extendable "^0.1.1" + is-plain-object "^2.0.3" + split-string "^3.0.1" + setimmediate@^1.0.4: version "1.0.5" resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285" @@ -6733,6 +8404,18 @@ sha.js@^2.4.0, sha.js@^2.4.8: dependencies: inherits "^2.0.1" +shebang-command@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea" + integrity sha1-RKrGW2lbAzmJaMOfNj/uXer98eo= + dependencies: + shebang-regex "^1.0.0" + +shebang-regex@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3" + integrity sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM= + shelljs@^0.7.5: version "0.7.8" resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.7.8.tgz#decbcf874b0d1e5fb72e14b164a9683048e9acb3" @@ -6783,6 +8466,36 @@ slice-ansi@0.0.4: version "0.0.4" resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-0.0.4.tgz#edbf8903f66f7ce2f8eafd6ceed65e264c831b35" +snapdragon-node@^2.0.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/snapdragon-node/-/snapdragon-node-2.1.1.tgz#6c175f86ff14bdb0724563e8f3c1b021a286853b" + integrity sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw== + dependencies: + define-property "^1.0.0" + isobject "^3.0.0" + snapdragon-util "^3.0.1" + +snapdragon-util@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/snapdragon-util/-/snapdragon-util-3.0.1.tgz#f956479486f2acd79700693f6f7b805e45ab56e2" + integrity sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ== + dependencies: + kind-of "^3.2.0" + +snapdragon@^0.8.1: + version "0.8.2" + resolved "https://registry.yarnpkg.com/snapdragon/-/snapdragon-0.8.2.tgz#64922e7c565b0e14204ba1aa7d6964278d25182d" + integrity sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg== + dependencies: + base "^0.11.1" + debug "^2.2.0" + define-property "^0.2.5" + extend-shallow "^2.0.1" + map-cache "^0.2.2" + source-map "^0.5.6" + source-map-resolve "^0.5.0" + use "^3.1.0" + sntp@1.x.x: version "1.0.9" resolved "https://registry.yarnpkg.com/sntp/-/sntp-1.0.9.tgz#6541184cc90aeea6c6e7b35e2659082443c66198" @@ -6903,23 +8616,25 @@ socket.io@^2.0.3: socket.io-client "~2.0.2" socket.io-parser "~3.1.1" -sockjs-client@^1.0.3: - version "1.1.4" - resolved "https://registry.yarnpkg.com/sockjs-client/-/sockjs-client-1.1.4.tgz#5babe386b775e4cf14e7520911452654016c8b12" +sockjs-client@1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/sockjs-client/-/sockjs-client-1.3.0.tgz#12fc9d6cb663da5739d3dc5fb6e8687da95cb177" + integrity sha512-R9jxEzhnnrdxLCNln0xg5uGHqMnkhPSTzUZH2eXcR03S/On9Yvoq2wyUZILRUhZCNVu2PmwWVoyuiPz8th8zbg== dependencies: - debug "^2.6.6" - eventsource "0.1.6" - faye-websocket "~0.11.0" - inherits "^2.0.1" + debug "^3.2.5" + eventsource "^1.0.7" + faye-websocket "~0.11.1" + inherits "^2.0.3" json3 "^3.3.2" - url-parse "^1.1.8" + url-parse "^1.4.3" -sockjs@^0.3.15: - version "0.3.18" - resolved "https://registry.yarnpkg.com/sockjs/-/sockjs-0.3.18.tgz#d9b289316ca7df77595ef299e075f0f937eb4207" +sockjs@0.3.19: + version "0.3.19" + resolved "https://registry.yarnpkg.com/sockjs/-/sockjs-0.3.19.tgz#d976bbe800af7bd20ae08598d582393508993c0d" + integrity sha512-V48klKZl8T6MzatbLlzzRNhMepEys9Y4oGFpypBFFn1gLI/QQ9HtLLyWJNbPlwGLelOVOEijUbTTJeLLI59jLw== dependencies: faye-websocket "^0.10.0" - uuid "^2.0.2" + uuid "^3.0.1" sort-keys@^1.0.0: version "1.1.2" @@ -6931,12 +8646,28 @@ source-list-map@^0.1.4, source-list-map@~0.1.7: version "0.1.8" resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-0.1.8.tgz#c550b2ab5427f6b3f21f5afead88c4f5587b2106" +source-map-resolve@^0.5.0: + version "0.5.3" + resolved "https://registry.yarnpkg.com/source-map-resolve/-/source-map-resolve-0.5.3.tgz#190866bece7553e1f8f267a2ee82c606b5509a1a" + integrity sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw== + dependencies: + atob "^2.1.2" + decode-uri-component "^0.2.0" + resolve-url "^0.2.1" + source-map-url "^0.4.0" + urix "^0.1.0" + source-map-support@~0.4.0: version "0.4.15" resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.4.15.tgz#03202df65c06d2bd8c7ec2362a193056fef8d3b1" dependencies: source-map "^0.5.6" +source-map-url@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/source-map-url/-/source-map-url-0.4.0.tgz#3e935d7ddd73631b97659956d55128e87b5084a3" + integrity sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM= + source-map@0.1.x, source-map@^0.1.41, source-map@~0.1.7: version "0.1.43" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.1.43.tgz#c24bc146ca517c1471f5dacbe2571b2b7f9e3346" @@ -6976,6 +8707,36 @@ spdx-license-ids@^1.0.2: version "1.2.2" resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-1.2.2.tgz#c9df7a3424594ade6bd11900d596696dc06bac57" +spdy-transport@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/spdy-transport/-/spdy-transport-3.0.0.tgz#00d4863a6400ad75df93361a1608605e5dcdcf31" + integrity sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw== + dependencies: + debug "^4.1.0" + detect-node "^2.0.4" + hpack.js "^2.1.6" + obuf "^1.1.2" + readable-stream "^3.0.6" + wbuf "^1.7.3" + +spdy@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/spdy/-/spdy-4.0.1.tgz#6f12ed1c5db7ea4f24ebb8b89ba58c87c08257f2" + integrity sha512-HeZS3PBdMA+sZSu0qwpCxl3DeALD5ASx8pAX0jZdKXSpPWbQ6SYGnlg3BBmYLx5LtiZrmkAZfErCm2oECBcioA== + dependencies: + debug "^4.1.0" + handle-thing "^2.0.0" + http-deceiver "^1.2.7" + select-hose "^2.0.0" + spdy-transport "^3.0.0" + +split-string@^3.0.1, split-string@^3.0.2: + version "3.1.0" + resolved "https://registry.yarnpkg.com/split-string/-/split-string-3.1.0.tgz#7cb09dda3a86585705c64b39a6466038682e8fe2" + integrity sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw== + dependencies: + extend-shallow "^3.0.0" + split@0.3, split@~0.3.3: version "0.3.3" resolved "https://registry.yarnpkg.com/split/-/split-0.3.3.tgz#cd0eea5e63a211dfff7eb0f091c4133e2d0dd28f" @@ -7014,6 +8775,14 @@ stack-trace@0.0.x: version "0.0.10" resolved "https://registry.yarnpkg.com/stack-trace/-/stack-trace-0.0.10.tgz#547c70b347e8d32b4e108ea1a2a159e5fdde19c0" +static-extend@^0.1.1: + version "0.1.2" + resolved "https://registry.yarnpkg.com/static-extend/-/static-extend-0.1.2.tgz#60809c39cbff55337226fd5e0b520f341f1fb5c6" + integrity sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY= + dependencies: + define-property "^0.2.5" + object-copy "^0.1.0" + "statuses@>= 1.3.1 < 2", statuses@~1.3.1: version "1.3.1" resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.3.1.tgz#faf51b9eb74aaef3b3acf4ad5f61abf24cb7b93e" @@ -7053,10 +8822,6 @@ stream-browserify@^2.0.1: inherits "~2.0.1" readable-stream "^2.0.2" -stream-cache@~0.0.1: - version "0.0.2" - resolved "https://registry.yarnpkg.com/stream-cache/-/stream-cache-0.0.2.tgz#1ac5ad6832428ca55667dbdee395dad4e6db118f" - stream-combiner@~0.0.4: version "0.0.4" resolved "https://registry.yarnpkg.com/stream-combiner/-/stream-combiner-0.0.4.tgz#4d5e433c185261dde623ca3f44c586bcf5c4ad14" @@ -7100,10 +8865,41 @@ string-width@^2.0.0: is-fullwidth-code-point "^2.0.0" strip-ansi "^3.0.0" +string-width@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e" + integrity sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw== + dependencies: + is-fullwidth-code-point "^2.0.0" + strip-ansi "^4.0.0" + +string.prototype.trimleft@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/string.prototype.trimleft/-/string.prototype.trimleft-2.1.1.tgz#9bdb8ac6abd6d602b17a4ed321870d2f8dcefc74" + integrity sha512-iu2AGd3PuP5Rp7x2kEZCrB2Nf41ehzh+goo8TV7z8/XDBbsvc6HQIlUl9RjkZ4oyrW1XM5UwlGl1oVEaDjg6Ag== + dependencies: + define-properties "^1.1.3" + function-bind "^1.1.1" + +string.prototype.trimright@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/string.prototype.trimright/-/string.prototype.trimright-2.1.1.tgz#440314b15996c866ce8a0341894d45186200c5d9" + integrity sha512-qFvWL3/+QIgZXVmJBfpHmxLB7xsUXz6HsUmP8+5dRaC3Q7oKUv9Vo6aMCRZC1smrtyECFsIT30PqBJ1gTjAs+g== + dependencies: + define-properties "^1.1.3" + function-bind "^1.1.1" + string_decoder@^0.10.25, string_decoder@~0.10.x: version "0.10.31" resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94" +string_decoder@^1.1.1: + version "1.3.0" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" + integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== + dependencies: + safe-buffer "~5.2.0" + string_decoder@~1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.0.2.tgz#b29e1f4e1125fa97a10382b8a533737b7491e179" @@ -7135,6 +8931,13 @@ strip-ansi@^3.0.0, strip-ansi@^3.0.1: dependencies: ansi-regex "^2.0.0" +strip-ansi@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-4.0.0.tgz#a8479022eb1ac368a871389b635262c505ee368f" + integrity sha1-qEeQIusaw2iocTibY1JixQXuNo8= + dependencies: + ansi-regex "^3.0.0" + strip-ansi@~0.1.0: version "0.1.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-0.1.1.tgz#39e8a98d044d150660abe4a6808acf70bb7bc991" @@ -7211,12 +9014,19 @@ supports-color@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7" -supports-color@^3.1.0, supports-color@^3.1.1, supports-color@^3.2.3: +supports-color@^3.1.0, supports-color@^3.2.3: version "3.2.3" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-3.2.3.tgz#65ac0504b3954171d8a64946b2ae3cbb8a5f54f6" dependencies: has-flag "^1.0.0" +supports-color@^5.1.0: + version "5.5.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" + integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== + dependencies: + has-flag "^3.0.0" + svgo@^0.7.0: version "0.7.2" resolved "https://registry.yarnpkg.com/svgo/-/svgo-0.7.2.tgz#9f5772413952135c6fefbf40afe6a4faa88b4bb5" @@ -7355,6 +9165,19 @@ tar@^2.0.0, tar@^2.2.1: fstream "^1.0.2" inherits "2" +tar@^4.4.2: + version "4.4.13" + resolved "https://registry.yarnpkg.com/tar/-/tar-4.4.13.tgz#43b364bc52888d555298637b10d60790254ab525" + integrity sha512-w2VwSrBoHa5BsSyH+KxEqeQBAllHhccyMFVHtGtdMpF4W7IRWfZjFiQceJPChOeTsSDVUpER2T8FA93pr0L+QA== + dependencies: + chownr "^1.1.1" + fs-minipass "^1.2.5" + minipass "^2.8.6" + minizlib "^1.2.1" + mkdirp "^0.5.0" + safe-buffer "^5.1.2" + yallist "^3.0.3" + temp@^0.8.1: version "0.8.3" resolved "https://registry.yarnpkg.com/temp/-/temp-0.8.3.tgz#e0c6bc4d26b903124410e4fed81103014dfc1f59" @@ -7411,6 +9234,11 @@ through@2, through@^2.3.6, through@~2.3, through@~2.3.1, through@~2.3.6, through version "2.3.8" resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" +thunky@^1.0.2: + version "1.1.0" + resolved "https://registry.yarnpkg.com/thunky/-/thunky-1.1.0.tgz#5abaf714a9405db0504732bbccd2cedd9ef9537d" + integrity sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA== + tildify@^1.0.0: version "1.2.0" resolved "https://registry.yarnpkg.com/tildify/-/tildify-1.2.0.tgz#dcec03f55dca9b7aa3e5b04f21817eb56e63588a" @@ -7437,6 +9265,13 @@ tmp@0.0.24: version "0.0.24" resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.24.tgz#d6a5e198d14a9835cc6f2d7c3d9e302428c8cf12" +tmp@0.0.30: + version "0.0.30" + resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.30.tgz#72419d4a8be7d6ce75148fd8b324e593a711c2ed" + integrity sha1-ckGdSovn1s51FI/YsyTlk6cRwu0= + dependencies: + os-tmpdir "~1.0.1" + tmp@0.0.31, tmp@0.0.x: version "0.0.31" resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.31.tgz#8f38ab9438e17315e5dbd8b3657e8bfb277ae4a7" @@ -7451,6 +9286,31 @@ to-arraybuffer@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz#7d229b1fcc637e466ca081180836a7aabff83f43" +to-object-path@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/to-object-path/-/to-object-path-0.3.0.tgz#297588b7b0e7e0ac08e04e672f85c1f4999e17af" + integrity sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68= + dependencies: + kind-of "^3.0.2" + +to-regex-range@^2.1.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-2.1.1.tgz#7c80c17b9dfebe599e27367e0d4dd5590141db38" + integrity sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg= + dependencies: + is-number "^3.0.0" + repeat-string "^1.6.1" + +to-regex@^3.0.1, to-regex@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/to-regex/-/to-regex-3.0.2.tgz#13cfdd9b336552f30b51f33a8ae1b42a7a7599ce" + integrity sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw== + dependencies: + define-property "^2.0.2" + extend-shallow "^3.0.2" + regex-not "^1.0.2" + safe-regex "^1.1.0" + toidentifier@1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553" @@ -7466,6 +9326,14 @@ tough-cookie@~2.3.0: dependencies: punycode "^1.4.1" +tough-cookie@~2.4.3: + version "2.4.3" + resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.4.3.tgz#53f36da3f47783b0925afa06ff9f3b165280f781" + integrity sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ== + dependencies: + psl "^1.1.24" + punycode "^1.4.1" + transformers@2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/transformers/-/transformers-2.1.0.tgz#5d23cb35561dd85dc67fb8482309b47d53cce9a7" @@ -7540,6 +9408,14 @@ type-is@^1.6.4, type-is@^1.6.9, type-is@~1.6.15: media-typer "0.3.0" mime-types "~2.1.15" +type-is@~1.6.17, type-is@~1.6.18: + version "1.6.18" + resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" + integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g== + dependencies: + media-typer "0.3.0" + mime-types "~2.1.24" + typedarray@^0.0.6, typedarray@~0.0.5: version "0.0.6" resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" @@ -7621,6 +9497,16 @@ underscore@~1.6.0: version "1.6.0" resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.6.0.tgz#8b38b10cacdef63337b8b24e4ff86d45aea529a8" +union-value@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/union-value/-/union-value-1.0.1.tgz#0b6fe7b835aecda61c6ea4d4f02c14221e109847" + integrity sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg== + dependencies: + arr-union "^3.1.0" + get-value "^2.0.6" + is-extendable "^0.1.1" + set-value "^2.0.1" + uniq@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/uniq/-/uniq-1.0.1.tgz#b31c5ae8254844a3a8281541ce2b04b865a734ff" @@ -7639,10 +9525,28 @@ unique-stream@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/unique-stream/-/unique-stream-1.0.0.tgz#d59a4a75427447d9aa6c91e70263f8d26a4b104b" +universalify@^0.1.0: + version "0.1.2" + resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66" + integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg== + unpipe@1.0.0, unpipe@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" +unset-value@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/unset-value/-/unset-value-1.0.0.tgz#8376873f7d2335179ffb1e6fc3a8ed0dfc8ab559" + integrity sha1-g3aHP30jNRef+x5vw6jtDfyKtVk= + dependencies: + has-value "^0.3.1" + isobject "^3.0.0" + +upath@^1.1.1: + version "1.2.0" + resolved "https://registry.yarnpkg.com/upath/-/upath-1.2.0.tgz#8f66dbcd55a883acdae4408af8b035a5044c1894" + integrity sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg== + upper-case@^1.1.1: version "1.1.3" resolved "https://registry.yarnpkg.com/upper-case/-/upper-case-1.1.3.tgz#f6b4501c2ec4cdd26ba78be7222961de77621598" @@ -7660,6 +9564,11 @@ uri-js@^4.2.2: dependencies: punycode "^2.1.0" +urix@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/urix/-/urix-0.1.0.tgz#da937f7a62e21fec1fd18d49b35c2935067a6c72" + integrity sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI= + url-join@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/url-join/-/url-join-0.0.1.tgz#1db48ad422d3402469a87f7d97bdebfe4fb1e3c8" @@ -7675,19 +9584,13 @@ url-loader@^0.5.7: loader-utils "^1.0.2" mime "1.3.x" -url-parse@1.0.x: - version "1.0.5" - resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.0.5.tgz#0854860422afdcfefeb6c965c662d4800169927b" +url-parse@^1.4.3: + version "1.4.7" + resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.4.7.tgz#a8a83535e8c00a316e403a5db4ac1b9b853ae278" + integrity sha512-d3uaVyzDB9tQoSXFvuSUNFibTd9zxd2bkVrDRvF5TmvWWQwqE4lgYJ5m+x1DbecWkw+LK4RNl2CU1hHuOKPVlg== dependencies: - querystringify "0.0.x" - requires-port "1.0.x" - -url-parse@^1.1.8: - version "1.1.9" - resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.1.9.tgz#c67f1d775d51f0a18911dd7b3ffad27bb9e5bd19" - dependencies: - querystringify "~1.0.0" - requires-port "1.0.x" + querystringify "^2.1.1" + requires-port "^1.0.0" url@0.10.3: version "0.10.3" @@ -7703,6 +9606,11 @@ url@^0.11.0: punycode "1.3.2" querystring "0.2.0" +use@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f" + integrity sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ== + user-home@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/user-home/-/user-home-1.1.1.tgz#2b5be23a32b63a7c9deb8d0f28d485724a3df190" @@ -7731,7 +9639,7 @@ utf8@2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/utf8/-/utf8-2.1.0.tgz#0cfec5c8052d44a23e3aaa908104e8075f95dfd5" -util-deprecate@~1.0.1: +util-deprecate@^1.0.1, util-deprecate@~1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" @@ -7745,13 +9653,19 @@ utils-merge@1.0.0, utils-merge@1.x.x: version "1.0.0" resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.0.tgz#0294fb922bb9375153541c4f7096231f287c8af8" +utils-merge@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" + integrity sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM= + uuid@3.0.1, uuid@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.0.1.tgz#6544bba2dfda8c1cf17e629a3a305e2bb1fee6c1" -uuid@^2.0.2: - version "2.0.3" - resolved "https://registry.yarnpkg.com/uuid/-/uuid-2.0.3.tgz#67e2e863797215530dff318e5bf9dcebfd47b21a" +uuid@^3.0.1, uuid@^3.3.2: + version "3.4.0" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" + integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== uws@~0.14.4: version "0.14.5" @@ -7783,10 +9697,15 @@ validator@^6.0.0: version "6.3.0" resolved "https://registry.yarnpkg.com/validator/-/validator-6.3.0.tgz#47ce23ed8d4eaddfa9d4b8ef0071b6cf1078d7c8" -vary@^1, vary@~1.1.0, vary@~1.1.1: +vary@^1, vary@~1.1.0: version "1.1.1" resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.1.tgz#67535ebb694c1d52257457984665323f587e8d37" +vary@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" + integrity sha1-IpnwLG3tMNSllhsLn3RSShj2NPw= + vasync@^1.6.4: version "1.6.4" resolved "https://registry.yarnpkg.com/vasync/-/vasync-1.6.4.tgz#dfe93616ad0e7ae801b332a9d88bfc5cdc8e1d1f" @@ -7863,6 +9782,21 @@ watchpack@^0.2.1: chokidar "^1.0.0" graceful-fs "^4.1.2" +wbuf@^1.1.0, wbuf@^1.7.3: + version "1.7.3" + resolved "https://registry.yarnpkg.com/wbuf/-/wbuf-1.7.3.tgz#c1d8d149316d3ea852848895cb6a0bfe887b87df" + integrity sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA== + dependencies: + minimalistic-assert "^1.0.0" + +webdriver-js-extender@2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/webdriver-js-extender/-/webdriver-js-extender-2.1.0.tgz#57d7a93c00db4cc8d556e4d3db4b5db0a80c3bb7" + integrity sha512-lcUKrjbBfCK6MNsh7xaY2UAUmZwe+/ib03AjVOpFobX4O7+83BUveSrLfU0Qsyb1DaKJdQRbuU+kM9aZ6QUhiQ== + dependencies: + "@types/selenium-webdriver" "^3.0.0" + selenium-webdriver "^3.0.1" + webdriver-manager@^10.3.0: version "10.3.0" resolved "https://registry.yarnpkg.com/webdriver-manager/-/webdriver-manager-10.3.0.tgz#99314588a0b1dbe688c441d74288c6cb1875fa8b" @@ -7878,6 +9812,23 @@ webdriver-manager@^10.3.0: rimraf "^2.5.2" semver "^5.3.0" +webdriver-manager@^12.0.6: + version "12.1.7" + resolved "https://registry.yarnpkg.com/webdriver-manager/-/webdriver-manager-12.1.7.tgz#ed4eaee8f906b33c146e869b55e850553a1b1162" + integrity sha512-XINj6b8CYuUYC93SG3xPkxlyUc3IJbD6Vvo75CVGuG9uzsefDzWQrhz0Lq8vbPxtb4d63CZdYophF8k8Or/YiA== + dependencies: + adm-zip "^0.4.9" + chalk "^1.1.1" + del "^2.2.0" + glob "^7.0.3" + ini "^1.3.4" + minimist "^1.2.0" + q "^1.4.1" + request "^2.87.0" + rimraf "^2.5.2" + semver "^5.3.0" + xml2js "^0.4.17" + webpack-core@~0.6.9: version "0.6.9" resolved "https://registry.yarnpkg.com/webpack-core/-/webpack-core-0.6.9.tgz#fc571588c8558da77be9efb6debdc5a3b172bdc2" @@ -7885,7 +9836,17 @@ webpack-core@~0.6.9: source-list-map "~0.1.7" source-map "~0.4.1" -webpack-dev-middleware@^1.0.11, webpack-dev-middleware@^1.10.2: +webpack-dev-middleware@3.4.0: + version "3.4.0" + resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-3.4.0.tgz#1132fecc9026fd90f0ecedac5cbff75d1fb45890" + integrity sha512-Q9Iyc0X9dP9bAsYskAVJ/hmIZZQwf/3Sy4xCAZgL5cUkjZmUZLt4l5HpbST/Pdgjn3u6pE7u5OdGd1apgzRujA== + dependencies: + memory-fs "~0.4.1" + mime "^2.3.1" + range-parser "^1.0.3" + webpack-log "^2.0.0" + +webpack-dev-middleware@^1.0.11: version "1.10.2" resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-1.10.2.tgz#2e252ce1dfb020dbda1ccb37df26f30ab014dbd1" dependencies: @@ -7894,23 +9855,49 @@ webpack-dev-middleware@^1.0.11, webpack-dev-middleware@^1.10.2: path-is-absolute "^1.0.0" range-parser "^1.0.3" -webpack-dev-server@^1.14.1: - version "1.16.5" - resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-1.16.5.tgz#0cbd5f2d2ac8d4e593aacd5c9702e7bbd5e59892" +webpack-dev-server@^3.1.11: + version "3.1.11" + resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-3.1.11.tgz#3b698b5b32476f1f0d3d4014952fcf42ab118205" + integrity sha512-E/uGbO9ndXrXgNUzw+O2UrrvYY/eIw10fpJnbvJf8VOH/NWZuY3nUG7arbgB/kbkORlF/sPHxnv10tKFtKf3aA== dependencies: + ansi-html "0.0.7" + bonjour "^3.5.0" + chokidar "^2.0.0" compression "^1.5.2" connect-history-api-fallback "^1.3.0" - express "^4.13.3" - http-proxy-middleware "~0.17.1" - open "0.0.5" - optimist "~0.6.1" + debug "^3.1.0" + del "^3.0.0" + express "^4.16.2" + html-entities "^1.2.0" + http-proxy-middleware "~0.18.0" + import-local "^2.0.0" + internal-ip "^3.0.1" + ip "^1.1.5" + killable "^1.0.0" + loglevel "^1.4.1" + opn "^5.1.0" + portfinder "^1.0.9" + schema-utils "^1.0.0" + selfsigned "^1.9.1" + semver "^5.6.0" serve-index "^1.7.2" - sockjs "^0.3.15" - sockjs-client "^1.0.3" - stream-cache "~0.0.1" + sockjs "0.3.19" + sockjs-client "1.3.0" + spdy "^4.0.0" strip-ansi "^3.0.0" - supports-color "^3.1.1" - webpack-dev-middleware "^1.10.2" + supports-color "^5.1.0" + url "^0.11.0" + webpack-dev-middleware "3.4.0" + webpack-log "^2.0.0" + yargs "12.0.2" + +webpack-log@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/webpack-log/-/webpack-log-2.0.0.tgz#5b7928e0637593f119d32f6227c1e0ac31e1b47f" + integrity sha512-cX8G2vR/85UYG59FgkoMamwHUIkSSlV3bBMRsbxVXVUk2j6NleCKjQ/WE9eYg9WY4w25O9w8wKP4rzNZFmUcUg== + dependencies: + ansi-colors "^3.0.0" + uuid "^3.3.2" webpack-sources@^0.1.0: version "0.1.5" @@ -8057,10 +10044,12 @@ ws@^1.0.1: ultron "1.0.x" ws@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/ws/-/ws-3.0.0.tgz#98ddb00056c8390cb751e7788788497f99103b6c" + version "3.3.1" + resolved "https://registry.yarnpkg.com/ws/-/ws-3.3.1.tgz#d97e34dee06a1190c61ac1e95f43cb60b78cf939" + integrity sha512-8A/uRMnQy8KCQsmep1m7Bk+z/+LIkeF7w+TDMLtX1iZm5Hq9HsUDmgFGaW1ACW5Cj0b2Qo7wCvRhYN2ErUVp/A== dependencies: - safe-buffer "~5.0.1" + async-limiter "~1.0.0" + safe-buffer "~5.1.0" ultron "~1.1.0" ws@~2.3.1: @@ -8109,6 +10098,14 @@ xml2js@0.4.4: sax "0.6.x" xmlbuilder ">=1.0.0" +xml2js@^0.4.17: + version "0.4.23" + resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.4.23.tgz#a0c69516752421eb2ac758ee4d4ccf58843eac66" + integrity sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug== + dependencies: + sax ">=0.6.0" + xmlbuilder "~11.0.0" + xmlbuilder@2.5.x: version "2.5.2" resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-2.5.2.tgz#5ab88fc508ab2ff14873010b56163d3f92b19325" @@ -8125,6 +10122,18 @@ xmlbuilder@8.2.2, xmlbuilder@>=1.0.0: version "8.2.2" resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-8.2.2.tgz#69248673410b4ba42e1a6136551d2922335aa773" +xmlbuilder@~11.0.0: + version "11.0.1" + resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-11.0.1.tgz#be9bae1c8a046e76b31127726347d0ad7002beb3" + integrity sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA== + +xmldoc@^0.5.1: + version "0.5.1" + resolved "https://registry.yarnpkg.com/xmldoc/-/xmldoc-0.5.1.tgz#92e437e900dbff04450efae90d3ca5f16565f738" + integrity sha1-kuQ36QDb/wRFDvrpDTyl8WVl9zg= + dependencies: + sax "~1.1.1" + xmldom@0.1.x, xmldom@^0.1.22, xmldom@~0.1.15: version "0.1.27" resolved "https://registry.yarnpkg.com/xmldom/-/xmldom-0.1.27.tgz#d501f97b3bdb403af8ef9ecc20573187aadac0e9" @@ -8149,6 +10158,11 @@ xpath@0.0.5: version "0.0.5" resolved "https://registry.yarnpkg.com/xpath/-/xpath-0.0.5.tgz#454036f6ef0f3df5af5d4ba4a119fb75674b3e6c" +xregexp@4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/xregexp/-/xregexp-4.0.0.tgz#e698189de49dd2a18cc5687b05e17c8e43943020" + integrity sha512-PHyM+sQouu7xspQQwELlGwwd05mXUFqwFYfqPO0cC7x4fxyHnnuetmQr6CjJiafIDoH4MogHb9dOoJzR/Y4rFg== + "xtend@>=4.0.0 <4.1.0-0", xtend@^4.0.0, xtend@~4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af" @@ -8157,10 +10171,27 @@ y18n@^3.2.0, y18n@^3.2.1: version "3.2.1" resolved "https://registry.yarnpkg.com/y18n/-/y18n-3.2.1.tgz#6d15fba884c08679c0d77e88e7759e811e07fa41" +"y18n@^3.2.1 || ^4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.0.tgz#95ef94f85ecc81d007c264e190a120f0a3c8566b" + integrity sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w== + yallist@^2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52" +yallist@^3.0.0, yallist@^3.0.3: + version "3.1.1" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd" + integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g== + +yargs-parser@^10.1.0: + version "10.1.0" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-10.1.0.tgz#7202265b89f7e9e9f2e5765e0fe735a905edbaa8" + integrity sha512-VCIyR1wJoEBZUqk5PA+oOBF6ypbwh5aNB3I50guxAL/quggdfs4TtNHQrSazFA3fYZ+tEqfs0zIGlv0c/rgjbQ== + dependencies: + camelcase "^4.1.0" + yargs-parser@^4.2.0: version "4.2.1" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-4.2.1.tgz#29cceac0dc4f03c6c87b4a9f217dd18c9f74871c" @@ -8179,6 +10210,24 @@ yargs-parser@^7.0.0: dependencies: camelcase "^4.1.0" +yargs@12.0.2: + version "12.0.2" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-12.0.2.tgz#fe58234369392af33ecbef53819171eff0f5aadc" + integrity sha512-e7SkEx6N6SIZ5c5H22RTZae61qtn3PYUE8JYbBFlK9sYmh3DMQ6E5ygtaG/2BW0JZi4WGgTR2IV5ChqlqrDGVQ== + dependencies: + cliui "^4.0.0" + decamelize "^2.0.0" + find-up "^3.0.0" + get-caller-file "^1.0.1" + os-locale "^3.0.0" + require-directory "^2.1.1" + require-main-filename "^1.0.1" + set-blocking "^2.0.0" + string-width "^2.0.0" + which-module "^2.0.0" + y18n "^3.2.1 || ^4.0.0" + yargs-parser "^10.1.0" + yargs@^6.6.0: version "6.6.0" resolved "https://registry.yarnpkg.com/yargs/-/yargs-6.6.0.tgz#782ec21ef403345f830a808ca3d513af56065208"