diff --git a/.bowerrc b/.bowerrc index a13a285bc0..24dc4049b8 100644 --- a/.bowerrc +++ b/.bowerrc @@ -1,3 +1,4 @@ { + "registry": "https://registry.bower.io", "directory": "res/bower_components" } diff --git a/.github/ISSUE_TEMPLATE/bug-report-or-feature-request.md b/.github/ISSUE_TEMPLATE/bug-report-or-feature-request.md new file mode 100644 index 0000000000..feb68c4a27 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug-report-or-feature-request.md @@ -0,0 +1,15 @@ +--- +name: Bug report or Feature Request +about: This project is no longer maintained +title: '' +labels: '' +assignees: '' + +--- + + diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 3d2ba38dcd..d8706946cf 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -1,6 +1,9 @@ --- name: Bug report about: Create a report to help us improve +title: '' +labels: '' +assignees: '' --- diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md deleted file mode 100644 index 1ac2fc2883..0000000000 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -name: Feature request -about: Suggest an idea for this project - ---- - -**Is your feature request related to a problem? Please describe.** -A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] - -**Describe the solution you'd like** -A clear and concise description of what you want to happen. - -**Describe alternatives you've considered** -A clear and concise description of any alternative solutions or features you've considered. - -**Additional context** -Add any other context or screenshots about the feature request here. - - diff --git a/.gitignore b/.gitignore index 9abdfafaf2..698630e883 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,4 @@ /rethinkdb_data/ /temp/ /tmp/ +package-lock.json diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000000..559d6961d5 --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +8.16.1 diff --git a/.tool-versions b/.tool-versions new file mode 100644 index 0000000000..f09ebaed90 --- /dev/null +++ b/.tool-versions @@ -0,0 +1 @@ +nodejs 8.16.1 diff --git a/.travis.yml b/.travis.yml index 1d3f969df3..6ecc3ae1d3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -15,13 +15,14 @@ addons: - yasm env: matrix: - - NODE_VERSION=6 + - NODE_VERSION=8.16.1 matrix: allow_failures: - os: osx fast_finish: true before_install: - rm -rf ~/.nvm && git clone --depth 1 https://github.com/creationix/nvm.git ~/.nvm +- rm .nvmrc - source ~/.nvm/nvm.sh - nvm install $NODE_VERSION - node --version diff --git a/CHANGELOG.md b/CHANGELOG.md index 6b5a9049ef..ebd625d4f0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,27 @@ # Changelog +## 3.4.2 (2020-05-31) + +### Fixes + +- Fixed SAML_ID_PROVIDER_CERT_PATH in example SAML auth provider unit [#1061](https://github.com/openstf/stf/pull/1061). Thanks @mitchtech! +- Fixed Chinese characters handling [#1204](https://github.com/openstf/stf/pull/1204). Thanks @xu-duqing! + +### Enhancements + +- Updated STFService.apk, minitouch and minicap to support Android 10. Thanks @denis99999 and @pcrepieux! +- Added addAdbPublicKey endpoint [#770](https://github.com/openstf/stf/pull/770). Thanks @neofreko! +- Added pt_BR translation [#1038](https://github.com/openstf/stf/pull/1038). Thanks @esmiralha! +- Added market name to device properties [#1002](https://github.com/openstf/stf/pull/1002). +- Added [group feature](https://github.com/openstf/stf/blob/master/doc/GroupFeature.pdf) [#1056](https://github.com/openstf/stf/pull/1056). Thanks @denis99999! +- Added validation for filter.pid/ filter.tid/ filter.data input objects [#1074](https://github.com/openstf/stf/pull/1074). Thanks @lukzeg! +- Added opening device view via reselecting device from device view [#1077](https://github.com/openstf/stf/pull/1077). Thanks @lukzeg! + - Added device logs separation [#1095](https://github.com/openstf/stf/pull/1095). Thanks @lukzeg! + - Updated NodeJS to 8.9.3 [#1169](https://github.com/openstf/stf/pull/1169). + - Added alternative dockerfiles for armhf and x86_64 architectures [#1174](https://github.com/openstf/stf/pull/1174), [#1191](https://github.com/openstf/stf/pull/1191). Thanks @denis99999 and @petemyron! + - Added touchMove error handling [#1189](https://github.com/openstf/stf/pull/1189). Thanks @att55! + + ## 3.4.1 (2019-05-03) ### Fixes diff --git a/Dockerfile b/Dockerfile index 5e63093edb..94b68584ca 100644 --- a/Dockerfile +++ b/Dockerfile @@ -28,7 +28,7 @@ RUN export DEBIAN_FRONTEND=noninteractive && \ apt-get -y install wget python build-essential && \ cd /tmp && \ wget --progress=dot:mega \ - https://nodejs.org/dist/v6.11.2/node-v6.11.2-linux-x64.tar.xz && \ + https://nodejs.org/dist/v8.9.3/node-v8.9.3-linux-x64.tar.xz && \ tar -xJf node-v*.tar.xz --strip-components 1 -C /usr/local && \ rm node-v*.tar.xz && \ su stf-build -s /bin/bash -c '/usr/local/lib/node_modules/npm/node_modules/node-gyp/bin/node-gyp.js install' && \ @@ -56,7 +56,6 @@ RUN set -x && \ bower cache clean && \ npm prune --production && \ mv node_modules /app && \ - npm cache clean && \ rm -rf ~/.node-gyp && \ cd /app && \ rm -rf /tmp/* diff --git a/Dockerfile-debian-armhf b/Dockerfile-debian-armhf new file mode 100644 index 0000000000..3fa54a37c1 --- /dev/null +++ b/Dockerfile-debian-armhf @@ -0,0 +1,70 @@ +# +# Copyright © 2020 code initially contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0 +# + +FROM arm32v7/debian:stretch-slim + +LABEL Maintainer="Denis Barbaron " +LABEL Name="STF" +LABEL Url="https://github.com/openstf/stf/" +LABEL Description="STF docker image for armhf architecture" + +# Sneak the stf executable into $PATH. +ENV PATH /app/bin:$PATH + +# Work in app dir by default. +WORKDIR /app + +# Copy app source. +COPY . /tmp/build/ + +# Install app requirement +RUN export DEBIAN_FRONTEND=noninteractive && \ + echo '--- Updating repositories' && \ + apt-get update && \ + echo '--- Building node' && \ + apt-get -y install wget python build-essential && \ + cd /tmp && \ + wget --progress=dot:mega \ + https://nodejs.org/dist/v9.9.0/node-v9.9.0-linux-armv7l.tar.xz && \ + tar -xJf node-v*.tar.xz --strip-components 1 -C /usr/local && \ + rm node-v*.tar.xz && \ + useradd --system \ + --create-home \ + --shell /usr/sbin/nologin \ + stf && \ + su stf -s /bin/bash -c '/usr/local/lib/node_modules/npm/node_modules/node-gyp/bin/node-gyp.js install' && \ + apt-get -y install --no-install-recommends libzmq3-dev libprotobuf-dev git graphicsmagick yasm && \ + echo '--- Building app' && \ + mkdir -p /app && \ + chown -R stf:stf /tmp/build && \ + set -x && \ + cd /tmp/build && \ + export PATH=$PWD/node_modules/.bin:$PATH && \ + sed -i'' -e '/phantomjs/d' package.json && \ + npm config set unsafe-perm true && \ + npm install -g npm && \ + echo 'npm cache clean --force' | su stf -s /bin/bash && \ + echo 'npm install --no-optional --loglevel http' | su stf -s /bin/bash && \ + echo '--- Assembling app' && \ + echo 'npm pack' | su stf -s /bin/bash && \ + tar xzf stf-*.tgz --strip-components 1 -C /app && \ + echo '/tmp/build/node_modules/.bin/bower cache clean' | su stf -s /bin/bash && \ + echo 'npm prune --production' | su stf -s /bin/bash && \ + mv node_modules /app && \ + chown -R root:root /app && \ + echo '--- Cleaning up' && \ + echo 'npm cache clean --force' | su stf -s /bin/bash && \ + rm -rf ~/.node-gyp && \ + apt-get -y purge wget python build-essential && \ + apt-get -y clean && \ + apt-get -y autoremove && \ + rm -rf /var/cache/apt/* /var/lib/apt/lists/* && \ + cd /app && \ + rm -rf /tmp/* + +# Switch to the app user. +USER stf + +# Show help by default. +CMD stf --help diff --git a/Dockerfile-debian-x86_64 b/Dockerfile-debian-x86_64 new file mode 100644 index 0000000000..b2256a2230 --- /dev/null +++ b/Dockerfile-debian-x86_64 @@ -0,0 +1,68 @@ +# +# Copyright © 2020 code initially contributed by Orange SA, authors: Denis Barbaron - Licensed under the Apache license 2.0 +# + +FROM debian:stretch-slim + +LABEL Maintainer="Denis Barbaron " +LABEL Name="STF" +LABEL Url="https://github.com/openstf/stf/" +LABEL Description="STF docker image for x86_64 architecture" + +# Sneak the stf executable into $PATH. +ENV PATH /app/bin:$PATH + +# Work in app dir by default. +WORKDIR /app + +# Copy app source. +COPY . /tmp/build/ + +# Install app requirements +RUN export DEBIAN_FRONTEND=noninteractive && \ + echo '--- Updating repositories' && \ + apt-get update && \ + echo '--- Building node' && \ + apt-get -y install wget python build-essential && \ + cd /tmp && \ + wget --progress=dot:mega \ + https://nodejs.org/dist/v8.9.3/node-v8.9.3-linux-x64.tar.xz && \ + tar -xJf node-v*.tar.xz --strip-components 1 -C /usr/local && \ + rm node-v*.tar.xz && \ + useradd --system \ + --create-home \ + --shell /usr/sbin/nologin \ + stf && \ + su stf -s /bin/bash -c '/usr/local/lib/node_modules/npm/node_modules/node-gyp/bin/node-gyp.js install' && \ + apt-get -y install --no-install-recommends libzmq3-dev libprotobuf-dev git graphicsmagick yasm && \ + echo '--- Building app' && \ + mkdir -p /app && \ + chown -R stf:stf /tmp/build && \ + set -x && \ + cd /tmp/build && \ + export PATH=$PWD/node_modules/.bin:$PATH && \ + sed -i'' -e '/phantomjs/d' package.json && \ + npm install -g npm && \ + echo 'npm install --no-optional --loglevel http' | su stf -s /bin/bash && \ + echo '--- Assembling app' && \ + echo 'npm pack' | su stf -s /bin/bash && \ + tar xzf stf-*.tgz --strip-components 1 -C /app && \ + echo '/tmp/build/node_modules/.bin/bower cache clean' | su stf -s /bin/bash && \ + echo 'npm prune --production' | su stf -s /bin/bash && \ + mv node_modules /app && \ + chown -R root:root /app && \ + echo '--- Cleaning up' && \ + echo 'npm cache clean --force' | su stf -s /bin/bash && \ + rm -rf ~/.node-gyp && \ + apt-get -y purge wget python build-essential && \ + apt-get -y clean && \ + apt-get -y autoremove && \ + rm -rf /var/cache/apt/* /var/lib/apt/lists/* && \ + cd /app && \ + rm -rf /tmp/* + +# Switch to the app user. +USER stf + +# Show help by default. +CMD stf --help diff --git a/LICENSE b/LICENSE index ff328e27c6..84a0516566 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/README.md b/README.md index 19a52bb468..335c98de0f 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,8 @@ +# Warning +# This project along with other ones in [OpenSTF](https://github.com/openstf) organisation is provided as is for community, without active development. +# You can check any other forks that may be actively developed and offer new/different features [here](https://github.com/openstf/stf/network). +# Active development has been moved to [DeviceFarmer](https://github.com/DeviceFarmer) organisation. Version **3.4.2** is the last release of OpenSTF on DockerHub and **3.4.1** on npmjs. + STF [![Build Status](https://travis-ci.org/openstf/stf.svg?branch=master)](https://travis-ci.org/openstf/stf) @@ -23,9 +28,9 @@ Thank you to all our sponsors! (please ask your company to also support this ope #### Gold Sponsor -[HeadSpin](https://headspin.io/) +[HeadSpin](https://ui.headspin.io/register?referral=start-testing-hs) -> [HeadSpin](https://headspin.io/) provides secure and scalable STF for iOS integrated with Appium/XCTest/Selenium/Espresso, High speed interaction Audio/Video/Game testing and AI based Root cause analysis for Performance Management. It's free to start using HeadSpin in 150+ locations worldwide! [Try it out for free.](https://ui-dev.headspin.io/signup/23feaa95fec34c49a1da309380807795) +> [HeadSpin](https://headspin.io/) provides secure and scalable STF for iOS integrated with Appium/XCTest/Selenium/Espresso, High speed interaction Audio/Video/Game testing and AI based Root cause analysis for Performance Management. It's free to start using HeadSpin in 150+ locations worldwide! [Try it out for free.](https://ui.headspin.io/register?referral=start-testing-hs) HeadSpin offers a generous monthly contribution towards STF development. @@ -75,15 +80,41 @@ Please use our [open collective](https://opencollective.com/openstf#sponsor) or * Supports [Chrome remote debug tools](https://developer.chrome.com/devtools/docs/remote-debugging) - File Explorer to access device file system - Experimental VNC support (work in progress) -* Manage your device inventory +* Monitor your device inventory - See which devices are connected, offline/unavailable (indicating a weak USB connection), unauthorized or unplugged - See who's using a device - - Search devices by phone number, IMEI, ICCID, Android version, operator, product name and/or many other attributes with easy but powerful queries + - Search devices by phone number, IMEI, ICCID, Android version, operator, product name, group name and/or many other attributes with easy but powerful queries - Show a bright red screen with identifying information on a device you need to locate physically - Track battery level and health - Rudimentary Play Store account management * List, remove and add new accounts (adding may not work on all devices) - Display hardware specs +* Use the Booking & Partitioning systems + - Overview + * The partitioning system allow you `[administrator level]` to allocate distinct sets of devices to different projects or organizations (i.e. represented by user sets) for an unlimited period + * The booking system allows you to reserve a set of devices for a set of users during a limited time (e.g. from 3:00 am to 4:00 am during 5 days) + * What is common to the booking & partitioning systems is the concept of Group, that is, an association of devices, users and a specification of time + * Report to [GroupFeature.pdf](doc/GroupFeature.pdf) for detailed documentation on how to use this feature + - Monitor your group inventory + * See which groups are active, ready or pending, as well as other group properties: name, identifier, owner, devices, users, class, duration, repetition, starting date, expiration date + * Search groups by their property values + * Contact by email the owners of the selected groups + - Manage your groups + * Create a group by specifying its name, devices, users and schedule + * Get ready your group in order it is scheduled by the system + * Search groups by their property values + * Remove your group or a selection of your groups + * Contact by email the owners of the selected groups `[administrator level]` +* Manage the devices `[administrator level]` + - Search the devices by their property values + - Remove a device or a selection of devices meeting a set of filters: present, booked, annotated, controlled +* Manage the users `[administrator level]` + - Create a user by providing his name and his email + - Search the users by their property values + - Remove a user or a selection of users meeting a set of filters: group owner + - Contact a user or a selection of users by email + - Set the default groups quotas applicable to all users + - Set the groups quotas applicable to a specific user * Simple REST [API](doc/API.md) ## Status @@ -114,7 +145,7 @@ As the product has evolved from an internal tool running in our internal network ## Requirements -* [Node.js](https://nodejs.org/) >= 6.9 (latest stable version preferred) +* [Node.js](https://nodejs.org/) 8.x **required** (some dependencies don't support newer versions) * [ADB](http://developer.android.com/tools/help/adb.html) properly set up * [RethinkDB](http://rethinkdb.com/) >= 2.2 * [GraphicsMagick](http://www.graphicsmagick.org/) (for resizing screenshots) @@ -183,6 +214,19 @@ _Note: if it takes a long time for RethinkDB to start up, you may be running int You should now have RethinkDB running locally. Running the command again in the same folder will reuse the data from the previous session. +An administrator level is available in STF in addition of the native user one, with increased rights on some features (e.g. booking & partitioning systems, management of users & devices, ...). The corresponding built-in administrator user has the following default credentials: +- name: `administrator` +- email: `administrator@fakedomain.com` + +Another built-in object exists, this is the root standard group to which the users and devices belong the first time they register to the STF database, its default name is `Common` + +These built-in objects are created in the STF database if they do not already exist + +Of course, you can override the default values of these built-in objects by settings the following environment variables before to initialize the STF database through `stf local` or `stf migrate` commands: +- root standard group name: `STF_ROOT_GROUP_NAME` +- administrator user name: `STF_ADMIN_NAME` +- administrator user email: `STF_ADMIN_EMAIL` + You're now ready to start up STF itself: ```bash diff --git a/TESTING.md b/TESTING.md index 3b0fcac5e7..acb57e0567 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 + + +## 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 -- `gulp protractor` + ``` + ./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 +## 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/doc/DEPLOYMENT.md b/doc/DEPLOYMENT.md index f2be6de9c6..4484859b93 100644 --- a/doc/DEPLOYMENT.md +++ b/doc/DEPLOYMENT.md @@ -56,6 +56,7 @@ The app role can contain any of the following units. You may distribute them as * [stf-triproxy-dev.service](#stf-triproxy-devservice) * [stf-websocket@.service](#stf-websocketservice) * [stf-api@.service](#stf-apiservice) +* [stf-groups-engine.service](#stf-groups-engineservice) ### Database role @@ -360,6 +361,9 @@ ExecStartPre=-/usr/bin/docker rm %p ExecStart=/usr/bin/docker run --rm \ --name %p \ --link rethinkdb-proxy-28015:rethinkdb \ + -e "STF_ROOT_GROUP_NAME=YOUR_ROOT_GROUP_NAME_HERE" \ + -e "STF_ADMIN_NAME=YOUR_ADMIN_NAME_HERE" \ + -e "STF_ADMIN_EMAIL=YOUR_ADMIN_EMAIL_HERE" \ openstf/stf:latest \ stf migrate ``` @@ -691,10 +695,54 @@ ExecStart=/usr/bin/docker run --rm \ openstf/stf:latest \ stf api --port 3000 \ --connect-sub tcp://appside.stf.example.org:7150 \ - --connect-push tcp://appside.stf.example.org:7170 + --connect-push tcp://appside.stf.example.org:7170 \ + --connect-sub-dev tcp://devside.stf.example.org:7250 \ + --connect-push-dev tcp://devside.stf.example.org:7270 ExecStop=-/usr/bin/docker stop -t 10 %p-%i ``` +### `stf-groups-engine.service` + +**Requires** the `rethinkdb-proxy-28015.service` unit on the same host. + +The groups-engine unit is the core of the device booking/partitioning system, it is made of four main functions ensuring in particular the consistency of operations ordered by the client side on groups (i.e. a group is an association of users, devices and a specification of time): + +- groups’ scheduler: triggered each second to manage lifecycle of groups: updates group state and group schedule dates, removes terminated groups, etc. + +- groups’ watcher: relied on changefeeds mechanism of rethinkdb database, so taking actions on group creation, updating and removing: notifies API unit and front-end UI, releases device control, updates device current group, etc. + +- devices’ watcher: relied on changefeeds mechanism of rethinkdb database, so taking actions on device creation, updating and removing: notifies front-end UI, releases device control, etc. + +- users’ watcher: relied on changefeeds mechanism of rethinkdb database, so taking actions on user creation, updating and removing: notifies front-end UI, etc. + +Note that it doesn't make sense to have more than one `groups-engine.service` unit running at once. + +```ini +[Unit] +Description=STF groups engine +After=rethinkdb-proxy-28015.service +BindsTo=rethinkdb-proxy-28015.service + +[Service] +EnvironmentFile=/etc/environment +TimeoutStartSec=0 +Restart=always +ExecStartPre=/usr/bin/docker pull openstf/stf:latest +ExecStartPre=-/usr/bin/docker kill %p +ExecStartPre=-/usr/bin/docker rm %p +ExecStart=/usr/bin/docker run --rm \ + --name %p \ + --link rethinkdb-proxy-28015:rethinkdb \ + -e "SECRET=YOUR_SESSION_SECRET_HERE" \ + openstf/stf:latest \ + stf groups-engine \ + --connect-sub tcp://appside.stf.example.org:7150 \ + --connect-push tcp://appside.stf.example.org:7170 \ + --connect-sub-dev tcp://devside.stf.example.org:7250 \ + --connect-push-dev tcp://devside.stf.example.org:7270 +ExecStop=-/usr/bin/docker stop -t 10 %p +``` + ## Optional units These units are optional and don't affect the way STF works in any way. @@ -821,6 +869,35 @@ ExecStart=/usr/bin/docker run --rm \ ExecStop=-/usr/bin/docker stop -t 10 %p-%i ``` +### `swagger-ui@.service` + +**Requires** the main HTTP server on the same host. + +If you want to play with STF API against your STF platform using swagger UI tool through a web access, then you can use this optional unit. In this example, the unit requires to put the STF swagger file `api_v1.yaml` to the `/opt/stf/swagger` folder of the host. You can have multiple instances running on the same host by using different ports. + +```ini +[Unit] +Description=Swagger UI (runs on %i port) +After=docker.service +BindsTo=docker.service + +[Service] +EnvironmentFile=/etc/environment +TimeoutStartSec=0 +Restart=always +ExecStartPre=/usr/bin/docker pull swaggerapi/swagger-ui:latest +ExecStartPre=-/usr/bin/docker kill %p-%i +ExecStartPre=-/usr/bin/docker rm %p-%i +ExecStart=/usr/bin/docker run --rm \ + --name %p-%i \ + -e "VALIDATOR_URL=null" \ + -e "SWAGGER_JSON=/foo/api_v1.yaml" \ + -p %i:8080 \ + -v /opt/stf/swagger:/foo \ + swaggerapi/swagger-ui:latest +ExecStop=/usr/bin/docker stop -t 2 %p-%i +``` + ## Nginx configuration Now that you've got all the units ready, it's time to set up [nginx](http://nginx.org/) to tie all the processes together with a clean URL. @@ -835,6 +912,8 @@ So, to recap, our example setup is as follows: | [stf-storage-plugin-image@3400.service](#stf-storage-plugin-imageservice) | 192.168.255.100 | 3400 | | [stf-storage-temp@3500.service](#stf-storage-tempservice) | 192.168.255.100 | 3500 | | [stf-websocket@3600.service](#stf-websocketservice) | 192.168.255.100 | 3600 | +| [stf-api@3700.service](#stf-apiservice) | 192.168.255.100 | 3700 | +| [swagger-ui@.service](#swagger-uiservice) | 192.168.255.100 | 3800 | Furthermore, let's assume that we have the following providers set up: @@ -881,6 +960,10 @@ http { upstream stf_api { server 192.168.255.100:3700 max_fails=0; } + + upstream swagger_ui { + server 192.168.255.100:3800 max_fails=0; + } types { application/javascript js; @@ -977,6 +1060,12 @@ http { proxy_set_header X-Real-IP $http_x_real_ip; } + location /swaggerui/ { + proxy_pass http://swagger_ui/; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Real-IP $http_x_real_ip; + } + location / { proxy_pass http://stf_app; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; @@ -1003,7 +1092,7 @@ ConditionPathExists=/srv/nginx/nginx.conf EnvironmentFile=/etc/environment TimeoutStartSec=0 Restart=always -ExecStartPre=/usr/bin/docker pull nginx:1.7.10 +ExecStartPre=/usr/bin/docker pull nginx:1.17.4 ExecStartPre=-/usr/bin/docker kill %p ExecStartPre=-/usr/bin/docker rm %p ExecStart=/usr/bin/docker run --rm \ @@ -1013,7 +1102,7 @@ ExecStart=/usr/bin/docker run --rm \ -v /srv/ssl/stf.example.org.key:/etc/nginx/ssl/cert.key:ro \ -v /srv/ssl/dhparam.pem:/etc/nginx/ssl/dhparam.pem:ro \ -v /srv/nginx/nginx.conf:/etc/nginx/nginx.conf:ro \ - nginx:1.7.10 \ + nginx:1.17.4 \ nginx ExecStop=/usr/bin/docker stop -t 2 %p ``` diff --git a/doc/GroupFeature.pdf b/doc/GroupFeature.pdf new file mode 100644 index 0000000000..0cdd0f023d Binary files /dev/null and b/doc/GroupFeature.pdf differ diff --git a/lib/cli/api/index.js b/lib/cli/api/index.js index 6654f95322..78ef946880 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 249e3da70e..7e8540a78a 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 0000000000..aaecfbf380 --- /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 0000000000..581c31d11d --- /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 0000000000..83841218bc --- /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 48b57e0365..3f7c9ee06e 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 11fcfb27ba..4e1b0b97bf 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 f5954c0d9e..ce5072891a 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 8cfe988128..8281c7ca4f 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,948 @@ 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() + + function updateUsersForMigration(group) { + return dbapi.getUsers().then(function(users) { + return Promise.map(users, function(user) { + return db.run(r.table('users').get(user.email).update({ + privilege: user.email !== group.owner.email ? apiutil.USER : apiutil.ADMIN + , groups: { + subscribed: [] + , lock: false + , quotas: { + allocated: { + number: group.envUserGroupsNumber + , duration: group.envUserGroupsDuration + } + , consumed: { + number: 0 + , duration: 0 + } + , defaultGroupsNumber: user.email !== group.owner.email ? + 0 : + group.envUserGroupsNumber + , defaultGroupsDuration: user.email !== group.owner.email ? + 0 : + group.envUserGroupsDuration + , defaultGroupsRepetitions: user.email !== group.owner.email ? + 0 : + group.envUserGroupsRepetitions + , repetitions: group.envUserGroupsRepetitions + } + } + })) + .then(function(stats) { + if (stats.replaced) { + return dbapi.addGroupUser(group.id, user.email) + } + return stats + }) + }) + }) + } + + function getDevices() { + return db.run(r.table('devices')) + .then(function(cursor) { + return cursor.toArray() + }) + } + + function updateDevicesForMigration(group) { + return getDevices().then(function(devices) { + return Promise.map(devices, function(device) { + return db.run(r.table('devices').get(device.serial).update({ + 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 + }} + )) + .then(function(stats) { + if (stats.replaced) { + return dbapi.addOriginGroupDevice(group, device.serial) + } + return stats + }) + }) + }) + } + + 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 updateUsersForMigration(group) + }) + .then(function() { + return updateDevicesForMigration(group) + }) + .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 +977,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 +1064,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 +1091,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 @@ -154,16 +1102,34 @@ dbapi.saveDeviceInitialState = function(serial, device) { , remoteConnect: false , remoteConnectUrl: null , 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) { @@ -224,6 +1190,7 @@ dbapi.unsetDeviceUsage = function(serial) { return db.run(r.table('devices').get(serial).update({ usage: null , usageChangedAt: r.now() + , logs_enabled: false })) } @@ -324,11 +1291,48 @@ dbapi.saveDeviceIdentity = function(serial, identity) { , product: identity.product , cpuPlatform: identity.cpuPlatform , openGLESVersion: identity.openGLESVersion + , marketName: identity.marketName + })) +} + +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.loadDevices = function() { - return db.run(r.table('devices')) +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() { @@ -337,17 +1341,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) { @@ -356,6 +1392,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 231f597145..f3c104e5ef 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 = { @@ -43,15 +47,37 @@ module.exports = { ) } } + , logs_enabled: false , present: null , providerChannel: { indexFunction: function(device) { 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 ba236b4fda..7b423a16d5 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 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) + }) +} + +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' + }) + .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.loadDevices() + dbapi.loadDevice(req.user.groups.subscribed, serial) .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) + 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) - res.json({ - success: true - , devices: deviceList - }) + if (fields) { + responseDevice = _.pick(device, fields.split(',')) + } + res.json({ + success: true + , device: responseDevice }) + }) }) .catch(function(err) { - log.error('Failed to load device list: ', err.stack) + log.error('Failed to load device "%s": ', serial, err.stack) res.status(500).json({ success: false }) }) } -function getDeviceBySerial(req, res) { - var serial = req.swagger.params.serial.value - var fields = req.swagger.params.fields.value +function getDeviceGroups(req, res) { + const serial = req.swagger.params.serial.value + const 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) { + 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) + }) +} - datautil.normalize(device, req.user) - var responseDevice = device +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) + }) +} - if (fields) { - responseDevice = _.pick(device, fields.split(',')) +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 - res.json({ - success: true - , device: responseDevice + 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) { - log.error('Failed to load device "%s": ', req.params.serial, err.stack) - res.status(500).json({ - success: false + 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 0000000000..638669984d --- /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 7652244894..43953c2abc 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' - }) - } + 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(',')) - } + var responseDevice = device + if (fields) { + responseDevice = _.pick(device, fields.split(',')) + } - res.json({ - success: true - , device: responseDevice + res.json({ + success: true + , description: 'Controlled device information' + , 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 - - 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' - }) - } + 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 - // 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) - - 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) - - return res.json({ - success: true - , description: 'Device successfully added' - }) - } - }) - .handler() - - req.options.channelRouter.on(wireutil.global, messageListener) - var usage = 'automation' + , description: 'Device not found' + }) + } - 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' + 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({ + success: false + , description: 'Device is not responding' + }) + }, 5000) + + 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) + + 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) - - 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) - - return res.json({ - success: true - , description: 'Device successfully removed' - }) - } - }) - .handler() - - req.options.channelRouter.on(wireutil.global, messageListener) + , description: 'Device not found' + }) + } - req.options.push.send([ - device.channel - , wireutil.envelope( - new wire.UngroupMessage( - wireutil.toDeviceRequirements({ - serial: { - value: serial - , match: 'exact' - } - }) + 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({ + success: false + , description: 'Device is not responding' + }) + }, 5000) + + 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) + + 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) - - 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) + , description: 'Device not found' + }) + } - return res.json({ - success: true - , remoteConnectUrl: message.url - }) - } - }) - .handler() + 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({ + success: false + , description: 'Device is not responding' + }) + }, 5000) + + 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.channelRouter.on(responseChannel, messageListener) - req.options.push.send([ - device.channel - , wireutil.transaction( - responseChannel - , new wire.ConnectStartMessage() - ) - ]) + 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) - - 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() + }) + }, 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) + 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 0000000000..7820710d06 --- /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 99eedd1c85..600be491d2 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 76d8cad7dd..14929cdd48 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 1d04da03da..073463d8de 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,37 +26,1711 @@ 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: - summary: User Profile - description: The User Profile endpoint returns information about current authorized user - operationId: getUser + summary: User Profile + description: The User Profile endpoint returns information about current authorized user + operationId: getUser + tags: + - user + responses: + "200": + description: Current User Profile information + schema: + $ref: "#/definitions/UserResponse" + default: + description: Unexpected Error + schema: + $ref: "#/definitions/ErrorResponse" + security: + - accessTokenAuth: [] + /user/devices: + x-swagger-router-controller: user + get: + summary: User Devices + description: The User Devices endpoint returns device list owner by current authorized user + operationId: getUserDevices + tags: + - user + parameters: + - name: fields + in: query + description: Fields query parameter takes a comma seperated list of fields. Only listed field will be return in response + required: false + type: string + responses: + "200": + description: Current User Devices List + schema: + $ref: "#/definitions/DeviceListResponse" + default: + description: > + Unexpected Error: + * 401: Unauthorized => bad credentials + * 500: Internal Server Error + schema: + $ref: "#/definitions/UnexpectedErrorResponse" + security: + - accessTokenAuth: [] + post: + summary: Add a device to a user + description: The User Devices endpoint will request stf server for a new device. + operationId: addUserDevice + tags: + - user + parameters: + - name: device + in: body + description: Device to add + required: true + schema: + $ref: "#/definitions/AddUserDevicePayload" + responses: + "200": + description: Add User Device Status + 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: [] + /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 + operationId: getUserDeviceBySerial tags: - user + parameters: + - name: serial + in: path + description: Device 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: Current User Profile information + description: Device Information owned by user schema: - $ref: "#/definitions/UserResponse" + $ref: "#/definitions/DeviceResponse" + 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 + schema: + $ref: "#/definitions/UnexpectedErrorResponse" + security: + - accessTokenAuth: [] + delete: + summary: Delete User Device + description: The User Devices endpoint will request for device release from stf server. It will return request accepted if device is being used by current user + operationId: deleteUserDeviceBySerial + tags: + - user + parameters: + - name: serial + in: path + description: Device Serial + required: true + type: string + responses: + "200": + description: Delete User Device Status + 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 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 + # understand in comparision of using PUT/PATCH + /user/devices/{serial}/remoteConnect: + x-swagger-router-controller: user + post: + summary: Remote Connect + description: The device connect endpoint will request stf server to connect remotely + operationId: remoteConnectUserDeviceBySerial + tags: + - user + parameters: + - name: serial + in: path + description: Device Serial + required: true + type: string + responses: + "200": + description: Remote Connect User Device Request Status + 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 device + * 500: Internal Server Error + * 504: Gateway Time-out => device is not responding + schema: + $ref: "#/definitions/UnexpectedErrorResponse" + security: + - accessTokenAuth: [] + delete: + summary: Remote Disconnect + description: The device connect endpoint will request stf server to disconnect remotely + operationId: remoteDisconnectUserDeviceBySerial + tags: + - user + parameters: + - name: serial + in: path + description: Device Serial + required: true + type: string + responses: + "200": + description: Remote Disonnect User Device Request Status + 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 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 + operationId: getUserAccessTokens + tags: + - user + responses: + "200": + description: Access Tokens titles + schema: + $ref: "#/definitions/AccessTokensResponse" default: description: Unexpected Error schema: $ref: "#/definitions/ErrorResponse" security: - accessTokenAuth: [] - /user/devices: + 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: User Devices - description: The User Devices endpoint returns device list owner by current authorized user - operationId: getUserDevices + 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: + summary: Adb public keys + description: Add adb public key for current user + operationId: addAdbPublicKey + consumes: + - application/json + produces: + - application/json tags: - user parameters: + - name: adb + in: body + schema: + type: object + required: + - publickey + properties: + publickey: + type: string + description: adb public key (~/.android/id_rsa.pub) + title: + type: string + description: By default will be extracted from public key + responses: + "200": + description: Add adb key response + default: + description: Unexpected Error + schema: + $ref: "#/definitions/ErrorResponse" + security: + - accessTokenAuth: [] + /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 + operationId: getDevices + 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 @@ -60,45 +1738,75 @@ paths: type: string responses: "200": - description: Current User Devices List + 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: [] - post: - summary: Add a device to a user - description: The User Devices endpoint will request stf server for a new device. - operationId: addUserDevice + delete: + summary: Removes devices + description: Removes devices from the database + operationId: deleteDevices tags: - - user + - admin parameters: - - name: device + - 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: Device to add - required: true + 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/AddUserDevicePayload" + $ref: "#/definitions/DevicesPayload" responses: "200": - description: Add User Device Status + description: Devices removing is OK (or no devices to remove) + schema: + $ref: "#/definitions/Response" default: - description: Unexpected Error + 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/ErrorResponse" + $ref: "#/definitions/UnexpectedErrorResponse" security: - accessTokenAuth: [] - /user/devices/{serial}: - x-swagger-router-controller: user + /devices/{serial}: + x-swagger-router-controller: devices get: - summary: User Device - description: The devices enpoint return information about device owned by user - operationId: getUserDeviceBySerial + summary: Device Information + description: The devices serial enpoint return information about a single device + operationId: getDeviceBySerial tags: - - user + - devices parameters: - name: serial in: path @@ -112,7 +1820,7 @@ paths: type: string responses: "200": - description: Device Information owned by user + description: Device Information schema: $ref: "#/definitions/DeviceResponse" default: @@ -122,137 +1830,190 @@ paths: security: - accessTokenAuth: [] delete: - summary: Delete User Device - description: The User Devices endpoint will request for device release from stf server. It will return request accepted if device is being used by current user - operationId: deleteUserDeviceBySerial + summary: Removes a device + description: Removes a device from the database + operationId: deleteDevice tags: - - user + - admin parameters: - name: serial in: path - description: Device Serial + 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: Delete User Device Status + description: Device removing is OK + schema: + $ref: "#/definitions/Response" default: - description: Unexpected Error + 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/ErrorResponse" + $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 - # understand in comparision of using PUT/PATCH - /user/devices/{serial}/remoteConnect: - x-swagger-router-controller: user - post: - summary: Remote Connect - description: The device connect endpoint will request stf server to connect remotely - operationId: remoteConnectUserDeviceBySerial + /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: - - user + - admin parameters: - - name: serial + - name: id in: path - description: Device Serial + 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: Remote Connect User Device Request Status + description: Devices information (an empty device list is returned if no change is made) schema: - $ref: "#/definitions/RemoteConnectUserDeviceResponse" + $ref: "#/definitions/DeviceListResponse" default: - description: Unexpected Error + 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/ErrorResponse" + $ref: "#/definitions/UnexpectedErrorResponse" security: - accessTokenAuth: [] delete: - summary: Remote Disconnect - description: The device connect endpoint will request stf server to disconnect remotely - operationId: remoteDisconnectUserDeviceBySerial + 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: - - user + - admin parameters: - - name: serial + - name: id in: path - description: Device Serial + description: Group identifier required: true type: string - responses: - "200": - description: Remote Disonnect User Device Request Status - default: - description: Unexpected Error + - 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/ErrorResponse" - security: - - accessTokenAuth: [] - /user/accessTokens: - x-swagger-router-controller: user - get: - summary: Access Tokens - description: The Access Tokens endpoints returns titles of all the valid access tokens - operationId: getUserAccessTokens - tags: - - user + $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: Access Tokens titles + description: Devices information (an empty device list is returned if no change is made) schema: - $ref: "#/definitions/AccessTokensResponse" + $ref: "#/definitions/DeviceListResponse" default: - description: Unexpected Error + 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/ErrorResponse" + $ref: "#/definitions/UnexpectedErrorResponse" security: - accessTokenAuth: [] - /user/adbPublicKeys: - x-swagger-router-controller: user - post: - summary: Adb public keys - description: Add adb public key for current user - operationId: addAdbPublicKey - consumes: - - application/json - produces: - - application/json + /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: - - user + - admin parameters: - - name: adb - in: body - schema: - type: object - required: - - publickey - properties: - publickey: - type: string - description: adb public key (~/.android/id_rsa.pub) - title: - type: string - description: By default will be extracted from public key + - 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: Add adb key response + description: Groups information + schema: + $ref: "#/definitions/GroupListResponse" default: - description: Unexpected Error + description: > + Unexpected Error: + * 401: Unauthorized => bad credentials + * 404: Not Found => unknown device + * 500: Internal Server Error schema: - $ref: "#/definitions/ErrorResponse" + $ref: "#/definitions/UnexpectedErrorResponse" security: - accessTokenAuth: [] - /devices: + /devices/{serial}/bookings: x-swagger-router-controller: devices get: - summary: Device List - description: The devices endpoint return list of all the STF devices including Disconnected and Offline - operationId: getDevices + 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 @@ -260,84 +2021,300 @@ paths: type: string responses: "200": - description: List of Devices + description: Bookings information schema: - $ref: "#/definitions/DeviceListResponse" + $ref: "#/definitions/GroupListResponse" default: - description: Unexpected Error + description: > + Unexpected Error: + * 401: Unauthorized => bad credentials + * 404: Not Found => unknown device + * 500: Internal Server Error schema: - $ref: "#/definitions/ErrorResponse" + $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 - operationId: getDeviceBySerial + /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: - - devices + - admin parameters: - name: serial in: path - description: Device Serial + 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 + - name: id + in: path + description: Group identifier + required: true type: string responses: "200": - description: Device Information + description: Device information schema: $ref: "#/definitions/DeviceResponse" default: - description: Unexpected Error + 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/ErrorResponse" + $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 + 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 (in UTC, conforming to RFC 3339 section 5.6); default value => group creation time + type: string + format: date-time + stopTime: + description: Group expiration time (in UTC, conforming to RFC 3339 section 5.6); 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 e960f4f345..bf3a559540 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 5388407ee1..c0f6b61cb2 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 dc65b770c8..f3ed493233 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/auth/oauth2/index.js b/lib/units/auth/oauth2/index.js index 1e8a23cc07..30144d55d3 100644 --- a/lib/units/auth/oauth2/index.js +++ b/lib/units/auth/oauth2/index.js @@ -57,8 +57,7 @@ module.exports = function(options) { } else { log.warn('Missing or disallowed email in profile', req.user) - res.send('Missing or rejected email address ' + - 'Retry') + res.render('rejected-email') } } ) diff --git a/lib/units/device/plugins/connect.js b/lib/units/device/plugins/connect.js index 90bf3f7901..b70efc3437 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 459c1e5a2b..b2e132b5ff 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 ae78a2d030..e3ba1f4047 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 fb8c47c672..7b5665dced 100644 --- a/lib/units/device/resources/minicap.js +++ b/lib/units/device/resources/minicap.js @@ -23,7 +23,7 @@ module.exports = syrup.serial() bin: new Resource({ src: pathutil.requiredMatch(abi.all.map(function(supportedAbi) { return pathutil.module(util.format( - 'minicap-prebuilt/prebuilt/%s/bin/minicap%s' + 'minicap-prebuilt-beta/prebuilt/%s/bin/minicap%s' , supportedAbi , abi.pie ? '' : '-nopie' )) @@ -41,12 +41,12 @@ module.exports = syrup.serial() src: pathutil.requiredMatch(abi.all.reduce(function(all, supportedAbi) { return all.concat([ pathutil.module(util.format( - 'minicap-prebuilt/prebuilt/%s/lib/android-%s/minicap.so' + 'minicap-prebuilt-beta/prebuilt/%s/lib/android-%s/minicap.so' , supportedAbi , sdk.previewLevel )) , pathutil.module(util.format( - 'minicap-prebuilt/prebuilt/%s/lib/android-%s/minicap.so' + 'minicap-prebuilt-beta/prebuilt/%s/lib/android-%s/minicap.so' , supportedAbi , sdk.level )) @@ -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 37b28401f3..802998c397 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 ba1327fcda..725d297b53 100644 --- a/lib/units/device/resources/minitouch.js +++ b/lib/units/device/resources/minitouch.js @@ -20,7 +20,7 @@ module.exports = syrup.serial() bin: new Resource({ src: pathutil.requiredMatch(abi.all.map(function(supportedAbi) { return pathutil.module(util.format( - 'minitouch-prebuilt/prebuilt/%s/bin/minitouch%s' + 'minitouch-prebuilt-beta/prebuilt/%s/bin/minitouch%s' , supportedAbi , abi.pie ? '' : '-nopie' )) @@ -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/device/resources/service.js b/lib/units/device/resources/service.js index 17eaaae92e..7225bedc01 100644 --- a/lib/units/device/resources/service.js +++ b/lib/units/device/resources/service.js @@ -17,7 +17,7 @@ module.exports = syrup.serial() pathutil.vendor('STFService/wire.proto')) var resource = { - requiredVersion: '2.3.0' + requiredVersion: '2.4.3' , pkg: 'jp.co.cyberagent.stf' , main: 'jp.co.cyberagent.stf.Agent' , apk: pathutil.vendor('STFService/STFService.apk') diff --git a/lib/units/groups-engine/index.js b/lib/units/groups-engine/index.js new file mode 100644 index 0000000000..2a5ad76bd0 --- /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 0000000000..67e70738ba --- /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 0000000000..b3100a2899 --- /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 0000000000..7bb7e9afdc --- /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 0000000000..ce23553c0b --- /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 d2a866c191..a2c8995259 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 3d1f93baec..9ed22bca0e 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/storage/temp.js b/lib/units/storage/temp.js index 8556725c1a..f5df76bb37 100644 --- a/lib/units/storage/temp.js +++ b/lib/units/storage/temp.js @@ -1,6 +1,7 @@ var http = require('http') var util = require('util') var path = require('path') +var crypto = require('crypto') var express = require('express') var validator = require('express-validator') @@ -89,6 +90,10 @@ module.exports = function(options) { if (options.saveDir) { form.uploadDir = options.saveDir } + form.on('fileBegin', function(name, file) { + var md5 = crypto.createHash('md5') + file.name = md5.update(file.name).digest('hex') + }) Promise.promisify(form.parse, form)(req) .spread(function(fields, files) { return Object.keys(files).map(function(field) { diff --git a/lib/units/websocket/index.js b/lib/units/websocket/index.js index 5ed6919495..31d8c943b1 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) { @@ -461,16 +548,22 @@ module.exports = function(options) { ]) }) .on('input.touchMove', function(channel, data) { - push.send([ - channel - , wireutil.envelope(new wire.TouchMoveMessage( - data.seq - , data.contact - , data.x - , data.y - , data.pressure - )) - ]) + try { + push.send([ + channel + , wireutil.envelope(new wire.TouchMoveMessage( + data.seq + , data.contact + , data.x + , data.y + , data.pressure + )) + ]) + } + catch(err) { + //workaround for https://github.com/openstf/stf/issues/1180 + log.error('input.touchMove had an error', err.stack) + } }) .on('input.touchUp', function(channel, data) { push.send([ @@ -916,6 +1009,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 +1017,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 0000000000..016e0f1881 --- /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 d1cfaa27d5..eb691d7473 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 7b125d9a6f..257d9249d6 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 da61b0c331..0772cfd7ca 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 966f2d2721..6a6de91f5b 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 0000000000..00ad6f200e --- /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 0000000000..8ca850dad1 --- /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 0000000000..1e2c77e737 --- /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 4c6fb28486..118071ac25 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 0000000000..9899238919 --- /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 2b2c60ca69..3bc8cdf5d1 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 e25c2831b4..40dbbf9e2d 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", @@ -50,6 +51,7 @@ "eventemitter3": "^1.2.0", "express": "^4.14.0", "express-validator": "^2.20.8", + "file-saver": "1.3.3", "formidable": "^1.2.0", "gm": "^1.23.0", "hipchatter": "^0.3.1", @@ -61,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", @@ -99,7 +101,7 @@ }, "devDependencies": { "async": "^2.0.1", - "bower": "^1.7.2", + "bower": "^1.8.8", "chai": "^3.4.1", "css-loader": "^0.23.1", "del": "^2.0.1", @@ -108,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", @@ -117,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", @@ -135,10 +139,10 @@ "less-loader": "^2.2.2", "memory-fs": "^0.3.0", "node-libs-browser": "^1.0.0", - "node-sass": "^3.4.2", + "node-sass": "^4.13.1", "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", @@ -150,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 5a3a4b774b..f5073847e3 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 0000000000..997ce854c3 --- /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 0000000000..b27a8f395f --- /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 0000000000..1dd5b6a7e7 --- /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 0000000000..33a2ddd67f --- /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 6264cf60f3..8b468bce78 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/common/modals.css b/res/app/components/stf/common-ui/modals/common/modals.css index b98f8e3495..2569fad13d 100644 --- a/res/app/components/stf/common-ui/modals/common/modals.css +++ b/res/app/components/stf/common-ui/modals/common/modals.css @@ -6,6 +6,10 @@ padding-left: 5px; } +.stf-modal .bold { + font-weight: bold; +} + .stf-modal .modal-body { background: #fff; } 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 0000000000..166527c119 --- /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 0000000000..6ce25b01ad --- /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 0000000000..ca43b2aad7 --- /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 0000000000..529fb6bd28 --- /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 5b09337cdb..5413fb6d15 100644 --- a/res/app/components/stf/common-ui/modals/index.js +++ b/res/app/components/stf/common-ui/modals/index.js @@ -1,6 +1,12 @@ +/** +* 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, - require('./add-adb-key-modal').name + require('./add-adb-key-modal').name, + require('./save-log-modal').name ]) diff --git a/res/app/components/stf/common-ui/modals/save-log-modal/index.js b/res/app/components/stf/common-ui/modals/save-log-modal/index.js new file mode 100644 index 0000000000..87b821d2d6 --- /dev/null +++ b/res/app/components/stf/common-ui/modals/save-log-modal/index.js @@ -0,0 +1,9 @@ +require('./save-log.css') + +require('angular-route') + +module.exports = angular.module('stf.save-log-message', [ + require('stf/common-ui/modals/common').name, + 'ngRoute' +]) + .factory('SaveLogService', require('./save-log-service')) diff --git a/res/app/components/stf/common-ui/modals/save-log-modal/save-log-service.js b/res/app/components/stf/common-ui/modals/save-log-modal/save-log-service.js new file mode 100644 index 0000000000..8111dd625c --- /dev/null +++ b/res/app/components/stf/common-ui/modals/save-log-modal/save-log-service.js @@ -0,0 +1,139 @@ +var FileSaver = require('file-saver') + +module.exports = + function SaveLogsServiceFactory($uibModal, $location, $route) { + var SaveLogService = {} + var logExtentension = ['json', 'log'] + var selectedExtension = logExtentension[0] + + function parseLogsToDefinedExtenstion(device, logExtension, lineLimitation) { + var lineLimiter = ((isNaN(lineLimitation)) ? device.length : lineLimitation) + var output = '' + if (device.length > 0) { + if (logExtension === 'log') { + for (let line = 0; line < lineLimiter; line++) { + output += [device[line].date, device[line].pid, + device[line].tag, device[line].priorityLabel, + device[line].message].join('\t') + '\n' + } + } else { + output = {'deviceOS': device[0].deviceLabel, + 'serial': device[0].serial, + 'logs': []} + for (let line = 0; line < lineLimiter; line++) { + output.logs.push({'date': device[line].date, + 'pid': device[line].pid, + 'tag': device[line].tag, + 'priorityLabel': device[line].priorityLabel, + 'message': device[line].message}) + } + } + } + return output + } + + function createSamplePresentation(device, logExtension, scope) { + var toSave = parseLogsToDefinedExtenstion(device, + logExtension, 4) + if (toSave.length > 0 || typeof toSave === 'object') { + switch(logExtension) { + case 'json': + scope.samplePresentation = JSON.stringify(toSave) + break + case 'log': + scope.samplePresentation = toSave + break + default: + scope.samplePresentation = toSave + break + } + } + } + + var ModalInstanceCtrl = function($scope, $uibModalInstance, device) { + $scope.ok = function() { + $uibModalInstance.close(true) + $route.reload() + } + + $scope.logExtentension = logExtentension + $scope.selectedExtension = $scope.logExtentension[0] + + createSamplePresentation(device, $scope.selectedExtension, $scope) + + $scope.second = function() { + $uibModalInstance.dismiss() + $location.path('/devices/') + } + + $scope.cancel = function() { + $uibModalInstance.dismiss('cancel') + } + + $scope.saveLogs = function() { + var parsedOutput = NaN + + switch(selectedExtension) { + case 'json': + parsedOutput = new Blob( + [JSON.stringify(parseLogsToDefinedExtenstion(device, selectedExtension))], + {type: 'application/json;charset=utf-8'}) + break + case 'log': + parsedOutput = new Blob( + [parseLogsToDefinedExtenstion(device, selectedExtension)], + {type: 'text/plain;charset=utf-8'}) + break + default: + // ToDo + // Add support for other types + // Ad-hoc save file as plain text + parsedOutput = new Blob( + [parseLogsToDefinedExtenstion(device, selectedExtension)], + {type: 'text/plain;charset=utf-8'}) + break + } + + if (typeof $scope.saveLogFileName === 'undefined' || + $scope.saveLogFileName.length === 0) { + FileSaver.saveAs(parsedOutput, + (window.location.href).split('/').pop() + '_logs.' + selectedExtension) + } + else { + FileSaver.saveAs(parsedOutput, + $scope.saveLogFileName + '.' + selectedExtension) + } + $uibModalInstance.dismiss('cancel') + } + + $scope.$watch('selectedExtension', function(newValue, oldValue) { + if (newValue !== oldValue) { + selectedExtension = newValue + createSamplePresentation(device, newValue, $scope) + } + }) + } + + SaveLogService.open = function(device, tryToReconnect) { + var modalInstance = $uibModal.open({ + template: require('./save-log.pug'), + controller: ModalInstanceCtrl, + resolve: { + device: function() { + return device + }, + tryToReconnect: function() { + return tryToReconnect + } + } + }) + + modalInstance.result.then(function() { + }, function() { + + }) + } + + + return SaveLogService + } diff --git a/res/app/components/stf/common-ui/modals/save-log-modal/save-log-spec.js b/res/app/components/stf/common-ui/modals/save-log-modal/save-log-spec.js new file mode 100644 index 0000000000..e105e65411 --- /dev/null +++ b/res/app/components/stf/common-ui/modals/save-log-modal/save-log-spec.js @@ -0,0 +1,11 @@ +describe('SaveLogService', function() { + + beforeEach(angular.mock.module(require('./').name)) + + it('should ...', inject(function() { + + //expect(SaveLogService.doSomething()).toEqual('something'); + + })) + +}) diff --git a/res/app/components/stf/common-ui/modals/save-log-modal/save-log.css b/res/app/components/stf/common-ui/modals/save-log-modal/save-log.css new file mode 100644 index 0000000000..da7e51ff1c --- /dev/null +++ b/res/app/components/stf/common-ui/modals/save-log-modal/save-log.css @@ -0,0 +1,9 @@ + +.save-log-textarea { + resize: none; + cursor: text; + font-family: Monaco, Menlo, Consolas, "Courier New", monospace; + font-size: 12px; + width: 100%; +} + diff --git a/res/app/components/stf/common-ui/modals/save-log-modal/save-log.pug b/res/app/components/stf/common-ui/modals/save-log-modal/save-log.pug new file mode 100644 index 0000000000..fcf2e6ce50 --- /dev/null +++ b/res/app/components/stf/common-ui/modals/save-log-modal/save-log.pug @@ -0,0 +1,32 @@ +.stf-save-log-message.stf-modal + .modal-header.dialog-header-errorX + button(type='button', ng-click='cancel()').close × + h4.modal-title.text-danger + i.fa.fa-save + .button-spacer + span(translate) Save Logs + .modal-body + table.table(ng-show='true') + tr.additional-modal + td(width='50%') + input(ng-model='saveLogFileName', type='text', placeholder='{{"File Name"|translate}}').input-sm.form-control + td(width='1%') + span . + td(width='20%') + select(ng-model='selectedExtension', data-ng-options='l for l in logExtentension') + + tr.additional-modal + tr.additional-modal + td(width='100%') + h5.modal-title.bold + span(translate) Sample of log format + tr.additional-modal + td(width='50%', height='30%') + textarea(ng-model='samplePresentation', type='text', placeholder='...', rows="4", readonly="readonly").save-log-textarea + td(width="5%", colspan="2") + i.fa.fa-file-text-o.fa-5x + + .modal-footer + button(ng-click='saveLogs()', ng-disabled='false', title='{{"Save Logs"|translate}}').btn.btn-xs.btn-danger-outline + i.fa.fa-save + span(translate) Save Logs 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 0000000000..f8609619b1 --- /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 0000000000..3199179ac3 --- /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 0000000000..23c2337eb2 --- /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 0000000000..1b4f7d7205 --- /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 0000000000..cd1fa8d8f6 --- /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 0000000000..1f3d8e587d --- /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/common-ui/table/table.css b/res/app/components/stf/common-ui/table/table.css index 727a87ee07..d07a1e22e2 100644 --- a/res/app/components/stf/common-ui/table/table.css +++ b/res/app/components/stf/common-ui/table/table.css @@ -99,6 +99,13 @@ margin-top: 0; } +tr.additional-modal, td.additional-modal { + border-top: 10px solid; + border-top-color: transparent; + border-bottom: 1px solid; + border-bottom-color: transparent; +} + @media only screen and (max-width: 800px) { .ng-table-responsive { border-bottom: 1px solid #999999; diff --git a/res/app/components/stf/device/device-service.js b/res/app/components/stf/device/device-service.js index a5672a46f8..5300990619 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 b55701a16c..4346dffdbf 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 0000000000..bd0b551c52 --- /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 0000000000..622da50c5c --- /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 0000000000..8f4a4c2dbc --- /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 0000000000..0966131da1 --- /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/logcat-table/logcat-table-directive.js b/res/app/components/stf/logcat-table/logcat-table-directive.js index fb285eaed3..b84900b4ad 100644 --- a/res/app/components/stf/logcat-table/logcat-table-directive.js +++ b/res/app/components/stf/logcat-table/logcat-table-directive.js @@ -1,7 +1,7 @@ var _ = require('lodash') module.exports = - function logcatTableDirective($rootScope, $timeout, LogcatService) { + function logcatTableDirective($rootScope, $timeout, LogcatService, SaveLogService) { return { restrict: 'E', replace: true, @@ -14,28 +14,67 @@ module.exports = var parent = element[0] var body = element.find('tbody')[0] var maxEntriesBuffer = 3000 - var numberOfEntries = 0 + var maxVisibleEntries = 100 + var deviceSerial = (window.location.href).split('/').pop() - function incrementNumberEntry() { - numberOfEntries++ - if (numberOfEntries > maxEntriesBuffer) { - scope.clearTable() + scope.started = checkLoggerServiceStatus(true) + scope.allowClean = checkAllowClean() + + function checkAllowClean() { + if (Object.keys(LogcatService.deviceEntries).includes(deviceSerial)) { + return LogcatService.deviceEntries[deviceSerial].allowClean + } + + return false + } + + function checkLoggerServiceStatus(loadLogs = false) { + var collectedLogs = [] + var isStarted = false + if (Object.keys($rootScope).includes('LogcatService')) { + LogcatService.deviceEntries = $rootScope.LogcatService.deviceEntries } + + if (Object.keys(LogcatService.deviceEntries).includes(deviceSerial)) { + collectedLogs = LogcatService.deviceEntries[deviceSerial].logs + isStarted = LogcatService.deviceEntries[deviceSerial].started + } + + if (loadLogs) { + restoreLogs(collectedLogs) + } + return isStarted + } + + function limitVisibleEntries() { + var limiter = '' + if (maxVisibleEntries > maxEntriesBuffer) { + limiter = maxEntriesBuffer + } else { + limiter = maxVisibleEntries + } + + if (element.find('tbody')[0].rows.length > limiter) { + removeFirstLogTableEntry() + } + } + + function removeFirstLogTableEntry() { + element.find('tbody')[0].deleteRow(0) } LogcatService.addEntryListener = function(entry) { - incrementNumberEntry() - addRow(body, entry) + if (deviceSerial === entry.serial) { + limitVisibleEntries() + if (LogcatService.deviceEntries[deviceSerial].logs.length > maxEntriesBuffer) { + LogcatService.deviceEntries[deviceSerial].logs.shift() + } + addRow(body, entry) + } } LogcatService.addFilteredEntriesListener = function(entries) { - clearTable() - //var fragment = document.createDocumentFragment() - _.each(entries, function(entry) { - // TODO: This is not adding all the entries after first scope creation - incrementNumberEntry() - addRow(body, entry, true) - }) + checkLoggerServiceStatus() } function shouldAutoScroll() { @@ -65,11 +104,6 @@ module.exports = var newRow = rowParent.insertRow(-1) newRow.classList.add('log-' + data.priorityLabel) - - //newRow.insertCell(-1) - // .appendChild(document.createTextNode(LogcatService.numberOfEntries)) - //newRow.insertCell(-1) - // .appendChild(document.createTextNode(data.deviceLabel)) newRow.insertCell(-1) .appendChild(document.createTextNode(data.priorityLabel)) newRow.insertCell(-1) @@ -79,8 +113,6 @@ module.exports = .appendChild(document.createTextNode(data.pid)) newRow.insertCell(-1) .appendChild(document.createTextNode(data.tid)) - //newRow.insertCell(-1) - // .appendChild(document.createTextNode(data.app)) newRow.insertCell(-1) .appendChild(document.createTextNode(data.tag)) } @@ -101,10 +133,74 @@ module.exports = scope.clearTable = function() { LogcatService.clear() - numberOfEntries = 0 clearTable() } + function restoreLogs(collectedLogs) { + clearTable() + + var startFrom = 0 + if (collectedLogs.length - maxVisibleEntries >= 0) { + startFrom = collectedLogs.length - maxVisibleEntries + } + + for (var logLine = startFrom; logLine < collectedLogs.length; logLine++) { + if (deviceSerial === collectedLogs[logLine].serial) { + addRow(body, collectedLogs[logLine], true) + } + } + } + + /** + * Validate filter.data object value and assign bordercolor to red if value + * doesn't match regex(pattern): + * - HH:mm:ss.SSS + * - H:mm:ss.SSS + * - :mm:SS.SSS + * - mm:ss.SSS + * - m:ss.SSS + * -... combinations + * in other case colour will be set to default. + * + * @param {event} event object + * @returns {None} NaN + */ + scope.validateDate = function(e) { + var pattern = ['^(?:(?:([0-1]?\\d|2[0-3]):)?(:[0-5]\\d|[0-5]\\d):|\\d)', + '?(:[0-5]\\d|[0-5]\\d{1,2})?(\\.[0-9]?\\d{0,2}|:[0-5]?\\d{0,1})|(\\d{0,2})'].join([]) + var regex = new RegExp(pattern, 'g') + var inputValue = event.srcElement.value + var matchArray = inputValue.match(regex) + var isTextValid = false + if (matchArray) { + matchArray.forEach(function(item, index) { + if (item === inputValue) { + isTextValid = true + event.srcElement.style.borderColor = '' + } + }) + } + + if (isTextValid === false) { + event.srcElement.style.borderColor = 'red' + } + } + + /** + * Show "Save Log" modal. + * + * @returns {None} NaN + */ + scope.saveLogs = function() { + var collectedLogs = [] + + if (Object.keys(LogcatService.deviceEntries).includes(deviceSerial)) { + collectedLogs = LogcatService.deviceEntries[deviceSerial].logs + } + + SaveLogService.open(collectedLogs, false) + } + scope.$on('$destroy', function() { parent.removeEventListener('scroll', throttledScrollListener) }) diff --git a/res/app/components/stf/logcat/logcat-service.js b/res/app/components/stf/logcat/logcat-service.js index ff36ae6b74..07ae85911e 100644 --- a/res/app/components/stf/logcat/logcat-service.js +++ b/res/app/components/stf/logcat/logcat-service.js @@ -4,7 +4,6 @@ var _s = require('underscore.string') module.exports = function LogcatServiceFactory(socket, FilterStringService) { var service = {} service.started = false - service.numberOfEntries = 0 service.serverFilters = [ { @@ -14,7 +13,6 @@ module.exports = function LogcatServiceFactory(socket, FilterStringService) { ] service.filters = { - numberOfEntries: 0, entries: [ ], levelNumbers: [] @@ -48,8 +46,9 @@ module.exports = function LogcatServiceFactory(socket, FilterStringService) { 'priority' ]) - service.entries = [ - ] + service.deviceSerial = [] + + service.deviceEntries = {} service.logLevels = [ 'UNKNOWN', @@ -90,24 +89,56 @@ module.exports = function LogcatServiceFactory(socket, FilterStringService) { return data } + function deviceSerialExist(serial) { + if (service.deviceSerial !== serial) { + service.deviceSerial.push(serial) + } + } + + service.initDeviceLogCollector = function(serial) { + service.deviceEntries[serial] = { + logs: [], selectedLogLevel: 2, started: false, allowClean: false, filters: { + 'levelNumber': service.filters.levelNumbers, + 'message': '', + 'pid': '', + 'tid': '', + 'dateLabel': '', + 'date': '', + 'tag': '' + } + } + } + socket.on('logcat.entry', function(rawData) { - service.numberOfEntries++ - service.entries.push(enhanceEntry(rawData)) + deviceSerialExist(rawData.serial) + var TmpObject = enhanceEntry(rawData) + if (!Object.keys(service.deviceEntries).includes(rawData.serial)) { + service.deviceEntries[rawData.serial] = {logs: [], selectedLogLevel: 2, started: true} + } + + service.deviceEntries[rawData.serial].logs.push(enhanceEntry(TmpObject)) if (typeof (service.addEntryListener) === 'function') { if (filterLine(rawData)) { + rawData.logsSerial = service.deviceSerial service.addEntryListener(rawData) } } }) service.clear = function() { - service.numberOfEntries = 0 - service.entries = [] + var devSerial = (window.location.href).split('/').pop() + if (Object.keys(service.deviceEntries).includes(devSerial)) { + service.deviceEntries[devSerial].logs = [] + } } service.filters.filterLines = function() { - service.filters.entries = _.filter(service.entries, filterLine) + var devSerial = (window.location.href).split('/').pop() + + if (Object.keys(service.deviceEntries).includes(devSerial)) { + service.filters.entries = _.filter(service.deviceEntries[devSerial].logs.entries, filterLine) + } if (typeof (service.addFilteredEntriesListener) === 'function') { service.addFilteredEntriesListener(service.filters.entries) @@ -115,13 +146,14 @@ module.exports = function LogcatServiceFactory(socket, FilterStringService) { } function filterLine(line) { - var enabled = true - var filters = service.filters - var matched = true - if (enabled) { - if (!_.isEmpty(filters.priority)) { - matched &= line.priority >= filters.priority.number + var devSerial = line.serial + var filters = service.deviceEntries[devSerial].filters + + if (typeof filters !== 'undefined') { + + if (!_.isEmpty(filters.priority.toString())) { + matched &= line.priority >= filters.priority } if (!_.isEmpty(filters.date)) { matched &= FilterStringService.filterString(filters.date, line.dateLabel) @@ -138,12 +170,9 @@ module.exports = function LogcatServiceFactory(socket, FilterStringService) { if (!_.isEmpty(filters.message)) { matched &= FilterStringService.filterString(filters.message, line.message) } - } else { - matched = true } return matched } - return service } diff --git a/res/app/components/stf/tokens/access-token-service.js b/res/app/components/stf/tokens/access-token-service.js index 214aa8cfed..00a4e388bc 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 d547328d95..899bfb36ea 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 0c9e449f32..9bba785c35 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 0000000000..7e8a57ae1f --- /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 0000000000..ef6df566f3 --- /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 0000000000..b4f6b5f5d1 --- /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 0000000000..394bc76170 --- /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 e0a3d550b0..c013cfe93b 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 5a5db16497..1fccfdf702 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/control-panes/device-control/device-control-controller.js b/res/app/control-panes/device-control/device-control-controller.js index 6424666363..3705de7399 100644 --- a/res/app/control-panes/device-control/device-control-controller.js +++ b/res/app/control-panes/device-control/device-control-controller.js @@ -1,7 +1,7 @@ var _ = require('lodash') module.exports = function DeviceControlCtrl($scope, DeviceService, GroupService, - $location, $timeout, $window, $rootScope) { + $location, $timeout, $window, $rootScope, LogcatService) { $scope.showScreen = true @@ -9,7 +9,18 @@ module.exports = function DeviceControlCtrl($scope, DeviceService, GroupService, $scope.groupDevices = $scope.groupTracker.devices + $scope.$on('$locationChangeStart', function(event, next, current) { + $scope.LogcatService = LogcatService + $rootScope.LogcatService = LogcatService + }) + $scope.kickDevice = function(device) { + if (Object.keys(LogcatService.deviceEntries).includes(device.serial)) { + LogcatService.deviceEntries[device.serial].allowClean = true + } + + $scope.LogcatService = LogcatService + $rootScope.LogcatService = LogcatService if (!device || !$scope.device) { alert('No device found') diff --git a/res/app/control-panes/logs/logs-controller.js b/res/app/control-panes/logs/logs-controller.js index 86f91b9836..c850500021 100644 --- a/res/app/control-panes/logs/logs-controller.js +++ b/res/app/control-panes/logs/logs-controller.js @@ -1,20 +1,101 @@ -module.exports = function LogsCtrl($scope, LogcatService) { +module.exports = function LogsCtrl($scope, $rootScope, $routeParams, LogcatService) { - $scope.started = LogcatService.started + var deviceSerial = $routeParams.serial + var cleanDevice = (window.location.href).split('/').pop() + cleanDeviceSettings() + $scope.started = checkLogBtnStatus() === null ? false : checkLogBtnStatus() $scope.filters = {} $scope.filters.levelNumbers = LogcatService.filters.levelNumbers LogcatService.filters.filterLines() + restoreFilters() + setFiltersPriority() + + function cleanDeviceSettings() { + if (Object.keys($rootScope).includes('LogcatService')) { + LogcatService.deviceEntries = $rootScope.LogcatService.deviceEntries + } + + if (Object.keys(LogcatService.deviceEntries).includes(deviceSerial)) { + if (LogcatService.deviceEntries[deviceSerial].allowClean) { + delete LogcatService.deviceEntries[deviceSerial] + if ($scope.control !== null) { + $scope.control.stopLogcat() + } + } + } + } + + function setFiltersPriority() { + if (Object.keys(LogcatService.deviceEntries).includes(deviceSerial)) { + $scope.filters.priority = $scope.filters.levelNumbers[ + LogcatService.deviceEntries[deviceSerial].selectedLogLevel - 2] + } else { + if ($scope.started) { + $scope.filters.priority = $scope.filters.levelNumbers[0] + } + } + } + + function restoreFilters() { + if (Object.keys(LogcatService.deviceEntries).includes(deviceSerial)) { + Object.keys(LogcatService.deviceEntries[deviceSerial].filters).forEach(function(entry) { + if ('filter.' + entry !== 'filter.priority') { + $scope.filters[entry] = LogcatService.deviceEntries[deviceSerial].filters[entry] + } else { + setFiltersPriority() + } + }) + } + } + + function checkLogBtnStatus() { + if (Object.keys(LogcatService.deviceEntries).includes(deviceSerial)) { + if ($scope !== null && $scope.device !== null) { + if($scope.device.logs_enabled && LogcatService.deviceEntries[deviceSerial].started) { + return LogcatService.deviceEntries[deviceSerial].started + } + } + } + return null + } + $scope.$watch('started', function(newValue, oldValue) { + if (!Object.keys(LogcatService.deviceEntries).includes(deviceSerial)) { + LogcatService.deviceEntries[deviceSerial] = {logs: [], selectedLogLevel: 2, started: false, + filters: { + 'message': '', + 'pid': '', + 'tid': '', + 'dateLabel': '', + 'date': '', + 'tag': '', + 'priority': '', + } + } + } + if (newValue !== oldValue) { - LogcatService.started = newValue - if (newValue) { + LogcatService.deviceEntries[deviceSerial].started = newValue + + if (LogcatService.deviceEntries[deviceSerial].started) { $scope.control.startLogcat([]).then(function() { }) + + LogcatService.deviceEntries[deviceSerial].started = true + $scope.device.logs_enabled = true + setFiltersPriority() + } else { + if (Object.keys(LogcatService.deviceEntries).includes(deviceSerial)) { + LogcatService.deviceEntries[deviceSerial].started = false + } + + LogcatService.deviceEntries[deviceSerial].started = false + $scope.device.logs_enabled = false $scope.control.stopLogcat() } } @@ -22,19 +103,78 @@ module.exports = function LogsCtrl($scope, LogcatService) { window.onbeforeunload = function() { if ($scope.control) { + for(var i = 0; i < LogcatService.deviceEntries.length; i++) { + if(LogcatService.deviceEntries[i] === deviceSerial) { + LogcatService.deviceEntries.splice(i, 1) + } + } + LogcatService.deviceEntries[deviceSerial].started = false $scope.control.stopLogcat() } } $scope.clear = function() { - LogcatService.clear() + var deviceSerial = (window.location.href).split('/').pop() + if (Object.keys(LogcatService.deviceEntries).includes(deviceSerial)) { + for (var i = LogcatService.deviceSerial.length - 1; i >= 0; i++) { + if (LogcatService.deviceSerial[i] === deviceSerial) { + LogcatService.deviceSerial.splice(i, 1) + } + } + } } function defineFilterWatchers(props) { angular.forEach(props, function(prop) { $scope.$watch('filters.' + prop, function(newValue, oldValue) { if (!angular.equals(newValue, oldValue)) { + var deviceSerial = (window.location.href).split('/').pop() LogcatService.filters[prop] = newValue + if (!Object.keys(LogcatService.deviceEntries).includes(deviceSerial)) { + LogcatService.initDeviceLogCollector(deviceSerial) + } + + var transformedInput = '' + switch('filters.' + prop) { + case 'filters.priority': + case 'filters.levelNumber': + if (newValue !== null && !isNaN(newValue.number)) { + LogcatService.deviceEntries[deviceSerial].selectedLogLevel = newValue.number + $scope.filters.priority = $scope.filters.levelNumbers[ + LogcatService.deviceEntries[deviceSerial].selectedLogLevel - 2] + transformedInput = LogcatService.deviceEntries[deviceSerial].selectedLogLevel + } + break + case 'filters.pid': + transformedInput = newValue.replace(/[^0-9:]/g, '') + if (transformedInput !== newValue) { + $scope.filters.pid = transformedInput + } + break + case 'filters.tid': + transformedInput = newValue.replace(/[^0-9]/g, '') + if (transformedInput !== newValue) { + $scope.filters.tid = transformedInput + } + break + default: + transformedInput = newValue + } + + // Exclude Debug Level info + if (prop !== 'levelNumber') { + LogcatService.deviceEntries[deviceSerial].filters[prop] = transformedInput + } + + LogcatService.filters[prop] = transformedInput + + // Check if scope is defined + if ($scope !== 'undefined') { + setFiltersPriority() + } + + LogcatService.deviceEntries[deviceSerial].allowClean = false + LogcatService.allowClean = false } }) }) diff --git a/res/app/control-panes/logs/logs-spec.js b/res/app/control-panes/logs/logs-spec.js index a07264a407..e8153c0177 100644 --- a/res/app/control-panes/logs/logs-spec.js +++ b/res/app/control-panes/logs/logs-spec.js @@ -6,6 +6,9 @@ describe('LogsCtrl', function() { beforeEach(inject(function($rootScope, $controller) { scope = $rootScope.$new() + if (Object.keys($rootScope.LogcatService).length > 0) { + scope.deviceEntries = $rootScope.LogcatService + } ctrl = $controller('LogsCtrl', {$scope: scope}) })) diff --git a/res/app/control-panes/logs/logs.pug b/res/app/control-panes/logs/logs.pug index 4ae8afb6d2..b44052fed3 100644 --- a/res/app/control-panes/logs/logs.pug +++ b/res/app/control-panes/logs/logs.pug @@ -9,10 +9,10 @@ span(ng-if='started') {{"Stop"|translate}} span(ng-if='!started') {{"Get"|translate}} td(width='6%') - select(ng-model='filters.priority', ng-options='l.name for l in filters.levelNumbers') - option(value='', disabled, selected) {{"Level"|translate}} + select(ng-model='filters.priority', data-ng-options='l.name for l in filters.levelNumbers') + option(value='') {{"Logcat Level"|translate}} td(width='10%') - input(ng-model='filters.date', type='text', placeholder='{{"Time"|translate}}').input-sm.form-control + input(ng-model='filters.date', type='text', placeholder='{{"Time"|translate}}', ng-keyup='validateDate(event=$event)').input-sm.form-control td(width='8%', ng-if='$root.platform == "native"') input(ng-model='filters.pid', type='text', placeholder='{{"PID"|translate}}').input-sm.form-control td(width='8%', ng-if='$root.platform == "native"') @@ -25,5 +25,9 @@ button(ng-click='clearTable()', ng-disabled='false', title='{{"Clear"|translate}}').btn.btn-xs.btn-danger-outline i.fa.fa-trash-o span(translate) Clear + td(width='0') + button(ng-click='saveLogs()', ng-disabled='false', title='{{"Save Logs"|translate}}').btn.btn-xs.btn-danger-outline + i.fa.fa-save + span(translate) Save Logs logcat-table(add-row='lastEntry') diff --git a/res/app/device-list/column/device-column-service.js b/res/app/device-list/column/device-column-service.js index 96dafd305a..1dd2b58faf 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 8f4323a581..e1aa0fdf8c 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/details/device-list-details-directive.js b/res/app/device-list/details/device-list-details-directive.js index 2c77b9caad..50dee58ed4 100644 --- a/res/app/device-list/details/device-list-details-directive.js +++ b/res/app/device-list/details/device-list-details-directive.js @@ -10,6 +10,7 @@ module.exports = function DeviceListDetailsDirective( , DeviceService , LightboxImageService , StandaloneService +, LogcatService ) { return { restrict: 'E' @@ -34,6 +35,11 @@ module.exports = function DeviceListDetailsDirective( function kickDevice(device, force) { + LogcatService.allowClean = true + if (Object.keys(LogcatService.deviceEntries).includes(device.serial)) { + LogcatService.deviceEntries[device.serial].allowClean = true + } + $rootScope.LogcatService = LogcatService return GroupService.kick(device, force).catch(function(e) { alert($filter('translate')(gettext('Device cannot get kicked from the group'))) throw new Error(e) @@ -41,6 +47,7 @@ module.exports = function DeviceListDetailsDirective( } function inviteDevice(device) { + $rootScope.usedDevices.push(device.serial) return GroupService.invite(device).then(function() { scope.$digest() }) diff --git a/res/app/device-list/device-list-controller.js b/res/app/device-list/device-list-controller.js index f14109e45c..afcff67f2f 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 084ff4544a..1ab79335a7 100644 --- a/res/app/device-list/icons/device-list-icons-directive.js +++ b/res/app/device-list/icons/device-list-icons-directive.js @@ -6,6 +6,8 @@ module.exports = function DeviceListIconsDirective( , DeviceColumnService , GroupService , StandaloneService +, LogcatService +, $rootScope ) { function DeviceItem() { return { @@ -83,15 +85,17 @@ module.exports = function DeviceListIconsDirective( name.classList.remove('state-available') } - if (device.usable) { + if (device.usable && !device.using) { a.href = '#!/control/' + device.serial li.classList.remove('device-is-busy') } + else if (device.using && device.usable) { + a.href = '#!/control/' + device.serial + } else { a.removeAttribute('href') li.classList.add('device-is-busy') } - return li } } @@ -119,6 +123,11 @@ module.exports = function DeviceListIconsDirective( function kickDevice(device, force) { + if (Object.keys(LogcatService.deviceEntries).includes(device.serial)) { + LogcatService.deviceEntries[device.serial].allowClean = true + } + LogcatService.allowClean = true + $rootScope.LogcatService = LogcatService return GroupService.kick(device, force).catch(function(e) { alert($filter('translate')(gettext('Device cannot get kicked from the group'))) throw new Error(e) @@ -159,8 +168,10 @@ module.exports = function DeviceListIconsDirective( } if (device.using) { - kickDevice(device) - e.preventDefault() + 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 bd8c6f6cee..ac47dda8ee 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 0000000000..d86315df8d --- /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 0000000000..f6a9c4d616 --- /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 0000000000..7a109cee86 --- /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 0000000000..ad18cf0da6 --- /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 0000000000..ca9608e8b0 --- /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 0000000000..08ca3973e5 --- /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 0000000000..1638c61941 --- /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 89f72ea5c6..88ab1128c1 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 b053b9f8df..82fac7e0cf 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) { +/** +* 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' @@ -7,11 +20,29 @@ module.exports = function MenuCtrl($scope, $rootScope, SettingsService, SettingsService.bind($rootScope, { target: 'platform', - defaultValue: 'native' + defaultValue: 'native', + deviceEntries: LogcatService.deviceEntries }) $scope.$on('$routeChangeSuccess', function() { $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 17b83e8cad..49a241a008 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 0000000000..0d539a0c89 --- /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 0000000000..b6371fc664 --- /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 0000000000..11ff8c089f --- /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 0000000000..a7f90f4d4d --- /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 0000000000..3b6a82679c --- /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 0000000000..3f10f0c48d --- /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 0000000000..24c8396850 --- /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 0000000000..bbc20d5e70 --- /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 0000000000..6c8a940b90 --- /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 0000000000..7f4e59d293 --- /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 0000000000..3e2cc4fa7b --- /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 26a26a96b3..0fa05bdb28 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 f2df94da8d..5033197a48 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 0000000000..c250317f3c --- /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 0000000000..0a39d331c2 --- /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 0000000000..54b1bb41f8 --- /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 0000000000..f102b00135 --- /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 0000000000..3dbee0f5ef --- /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 0000000000..395870181e --- /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 0000000000..6c9cf63192 --- /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 0000000000..cf2b9023e5 --- /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 0000000000..10620dbf3b --- /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 0000000000..2810d22cba --- /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 0000000000..9c6aae4dc2 --- /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 8fc745252a..ec0ddd388e 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 5e5b7a6522..c1fb719d10 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 49877d9806..5be17fa305 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 d157fc129b..3552934531 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 0000000000..7991ce906c --- /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 0000000000..e8e460eeec --- /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 0000000000..a437dae8a9 --- /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 0000000000..76fd1a543a --- /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 0000000000..f5e43d546c --- /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 0000000000..00d201c4e7 --- /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/app/views/rejected-email.pug b/res/app/views/rejected-email.pug new file mode 100644 index 0000000000..920c065655 --- /dev/null +++ b/res/app/views/rejected-email.pug @@ -0,0 +1,10 @@ +doctype html +html + head + meta(charset='utf-8') + base(href='/') + title("STF") + body + Missing or rejected email address + a(href='/auth/oauth/') + Retry diff --git a/res/auth/ldap/scripts/signin/index.js b/res/auth/ldap/scripts/signin/index.js index 0e6d9fdef7..4525ac9711 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 25aee8d4d2..dd44c24766 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 b73d4d44e1..125f87688d 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 2f5afe3c1d..6becbb50fe 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 70ce9011c6..410c4d35a1 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 1baa352ad4..0e5076f97f 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 cb6429a00d..dd2fab0733 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 bfc9224d1e..86e2f447a6 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 94e473f9c6..da41454ecc 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,32 +321,32 @@ msgstr "Développeur" msgid "Device" msgstr "Terminal" +#: app/control-panes/info/info.html:1 +msgid "Device Photo" +msgstr "Photos du Terminal" + +#: app/control-panes/automation/device-settings/device-settings.html:1 +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:122 +#: 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:38 +#: 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:39 +#: 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/control-panes/info/info.html:1 -msgid "Device Photo" -msgstr "Photos du Terminal" - -#: app/control-panes/automation/device-settings/device-settings.html:1 -msgid "Device Settings" -msgstr "Paramètres du Terminal" - #: 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 ab17d87964..5848c05bc1 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,32 +318,32 @@ 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" +#: app/control-panes/info/info.html:1 +msgid "Device Photo" msgstr "" -#: app/components/stf/device/device-info-filter/index.js:38 -msgid "Device is not present anymore for some reason." +#: app/control-panes/automation/device-settings/device-settings.html:1 +msgid "Device Settings" msgstr "" -#: app/components/stf/device/device-info-filter/index.js:39 -msgid "Device is present but offline." +#: 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/control-panes/info/info.html:1 -msgid "Device Photo" +#: app/components/stf/device/device-info-filter/index.js:40 +msgid "Device is not present anymore for some reason." msgstr "" -#: app/control-panes/automation/device-settings/device-settings.html:1 -msgid "Device Settings" +#: 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/control-panes/info/info.html:1 -msgid "Orientation" +#: app/device-list/column/device-column-service.js:205 +msgid "OpenGL ES version" msgstr "" #: app/control-panes/info/info.html:1 -#: app/device-list/column/device-column-service.js:55 -msgid "OS" +msgid "Orientation" 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,28 +832,32 @@ 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/control-panes/info/info.html:1 -msgid "Physical Device" +#: app/device-list/column/device-column-service.js:223 +msgid "Phone IMSI" msgstr "" -#: app/control-panes/logs/logs.html:1 -msgid "PID" +#: app/control-panes/info/info.html:1 +msgid "Physical Device" msgstr "" #: app/control-panes/info/info.html:1 @@ -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 0000000000..f47abb6ca4 --- /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 de4276251c..7e701ab504 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","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","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 d5a5b1a731..558b13d2c5 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","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","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.ja.json b/res/common/lang/translations/stf.ja.json index e1d4811ac3..08754d1dc7 100644 --- a/res/common/lang/translations/stf.ja.json +++ b/res/common/lang/translations/stf.ja.json @@ -1 +1 @@ -{"ja":{"-":"-","A new version of STF is available":"STFの新しいバージョンがリリースされました","ABI":"ABI","AC":"AC","Access Tokens":"アクセストークン","Account":"アカウント","Action":"アクション","Actions":"アクション","Activity":"アクティビティ","ADB Keys":"ADBキー","Add":"追加","Add ADB Key":"ADBキーを追加","Add Key":"キー追加","Add the following ADB Key to STF?":"STFに下記のADBキーを追加しますか?","Admin mode has been disabled.":"管理モードは無効になりました。","Admin mode has been enabled.":"管理モードは有効になりました。","Advanced":"高度機能","Advanced Input":"高度な入力","Airplane Mode":"機内モード","App Store":"アプリストア","App Upload":"アプリアップロード","Apps":"アプリ","Are you sure you want to reboot this device?":"この端末を再起動しますか?","Automation":"自動化","Available":"利用可能","Back":"戻る","Battery":"バッテリー","Battery Health":"バッテリー健康状態","Battery Level":"バッテリーレベル","Battery Source":"バッテリー電力源","Battery Status":"バッテリー状態","Battery Temp":"バッテリー温度","Bluetooth":"Bluetooth","Browser":"ブラウザ","Busy":"貸し出し中","Busy Devices":"貸出し中","Camera":"カメラ","Cancel":"キャンセル","Cannot access specified URL":"指定されたURLはアクセスできません","Carrier":"キャリア","Category":"カテゴリー","Charging":"充電中","Check errors below":"下記エラーがありました","Clear":"クリア","Clipboard":"クリップボード","Cold":"コールド","Connected":"接続中","Connected successfully.":"接続できました。","Control":"リモート操作","Cookies":"クッキー","Cores":"コア数","CPU":"CPU","Customize":"カスタマイズ","D-pad Center":"D-padセンター","D-pad Down":"D-pad下","D-pad Left":"D-pad左","D-pad Right":"D-pad右","D-pad Up":"D-pad上","Dashboard":"ダッシュボード","Data":"データ","Date":"日付","Dead":"残量なし","Delete":"削除","Density":"表示密度","Details":"詳細","Developer":"開発者","Device":"デバイス","Device cannot get kicked from the group":"このデバイスはグループからキックできません。","Device is not present anymore for some reason.":"実機が見えなくなりました。","Device is present but offline.":"デバイスは接続されているが、オフラインになっています。","Device Photo":"実機写真","Device Settings":"実機設定","Device was disconnected":"デバイスへの接続が切れました","Device was kicked by automatic timeout.":"デバイスは自動タイムアウトにより切断されました。","Devices":"端末リスト","Disable WiFi":"無線LANを無効にする","Discharging":"放電中","Disconnected":"切断中","Display":"ディスプレー","Drop file to upload":"ここにファイルをドロップ","Dummy":"ダミー","Enable notifications":"通知を有効にする","Enable WiFi":"無線LANを有効にする","Encrypted":"暗号化","Error":"エラー","Error while getting data":"データ取得中にエラーが発生しました。","Error while reconnecting":"再接続時にエラーが発生しました","Ethernet":"イーサーネット","Executes remote shell commands":"リモートシェルコマンドを実行する","Failed to download file":"ファイルのダウンロードが失敗しした。","Fast Forward":"早送り","File Explorer":"エクスプローラー","Filter":"フィルター","Find Device":"実機を探す","Fingerprint":"指紋","FPS":"FPS","Frequency":"クロック","Full":"フル","General":"一般","Generate Access Token":"アクセストークン生成","Generate Login for VNC":"VNC用にログイン情報生成","Generate New Token":"新規トークン生成","Get":"取得","Get clipboard contents":"クリップボードの中身を取得する","Go Back":"戻る","Go Forward":"進む","Go to Device List":"端末リストへ","Good":"良い","Hardware":"ハードウェア","Health":"健康状態","Height":"高さ","Help":"ヘルプ","Hide Screen":"画面を非表しない","Home":"ホーム","Host":"ホスト","Hostname":"ホスト名","ICCID":"ICCID","ID":"ID","IMEI":"IMEI","Incorrect login details":"不正ログイン情報","Info":"情報","Inspect Device":"端末の要素検証","Inspecting is currently only supported in WebView":"要素の検証機能は、現在WebViewのみ対応","Inspector":"要素の検証","Installation canceled by user.":"インストールはユーザーによってキャンセルされました。","Installation failed due to an unknown error.":"インストールが未知のエラーで失敗しました。","Installation succeeded.":"インストールが完了しました。","Installation timed out.":"インストールがタイムアウトしました。","Installing app...":"アプリをインストール中...","Key":"キー","Keys":"認証キー","Landscape":"横","Language":"言語","Launch Activity":"アクティビティを起動する","Launching activity...":"アクティビティを起動中...","Level":"レベル","Local Settings":"ローカル設定","Location":"場所","Lock Rotation":"回転ロック","Logs":"ログ","Maintenance":"メンテナンス","Manage Apps":"アプリ管理","Manner Mode":"マナーモード","Manufacturer":"メーカー","Media":"メディア","Memory":"メモリー","Menu":"メニュー","Mobile":"モバイル","Mobile DUN":"モバイルDUN","Mobile High Priority":"モバイル最優先","Mobile MMS":"モバイルMMS","Mobile SUPL":"モバイルSUPL","Model":"機種名","More about Access Tokens":"アクセストークンについて","More about ADB Keys":"ADBキーについて","Mute":"音を消す","Name":"名称","Native":"Native","Navigation":"ブラウジング","Network":"ネットワーク","Next":"次","No":"いいえ","No access tokens":"アクセストークンはありません","No ADB keys":"ADBキーはありません","No clipboard data":"クリップボードデータはありません","No cookies to show":"クッキーはありません","No device screen":"画面が表示できません","No devices connected":"端末が接続されていません","No photo available":"写真はありません","No Ports Forwarded":"フォワードされたポートはありません","No screenshots taken":"キャプチャはありません","Normal Mode":"通常モード","Not Charging":"充電されていない","Notes":"注釈","Nothing to inspect":"要素の検証対象はありません","Notifications":"通知","Number":"番号","Offline":"オフライン","Oops!":"おっと!","Open":"開く","Orientation":"方向","OS":"OS","Over Voltage":"過電圧","Overheat":"過熱","Package":"パッケージ","Password":"パスワード","Permissions":"権限","Phone":"電話番号","Phone ICCID":"携帯ICCID","Phone IMEI":"携帯IMEI","Physical Device":"物理デバイス","PID":"PID","Place":"場所","Platform":"プラットホーム","Play/Pause":"再生/停止","Please enter a valid email":"有効なメールアドレスを入力してください","Please enter your email":"メールアドレスを入力してください","Please enter your LDAP username":"LDAPユーザー名を入力してください","Please enter your name":"お名前を入力してください","Please enter your password":"パスワードを入力してください","Please enter your Store password":"ストアのパスワードを入力してください","Please enter your Store username":"ストアのユーザ名を入力してください","Port":"ポート","Port Forwarding":"ポートフォワーディング","Portrait":"縦","Power":"電源","Power Source":"電力源","Preparing":"準備中","Press Back button":"戻るボタンを押す","Press Home button":"ホームボタンを押す","Press Menu button":"メニューボタンを押す","Previous":"前","Processing...":"処理中...","Product":"型番","Pushing app...":"アプリをプッシュ中...","RAM":"RAM","Ready":"利用可能","Reconnected successfully.":"正常に再接続しました。","Refresh":"更新","Released":"発売日","Reload":"再読込","Remote debug":"リモートデバッグ","Remove":"削除","Reset":"初期化","Reset all browser settings":"ブラウザの設定をリセットする","Reset Settings":"すべての設定をリセット","Restart Device":"端末を再起動","Retrieving the device screen has timed out.":"実機画面の取得はタイムアウトになりました。","Retry":"再試行","Rewind":"巻き戻す","Roaming":"ローミング","ROM":"ROM","Rotate Left":"左回りに回転","Rotate Right":"右回りに回転","Run":"実行","Run JavaScript":"JavaScript注入","Run the following on your command line to debug the device from your Browser":"次のコマンドをコマンドラインで実行しますと、お使いのブラウザより端末のデバッグができます。","Run the following on your command line to debug the device from your IDE":"次のコマンドをコマンドラインで実行しますと、お使いのIDEより端末のデバッグができます。","Run this command to copy the key to your clipboard":"次のコマンドを実行しますと、キーがコピーされます","Save ScreenShot":"スクリーンショットを保存する","Save...":"保存する...","Screen":"解像度","Screenshot":"キャプチャ","Screenshots":"キャプチャ","SD Card Mounted":"SDカード","SDK":"SDK","Search":"検索","Selects Next IME":"入力モードの切り替え","Serial":"シリアル","Server":"サーバー","Server error. Check log output.":"サーバーエラー。ログを確認してください。","Set":"設定","Set Cookie":"クッキー設定","Settings":"設定","Shell":"シェル","Show Screen":"画面を表示する","Sign In":"サインイン","Sign Out":"サインアウト","Silent Mode":"マナーモード","SIM":"SIM","Size":"サイズ","Socket connection was lost":"ソケットへの接続が切れました","Someone stole your device.":"誰かはデバイスを盗みました。","Special Keys":"特別なキー","Start/Stop Logging":"ログ取得の開始/停止","Status":"ステータス","Stop":"停止","Stop Using":"停止する","Store Account":"ストアアカウント","Sub Type":"サブタイプ","Switch Charset":"文字入力の切り替え","Tag":"タグ","Take Pageshot (Needs WebView running)":"ページ全体ショットを撮る(現在はWebViewのみ対応)","Take Screenshot":"スクリーンショットを撮る","Temperature":"温度","Text":"テキスト","The device will be unavailable for a moment.":"しばらく端末が利用できなくなります。","The existing package could not be deleted.":"既存のパッケージは削除できませんでした。","The URI passed in is invalid.":"渡されたURIは無効です。","TID":"TID","Time":"時刻","Tip:":"ヒント:","Title":"タイトル","Toggle Web/Native":"ウェブ/ネイティブを選択","Total Devices":"全機種","Try to reconnect":"再接続する","Type":"タイプ","Unauthorized":"権限外","Uninstall":"削除","Unknown":"未知","Unknown reason.":"未知。","Unlock Rotation":"回転アンロック","Unspecified Failure":"未定義の失敗","Upload failed":"アップロードが失敗しました","Upload From Link":"リンク先よりアップロードする","Upload unknown error":"アップロード未知エラー","Uploaded file is not valid":"アップロードされたファイル","Uploading...":"アップロード中...","Usable Devices":"利用可能","USB":"USB","Usb speed":"USB速度","Use":"利用する","User":"ユーザ","Username":"ユーザ名","Using":"利用中","Using Fallback":"フォールバックを使用中","Version":"バージョン","Version Update":"バージョンアップ","Vibrate Mode":"マナーモード(バイブON)","VNC":"VNC","Voltage":"電圧","Volume":"音量","Volume Down":"音量↓","Volume Up":"音量↑","Warning:":"注意:","Web":"Web","Width":"幅","WiFi":"無線LAN","WiMAX":"WiMAX","Wireless":"無線","X DPI":"X DPI","Y DPI":"Y DPI","Yes":"はい","You (or someone else) kicked the device.":"この実機はキックされました。"}} \ No newline at end of file +{"ja":{"-":"-","A new version of STF is available":"STFの新しいバージョンがリリースされました","ABI":"ABI","AC":"AC","Access Tokens":"アクセストークン","Account":"アカウント","Action":"アクション","Actions":"アクション","Activity":"アクティビティ","ADB Keys":"ADBキー","Add":"追加","Add ADB Key":"ADBキーを追加","Add Key":"キー追加","Add the following ADB Key to STF?":"STFに下記のADBキーを追加しますか?","Admin mode has been disabled.":"管理モードは無効になりました。","Admin mode has been enabled.":"管理モードは有効になりました。","Advanced":"高度機能","Advanced Input":"高度な入力","Airplane Mode":"機内モード","App Store":"アプリストア","App Upload":"アプリアップロード","Apps":"アプリ","Are you sure you want to reboot this device?":"この端末を再起動しますか?","Automation":"自動化","Available":"利用可能","Back":"戻る","Battery":"バッテリー","Battery Health":"バッテリー健康状態","Battery Level":"バッテリーレベル","Battery Source":"バッテリー電力源","Battery Status":"バッテリー状態","Battery Temp":"バッテリー温度","Bluetooth":"Bluetooth","Browser":"ブラウザ","Busy":"貸し出し中","Busy Devices":"貸出し中","Camera":"カメラ","Cancel":"キャンセル","Cannot access specified URL":"指定されたURLはアクセスできません","Carrier":"キャリア","Category":"カテゴリー","Charging":"充電中","Check errors below":"下記エラーがありました","Clear":"クリア","Clipboard":"クリップボード","Cold":"コールド","Connected":"接続中","Connected successfully.":"接続できました。","Control":"リモート操作","Cookies":"クッキー","Cores":"コア数","CPU":"CPU","Customize":"カスタマイズ","D-pad Center":"D-padセンター","D-pad Down":"D-pad下","D-pad Left":"D-pad左","D-pad Right":"D-pad右","D-pad Up":"D-pad上","Dashboard":"ダッシュボード","Data":"データ","Date":"日付","Dead":"残量なし","Delete":"削除","Density":"表示密度","Details":"詳細","Developer":"開発者","Device":"デバイス","Device cannot get kicked from the group":"このデバイスはグループからキックできません。","Device is not present anymore for some reason.":"実機が見えなくなりました。","Device is present but offline.":"デバイスは接続されているが、オフラインになっています。","Device Photo":"実機写真","Device Settings":"実機設定","Device was disconnected":"デバイスへの接続が切れました","Device was kicked by automatic timeout.":"デバイスは自動タイムアウトにより切断されました。","Devices":"端末リスト","Disable WiFi":"無線LANを無効にする","Discharging":"放電中","Disconnected":"切断中","Display":"ディスプレー","Drop file to upload":"ここにファイルをドロップ","Dummy":"ダミー","Enable notifications":"通知を有効にする","Enable WiFi":"無線LANを有効にする","Encrypted":"暗号化","Error":"エラー","Error while getting data":"データ取得中にエラーが発生しました。","Error while reconnecting":"再接続時にエラーが発生しました","Ethernet":"イーサーネット","Executes remote shell commands":"リモートシェルコマンドを実行する","Failed to download file":"ファイルのダウンロードが失敗しした。","Fast Forward":"早送り","File Explorer":"エクスプローラー","File Name":"ファイル名","Filter":"フィルター","Find Device":"実機を探す","Fingerprint":"指紋","FPS":"FPS","Frequency":"クロック","Full":"フル","General":"一般","Generate Access Token":"アクセストークン生成","Generate Login for VNC":"VNC用にログイン情報生成","Generate New Token":"新規トークン生成","Get":"取得","Get clipboard contents":"クリップボードの中身を取得する","Go Back":"戻る","Go Forward":"進む","Go to Device List":"端末リストへ","Good":"良い","Hardware":"ハードウェア","Health":"健康状態","Height":"高さ","Help":"ヘルプ","Hide Screen":"画面を非表しない","Home":"ホーム","Host":"ホスト","Hostname":"ホスト名","ICCID":"ICCID","ID":"ID","IMEI":"IMEI","Incorrect login details":"不正ログイン情報","Info":"情報","Inspect Device":"端末の要素検証","Inspecting is currently only supported in WebView":"要素の検証機能は、現在WebViewのみ対応","Inspector":"要素の検証","Installation canceled by user.":"インストールはユーザーによってキャンセルされました。","Installation failed due to an unknown error.":"インストールが未知のエラーで失敗しました。","Installation succeeded.":"インストールが完了しました。","Installation timed out.":"インストールがタイムアウトしました。","Installing app...":"アプリをインストール中...","Key":"キー","Keys":"認証キー","Landscape":"横","Language":"言語","Launch Activity":"アクティビティを起動する","Launching activity...":"アクティビティを起動中...","Level":"レベル","Local Settings":"ローカル設定","Location":"場所","Lock Rotation":"回転ロック","Logs":"ログ","Maintenance":"メンテナンス","Manage Apps":"アプリ管理","Manner Mode":"マナーモード","Manufacturer":"メーカー","Media":"メディア","Memory":"メモリー","Menu":"メニュー","Mobile":"モバイル","Mobile DUN":"モバイルDUN","Mobile High Priority":"モバイル最優先","Mobile MMS":"モバイルMMS","Mobile SUPL":"モバイルSUPL","Model":"機種名","More about Access Tokens":"アクセストークンについて","More about ADB Keys":"ADBキーについて","Mute":"音を消す","Name":"名称","Native":"Native","Navigation":"ブラウジング","Network":"ネットワーク","Next":"次","No":"いいえ","No access tokens":"アクセストークンはありません","No ADB keys":"ADBキーはありません","No clipboard data":"クリップボードデータはありません","No cookies to show":"クッキーはありません","No device screen":"画面が表示できません","No devices connected":"端末が接続されていません","No photo available":"写真はありません","No Ports Forwarded":"フォワードされたポートはありません","No screenshots taken":"キャプチャはありません","Normal Mode":"通常モード","Not Charging":"充電されていない","Notes":"注釈","Nothing to inspect":"要素の検証対象はありません","Notifications":"通知","Number":"番号","Offline":"オフライン","Oops!":"おっと!","Open":"開く","Orientation":"方向","OS":"OS","Over Voltage":"過電圧","Overheat":"過熱","Package":"パッケージ","Password":"パスワード","Permissions":"権限","Phone":"電話番号","Phone ICCID":"携帯ICCID","Phone IMEI":"携帯IMEI","Physical Device":"物理デバイス","PID":"PID","Place":"場所","Platform":"プラットホーム","Play/Pause":"再生/停止","Please enter a valid email":"有効なメールアドレスを入力してください","Please enter your email":"メールアドレスを入力してください","Please enter your LDAP username":"LDAPユーザー名を入力してください","Please enter your name":"お名前を入力してください","Please enter your password":"パスワードを入力してください","Please enter your Store password":"ストアのパスワードを入力してください","Please enter your Store username":"ストアのユーザ名を入力してください","Port":"ポート","Port Forwarding":"ポートフォワーディング","Portrait":"縦","Power":"電源","Power Source":"電力源","Preparing":"準備中","Press Back button":"戻るボタンを押す","Press Home button":"ホームボタンを押す","Press Menu button":"メニューボタンを押す","Previous":"前","Processing...":"処理中...","Product":"型番","Pushing app...":"アプリをプッシュ中...","RAM":"RAM","Ready":"利用可能","Reconnected successfully.":"正常に再接続しました。","Refresh":"更新","Released":"発売日","Reload":"再読込","Remote debug":"リモートデバッグ","Remove":"削除","Reset":"初期化","Reset all browser settings":"ブラウザの設定をリセットする","Reset Settings":"すべての設定をリセット","Restart Device":"端末を再起動","Retrieving the device screen has timed out.":"実機画面の取得はタイムアウトになりました。","Retry":"再試行","Rewind":"巻き戻す","Roaming":"ローミング","ROM":"ROM","Rotate Left":"左回りに回転","Rotate Right":"右回りに回転","Run":"実行","Run JavaScript":"JavaScript注入","Run the following on your command line to debug the device from your Browser":"次のコマンドをコマンドラインで実行しますと、お使いのブラウザより端末のデバッグができます。","Run the following on your command line to debug the device from your IDE":"次のコマンドをコマンドラインで実行しますと、お使いのIDEより端末のデバッグができます。","Run this command to copy the key to your clipboard":"次のコマンドを実行しますと、キーがコピーされます","Sample of log format":"ログ形式のサンプル","Save Logs":"ログを保存する","Save ScreenShot":"スクリーンショットを保存する","Save...":"保存する...","Screen":"解像度","Screenshot":"キャプチャ","Screenshots":"キャプチャ","SD Card Mounted":"SDカード","SDK":"SDK","Search":"検索","Selects Next IME":"入力モードの切り替え","Serial":"シリアル","Server":"サーバー","Server error. Check log output.":"サーバーエラー。ログを確認してください。","Set":"設定","Set Cookie":"クッキー設定","Settings":"設定","Shell":"シェル","Show Screen":"画面を表示する","Sign In":"サインイン","Sign Out":"サインアウト","Silent Mode":"マナーモード","SIM":"SIM","Size":"サイズ","Socket connection was lost":"ソケットへの接続が切れました","Someone stole your device.":"誰かはデバイスを盗みました。","Special Keys":"特別なキー","Start/Stop Logging":"ログ取得の開始/停止","Status":"ステータス","Stop":"停止","Stop Using":"停止する","Store Account":"ストアアカウント","Sub Type":"サブタイプ","Switch Charset":"文字入力の切り替え","Tag":"タグ","Take Pageshot (Needs WebView running)":"ページ全体ショットを撮る(現在はWebViewのみ対応)","Take Screenshot":"スクリーンショットを撮る","Temperature":"温度","Text":"テキスト","The device will be unavailable for a moment.":"しばらく端末が利用できなくなります。","The existing package could not be deleted.":"既存のパッケージは削除できませんでした。","The URI passed in is invalid.":"渡されたURIは無効です。","TID":"TID","Time":"時刻","Tip:":"ヒント:","Title":"タイトル","Toggle Web/Native":"ウェブ/ネイティブを選択","Total Devices":"全機種","Try to reconnect":"再接続する","Type":"タイプ","Unauthorized":"権限外","Uninstall":"削除","Unknown":"未知","Unknown reason.":"未知。","Unlock Rotation":"回転アンロック","Unspecified Failure":"未定義の失敗","Upload failed":"アップロードが失敗しました","Upload From Link":"リンク先よりアップロードする","Upload unknown error":"アップロード未知エラー","Uploaded file is not valid":"アップロードされたファイル","Uploading...":"アップロード中...","Usable Devices":"利用可能","USB":"USB","Usb speed":"USB速度","Use":"利用する","User":"ユーザ","Username":"ユーザ名","Using":"利用中","Using Fallback":"フォールバックを使用中","Version":"バージョン","Version Update":"バージョンアップ","Vibrate Mode":"マナーモード(バイブON)","VNC":"VNC","Voltage":"電圧","Volume":"音量","Volume Down":"音量↓","Volume Up":"音量↑","Warning:":"注意:","Web":"Web","Width":"幅","WiFi":"無線LAN","WiMAX":"WiMAX","Wireless":"無線","X DPI":"X DPI","Y DPI":"Y DPI","Yes":"はい","You (or someone else) kicked the device.":"この実機はキックされました。"}} \ No newline at end of file diff --git a/res/common/lang/translations/stf.ko_KR.json b/res/common/lang/translations/stf.ko_KR.json index 1b060b2f82..b12bcde89f 100644 --- a/res/common/lang/translations/stf.ko_KR.json +++ b/res/common/lang/translations/stf.ko_KR.json @@ -1 +1 @@ -{"ko_KR":{"-":"-","A new version of STF is available":"새 버전의 STF를 사용 가능 합니다","A package is already installed with the same name.":"동일한 패키지가 설치되어 있습니다.","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).":"이전에 설치된 패키지와 새로운 패키지의 서명이 다릅니다(또한 이전에 설치된 패키지 데이터가 삭제되지 않았습니다).","ABI":"ABI","AC":"AC","Access Tokens":"액세스 토큰","Account":"계정","Action":"동작","Actions":"동작","Activity":"액티비티","ADB Keys":"ADB 키","Add":"추가","Add ADB Key":"ADB 키 추가","Add Key":"키 추가","Add the following ADB Key to STF?":"다음 ADB 키를 STF에 추가 하시겠습니까?","Admin mode has been disabled.":"관리자 모드가 비활성화 되었습니다.","Admin mode has been enabled.":"관리자 모드가 활성화 되었습니다.","Advanced":"고급","Advanced Input":"고급 입력","Airplane Mode":"비행기 모드","App Store":"앱 스토어","App Upload":"앱 업로드","Apps":"앱","Are you sure you want to reboot this device?":"해당 단말기를 재시작 하시겠습니까?","Automation":"자동화","Available":"사용 가능","Back":"뒤로 가기","Battery":"배터리","Battery Health":"베터리 상태","Battery Level":"베터리 수준","Battery Source":"베터리 종류","Battery Status":"배터리 충전 상태","Battery Temp":"배터리 온도","Bluetooth":"블루투스","Browser":"브라우저","Busy":"점유중","Busy Devices":"사용 중인 단말기","Camera":"카메라","Cancel":"취소","Cannot access specified URL":"지정한 URL에 접근 할 수 없습니다","Carrier":"통신사","Category":"범주","Charging":"충전중","Check errors below":"아래 오류를 확인 하세요","Clear":"지우기","Clipboard":"클립보드","Cold":"양호","Connected":"연결","Connected successfully.":"연결했습니다","Control":"컨트롤 화면","Cookies":"쿠키","Cores":"코어 종류","CPU":"CPU","Customize":"사용자 지정","D-pad Center":"D-pad 가운데","D-pad Down":"D-pad 아래쪽","D-pad Left":"D-pad 왼쪽","D-pad Right":"D-pad 오른쪽","D-pad Up":"D-pad 위쪽","Dashboard":"대시보드","Data":"데이터","Date":"날짜","Dead":"정지","Delete":"삭제","Density":"해상도","Details":"세부 정보","Developer":"개발자 옵션","Device":"단말기","Device is not present anymore for some reason.":"더이상 단말기가 존재하지 않습니다.","Device is present but offline.":"장치가 연결되어 있지만 오프라인 상태입니다.","Device Photo":"단말기 사진","Device Settings":"단말기 설정","Device was disconnected":"연결이 끊어졌습니다","Device was kicked by automatic timeout.":"시간초과로 인해 단말기 사용이 종료되었습니다.","Devices":"단말기 리스트","Disable WiFi":"WiFi 비활성화","Discharging":"충전중이 아님","Disconnected":"연결 끊김","Display":"화면","Drop file to upload":"업로드 할 파일을 올려놓으세요","Dummy":"더미","Enable notifications":"알림 사용","Enable WiFi":"WiFi 활성화","Encrypted":"암호화","Error":"오류","Error while getting data":"데이터를 얻어오는데 실패했습니다","Error while reconnecting":"재연결이 실패 했습니다","Ethernet":"이더넷","Executes remote shell commands":"원격 쉘 명령을 실행합니다","Failed to download file":"파일을 다운로드 할 수 없습니다","Fast Forward":"빨리 감기","File Explorer":"파일 탐색기","Filter":"필터","Find Device":"장치 찾기","Fingerprint":"지문","FPS":"FPS","Frequency":"속도","Full":"전체","General":"일반","Generate Access Token":"액세스 토큰 생성","Generate Login for VNC":"VNC 로그인 생성","Generate New Token":"새로운 토큰 생성","Get":"시작","Get clipboard contents":"클립보드 내용을 가져옵니다","Go Back":"뒤로 이동","Go Forward":"앞으로 이동","Go to Device List":"단말기 목록으로 이동","Good":"양호","Hardware":"하드웨어","Health":"상태","Height":"높이","Help":"도움말","Hide Screen":"화면 숨김","Home":"홈","Host":"호스트","Hostname":"호스트이름","ICCID":"ICCID","ID":"ID","IMEI":"IMEI","Incorrect login details":"잘못된 로그인 정보","Info":"단말기 정보","Inspect Device":"단말기 검사","Inspecting is currently only supported in WebView":"검사는 웹뷰에서만 지원합니다","Inspector":"검사기","Installation canceled by user.":"사용자가 설치를 취소했습니다","Installation failed due to an unknown error.":"알 수 없는 오류로 설치가 실패했습니다","Installation succeeded.":"설치가 성공했습니다","Installation timed out.":"설치 시간 초과","Installing app...":"앱 설치중...","Key":"키","Keys":"키","Landscape":"가로","Language":"언어","Launch Activity":"액티비티 실행","Launching activity...":"액티비티 실행중...","Level":"수준","Local Settings":"로컬 설정","Location":"위치","Lock Rotation":"화면 잠금","Logs":"로그","Maintenance":"유지 관리","Make sure to copy your access token now. You won't be able to see it again!":"엑세스 토큰을 복사하세요. 다시 확인할 수 없습니다!","Manage Apps":"앱 관리","Manner Mode":"매너 모드","Manufacturer":"제조사","Media":"미디어","Memory":"메모리","Menu":"메뉴","Mobile":"모바일","Mobile DUN":"모바일 DUN","Mobile MMS":"모바일 MMS","Mobile SUPL":"모바일 SUPL","Model":"모델","More about Access Tokens":"좀 더 자세한 엑세스 토큰에 대해서 확인하기","More about ADB Keys":"좀 더 자세하게 ADB 키에 대해서 확인하기","Mute":"음소거","Name":"이름","Native":"네이티브","Navigation":"탐색","Network":"네트워크","Next":"다음","No":"아니오","No access tokens":"등록된 엑세스 토큰이 없습니다","No ADB keys":"등록된 ADB 키가 없습니다","No clipboard data":"클립보드 데이터가 없습니다","No cookies to show":"어떤 쿠키도 없습니다","No devices connected":"연결된 단말기가 없습니다","No photo available":"이용 가능한 사진이 없습니다","No Ports Forwarded":"포트 포워딩 설정이 없습니다","No screenshots taken":"저장된 스크린샷이 없습니다","Normal Mode":"표준 모드","Not Charging":"충전 안함","Notes":"메모","Notifications":"알림","Number":"전화번호","Offline":"오프라인","Oops!":"웁스!","Open":"열기","Orientation":"화면 방향","OS":"운영체제","Over Voltage":"과전압","Overheat":"과열","Package":"패키지","Password":"비밀번호","Permissions":"권한","Phone":"휴대폰","Phone ICCID":"휴대폰 ICCID","Phone IMEI":"휴대폰 IMEI","Physical Device":"물리 단말기","PID":"PID","Place":"위치","Platform":"플랫폼","Play/Pause":"재생/일시 중지","Please enter a valid email":"유효한 이메일 주소를 입력하세요","Please enter your email":"이메일 주소를 입력하세요","Please enter your LDAP username":"LDAP 사용자 이름을 입력하세요","Please enter your name":"이름을 입력하세요","Please enter your password":"비밀번호를 입력하세요","Please enter your Store password":"앱 스토어 비밀번호를 입력하세요","Please enter your Store username":"앱 스토어 아이디를 입력하세요","Port":"포트","Port Forwarding":"포트 포워딩","Portrait":"세로","Power":"전력","Power Source":"전력원","Preparing":"준비중","Press Back button":"뒤로가기 버튼을 누르세요","Press Home button":"홈 버튼을 누르세요","Press Menu button":"메뉴 버튼을 누르세요","Previous":"이전","Processing...":"처리중...","Product":"제품명","Pushing app...":"앱 전송중...","RAM":"RAM","Reconnected successfully.":"재연결이 성공했습니다","Refresh":"새로고침","Released":"릴리즈","Reload":"다시 로드","Remote debug":"원격 디버그","Remove":"제거","Reset":"초기화","Reset all browser settings":"모든 브라우저 설정 초기화","Reset Settings":"설정 초기화","Restart Device":"단말기 재시작","Retrieving the device screen has timed out.":"단말기 화면을 가져 오는 시간이 초과 되었습니다.","Retry":"재시도","Rewind":"되감기","Roaming":"로밍","ROM":"ROM","Rotate Left":"왼쪽으로 회전","Rotate Right":"오른쪽으로 회전","Run":"실행","Run JavaScript":"자바스크립트 실행","Run the following on your command line to debug the device from your IDE":"아래의 명령줄을 실행하여 IDE에서 디버그를 실행하세요","Save ScreenShot":"스크린 샷 저장","Save...":"저장...","Screen":"화면","Screenshot":"스크린 샷","Screenshots":"스크린 샷","SD Card Mounted":"SD카드 마운트","SDK":"SDK","Search":"검색","Serial":"일련 번호","Server":"서버","Server error. Check log output.":"서버 에러. 로그를 확인하세요","Set":"Set","Set Cookie":"쿠키 설정","Settings":"설정","Shell":"셸","Show Screen":"화면 표시","Sign In":"로그인","Sign Out":"로그아웃","Silent Mode":"음소거","SIM":"SIM","Size":"크기","Socket connection was lost":"소켓 연결이 끊겼습니다","Special Keys":"특수 키","Start/Stop Logging":"시작/종료 로깅","Status":"상태","Stop":"정지","Stop Using":"사용 종료","Store Account":"저장소 계정","Sub Type":"하위 유형","Switch Charset":"문자 집합 변경","Tag":"태그","Take Screenshot":"스크린샷 캡처","Temperature":"온도","Text":"텍스트","The device will be unavailable for a moment.":"이 단말기는 잠시동안 사용 할 수 없습니다.","The existing package could not be deleted.":"기존 패키지를 삭제 할 수 없습니다.","The new package couldn't be installed because the verification did not succeed.":"검증되지 않은 새로운 패키지는 설치 할 수 없습니다.","The new package couldn't be installed because the verification timed out.":"검증 시간이 초과하여 새로운 패키지를 설치 할 수 없습니다.","The new package couldn't be installed in the specified install location because the media is not available.":"미디어를 사용할 수 없어 지정한 위치에 새로운 패키지를 설치 할 수 없습니다.","The new package couldn't be installed in the specified install location.":"지정한 위치에 새로운 패키지를 설치 할 수 없습니다.","The new package failed because it contains a content provider with thesame authority as a provider already installed in the system.":"이미 동일한 패키지가 설치되어 있어 설치 할 수 없습니다.","The new package failed because the current SDK version is newer than that required by the package.":"SDK 버전이 높아 새로운 패키지를 설치 할 수 없습니다.","The new package failed because the current SDK version is older than that required by the package.":"SDK 버전이 낮아 새로운 패키지를 설치 할 수 없습니다.","The new package has an older version code than the currently installed package.":"새로운 패키지에 오래된 버전의 코드가 존재하여 설치 할 수 없습니다.","The new package is assigned a different UID than it previously held.":"새로운 패키지는 이전 패키지와 다른 UID가 할당 됐습니다.","The package archive file is invalid.":"패키지 아카이브 파일이 잘못 되었습니다.","The package is already installed.":"패키지가 이미 설치 되어 있습니다.","The parser encountered a bad or missing package name in the manifest.":"매니페스트에 잘못되거나 누락된 패키지 이름을 발견했습니다.","The parser encountered a bad shared user id name in the manifest.":"매니페스트에 잘못된 아이디나 이름을 발견했습니다.","The parser encountered an unexpected exception.":"예상하지 못한 예외가 발생하였습니다.","The parser encountered some structural problem in the manifest.":"매니페스트에 몇 가지 구조적인 문제가 발생 했습니다.","The parser found inconsistent certificates on the files in the .apk.":".apk 파일에서 일치하지 않은 인증서를 발견 했습니다.","The requested shared user does not exist.":"요청된 공용 사용자가 존재하지 않습니다.","The URI passed in is invalid.":"URL이 잘못 전달 됐습니다.","TID":"TID","Time":"시간","Tip:":"팁","Title":"제목","Toggle Web/Native":"웹/네이티브 전환","Total Devices":"총 단말기 수","translate":"번역","Try to reconnect":"다시 연결","Type":"유형","Unauthorized":"미인증","Uninstall":"설치 제거","Unknown":"알 수 없음","Unknown reason.":"알 수 없는 이유","Unlock Rotation":"회전 잠금 해제","Unspecified Failure":"지정되지 않은 오류","Upload failed":"업로드 실패","Upload From Link":"링크로 업로드","Upload unknown error":"업로드시 알수 없는 에러가 발생하였습니다","Uploaded file is not valid":"업로드된 파일이 유효하지 않습니다","Uploading...":"업로드중...","Usable Devices":"사용 가능한 단말기","USB":"USB","Usb speed":"Usb 속도","Use":"사용","User":"사용자","Username":"사용자 이름","Using":"사용중","Using Fallback":"대체","Version":"버전","Version Update":"버전 업데이트","Vibrate Mode":"진동","VNC":"VNC","Voltage":"전압","Volume":"음량","Volume Down":"음량 줄이기","Volume Up":"음량 올리기","Warning:":"경고","Web":"웹","Width":"너비","WiFi":"WiFi","WiMAX":"WiMAX","Wireless":"무선","X DPI":"X DPI","Y DPI":"Y DPI","Yes":"네","You (or someone else) kicked the device.":"당신(혹은 다른 누군가)이 단말기를 사용 종료 하였습니다."}} \ No newline at end of file +{"ko_KR":{"-":"-","A new version of STF is available":"새 버전의 STF를 사용 가능 합니다","A package is already installed with the same name.":"동일한 패키지가 설치되어 있습니다.","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).":"이전에 설치된 패키지와 새로운 패키지의 서명이 다릅니다(또한 이전에 설치된 패키지 데이터가 삭제되지 않았습니다).","ABI":"ABI","AC":"AC","Access Tokens":"액세스 토큰","Account":"계정","Action":"동작","Actions":"동작","Activity":"액티비티","ADB Keys":"ADB 키","Add":"추가","Add ADB Key":"ADB 키 추가","Add Key":"키 추가","Add the following ADB Key to STF?":"다음 ADB 키를 STF에 추가 하시겠습니까?","Admin mode has been disabled.":"관리자 모드가 비활성화 되었습니다.","Admin mode has been enabled.":"관리자 모드가 활성화 되었습니다.","Advanced":"고급","Advanced Input":"고급 입력","Airplane Mode":"비행기 모드","App Store":"앱 스토어","App Upload":"앱 업로드","Apps":"앱","Are you sure you want to reboot this device?":"해당 단말기를 재시작 하시겠습니까?","Automation":"자동화","Available":"사용 가능","Back":"뒤로 가기","Battery":"배터리","Battery Health":"베터리 상태","Battery Level":"베터리 수준","Battery Source":"베터리 종류","Battery Status":"배터리 충전 상태","Battery Temp":"배터리 온도","Bluetooth":"블루투스","Browser":"브라우저","Busy":"점유중","Busy Devices":"사용 중인 단말기","Camera":"카메라","Cancel":"취소","Cannot access specified URL":"지정한 URL에 접근 할 수 없습니다","Carrier":"통신사","Category":"범주","Charging":"충전중","Check errors below":"아래 오류를 확인 하세요","Clear":"지우기","Clipboard":"클립보드","Cold":"양호","Connected":"연결","Connected successfully.":"연결했습니다","Control":"컨트롤 화면","Cookies":"쿠키","Cores":"코어 종류","CPU":"CPU","Customize":"사용자 지정","D-pad Center":"D-pad 가운데","D-pad Down":"D-pad 아래쪽","D-pad Left":"D-pad 왼쪽","D-pad Right":"D-pad 오른쪽","D-pad Up":"D-pad 위쪽","Dashboard":"대시보드","Data":"데이터","Date":"날짜","Dead":"정지","Delete":"삭제","Density":"해상도","Details":"세부 정보","Developer":"개발자 옵션","Device":"단말기","Device is not present anymore for some reason.":"더이상 단말기가 존재하지 않습니다.","Device is present but offline.":"장치가 연결되어 있지만 오프라인 상태입니다.","Device Photo":"단말기 사진","Device Settings":"단말기 설정","Device was disconnected":"연결이 끊어졌습니다","Device was kicked by automatic timeout.":"시간초과로 인해 단말기 사용이 종료되었습니다.","Devices":"단말기 리스트","Disable WiFi":"WiFi 비활성화","Discharging":"충전중이 아님","Disconnected":"연결 끊김","Display":"화면","Drop file to upload":"업로드 할 파일을 올려놓으세요","Dummy":"더미","Enable notifications":"알림 사용","Enable WiFi":"WiFi 활성화","Encrypted":"암호화","Error":"오류","Error while getting data":"데이터를 얻어오는데 실패했습니다","Error while reconnecting":"재연결이 실패 했습니다","Ethernet":"이더넷","Executes remote shell commands":"원격 쉘 명령을 실행합니다","Failed to download file":"파일을 다운로드 할 수 없습니다","Fast Forward":"빨리 감기","File Explorer":"파일 탐색기","File Name":"파일 이름","Filter":"필터","Find Device":"장치 찾기","Fingerprint":"지문","FPS":"FPS","Frequency":"속도","Full":"전체","General":"일반","Generate Access Token":"액세스 토큰 생성","Generate Login for VNC":"VNC 로그인 생성","Generate New Token":"새로운 토큰 생성","Get":"시작","Get clipboard contents":"클립보드 내용을 가져옵니다","Go Back":"뒤로 이동","Go Forward":"앞으로 이동","Go to Device List":"단말기 목록으로 이동","Good":"양호","Hardware":"하드웨어","Health":"상태","Height":"높이","Help":"도움말","Hide Screen":"화면 숨김","Home":"홈","Host":"호스트","Hostname":"호스트이름","ICCID":"ICCID","ID":"ID","IMEI":"IMEI","Incorrect login details":"잘못된 로그인 정보","Info":"단말기 정보","Inspect Device":"단말기 검사","Inspecting is currently only supported in WebView":"검사는 웹뷰에서만 지원합니다","Inspector":"검사기","Installation canceled by user.":"사용자가 설치를 취소했습니다","Installation failed due to an unknown error.":"알 수 없는 오류로 설치가 실패했습니다","Installation succeeded.":"설치가 성공했습니다","Installation timed out.":"설치 시간 초과","Installing app...":"앱 설치중...","Key":"키","Keys":"키","Landscape":"가로","Language":"언어","Launch Activity":"액티비티 실행","Launching activity...":"액티비티 실행중...","Level":"수준","Local Settings":"로컬 설정","Location":"위치","Lock Rotation":"화면 잠금","Logs":"로그","Maintenance":"유지 관리","Make sure to copy your access token now. You won't be able to see it again!":"엑세스 토큰을 복사하세요. 다시 확인할 수 없습니다!","Manage Apps":"앱 관리","Manner Mode":"매너 모드","Manufacturer":"제조사","Media":"미디어","Memory":"메모리","Menu":"메뉴","Mobile":"모바일","Mobile DUN":"모바일 DUN","Mobile MMS":"모바일 MMS","Mobile SUPL":"모바일 SUPL","Model":"모델","More about Access Tokens":"좀 더 자세한 엑세스 토큰에 대해서 확인하기","More about ADB Keys":"좀 더 자세하게 ADB 키에 대해서 확인하기","Mute":"음소거","Name":"이름","Native":"네이티브","Navigation":"탐색","Network":"네트워크","Next":"다음","No":"아니오","No access tokens":"등록된 엑세스 토큰이 없습니다","No ADB keys":"등록된 ADB 키가 없습니다","No clipboard data":"클립보드 데이터가 없습니다","No cookies to show":"어떤 쿠키도 없습니다","No devices connected":"연결된 단말기가 없습니다","No photo available":"이용 가능한 사진이 없습니다","No Ports Forwarded":"포트 포워딩 설정이 없습니다","No screenshots taken":"저장된 스크린샷이 없습니다","Normal Mode":"표준 모드","Not Charging":"충전 안함","Notes":"메모","Notifications":"알림","Number":"전화번호","Offline":"오프라인","Oops!":"웁스!","Open":"열기","Orientation":"화면 방향","OS":"운영체제","Over Voltage":"과전압","Overheat":"과열","Package":"패키지","Password":"비밀번호","Permissions":"권한","Phone":"휴대폰","Phone ICCID":"휴대폰 ICCID","Phone IMEI":"휴대폰 IMEI","Physical Device":"물리 단말기","PID":"PID","Place":"위치","Platform":"플랫폼","Play/Pause":"재생/일시 중지","Please enter a valid email":"유효한 이메일 주소를 입력하세요","Please enter your email":"이메일 주소를 입력하세요","Please enter your LDAP username":"LDAP 사용자 이름을 입력하세요","Please enter your name":"이름을 입력하세요","Please enter your password":"비밀번호를 입력하세요","Please enter your Store password":"앱 스토어 비밀번호를 입력하세요","Please enter your Store username":"앱 스토어 아이디를 입력하세요","Port":"포트","Port Forwarding":"포트 포워딩","Portrait":"세로","Power":"전력","Power Source":"전력원","Preparing":"준비중","Press Back button":"뒤로가기 버튼을 누르세요","Press Home button":"홈 버튼을 누르세요","Press Menu button":"메뉴 버튼을 누르세요","Previous":"이전","Processing...":"처리중...","Product":"제품명","Pushing app...":"앱 전송중...","RAM":"RAM","Reconnected successfully.":"재연결이 성공했습니다","Refresh":"새로고침","Released":"릴리즈","Reload":"다시 로드","Remote debug":"원격 디버그","Remove":"제거","Reset":"초기화","Reset all browser settings":"모든 브라우저 설정 초기화","Reset Settings":"설정 초기화","Restart Device":"단말기 재시작","Retrieving the device screen has timed out.":"단말기 화면을 가져 오는 시간이 초과 되었습니다.","Retry":"재시도","Rewind":"되감기","Roaming":"로밍","ROM":"ROM","Rotate Left":"왼쪽으로 회전","Rotate Right":"오른쪽으로 회전","Run":"실행","Run JavaScript":"자바스크립트 실행","Run the following on your command line to debug the device from your IDE":"아래의 명령줄을 실행하여 IDE에서 디버그를 실행하세요","Sample of log format":"로그 형식의 샘플","Save Logs":"로그 샘플","Save ScreenShot":"스크린 샷 저장","Save...":"저장...","Screen":"화면","Screenshot":"스크린 샷","Screenshots":"스크린 샷","SD Card Mounted":"SD카드 마운트","SDK":"SDK","Search":"검색","Serial":"일련 번호","Server":"서버","Server error. Check log output.":"서버 에러. 로그를 확인하세요","Set":"Set","Set Cookie":"쿠키 설정","Settings":"설정","Shell":"셸","Show Screen":"화면 표시","Sign In":"로그인","Sign Out":"로그아웃","Silent Mode":"음소거","SIM":"SIM","Size":"크기","Socket connection was lost":"소켓 연결이 끊겼습니다","Special Keys":"특수 키","Start/Stop Logging":"시작/종료 로깅","Status":"상태","Stop":"정지","Stop Using":"사용 종료","Store Account":"저장소 계정","Sub Type":"하위 유형","Switch Charset":"문자 집합 변경","Tag":"태그","Take Screenshot":"스크린샷 캡처","Temperature":"온도","Text":"텍스트","The device will be unavailable for a moment.":"이 단말기는 잠시동안 사용 할 수 없습니다.","The existing package could not be deleted.":"기존 패키지를 삭제 할 수 없습니다.","The new package couldn't be installed because the verification did not succeed.":"검증되지 않은 새로운 패키지는 설치 할 수 없습니다.","The new package couldn't be installed because the verification timed out.":"검증 시간이 초과하여 새로운 패키지를 설치 할 수 없습니다.","The new package couldn't be installed in the specified install location because the media is not available.":"미디어를 사용할 수 없어 지정한 위치에 새로운 패키지를 설치 할 수 없습니다.","The new package couldn't be installed in the specified install location.":"지정한 위치에 새로운 패키지를 설치 할 수 없습니다.","The new package failed because it contains a content provider with thesame authority as a provider already installed in the system.":"이미 동일한 패키지가 설치되어 있어 설치 할 수 없습니다.","The new package failed because the current SDK version is newer than that required by the package.":"SDK 버전이 높아 새로운 패키지를 설치 할 수 없습니다.","The new package failed because the current SDK version is older than that required by the package.":"SDK 버전이 낮아 새로운 패키지를 설치 할 수 없습니다.","The new package has an older version code than the currently installed package.":"새로운 패키지에 오래된 버전의 코드가 존재하여 설치 할 수 없습니다.","The new package is assigned a different UID than it previously held.":"새로운 패키지는 이전 패키지와 다른 UID가 할당 됐습니다.","The package archive file is invalid.":"패키지 아카이브 파일이 잘못 되었습니다.","The package is already installed.":"패키지가 이미 설치 되어 있습니다.","The parser encountered a bad or missing package name in the manifest.":"매니페스트에 잘못되거나 누락된 패키지 이름을 발견했습니다.","The parser encountered a bad shared user id name in the manifest.":"매니페스트에 잘못된 아이디나 이름을 발견했습니다.","The parser encountered an unexpected exception.":"예상하지 못한 예외가 발생하였습니다.","The parser encountered some structural problem in the manifest.":"매니페스트에 몇 가지 구조적인 문제가 발생 했습니다.","The parser found inconsistent certificates on the files in the .apk.":".apk 파일에서 일치하지 않은 인증서를 발견 했습니다.","The requested shared user does not exist.":"요청된 공용 사용자가 존재하지 않습니다.","The URI passed in is invalid.":"URL이 잘못 전달 됐습니다.","TID":"TID","Time":"시간","Tip:":"팁","Title":"제목","Toggle Web/Native":"웹/네이티브 전환","Total Devices":"총 단말기 수","translate":"번역","Try to reconnect":"다시 연결","Type":"유형","Unauthorized":"미인증","Uninstall":"설치 제거","Unknown":"알 수 없음","Unknown reason.":"알 수 없는 이유","Unlock Rotation":"회전 잠금 해제","Unspecified Failure":"지정되지 않은 오류","Upload failed":"업로드 실패","Upload From Link":"링크로 업로드","Upload unknown error":"업로드시 알수 없는 에러가 발생하였습니다","Uploaded file is not valid":"업로드된 파일이 유효하지 않습니다","Uploading...":"업로드중...","Usable Devices":"사용 가능한 단말기","USB":"USB","Usb speed":"Usb 속도","Use":"사용","User":"사용자","Username":"사용자 이름","Using":"사용중","Using Fallback":"대체","Version":"버전","Version Update":"버전 업데이트","Vibrate Mode":"진동","VNC":"VNC","Voltage":"전압","Volume":"음량","Volume Down":"음량 줄이기","Volume Up":"음량 올리기","Warning:":"경고","Web":"웹","Width":"너비","WiFi":"WiFi","WiMAX":"WiMAX","Wireless":"무선","X DPI":"X DPI","Y DPI":"Y DPI","Yes":"네","You (or someone else) kicked the device.":"당신(혹은 다른 누군가)이 단말기를 사용 종료 하였습니다."}} \ No newline at end of file diff --git a/res/common/lang/translations/stf.pl.json b/res/common/lang/translations/stf.pl.json index 86ca58352e..3f951f448a 100644 --- a/res/common/lang/translations/stf.pl.json +++ b/res/common/lang/translations/stf.pl.json @@ -1 +1 @@ -{"pl":{"-":"-","A new version of STF is available":"Nowa wersja STF jest dostępna","A package is already installed with the same name.":"Pakiet o tej samej nazwie jest już zainstalowany","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).":"Poprzednio zainstalowana paczka o tej samej nazwie ma inną sygnaturę od nowej (oraz dane starej paczki nie zostały usunięte).","A secure container mount point couldn't be accessed on external media.":"Zewnętrzne media nie mogą uzyskać dostępu do punktu montowania zabezpieczonego kontenera.","ABI":"ABI","AC":"AC","Access Tokens":"Tokeny dostępu","Account":"Konto","Action":"Akcja","Actions":"Akcje","Activity":"Aktywność","ADB Keys":"Klucze ADB","Add":"Dodaj","Add ADB Key":"Dodaj klucz ADB","Add Key":"Dodaj klucz","Add the following ADB Key to STF?":"Dodać następujący klucz ADB do STF?","Admin mode has been disabled.":"Tryb admina został wyłączony.","Admin mode has been enabled.":"Tryb admina został włączony","Advanced":"Zaawansowany","Advanced Input":"Wprowadzanie zaawansowane","Airplane Mode":"Tryb samolotowy","App Store":"Sklep","App Upload":"Wysyłanie aplikacji","Apps":"Aplikacje","Are you sure you want to reboot this device?":"Czy na pewno chcesz zrestartować to urządzenie?","Automation":"Automatyzacja","Available":"Dostępny","Back":"Wróć","Battery":"Bateria","Battery Health":"Kondycja baterii","Battery Level":"Poziom baterii","Battery Source":"Źródło baterii","Battery Status":"Status baterii","Battery Temp":"Temperatura baterii","Bluetooth":"Bluetooth","Browser":"Przeglądarka","Busy":"Zajęty","Busy Devices":"Zajęte urządzenia","Camera":"Aparat","Cancel":"Anuluj","Cannot access specified URL":"Nie można uzyskać dostępu do podanego adresu URL","Carrier":"Kariera","Category":"Kategoria","Charging":"Ładowanie","Check errors below":"Zwróć uwagę na błędy","Clear":"Czysty","Clipboard":"Schowek","Cold":"Chłodny","Connected":"Połączony","Connected successfully.":"Połączono pomyślnie.","Control":"Kontrola","Cookies":"Ciasteczka","Cores":"Rdzenia","CPU":"CPU","Customize":"Personalizuj","D-pad Center":"D-Pad Środek","D-pad Down":"D-pad Dół","D-pad Left":"D-pad Lewo","D-pad Right":"D-pad Prawo","D-pad Up":"D-pad Góra","Dashboard":"Kokpit","Data":"Dane","Dead":"Martwy","Delete":"Usuń","Density":"Gęstość","Details":"Szczegóły","Developer":"Developer","Device":"Urządzenie","Device cannot get kicked from the group":"Urządzenie nie może zostać wyrzucone z grupy","Device is not present anymore for some reason.":"Urządzenie z jakiegoś powodu nie jest już dostępne","Device is present but offline.":"Urządzenie jest dostępne, lecz jest offline.","Device Photo":"Zdjęcie urządzenia","Device Settings":"Ustawienia urządzenia","Device was disconnected":"Urządzenie zostało odłączone","Device was kicked by automatic timeout.":"Urządzenie zostało wyrzucone z powodu braku aktywności.","Devices":"Urządzenia","Disable WiFi":"Wyłącz WiFi","Discharging":"Rozładowywanie","Disconnected":"Odłączone","Display":"Wyświetlacz","Drop file to upload":"Upuść plik aby go wysłać","Dummy":"Głupie","Enable notifications":"Włącz powiadomienia","Enable WiFi":"Włącz WiFi","Encrypted":"Zaszyfrowane","Error":"Błąd","Error while getting data":"Zapytanie o dane zakończone błędem","Error while reconnecting":"Ponowne połączenie nie powiodło się","Ethernet":"Ethernet","Executes remote shell commands":"Wykonuje zdalne polecenia powłoki","Failed to download file":"Pobieranie zakończone niepowodzeniem","Fast Forward":"Przekaż do","File Explorer":"Przeglądaj pliki","Filter":"Filtr","Find Device":"Znajdź urządzenie","Fingerprint":"Odcisk palca","FPS":"FPS","Frequency":"Częstotliwość","Full":"Pełny","General":"Ogólne","Generate Access Token":"Generuj token dostępu","Generate Login for VNC":"Generuj login do VNC","Generate New Token":"Wygeneruj nowy token","Get":"Weź","Get clipboard contents":"Kopiuj zawartość schowka","Go Back":"Wróć","Go Forward":"Przejdź do","Go to Device List":"Przejdź do listy urządzeń","Good":"Dobrze","Hardware":"Hardware","Health":"Kondycja","Height":"Wysokość","Help":"Pomoc","Hide Screen":"Ukryj ekran","Home":"Dom","Host":"Gospodarz","Hostname":"Nazwa gospodarza","ICCID":"ICCID","ID":"ID","IMEI":"IMEI","Incorrect login details":"Niepoprawne dane logowania","Info":"Info","Inspect Device":"Zbadaj urządzenie","Inspector":"Inspektor","Installation canceled by user.":"Instalacja anulowana przez użytkownika.","Installation failed due to an unknown error.":"Instalacja nie powiodła się z powodu nieznanego błedu.","Installation succeeded.":"Instalacja powiodła się.","Installing app...":"Instalowanie aplikacji..","Key":"Klucz","Keys":"Klucze","Language":"Język","Launch Activity":"Uruchom aktywność","Launching activity...":"Uruchamianie aktywności..","Level":"Poziom","Local Settings":"Ustawienia lokalne","Location":"Lokacja","Lock Rotation":"Zablokuj rotację","Logs":"Logi","Manage Apps":"Zarządzaj aplikacjami","Memory":"Pamięć","Menu":"Menu","Model":"Model","Mute":"Wycisz","Name":"Nazwa","Navigation":"Nawigacja","Network":"Sieć","Next":"Dalej","No":"Nie","No access tokens":"Brak kluczy dostępu","No ADB keys":"Brak kluczy ADB","No clipboard data":"Brak danych w schowku","No cookies to show":"Brak ciastek do pokazania","No device screen":"Brak obrazu urządzenia","No devices connected":"Brak podłączonych urządzeń","No photo available":"Brak zdjęć","No Ports Forwarded":"Brak przekierowań portów","Notifications":"Notyfikacje","Offline":"Offline","Oops!":"Ups!","Open":"Otwórz","Orientation":"Orientacja","OS":"OS","Package":"Paczka","Password":"Hasło","Permissions":"Pozwolenia","PID":"PID","Platform":"Platforma","Please enter a valid email":"Proszę wpisać poprawny adres email","Please enter your email":"Proszę wpisać adres email","Please enter your password":"Proszę wpisać swoje hasło","Please enter your Store password":"Proszę wpisać swoje hasło do sklepu","Port":"Port","Port Forwarding":"Przekierowanie portów","Power":"Zasilanie","Power Source":"Źródło zasilania","Preparing":"Przygotowywanie","Press Back button":"Naciśnij klawisz \"Wstecz\"","Press Home button":"Naciśnij klawicz \"Home\"","Press Menu button":"Naciśnij klawicz \"Menu\"","Previous":"Poprzednie","Processing...":"Przetwarzanie..","Product":"Produkt","Pushing app...":"Pushowanie aplikacji..","RAM":"RAM","Ready":"Gotowe","Refresh":"Odśwież","Remove":"Usuń"}} \ No newline at end of file +{"pl":{"-":"-","A new version of STF is available":"Nowa wersja STF jest dostępna","A package is already installed with the same name.":"Pakiet o tej samej nazwie jest już zainstalowany","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).":"Poprzednio zainstalowana paczka o tej samej nazwie ma inną sygnaturę od nowej (oraz dane starej paczki nie zostały usunięte).","A secure container mount point couldn't be accessed on external media.":"Zewnętrzne media nie mogą uzyskać dostępu do punktu montowania zabezpieczonego kontenera.","ABI":"ABI","AC":"AC","Access Tokens":"Tokeny dostępu","Account":"Konto","Action":"Akcja","Actions":"Akcje","Activity":"Aktywność","ADB Keys":"Klucze ADB","Add":"Dodaj","Add ADB Key":"Dodaj klucz ADB","Add Key":"Dodaj klucz","Add the following ADB Key to STF?":"Dodać następujący klucz ADB do STF?","Admin mode has been disabled.":"Tryb admina został wyłączony.","Admin mode has been enabled.":"Tryb admina został włączony","Advanced":"Zaawansowany","Advanced Input":"Wprowadzanie zaawansowane","Airplane Mode":"Tryb samolotowy","App Store":"Sklep","App Upload":"Wysyłanie aplikacji","Apps":"Aplikacje","Are you sure you want to reboot this device?":"Czy na pewno chcesz zrestartować to urządzenie?","Automation":"Automatyzacja","Available":"Dostępny","Back":"Wróć","Battery":"Bateria","Battery Health":"Kondycja baterii","Battery Level":"Poziom baterii","Battery Source":"Źródło baterii","Battery Status":"Status baterii","Battery Temp":"Temperatura baterii","Bluetooth":"Bluetooth","Browser":"Przeglądarka","Busy":"Zajęty","Busy Devices":"Zajęte urządzenia","Camera":"Aparat","Cancel":"Anuluj","Cannot access specified URL":"Nie można uzyskać dostępu do podanego adresu URL","Carrier":"Operator","Category":"Kategoria","Charging":"Ładowanie","Check errors below":"Zwróć uwagę na błędy","Clear":"Wyczyść logi","Clipboard":"Schowek","Cold":"Chłodny","Connected":"Połączony","Connected successfully.":"Połączono pomyślnie.","Control":"Kontrola","Cookies":"Ciasteczka","Cores":"Liczba Rdzeni","CPU":"CPU","Customize":"Personalizuj","D-pad Center":"D-Pad Środek","D-pad Down":"D-pad Dół","D-pad Left":"D-pad Lewo","D-pad Right":"D-pad Prawo","D-pad Up":"D-pad Góra","Dashboard":"Kokpit","Data":"Dane","Dead":"Martwy","Delete":"Usuń","Density":"Gęstość","Details":"Szczegóły","Developer":"Developer","Device":"Urządzenie","Device cannot get kicked from the group":"Urządzenie nie może zostać wyrzucone z grupy","Device is not present anymore for some reason.":"Urządzenie z jakiegoś powodu nie jest już dostępne","Device is present but offline.":"Urządzenie jest dostępne, lecz jest offline.","Device Photo":"Zdjęcie urządzenia","Device Settings":"Ustawienia urządzenia","Device was disconnected":"Urządzenie zostało odłączone","Device was kicked by automatic timeout.":"Urządzenie zostało wyrzucone z powodu braku aktywności.","Devices":"Urządzenia","Disable WiFi":"Wyłącz WiFi","Discharging":"Rozładowywanie","Disconnected":"Odłączone","Display":"Wyświetlacz","Drop file to upload":"Upuść plik aby go wysłać","Dummy":"Głupie","Enable notifications":"Włącz powiadomienia","Enable WiFi":"Włącz WiFi","Encrypted":"Zaszyfrowane","Error":"Błąd","Error while getting data":"Zapytanie o dane zakończone błędem","Error while reconnecting":"Ponowne połączenie nie powiodło się","Ethernet":"Ethernet","Executes remote shell commands":"Wykonuje zdalne polecenia powłoki","Failed to download file":"Pobieranie zakończone niepowodzeniem","Fast Forward":"Przekaż do","File Explorer":"Przeglądaj pliki","File Name":"Nazwa pliku","Filter":"Filtr","Find Device":"Znajdź urządzenie","Fingerprint":"Odcisk palca","FPS":"FPS","Frequency":"Częstotliwość","Full":"Pełny","General":"Ogólne","Generate Access Token":"Generuj token dostępu","Generate Login for VNC":"Generuj login do VNC","Generate New Token":"Wygeneruj nowy token","Get":"Pobierz","Get clipboard contents":"Kopiuj zawartość schowka","Go Back":"Wróć","Go Forward":"Przejdź do","Go to Device List":"Przejdź do listy urządzeń","Good":"Dobrze","Hardware":"Hardware","Health":"Kondycja","Height":"Wysokość","Help":"Pomoc","Hide Screen":"Ukryj ekran","Home":"Dom","Host":"Gospodarz","Hostname":"Nazwa gospodarza","ICCID":"ICCID","ID":"ID","IMEI":"IMEI","Incorrect login details":"Niepoprawne dane logowania","Info":"Info","Inspect Device":"Zbadaj urządzenie","Inspector":"Inspektor","Installation canceled by user.":"Instalacja anulowana przez użytkownika.","Installation failed due to an unknown error.":"Instalacja nie powiodła się z powodu nieznanego błedu.","Installation succeeded.":"Instalacja powiodła się.","Installing app...":"Instalowanie aplikacji..","Key":"Klucz","Keys":"Klucze","Language":"Język","Launch Activity":"Uruchom aktywność","Launching activity...":"Uruchamianie aktywności..","Level":"Poziom","Local Settings":"Ustawienia lokalne","Location":"Lokacja","Lock Rotation":"Zablokuj rotację","Logs":"Logi","Manage Apps":"Zarządzaj aplikacjami","Memory":"Pamięć","Menu":"Menu","Model":"Model","Mute":"Wycisz","Name":"Nazwa","Navigation":"Nawigacja","Network":"Sieć","Next":"Dalej","No":"Nie","No access tokens":"Brak kluczy dostępu","No ADB keys":"Brak kluczy ADB","No clipboard data":"Brak danych w schowku","No cookies to show":"Brak ciastek do pokazania","No device screen":"Brak obrazu urządzenia","No devices connected":"Brak podłączonych urządzeń","No photo available":"Brak zdjęć","No Ports Forwarded":"Brak przekierowań portów","Notifications":"Notyfikacje","Offline":"Offline","Oops!":"Ups!","Open":"Otwórz","Orientation":"Orientacja","OS":"OS","Package":"Paczka","Password":"Hasło","Permissions":"Pozwolenia","PID":"PID","Place":"Lokalizacja","Platform":"Platforma","Please enter a valid email":"Proszę wpisać poprawny adres email","Please enter your email":"Proszę wpisać adres email","Please enter your password":"Proszę wpisać swoje hasło","Please enter your Store password":"Proszę wpisać swoje hasło do sklepu","Port":"Port","Port Forwarding":"Przekierowanie portów","Power":"Zasilanie","Power Source":"Źródło zasilania","Preparing":"Przygotowywanie","Press Back button":"Naciśnij klawisz \"Wstecz\"","Press Home button":"Naciśnij klawicz \"Home\"","Press Menu button":"Naciśnij klawicz \"Menu\"","Previous":"Poprzednie","Processing...":"Przetwarzanie..","Product":"Produkt","Pushing app...":"Pushowanie aplikacji..","RAM":"RAM","Ready":"Gotowe","Refresh":"Odśwież","Remove":"Usuń","Sample of log format":"Przykładowy zapis logów","Save Logs": "Zapisz logi","Settings":"Ustawienia","Size":"Rozmiar","Temperature":"Temperatura","User":"Użytkownik","Width":"Szerokość","Version":"Wersja"}} \ No newline at end of file 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 0000000000..34550dac94 --- /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/common/lang/translations/stf.ru_RU.json b/res/common/lang/translations/stf.ru_RU.json index 53087fb4c5..b73c033671 100644 --- a/res/common/lang/translations/stf.ru_RU.json +++ b/res/common/lang/translations/stf.ru_RU.json @@ -1 +1 @@ -{"ru_RU":{"A new version of STF is available":"Доступна новая версия STF","A package is already installed with the same name.":"Пакет уже установлен с таким же названием.","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).":"Ранее установленный пакет с таким же названием имеет отличающуюся цифровую подпись (также не удалены данные старого пакета)","Account":"Учетная запись","Action":"Действие","Actions":"Действия","ADB Keys":"Ключи ADB","Add":"Добавить","Add ADB Key":"Добавить ADB ключ","Add Key":"Добавить ключ","Add the following ADB Key to STF?":"Добавить ADB ключ к STF?","Admin mode has been disabled.":"Режим администратора отключен.","Admin mode has been enabled.":"Режим администратора включен.","Advanced":"Расширенный","Advanced Input":"Расширенный ввод","Airplane Mode":"Режим В самолёте","App Store":"Play Маркет","App Upload":"Загрузка приложения","Apps":"Приложения","Are you sure you want to reboot this device?":"Вы уверены, что хотите перезагрузить устройство?","Automation":"Автоматизация","Available":"Доступен","Back":"Назад","Battery":"Аккумулятор","Battery Health":"Состояние батареи","Battery Level":"Уровень зарядки аккумулятора","Battery Status":"Статус аккумулятора","Battery Temp":"Температура аккумулятора","Browser":"Браузер","Busy":"Занято","Busy Devices":"Используемые устройства","Camera":"Камера","Cancel":"Отменить","Cannot access specified URL":"Невозможно отрыть заданный URL","Carrier":"Оператор","Category":"Категория","Charging":"Заряжается","Check errors below":"Проверьте сообщения об ошибках ниже","Clear":"Очистить","Clipboard":"Буфер обмена","Cold":"Холодно","Connected":"Подключено","Connected successfully.":"Подключено успешно.","Cores":"Ядер","CPU":"Процессор","Customize":"Настроить","D-pad Center":"Центральная кнопка D-pad","D-pad Down":"Вниз","D-pad Left":"Влево","D-pad Right":"Вправо","D-pad Up":"Вверх","Dashboard":"Приборная панель","Data":"Данные","Dead":"Не отвечает","Delete":"Удалить","Density":"Плотность","Details":"Детали","Developer":"Разработчик","Device":"Устройство","Device cannot get kicked from the group":"Устройство не может быть исключено из группы","Device is not present anymore for some reason.":"Подключение к устройству отсутствует по неизвестной причине.","Device is present but offline.":"Устройство подключено, но не активно.","Device Photo":"Фото устройства","Device Settings":"Настройки устройства","Device was disconnected":"Устройство было отключено","Device was kicked by automatic timeout.":"Устройство было отключено по таймауту.","Devices":"Устройства","Disable WiFi":"Отключить WiFi","Discharging":"Разряжается","Disconnected":"Отключено","Display":"Экран","Drop file to upload":"Перетащите файл для загрузки","Dummy":"Макет","Enable notifications":"Включить уведомления","Enable WiFi":"Включить WiFi","Encrypted":"Зашифровано","Error":"Ошибка","Error while getting data":"Ошибка во время получения данных","Error while reconnecting":"Ошибка переподключения","Ethernet":"Проводная сеть","Executes remote shell commands":"Выполняет удалённые команды shell","Failed to download file":"Не удалось загрузить файл","Fast Forward":"Перемотка вперёд","Filter":"Фильтр","Find Device":"Обнаружить устройство","Fingerprint":"Отпечаток пальца","Frequency":"Частота","Full":"Полный","General":"Общие","Get":"Получить","Get clipboard contents":"Получить содержимое буфера обмена","Go Back":"Назад","Go Forward":"Вперёд","Go to Device List":"Открыть список устройств","Good":"Хорошее","Hardware":"Железо","Health":"Состояние","Height":"Высота","Help":"Помощь","Hide Screen":"Спрятать экран","Home":"Домашний экран","Hostname":"Имя хоста","Incorrect login details":"Некорректные логин или пароль","Info":"Инфо","Inspect Device":"Инспектировать устройство","Inspecting is currently only supported in WebView":"Инспектирование пока поддерживается в WebView","Inspector":"Инспекто","Installation canceled by user.":"Установка отменена пользователем.","Installation failed due to an unknown error.":"Установка не удалась по неизвестной причине.","Installation succeeded.":"Установка прошла успешно.","Installation timed out.":"Время установки истекло.","Installing app...":"Устанавливаем приложение...","Key":"Ключ","Keys":"Ключи","Landscape":"Ландшафт","Language":"Язык","Launch Activity":"Запустить приложение","Launching activity...":"Приложение запускается...","Level":"Уровень","Local Settings":"Локальные настройки","Location":"Местоположение","Logs":"Журнал","Maintenance":"Обслуживание","Manage Apps":"Управление приложениями","Manufacturer":"Производитель","Memory":"Память","Menu":"Меню","Mobile":"Мобильный","Model":"Модель","More about Access Tokens":"Подробнее о ключах доступа","More about ADB Keys":"Подробнее о ADB ключах","Mute":"Выключить звук","Name":"Имя","Native":"Нативный","Navigation":"Навигация","Network":"Сеть","Next":"Следующий","No":"Нет","No access tokens":"Ключи доступа отсутствуют","No ADB keys":"ADB ключи отсутствуют","No clipboard data":"В буфере обмена нет данных","No cookies to show":"Отсутствуют cookies","No devices connected":"Нет подключенных устройств","No photo available":"Отсутствует фото","No Ports Forwarded":"Отсутствуют перенаправленные порты","No screenshots taken":"Снимки экрана отсутствуют","Normal Mode":"Нормальный режим","Not Charging":"Не заряжается","Notes":"Записи","Notifications":"Уведомления","Number":"Число","Oops!":"Ой","Open":"Открыть","Orientation":"Ориентация","Package":"Пакет","Password":"Пароль","Phone":"Телефон","Physical Device":"Физическое устройство","Place":"Место","Platform":"Платформа","Play/Pause":"Играть/Пауза","Please enter a valid email":"Пожалуйста, введите корректный email","Please enter your email":"Пожалуйста, введите email","Please enter your name":"Пожалуйста введите ваше имя","Please enter your password":"Пожалуйста введите пароль","Port":"Порт","Port Forwarding":"Перенаправление портов","Portrait":"Портетный","Power":"Энергия","Power Source":"Источник энергии","Preparing":"Подготовка","Press Back button":"Нажмите кнопку Назад","Press Home button":"Нажмите кнопку Домой","Press Menu button":"Нажмите кнопку Меню","Previous":"Предыдущий","Processing...":"Обрабатываю...","Product":"Продукт","Pushing app...":"Загружаю приложение на устройство...","Ready":"Готово","Reconnected successfully.":"Переподключился успешно.","Refresh":"Обновить","Released":"Свободно","Reload":"Перезагрузить","Remote debug":"Удалённая отладка","Remove":"Удалить","Reset":"Сбросить","Reset all browser settings":"Сбросить все настройки браузера","Reset Settings":"Сбросить настройки","Restart Device":"Перезагрузить устройство","Retrieving the device screen has timed out.":"Попытка получить снимок устройства не завершилась вовремя.","Retry":"Повторить","Rewind":"Перемотать назад","Rotate Left":"Повернуть влево","Rotate Right":"Повернуть вправо","Run":"Выполнить","Run JavaScript":"Выполнить JavaScript","Run the following on your command line to debug the device from your Browser":"Выполните эту команду, чтобы отладить устройство из вашего Браузера","Run the following on your command line to debug the device from your IDE":"Выполните эту команду, чтобы отладить устройство из вашего IDE","Run this command to copy the key to your clipboard":"Выполните эту команду, чтобы скопировать ключ в буфер обмена","Save ScreenShot":"Сохранить скриншот","Save...":"Сохранить..","Screen":"Экран","Screenshot":"Снимок экрана","Screenshots":"Снимки экрана","SD Card Mounted":"SD карта подключена","Search":"Поиск","Serial":"Серийный номер","Server error. Check log output.":"Ошибка сервера. Проверьте журнал.","Set":"Установить","Set Cookie":"Установить cookie","Settings":"Настройки","Shell":"Командная оболочка","Show Screen":"Показать экран","Sign In":"Войти","Sign Out":"Выйти","Silent Mode":"Тихий режим","Size":"Размер","Socket connection was lost":"Подключение через socket потеряно","Someone stole your device.":"Кто-то утащил ваше устройство","Special Keys":"Специальные кнопки","Start/Stop Logging":"Начать/Остановить журналирование","Status":"Статус","Stop":"Стоп","Stop Using":"Освободить","Store Account":"Учетная запись магазина приложений","Sub Type":"Подтип","Switch Charset":"Переключить кодировку","Take Pageshot (Needs WebView running)":"Сделать снимок странички (WebView должен быть запущен)","Take Screenshot":"Сделать снимок экрана","Temperature":"Температура","Text":"Текст","The current view is marked secure and cannot be viewed remotely.":"Текущий просмотр отмечен как безопасный и к нему нельзя получить доступ удалённо.","The device will be unavailable for a moment.":"Устройство будет временно недоступно.","The existing package could not be deleted.":"Существующий пакет не может быть удалён.","The new package couldn't be installed because the verification did not succeed.":"Пакет не может быть установлен из-за ошибки верификации.","The new package couldn't be installed because the verification timed out.":"Пакет не может быть установлен из-за превышения времени верификации.","The new package couldn't be installed in the specified install location because the media is not available.":"Пакет не может быть установлен в указанное место поскольку оно недоступно.","The new package couldn't be installed in the specified install location.":"Пакет не может быть установлен в указанное место.","The new package has an older version code than the currently installed package.":"Новый пакет имеет более старую версию, чем уже установленный.","The package archive file is invalid.":"Файл архива программы повреждён.","The package is already installed.":"Пакет уже установлен.","The parser encountered an unexpected exception.":"Возникла непредвиденная исключительная ситуация во время работы синтаксического анализатора.","The parser encountered some structural problem in the manifest.":"Синтаксический анализатор обнаружил структурные проблемы в манифесте.","The parser found inconsistent certificates on the files in the .apk.":"Синтаксический анализатор обнаружил несовместимый сертификат в .apk файле","The parser was given a path that is not a file, or does not end with the expected '.apk' extension.":"Синтаксическому анализатору был передан не путь к файлу, или имя файла не заканчивается на '.apk'.","The parser was unable to retrieve the AndroidManifest.xml file.":"Синтаксический анализатор не смог получить файл AndroidManifest.xml","The requested shared user does not exist.":"Требуемый общий пользователь не существует.","The system failed to install the package because of system issues.":"Не удалось установить пакет по причине системной ошибки","The system failed to install the package because the user is restricted from installing apps.":"Не удалось установить пакет, так как данному пользователю запрещена установка приложений.","The URI passed in is invalid.":"Невалидный URI.","TID":"TID","Time":"Время","Tip:":"Подсказка:","Toggle Web/Native":"Переключить Web/Native","Total Devices":"Всего устройств","Try to reconnect":"Попробовать подключиться","Type":"Тип","Unauthorized":"Неавторизован","Uninstall":"Удалить","Unknown":"Неизвестный","Unknown reason.":"Неизвестная причина.","Upload failed":"Загрузка завершилась неудачно","Upload From Link":"Загрузить по ссылке","Upload unknown error":"Неизвестная ошибка загрузки","Uploaded file is not valid":"Загруженный файл не валиден","Uploading...":"Загружается...","Usable Devices":"Доступные устройства","USB":"USB","Usb speed":"Скорость USB","Use":"Использовать","User":"Пользователь","Username":"Имя пользователя","Using":"Используется","Version":"Версия","Version Update":"Обновление версии","Vibrate Mode":"Режим вибрации","Voltage":"Напряжение","Volume":"Звук","Volume Down":"Тише","Volume Up":"Громче","Web":"Интернет","Width":"Ширина","WiFi":"WiFi","WiMAX":"WiMAX","Wireless":"Беспроводное","X DPI":"X DPI","Y DPI":"Y DPI","Yes":"Да","You (or someone else) kicked the device.":"Вы (или кто-то еще) отключили устройство."}} \ No newline at end of file +{"ru_RU":{"A new version of STF is available":"Доступна новая версия STF","A package is already installed with the same name.":"Пакет уже установлен с таким же названием.","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).":"Ранее установленный пакет с таким же названием имеет отличающуюся цифровую подпись (также не удалены данные старого пакета)","Account":"Учетная запись","Action":"Действие","Actions":"Действия","ADB Keys":"Ключи ADB","Add":"Добавить","Add ADB Key":"Добавить ADB ключ","Add Key":"Добавить ключ","Add the following ADB Key to STF?":"Добавить ADB ключ к STF?","Admin mode has been disabled.":"Режим администратора отключен.","Admin mode has been enabled.":"Режим администратора включен.","Advanced":"Расширенный","Advanced Input":"Расширенный ввод","Airplane Mode":"Режим В самолёте","App Store":"Play Маркет","App Upload":"Загрузка приложения","Apps":"Приложения","Are you sure you want to reboot this device?":"Вы уверены, что хотите перезагрузить устройство?","Automation":"Автоматизация","Available":"Доступен","Back":"Назад","Battery":"Аккумулятор","Battery Health":"Состояние батареи","Battery Level":"Уровень зарядки аккумулятора","Battery Status":"Статус аккумулятора","Battery Temp":"Температура аккумулятора","Browser":"Браузер","Busy":"Занято","Busy Devices":"Используемые устройства","Camera":"Камера","Cancel":"Отменить","Cannot access specified URL":"Невозможно отрыть заданный URL","Carrier":"Оператор","Category":"Категория","Charging":"Заряжается","Check errors below":"Проверьте сообщения об ошибках ниже","Clear":"Очистить","Clipboard":"Буфер обмена","Cold":"Холодно","Connected":"Подключено","Connected successfully.":"Подключено успешно.","Cores":"Ядер","CPU":"Процессор","Customize":"Настроить","D-pad Center":"Центральная кнопка D-pad","D-pad Down":"Вниз","D-pad Left":"Влево","D-pad Right":"Вправо","D-pad Up":"Вверх","Dashboard":"Приборная панель","Data":"Данные","Dead":"Не отвечает","Delete":"Удалить","Density":"Плотность","Details":"Детали","Developer":"Разработчик","Device":"Устройство","Device cannot get kicked from the group":"Устройство не может быть исключено из группы","Device is not present anymore for some reason.":"Подключение к устройству отсутствует по неизвестной причине.","Device is present but offline.":"Устройство подключено, но не активно.","Device Photo":"Фото устройства","Device Settings":"Настройки устройства","Device was disconnected":"Устройство было отключено","Device was kicked by automatic timeout.":"Устройство было отключено по таймауту.","Devices":"Устройства","Disable WiFi":"Отключить WiFi","Discharging":"Разряжается","Disconnected":"Отключено","Display":"Экран","Drop file to upload":"Перетащите файл для загрузки","Dummy":"Макет","Enable notifications":"Включить уведомления","Enable WiFi":"Включить WiFi","Encrypted":"Зашифровано","Error":"Ошибка","Error while getting data":"Ошибка во время получения данных","Error while reconnecting":"Ошибка переподключения","Ethernet":"Проводная сеть","Executes remote shell commands":"Выполняет удалённые команды shell","Failed to download file":"Не удалось загрузить файл","Fast Forward":"Перемотка вперёд","File Name":"Имя файла","Filter":"Фильтр","Find Device":"Обнаружить устройство","Fingerprint":"Отпечаток пальца","Frequency":"Частота","Full":"Полный","General":"Общие","Get":"Получить","Get clipboard contents":"Получить содержимое буфера обмена","Go Back":"Назад","Go Forward":"Вперёд","Go to Device List":"Открыть список устройств","Good":"Хорошее","Hardware":"Железо","Health":"Состояние","Height":"Высота","Help":"Помощь","Hide Screen":"Спрятать экран","Home":"Домашний экран","Hostname":"Имя хоста","Incorrect login details":"Некорректные логин или пароль","Info":"Инфо","Inspect Device":"Инспектировать устройство","Inspecting is currently only supported in WebView":"Инспектирование пока поддерживается в WebView","Inspector":"Инспекто","Installation canceled by user.":"Установка отменена пользователем.","Installation failed due to an unknown error.":"Установка не удалась по неизвестной причине.","Installation succeeded.":"Установка прошла успешно.","Installation timed out.":"Время установки истекло.","Installing app...":"Устанавливаем приложение...","Key":"Ключ","Keys":"Ключи","Landscape":"Ландшафт","Language":"Язык","Launch Activity":"Запустить приложение","Launching activity...":"Приложение запускается...","Level":"Уровень","Local Settings":"Локальные настройки","Location":"Местоположение","Logs":"Журнал","Maintenance":"Обслуживание","Manage Apps":"Управление приложениями","Manufacturer":"Производитель","Memory":"Память","Menu":"Меню","Mobile":"Мобильный","Model":"Модель","More about Access Tokens":"Подробнее о ключах доступа","More about ADB Keys":"Подробнее о ADB ключах","Mute":"Выключить звук","Name":"Имя","Native":"Нативный","Navigation":"Навигация","Network":"Сеть","Next":"Следующий","No":"Нет","No access tokens":"Ключи доступа отсутствуют","No ADB keys":"ADB ключи отсутствуют","No clipboard data":"В буфере обмена нет данных","No cookies to show":"Отсутствуют cookies","No devices connected":"Нет подключенных устройств","No photo available":"Отсутствует фото","No Ports Forwarded":"Отсутствуют перенаправленные порты","No screenshots taken":"Снимки экрана отсутствуют","Normal Mode":"Нормальный режим","Not Charging":"Не заряжается","Notes":"Записи","Notifications":"Уведомления","Number":"Число","Oops!":"Ой","Open":"Открыть","Orientation":"Ориентация","Package":"Пакет","Password":"Пароль","Phone":"Телефон","Physical Device":"Физическое устройство","Place":"Место","Platform":"Платформа","Play/Pause":"Играть/Пауза","Please enter a valid email":"Пожалуйста, введите корректный email","Please enter your email":"Пожалуйста, введите email","Please enter your name":"Пожалуйста введите ваше имя","Please enter your password":"Пожалуйста введите пароль","Port":"Порт","Port Forwarding":"Перенаправление портов","Portrait":"Портетный","Power":"Энергия","Power Source":"Источник энергии","Preparing":"Подготовка","Press Back button":"Нажмите кнопку Назад","Press Home button":"Нажмите кнопку Домой","Press Menu button":"Нажмите кнопку Меню","Previous":"Предыдущий","Processing...":"Обрабатываю...","Product":"Продукт","Pushing app...":"Загружаю приложение на устройство...","Ready":"Готово","Reconnected successfully.":"Переподключился успешно.","Refresh":"Обновить","Released":"Свободно","Reload":"Перезагрузить","Remote debug":"Удалённая отладка","Remove":"Удалить","Reset":"Сбросить","Reset all browser settings":"Сбросить все настройки браузера","Reset Settings":"Сбросить настройки","Restart Device":"Перезагрузить устройство","Retrieving the device screen has timed out.":"Попытка получить снимок устройства не завершилась вовремя.","Retry":"Повторить","Rewind":"Перемотать назад","Rotate Left":"Повернуть влево","Rotate Right":"Повернуть вправо","Run":"Выполнить","Run JavaScript":"Выполнить JavaScript","Run the following on your command line to debug the device from your Browser":"Выполните эту команду, чтобы отладить устройство из вашего Браузера","Run the following on your command line to debug the device from your IDE":"Выполните эту команду, чтобы отладить устройство из вашего IDE","Run this command to copy the key to your clipboard":"Выполните эту команду, чтобы скопировать ключ в буфер обмена","Sample of log format":"Образец формата журнал","Save Logs": "сохранить журнал","Save ScreenShot":"Сохранить скриншот","Save...":"Сохранить..","Screen":"Экран","Screenshot":"Снимок экрана","Screenshots":"Снимки экрана","SD Card Mounted":"SD карта подключена","Search":"Поиск","Serial":"Серийный номер","Server error. Check log output.":"Ошибка сервера. Проверьте журнал.","Set":"Установить","Set Cookie":"Установить cookie","Settings":"Настройки","Shell":"Командная оболочка","Show Screen":"Показать экран","Sign In":"Войти","Sign Out":"Выйти","Silent Mode":"Тихий режим","Size":"Размер","Socket connection was lost":"Подключение через socket потеряно","Someone stole your device.":"Кто-то утащил ваше устройство","Special Keys":"Специальные кнопки","Start/Stop Logging":"Начать/Остановить журналирование","Status":"Статус","Stop":"Стоп","Stop Using":"Освободить","Store Account":"Учетная запись магазина приложений","Sub Type":"Подтип","Switch Charset":"Переключить кодировку","Take Pageshot (Needs WebView running)":"Сделать снимок странички (WebView должен быть запущен)","Take Screenshot":"Сделать снимок экрана","Temperature":"Температура","Text":"Текст","The current view is marked secure and cannot be viewed remotely.":"Текущий просмотр отмечен как безопасный и к нему нельзя получить доступ удалённо.","The device will be unavailable for a moment.":"Устройство будет временно недоступно.","The existing package could not be deleted.":"Существующий пакет не может быть удалён.","The new package couldn't be installed because the verification did not succeed.":"Пакет не может быть установлен из-за ошибки верификации.","The new package couldn't be installed because the verification timed out.":"Пакет не может быть установлен из-за превышения времени верификации.","The new package couldn't be installed in the specified install location because the media is not available.":"Пакет не может быть установлен в указанное место поскольку оно недоступно.","The new package couldn't be installed in the specified install location.":"Пакет не может быть установлен в указанное место.","The new package has an older version code than the currently installed package.":"Новый пакет имеет более старую версию, чем уже установленный.","The package archive file is invalid.":"Файл архива программы повреждён.","The package is already installed.":"Пакет уже установлен.","The parser encountered an unexpected exception.":"Возникла непредвиденная исключительная ситуация во время работы синтаксического анализатора.","The parser encountered some structural problem in the manifest.":"Синтаксический анализатор обнаружил структурные проблемы в манифесте.","The parser found inconsistent certificates on the files in the .apk.":"Синтаксический анализатор обнаружил несовместимый сертификат в .apk файле","The parser was given a path that is not a file, or does not end with the expected '.apk' extension.":"Синтаксическому анализатору был передан не путь к файлу, или имя файла не заканчивается на '.apk'.","The parser was unable to retrieve the AndroidManifest.xml file.":"Синтаксический анализатор не смог получить файл AndroidManifest.xml","The requested shared user does not exist.":"Требуемый общий пользователь не существует.","The system failed to install the package because of system issues.":"Не удалось установить пакет по причине системной ошибки","The system failed to install the package because the user is restricted from installing apps.":"Не удалось установить пакет, так как данному пользователю запрещена установка приложений.","The URI passed in is invalid.":"Невалидный URI.","TID":"TID","Time":"Время","Tip:":"Подсказка:","Toggle Web/Native":"Переключить Web/Native","Total Devices":"Всего устройств","Try to reconnect":"Попробовать подключиться","Type":"Тип","Unauthorized":"Неавторизован","Uninstall":"Удалить","Unknown":"Неизвестный","Unknown reason.":"Неизвестная причина.","Upload failed":"Загрузка завершилась неудачно","Upload From Link":"Загрузить по ссылке","Upload unknown error":"Неизвестная ошибка загрузки","Uploaded file is not valid":"Загруженный файл не валиден","Uploading...":"Загружается...","Usable Devices":"Доступные устройства","USB":"USB","Usb speed":"Скорость USB","Use":"Использовать","User":"Пользователь","Username":"Имя пользователя","Using":"Используется","Version":"Версия","Version Update":"Обновление версии","Vibrate Mode":"Режим вибрации","Voltage":"Напряжение","Volume":"Звук","Volume Down":"Тише","Volume Up":"Громче","Web":"Интернет","Width":"Ширина","WiFi":"WiFi","WiMAX":"WiMAX","Wireless":"Беспроводное","X DPI":"X DPI","Y DPI":"Y DPI","Yes":"Да","You (or someone else) kicked the device.":"Вы (или кто-то еще) отключили устройство."}} \ No newline at end of file diff --git a/res/common/lang/translations/stf.zh-Hant.json b/res/common/lang/translations/stf.zh-Hant.json index a566ed4bd7..e34c2bca43 100644 --- a/res/common/lang/translations/stf.zh-Hant.json +++ b/res/common/lang/translations/stf.zh-Hant.json @@ -1 +1 @@ -{"zh-Hant":{"-":"-","A new version of STF is available":"STF有新版本可下载","A package is already installed with the same name.":"已安裝相同名稱的軟體囉。","Access Tokens":"存取憑證","Account":"帳號","Action":"動作","Actions":"更多動作","Activity":"活動","Add":"新增","Admin mode has been disabled.":"停用 Admin 模式","Admin mode has been enabled.":"啟用 Admin 模式","Advanced":"進階","Airplane Mode":"飛行模式","App Store":"App Store","App Upload":"上傳 App","Apps":"Apps","Are you sure you want to reboot this device?":"你確定要重開這台裝置 ?","Automation":"自動化","Available":"可用","Back":"返回","Battery":"電池","Battery Health":"電池健康","Battery Level":"電池電量","Battery Source":"電池電源","Battery Status":"電池狀態","Battery Temp":"電池溫度","Bluetooth":"藍芽","Browser":"瀏覽器","Busy":"忙碌","Busy Devices":"忙碌中的裝置","Camera":"相機","Cancel":"取消","Cannot access specified URL":"無法進入指定網址","Carrier":"電信商","Category":"分類","Charging":"充電中","Check errors below":"請確認下面的錯誤","Clear":"清除","Clipboard":"剪貼簿","Connected":"已連線","Connected successfully.":"連線成功","Control":"控制","Cores":"核心","CPU":"CPU","Current rotation:":"目前螢幕方向:","Customize":"自訂","Data":"資料","Date":"日期","Delete":"刪除","Details":"細項","Developer":"開發者","Device":"裝置","Device Photo":"裝置相片","Device Settings":"裝置設定","Device was disconnected":"裝置已離線","Devices":"裝置","Disable WiFi":"關閉 WiFi","Discharging":"放電中","Disconnected":"離線","Display":"顯示","Drop file to upload":"拖曳到此上傳","Enable notifications":"開啟通知","Enable WiFi":"開啟 WiFi","Error":"錯誤","Error while getting data":"取得資料時錯誤","Error while reconnecting":"重新連線時錯誤","Failed to download file":"下載檔案失敗","Fast Forward":"快轉","File Explorer":"檔案瀏覽器","Filter":"篩選","Find Device":"尋找裝置","Fingerprint":"指紋","FPS":"FPS","Frequency":"頻率","General":"一般","Generate Access Token":"產生存取憑證","Get":"擷取","Get clipboard contents":"取得剪貼簿內容","Go Back":"返回","Go to Device List":"前往裝置清單","Good":"良好","Hardware":"硬體","Health":"健康","Height":"高度","Help":"幫助","Hide Screen":"隱藏畫面","ID":"ID","IMEI":"IMEI","Incorrect login details":"不正確的登入資訊","Info":"資訊","Inspect Device":"查看裝置","Inspector":"檢查器","Installation canceled by user.":"使用者取消安裝","Installation failed due to an unknown error.":"不明原因的安裝錯誤","Installation succeeded.":"安裝成功","Installation timed out.":"安裝超時","Installing app...":"App 安裝中...","Landscape":"橫式","Language":"語言","Launch Activity":"執行活動","Launching activity...":"活動執行中","Level":"電量","Local Settings":"本機設定","Location":"位置","Lock Rotation":"鎖定螢幕旋轉","Maintenance":"維護","Manage Apps":"管理 Apps","Manner Mode":"管理模式","Manufacturer":"製造商","Media":"媒體","Memory":"記憶體","Menu":"選單","Model":"型號","More about Access Tokens":"關於存取憑證","Mute":"靜音","Name":"名稱","Navigation":"導航","Network":"網路","Next":"下一首","No access tokens":"沒有存取憑證","No clipboard data":"剪貼簿無資料","No device screen":"沒有裝置畫面","No devices connected":"無裝置連線","No photo available":"沒有可用的照片","No screenshots taken":"尚未拍截圖","Normal Mode":"正常模式","Not Charging":"未充電","Notes":"備註","Notifications":"通知","Offline":"離線","Oops!":"糟糕了!有錯誤發生","Open":"開啟","Orientation":"螢幕方向","OS":"平台","Overheat":"過熱","Package":"套件","Password":"密碼","Permissions":"權限","Phone":"手機","Phone ICCID":"手機 ICCID","Phone IMEI":"手機 IMEI","Physical Device":"實體裝置","Place":"位置","Platform":"平台","Play/Pause":"播放 / 暫停","Please enter a valid email":"請輸入正確的 email","Please enter your email":"請輸入你的 email","Please enter your LDAP username":"請輸入你的 LDAP 帳號","Please enter your name":"請輸入你的名字","Please enter your password":"請輸入你的密碼","Please enter your Store password":"請輸入你儲存的密碼","Please enter your Store username":"請輸入你儲存的帳號","Power":"電源鍵","Power Source":"電源","Preparing":"準備中","Press Back button":"按返回鍵","Press Home button":"按主目錄鍵","Press Menu button":"按選單鍵","Previous":"上一首","Processing...":"處理中...","Product":"產品","Pushing app...":"App 傳送中...","RAM":"記憶體","Ready":"就緒","Reconnected successfully.":"重新連線成功","Refresh":"更新","Released":"發佈","Reload":"重新讀取","Remote debug":"遠端除蟲","Remove":"移除","Reset":"重設","Reset all browser settings":"重設所有瀏覽器設定","Reset Settings":"重設設定","Restart Device":"裝置重新啟動","Retrieving the device screen has timed out.":"取得裝置畫面已超時","Retry":"重試","Rewind":"倒帶","ROM":"唯讀記憶體","Rotate Left":"向左旋轉","Rotate Right":"向右旋轉","Run":"執行","Run JavaScript":"執行 JavaScript","Save ScreenShot":"儲存螢幕截圖","Screen":"螢幕尺寸","Screenshot":"螢幕截圖","Screenshots":"更多螢幕截圖","SD Card Mounted":"已安裝 SD 卡","Search":"搜尋","Selects Next IME":"選擇下一個輸入法","Serial":"序號","Server":"伺服器","Server error. Check log output.":"伺服器錯誤. 請確認輸出的紀錄","Settings":"設定","Show Screen":"顯示畫面","Sign In":"登入","Sign Out":"登出","Silent Mode":"靜音模式","SIM":"SIM","Size":"尺寸","Socket connection was lost":"Socket 失去連線","Someone stole your device.":"別人偷了你的裝置.","Special Keys":"特殊按鍵","Start/Stop Logging":"開始 / 停止 紀錄","Status":"狀態","Stop":"停止","Stop Using":"停止使用","Store Account":"儲存帳號","Sub Type":"次類別","Switch Charset":"切換大小寫","Tag":"標籤","Take Pageshot (Needs WebView running)":"取得頁面截圖 (需執行 WebView)","Take Screenshot":"截圖","Temperature":"溫度","Text":"文字","The current view is marked secure and cannot be viewed remotely.":"目前的畫面已被保護,無法從遠端監看","The device will be unavailable for a moment.":"此裝置暫時無法使用","The existing package could not be deleted.":"現存的套件無法刪除","The new package couldn't be installed because the verification did not succeed.":"新套件無法安裝,驗證不成功.","The new package couldn't be installed because the verification timed out.":"驗證超時,新套件無法安裝","The new package couldn't be installed in the specified install location because the media is not available.":"目前媒體無法使用,無法安裝新套件到指定位置","The new package couldn't be installed in the specified install location.":"無法安裝新套件到指定位置","The new package failed because it contains a content provider with thesame authority as a provider already installed in the system.":"新套件安裝失敗,相同的內容供應商授權已存在.","The new package failed because the current SDK version is newer than that required by the package.":" 新套件安裝失敗,因為目前的 SDK 版本太新","The new package failed because the current SDK version is older than that required by the package.":"新套件安裝失敗,目前 SDK 版本太舊","The new package uses a feature that is not available.":"新套件使用尚未支援的功能","The package is already installed.":"套件已安裝","Time":"時間","Tip:":"提示:","Title":"標題","Toggle Web/Native":"切換 Web / Native","Total Devices":"所有裝置","translate":"翻譯","Try to reconnect":"嘗試重新連線","Type":"類型","Unauthorized":"未授權","Uninstall":"移除","Unknown":"未知","Unknown reason.":"未知原因","Unlock Rotation":"解除螢幕旋轉","Upload failed":"上傳失敗","Upload From Link":"從連結上傳","Upload unknown error":"未知的上傳錯誤","Uploaded file is not valid":"上傳的檔案無效","Uploading...":"上傳中...","Usable Devices":"可用的裝置","USB":"USB","Usb speed":"Usb 速度","Use":"使用","User":"使用者","Username":"帳號","Using":"使用中","Version":"版本","Version Update":"版本更新","Vibrate Mode":"震動模式","VNC":"VNC","Voltage":"電壓","Volume":"音量","Volume Down":"降低音量","Volume Up":"提高音量","Warning:":"警告:","Web":"網頁","Width":"寬度","WiFi":"WiFi","WiMAX":"WiMAX","You (or someone else) kicked the device.":"你 (或別人) 把裝置踢掉了."}} \ No newline at end of file +{"zh-Hant":{"-":"-","A new version of STF is available":"STF有新版本可下载","A package is already installed with the same name.":"已安裝相同名稱的軟體囉。","Access Tokens":"存取憑證","Account":"帳號","Action":"動作","Actions":"更多動作","Activity":"活動","Add":"新增","Admin mode has been disabled.":"停用 Admin 模式","Admin mode has been enabled.":"啟用 Admin 模式","Advanced":"進階","Airplane Mode":"飛行模式","App Store":"App Store","App Upload":"上傳 App","Apps":"Apps","Are you sure you want to reboot this device?":"你確定要重開這台裝置 ?","Automation":"自動化","Available":"可用","Back":"返回","Battery":"電池","Battery Health":"電池健康","Battery Level":"電池電量","Battery Source":"電池電源","Battery Status":"電池狀態","Battery Temp":"電池溫度","Bluetooth":"藍芽","Browser":"瀏覽器","Busy":"忙碌","Busy Devices":"忙碌中的裝置","Camera":"相機","Cancel":"取消","Cannot access specified URL":"無法進入指定網址","Carrier":"電信商","Category":"分類","Charging":"充電中","Check errors below":"請確認下面的錯誤","Clear":"清除","Clipboard":"剪貼簿","Connected":"已連線","Connected successfully.":"連線成功","Control":"控制","Cores":"核心","CPU":"CPU","Current rotation:":"目前螢幕方向:","Customize":"自訂","Data":"資料","Date":"日期","Delete":"刪除","Details":"細項","Developer":"開發者","Device":"裝置","Device Photo":"裝置相片","Device Settings":"裝置設定","Device was disconnected":"裝置已離線","Devices":"裝置","Disable WiFi":"關閉 WiFi","Discharging":"放電中","Disconnected":"離線","Display":"顯示","Drop file to upload":"拖曳到此上傳","Enable notifications":"開啟通知","Enable WiFi":"開啟 WiFi","Error":"錯誤","Error while getting data":"取得資料時錯誤","Error while reconnecting":"重新連線時錯誤","Failed to download file":"下載檔案失敗","Fast Forward":"快轉","File Explorer":"檔案瀏覽器","File Name":"文件名","Filter":"篩選","Find Device":"尋找裝置","Fingerprint":"指紋","FPS":"FPS","Frequency":"頻率","General":"一般","Generate Access Token":"產生存取憑證","Get":"擷取","Get clipboard contents":"取得剪貼簿內容","Go Back":"返回","Go to Device List":"前往裝置清單","Good":"良好","Hardware":"硬體","Health":"健康","Height":"高度","Help":"幫助","Hide Screen":"隱藏畫面","ID":"ID","IMEI":"IMEI","Incorrect login details":"不正確的登入資訊","Info":"資訊","Inspect Device":"查看裝置","Inspector":"檢查器","Installation canceled by user.":"使用者取消安裝","Installation failed due to an unknown error.":"不明原因的安裝錯誤","Installation succeeded.":"安裝成功","Installation timed out.":"安裝超時","Installing app...":"App 安裝中...","Landscape":"橫式","Language":"語言","Launch Activity":"執行活動","Launching activity...":"活動執行中","Level":"電量","Local Settings":"本機設定","Location":"位置","Lock Rotation":"鎖定螢幕旋轉","Maintenance":"維護","Manage Apps":"管理 Apps","Manner Mode":"管理模式","Manufacturer":"製造商","Media":"媒體","Memory":"記憶體","Menu":"選單","Model":"型號","More about Access Tokens":"關於存取憑證","Mute":"靜音","Name":"名稱","Navigation":"導航","Network":"網路","Next":"下一首","No access tokens":"沒有存取憑證","No clipboard data":"剪貼簿無資料","No device screen":"沒有裝置畫面","No devices connected":"無裝置連線","No photo available":"沒有可用的照片","No screenshots taken":"尚未拍截圖","Normal Mode":"正常模式","Not Charging":"未充電","Notes":"備註","Notifications":"通知","Offline":"離線","Oops!":"糟糕了!有錯誤發生","Open":"開啟","Orientation":"螢幕方向","OS":"平台","Overheat":"過熱","Package":"套件","Password":"密碼","Permissions":"權限","Phone":"手機","Phone ICCID":"手機 ICCID","Phone IMEI":"手機 IMEI","Physical Device":"實體裝置","Place":"位置","Platform":"平台","Play/Pause":"播放 / 暫停","Please enter a valid email":"請輸入正確的 email","Please enter your email":"請輸入你的 email","Please enter your LDAP username":"請輸入你的 LDAP 帳號","Please enter your name":"請輸入你的名字","Please enter your password":"請輸入你的密碼","Please enter your Store password":"請輸入你儲存的密碼","Please enter your Store username":"請輸入你儲存的帳號","Power":"電源鍵","Power Source":"電源","Preparing":"準備中","Press Back button":"按返回鍵","Press Home button":"按主目錄鍵","Press Menu button":"按選單鍵","Previous":"上一首","Processing...":"處理中...","Product":"產品","Pushing app...":"App 傳送中...","RAM":"記憶體","Ready":"就緒","Reconnected successfully.":"重新連線成功","Refresh":"更新","Released":"發佈","Reload":"重新讀取","Remote debug":"遠端除蟲","Remove":"移除","Reset":"重設","Reset all browser settings":"重設所有瀏覽器設定","Reset Settings":"重設設定","Restart Device":"裝置重新啟動","Retrieving the device screen has timed out.":"取得裝置畫面已超時","Retry":"重試","Rewind":"倒帶","ROM":"唯讀記憶體","Rotate Left":"向左旋轉","Rotate Right":"向右旋轉","Run":"執行","Run JavaScript":"執行 JavaScript","Sample of log format":"日誌格式示例","Save Logs":"保存日誌","Save ScreenShot":"儲存螢幕截圖","Screen":"螢幕尺寸","Screenshot":"螢幕截圖","Screenshots":"更多螢幕截圖","SD Card Mounted":"已安裝 SD 卡","Search":"搜尋","Selects Next IME":"選擇下一個輸入法","Serial":"序號","Server":"伺服器","Server error. Check log output.":"伺服器錯誤. 請確認輸出的紀錄","Settings":"設定","Show Screen":"顯示畫面","Sign In":"登入","Sign Out":"登出","Silent Mode":"靜音模式","SIM":"SIM","Size":"尺寸","Socket connection was lost":"Socket 失去連線","Someone stole your device.":"別人偷了你的裝置.","Special Keys":"特殊按鍵","Start/Stop Logging":"開始 / 停止 紀錄","Status":"狀態","Stop":"停止","Stop Using":"停止使用","Store Account":"儲存帳號","Sub Type":"次類別","Switch Charset":"切換大小寫","Tag":"標籤","Take Pageshot (Needs WebView running)":"取得頁面截圖 (需執行 WebView)","Take Screenshot":"截圖","Temperature":"溫度","Text":"文字","The current view is marked secure and cannot be viewed remotely.":"目前的畫面已被保護,無法從遠端監看","The device will be unavailable for a moment.":"此裝置暫時無法使用","The existing package could not be deleted.":"現存的套件無法刪除","The new package couldn't be installed because the verification did not succeed.":"新套件無法安裝,驗證不成功.","The new package couldn't be installed because the verification timed out.":"驗證超時,新套件無法安裝","The new package couldn't be installed in the specified install location because the media is not available.":"目前媒體無法使用,無法安裝新套件到指定位置","The new package couldn't be installed in the specified install location.":"無法安裝新套件到指定位置","The new package failed because it contains a content provider with thesame authority as a provider already installed in the system.":"新套件安裝失敗,相同的內容供應商授權已存在.","The new package failed because the current SDK version is newer than that required by the package.":" 新套件安裝失敗,因為目前的 SDK 版本太新","The new package failed because the current SDK version is older than that required by the package.":"新套件安裝失敗,目前 SDK 版本太舊","The new package uses a feature that is not available.":"新套件使用尚未支援的功能","The package is already installed.":"套件已安裝","Time":"時間","Tip:":"提示:","Title":"標題","Toggle Web/Native":"切換 Web / Native","Total Devices":"所有裝置","translate":"翻譯","Try to reconnect":"嘗試重新連線","Type":"類型","Unauthorized":"未授權","Uninstall":"移除","Unknown":"未知","Unknown reason.":"未知原因","Unlock Rotation":"解除螢幕旋轉","Upload failed":"上傳失敗","Upload From Link":"從連結上傳","Upload unknown error":"未知的上傳錯誤","Uploaded file is not valid":"上傳的檔案無效","Uploading...":"上傳中...","Usable Devices":"可用的裝置","USB":"USB","Usb speed":"Usb 速度","Use":"使用","User":"使用者","Username":"帳號","Using":"使用中","Version":"版本","Version Update":"版本更新","Vibrate Mode":"震動模式","VNC":"VNC","Voltage":"電壓","Volume":"音量","Volume Down":"降低音量","Volume Up":"提高音量","Warning:":"警告:","Web":"網頁","Width":"寬度","WiFi":"WiFi","WiMAX":"WiMAX","You (or someone else) kicked the device.":"你 (或別人) 把裝置踢掉了."}} \ No newline at end of file diff --git a/res/common/lang/translations/stf.zh_CN.json b/res/common/lang/translations/stf.zh_CN.json index a16b3f962d..36c48c38eb 100644 --- a/res/common/lang/translations/stf.zh_CN.json +++ b/res/common/lang/translations/stf.zh_CN.json @@ -1 +1 @@ -{"zh_CN":{"-":"VNC(虚拟网络计算机远程工具)远程登录","A new version of STF is available":"STF有新版本可下载","A package is already installed with the same name.":"已经安装了一个相同名字的安装包","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).":"新安装包同之前的某个同名安装包发生了签名冲突(老的安装包的数据没有移除)","A secure container mount point couldn't be accessed on external media.":"外部存储无法访问安全容器挂载点","ABI":"ABI","AC":"AC","Access Tokens":"访问令牌","Account":"帐户","Action":"动作","Actions":"更多动作","Activity":"活动","ADB Keys":"安卓调试桥密钥","Add":"添加","Add ADB Key":"添加ADB Key","Add Key":"添加Key","Add the following ADB Key to STF?":"添加以下的ADB Key到STF?","Admin mode has been disabled.":"管理员模式已关闭","Admin mode has been enabled.":"管理员模式已启用","Advanced":"高级","Advanced Input":"高级输入","Airplane Mode":"飞行模式","App Store":"应用商店","App Upload":"上传APP","Apps":"应用程序","Are you sure you want to reboot this device?":"你确定要重启这台设备么?","Automation":"自动化","Available":"可用","Back":"返回","Battery":"电池","Battery Health":"电池健康","Battery Level":"电池电量","Battery Source":"电池电源","Battery Status":"电池状态","Battery Temp":"电池温度","Bluetooth":"蓝牙","Browser":"浏览器","Busy":"繁忙","Busy Devices":"繁忙的设备","Camera":"相机","Cancel":"取消","Cannot access specified URL":"无法访问指定的URL","Carrier":"信号","Category":"分类","Charging":"充电中","Check errors below":"检查下列错误","Clear":"清除","Clipboard":"剪贴板","Cold":"冷却","Connected":"已连接","Connected successfully.":"连接成功","Control":"控制","Cookies":"Cookies","Cores":"核","CPU":"CPU","CPU Platform":"CPU平台","Current rotation:":"当前屏幕旋转","Customize":"自定义","D-pad Center":"模拟键--中间","D-pad Down":"模拟键--下","D-pad Left":"模拟键--左","D-pad Right":"模拟键--右","D-pad Up":"模拟键--上","Dashboard":"控制面板","Data":"数据","Date":"日期","Dead":"无效","Delete":"删除","Density":"密度","Details":"细节","Developer":"开发者","Device":"设备","Device cannot get kicked from the group":"设备无法从该组移出","Device is not present anymore for some reason.":"设备由于某些原因找不到","Device is present but offline.":"设备已找到但处于离线状态","Device Photo":"设备照片","Device Settings":"设备设置","Device was disconnected":"设备已断开连接","Device was kicked by automatic timeout.":"设备由于超时已被移出","Devices":"设备","Disable WiFi":"关闭WIFI","Discharging":"未充电","Disconnected":"断开连接","Display":"播放","Drop file to upload":"拖放文件到这里以上传","Dummy":"虚拟的","Enable notifications":"允许提醒","Enable WiFi":"启用WIFI","Encrypted":"加密的","Error":"错误","Error while getting data":"获取数据时发生错误","Error while reconnecting":"重新连接时发生错误","Ethernet":"以太网","Executes remote shell commands":"执行远程shell命令","Failed to download file":"文件下载失败","Fast Forward":"快进","File Explorer":"文件管理器","Filter":"过滤器","Find Device":"查找设备","Fingerprint":"指纹","FPS":"FPS","Frequency":"频率","Full":"全部","General":"通用","Generate Access Token":"生成访问令牌","Generate Login for VNC":"生成登录VNC","Generate New Token":"生成新令牌","Get":"获取","Get clipboard contents":"获取剪贴板内容","Go Back":"返回","Go Forward":"前进","Go to Device List":"跳转到设备列表","Good":"良好","Hardware":"硬件","Health":"健康","Height":"高度","Help":"帮助","Hide Screen":"隐藏屏幕","Home":"主屏界面","Host":"主机地址","Hostname":"主机名","ICCID":"集成电路卡识别码","ID":"ID","IMEI":"IMEI","Incorrect login details":"登录信息错误","Info":"信息","Inspect Device":"被检查设备","Inspecting is currently only supported in WebView":"检查目前只支持网页视图","Inspector":"检查器","Installation canceled by user.":"用户已取消安装。","Installation failed due to an unknown error.":"未知原因导致安装失败","Installation succeeded.":"安装成功","Installation timed out.":"安装超时","Installing app...":"安装 app...","Key":"密钥","Keys":"按键","Landscape":"横排","Language":"语言","Launch Activity":"启动活动","Launching activity...":"活动启动中...","Level":"等级","Local Settings":"本地设置","Location":"位置","Lock Rotation":"锁定屏幕旋转","Logs":"日志","Maintenance":"维护","Make sure to copy your access token now. You won't be able to see it again.":"请确保已备份您的身份验证凭证,此凭证后续将不再显示!","Manage Apps":"管理Apps","Manner Mode":"管理模式","Manufacturer":"制造商","Media":"媒体","Memory":"内存","Menu":"菜单","Mobile":"手机","Mobile DUN":"手机网络桥接","Mobile High Priority":"移动网络最高优先级","Mobile MMS":"手机彩信","Mobile SUPL":"平面定位特定移动数据连接","Model":"型号","More about Access Tokens":"关于身份验证凭证","More about ADB Keys":"更多关于安卓调试桥密钥","Mute":"静音","Name":"名称","Native":"本地","Navigation":"导航","Network":"网络","Next":"下一步","No":"否","No access tokens":"没有身份验证凭证","No ADB keys":"没有安卓调试桥的密钥","No clipboard data":"剪贴板没有数据","No cookies to show":"没有本地cookies缓存","No device screen":"没有设备画面","No devices connected":"无设备连接","No photo available":"沒有可用的照片","No Ports Forwarded":"没有端口转发","No screenshots taken":"未拍截图","Normal Mode":"正常模式\"","Not Charging":"未充电","Notes":"标注","Nothing to inspect":"无需检查","Notifications":"通知","Number":"数字","Offline":"离线","Oops!":"出错了!","Open":"打开","Orientation":"屏幕方向","OS":"操作系统","Over Voltage":"电压过高","Overheat":"过热","Package":"程序安装包","Password":"密码","Permissions":"权限","Phone":"手机","Phone ICCID":"手机集成电路卡识别码(手机SIM卡唯一识别码)","Phone IMEI":"手机国际移动设备标识","Physical Device":"物理设备","PID":"进程号","Place":"位置","Platform":"平台","Play/Pause":"播放/暂停","Please enter a valid email":"请输入正确格式的 email","Please enter your email":"请输入您的 email","Please enter your LDAP username":"请输入您的LDAP用户名","Please enter your name":"请输入您的姓名","Please enter your password":"请输入您的密码","Please enter your Store password":"请输入您所保存的密码","Please enter your Store username":"输入您所保存的用户名","Port":"端口","Port Forwarding":"转发端口","Portrait":"竖排","Power":"电源","Power Source":"电源来源","Preparing":"准备中","Press Back button":"按后退键","Press Home button":"按Home键","Press Menu button":"按菜单键","Previous":"先前的","Processing...":"处理中...","Product":"产品","Pushing app...":"正在推送 app...","RAM":"随机存取存储器","Ready":"就绪","Reconnected successfully.":"重新连接成功","Refresh":"刷新","Released":"释放","Reload":"重新加载","Remote debug":"远程调试","Remove":"移除","Reset":"重置","Reset all browser settings":"重置所有浏览器设置","Reset Settings":"重置设置","Restart Device":"重启设备","Retrieving the device screen has timed out.":"获取设备画面超时","Retry":"重试","Rewind":"回滚","Roaming":"漫游状态","ROM":"ROM","Rotate Left":"向左翻转","Rotate Right":"向右翻转","Run":"运行","Run JavaScript":"运行 JavaScript","Run the following on your command line to debug the device from your Browser":"运行下面的命令行从您的浏览器中调试设备","Run the following on your command line to debug the device from your IDE":"运行下面命令行从您的IDE调试设备","Run this command to copy the key to your clipboard":"运行此命令将密钥复制到剪贴板","Save ScreenShot":"保存屏幕截图","Save...":"正在保存","Screen":"屏幕","Screenshot":"屏幕截图","Screenshots":"更多屏幕截图","SD Card Mounted":"SD卡已加载","SDK":"SDK","Search":"搜索","Selects Next IME":"选择下一个输入法","Serial":"串行","Server":"服务器","Server error. Check log output.":"服务器错误,请检查输出的日志纪录。","Set":"设置","Set Cookie":"设置cookies缓存","Settings":"设置","Shell":"Shell脚本","Show Screen":"显示屏幕","Sign In":"登录","Sign Out":"注销","Silent Mode":"静音模式","SIM":"SIM","Size":"大小","Socket connection was lost":"双向的通信连接丢失","Someone stole your device.":"有人占用了你的设备","Special Keys":"特殊要点","Start/Stop Logging":"开始 / 停止 日志纪录","Status":"状态","Stop":"停止","Stop Using":"停止使用","Store Account":"保存账号","Sub Type":"子类型","Switch Charset":"切换字符集","Tag":"标签","Take Pageshot (Needs WebView running)":"使用截图(需要WebView 运行)","Take Screenshot":"屏幕截图","Temperature":"温度","Text":"文字","The current view is marked secure and cannot be viewed remotely.":"当前视图有安全标记,并且不能远程查看","The device will be unavailable for a moment.":"此设备将暂时无法使用","The existing package could not be deleted.":"现有的程序包可能不会被删除","The new package couldn't be installed because the verification did not succeed.":"新包不能安装,因为验证没有成功。","The new package couldn't be installed because the verification timed out.":"新包不能安装,因为验证超时。","The new package couldn't be installed in the specified install location because the media is not available.":"新包不能安装在指定的安装位置,因为媒体是不可用。","The new package couldn't be installed in the specified install location.":"新包不能安装在指定的安装位置。","The new package failed because it contains a content provider with thesame authority as a provider already installed in the system.":"新的包安装失败,因为它包含了相同名字的认证已经安装在系统中提供一个内容提供商。","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.":"新包安装失败,因为它已被指定,它是一个用于测试的包和调用者并没有给它提供INSTALL_ALLOW_TEST标志。","The new package failed because the current SDK version is newer than that required by the package.":"新的包安装失败,因为当前的SDK版本比程序包所依赖的版本高。","The new package failed because the current SDK version is older than that required by the package.":"新的包安装失败,因为当前的SDK比安装包所依赖的版本低。","The new package failed while optimizing and validating its dex files, either because there was not enough storage or the validation failed.":"新的包安装失败,可能没有验证dex文件或者没有足够的存储导致验证失败。","The new package has an older version code than the currently installed package.":"新的软件包比目前安装的软件包代码版本旧。","The new package is assigned a different UID than it previously held.":"新包分配置的UID与它以前持有的不同。","The new package uses a feature that is not available.":"新包使用功能不可用。","The new package uses a shared library that is not available.":"新包使用的共享库不可用","The package archive file is invalid.":"包存档文件无效。","The package being installed contains native code, but none that is compatible with the device's CPU_ABI.":"正在安装软件包中包含源生内核,但都没有与该设备的CPU_ABI兼容。","The package changed from what the calling program expected.":"程序包被预期调用的程序所修改。","The package is already installed.":"程序包已经安装。","The package manager service found that the device didn't have enough storage space to install the app.":"包管理器服务发现该设备没有足够的存储空间来安装应用程序。","The parser did not find any actionable tags (instrumentation or application) in the manifest.":"解析器没有发现清单任何可操作的标签(工具或应用程序) 。","The parser did not find any certificates in the .apk.":"解析器没有发现在.apk文件的任何证书。","The parser encountered a bad or missing package name in the manifest.":"分析器在清单中遇到损坏或丢失的包名。","The parser encountered a bad shared user id name in the manifest.":"分析器在清单中遇到坏共享的用户ID名称。","The parser encountered a CertificateEncodingException in one of the files in the .apk.":"解析器在的apk的一个文件时遇到证书编码异常。","The parser encountered an unexpected exception.":"解析器遇到意外的异常。","The parser encountered some structural problem in the manifest.":"分析器在清单中遇到的一些结构性问题。","The parser found inconsistent certificates on the files in the .apk.":"分析器发现在的apk文件不一致的证书。","The parser was given a path that is not a file, or does not end with the expected '.apk' extension.":"解析器所获得的是一个路径而不是一个文件,或者文件没有以‘.apk’结尾。","The parser was unable to retrieve the AndroidManifest.xml file.":"分析器无法检索AndroidManifest.xml文件。","The requested shared user does not exist.":"所请求的共享用户不存在","The system failed to install the package because its packaged native code did not match any of the ABIs supported by the system.":"该系统无法安装此软件包,因为其包装原生内核没有匹配到应系统所支持的的ABI。","The system failed to install the package because of system issues.":"因为系统问题无法安装程序。","The system failed to install the package because the user is restricted from installing apps.":"程序安装失败,因为用户被限制安装该应用软件","The URI passed in is invalid.":"该URI传递无效","TID":"线程号","Time":"时间","Tip:":"提示:","Title":"标题","Toggle Web/Native":"切换网络/本地","Total Devices":"设备总数","translate":"翻译","Try to reconnect":"尝试重新连接","Type":"类型","Unauthorized":"未授权的","Uninstall":"卸载","Unknown":"未知","Unknown reason.":"未知的原因。","Unlock Rotation":"旋转解锁","Unspecified Failure":"未指定的故障","Upload failed":"上传失败","Upload From Link":"从超链接上传","Upload unknown error":"未知的上传错误","Uploaded file is not valid":"上传的文件是无效的","Uploading...":"上传中...","Usable Devices":"可用的设备","USB":"USB","Usb speed":"USB速度","Use":"使用","User":"用户","Username":"用户名","Using":"使用中","Using Fallback":"使用回退","Version":"版本","Version Update":"版本更新","Vibrate Mode":"振动模式","VNC":"VNC","Voltage":"电压","Volume":"音量","Volume Down":"减小音量","Volume Up":"增加音量","Warning:":"警告:","Web":"Web","Width":"宽度","WiFi":"WiFi","WiMAX":"WiMAX","Wireless":"无线","X DPI":"X DPI","Y DPI":"Y DPI","Yes":"是","You (or someone else) kicked the device.":"你 (或別人) 已移出了该设备。"}} +{"zh_CN":{"-":"VNC(虚拟网络计算机远程工具)远程登录","A new version of STF is available":"STF有新版本可下载","A package is already installed with the same name.":"已经安装了一个相同名字的安装包","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).":"新安装包同之前的某个同名安装包发生了签名冲突(老的安装包的数据没有移除)","A secure container mount point couldn't be accessed on external media.":"外部存储无法访问安全容器挂载点","ABI":"ABI","AC":"AC","Access Tokens":"访问令牌","Account":"帐户","Action":"动作","Actions":"更多动作","Activity":"活动","ADB Keys":"安卓调试桥密钥","Add":"添加","Add ADB Key":"添加ADB Key","Add Key":"添加Key","Add the following ADB Key to STF?":"添加以下的ADB Key到STF?","Admin mode has been disabled.":"管理员模式已关闭","Admin mode has been enabled.":"管理员模式已启用","Advanced":"高级","Advanced Input":"高级输入","Airplane Mode":"飞行模式","App Store":"应用商店","App Upload":"上传APP","Apps":"应用程序","Are you sure you want to reboot this device?":"你确定要重启这台设备么?","Automation":"自动化","Available":"可用","Back":"返回","Battery":"电池","Battery Health":"电池健康","Battery Level":"电池电量","Battery Source":"电池电源","Battery Status":"电池状态","Battery Temp":"电池温度","Bluetooth":"蓝牙","Browser":"浏览器","Busy":"繁忙","Busy Devices":"繁忙的设备","Camera":"相机","Cancel":"取消","Cannot access specified URL":"无法访问指定的URL","Carrier":"信号","Category":"分类","Charging":"充电中","Check errors below":"检查下列错误","Clear":"清除","Clipboard":"剪贴板","Cold":"冷却","Connected":"已连接","Connected successfully.":"连接成功","Control":"控制","Cookies":"Cookies","Cores":"核","CPU":"CPU","CPU Platform":"CPU平台","Current rotation:":"当前屏幕旋转","Customize":"自定义","D-pad Center":"模拟键--中间","D-pad Down":"模拟键--下","D-pad Left":"模拟键--左","D-pad Right":"模拟键--右","D-pad Up":"模拟键--上","Dashboard":"控制面板","Data":"数据","Date":"日期","Dead":"无效","Delete":"删除","Density":"密度","Details":"细节","Developer":"开发者","Device":"设备","Device cannot get kicked from the group":"设备无法从该组移出","Device is not present anymore for some reason.":"设备由于某些原因找不到","Device is present but offline.":"设备已找到但处于离线状态","Device Photo":"设备照片","Device Settings":"设备设置","Device was disconnected":"设备已断开连接","Device was kicked by automatic timeout.":"设备由于超时已被移出","Devices":"设备","Disable WiFi":"关闭WIFI","Discharging":"未充电","Disconnected":"断开连接","Display":"播放","Drop file to upload":"拖放文件到这里以上传","Dummy":"虚拟的","Enable notifications":"允许提醒","Enable WiFi":"启用WIFI","Encrypted":"加密的","Error":"错误","Error while getting data":"获取数据时发生错误","Error while reconnecting":"重新连接时发生错误","Ethernet":"以太网","Executes remote shell commands":"执行远程shell命令","Failed to download file":"文件下载失败","Fast Forward":"快进","File Explorer":"文件管理器","File Name":"文件名","Filter":"过滤器","Find Device":"查找设备","Fingerprint":"指纹","FPS":"FPS","Frequency":"频率","Full":"全部","General":"通用","Generate Access Token":"生成访问令牌","Generate Login for VNC":"生成登录VNC","Generate New Token":"生成新令牌","Get":"获取","Get clipboard contents":"获取剪贴板内容","Go Back":"返回","Go Forward":"前进","Go to Device List":"跳转到设备列表","Good":"良好","Hardware":"硬件","Health":"健康","Height":"高度","Help":"帮助","Hide Screen":"隐藏屏幕","Home":"主屏界面","Host":"主机地址","Hostname":"主机名","ICCID":"集成电路卡识别码","ID":"ID","IMEI":"IMEI","Incorrect login details":"登录信息错误","Info":"信息","Inspect Device":"被检查设备","Inspecting is currently only supported in WebView":"检查目前只支持网页视图","Inspector":"检查器","Installation canceled by user.":"用户已取消安装。","Installation failed due to an unknown error.":"未知原因导致安装失败","Installation succeeded.":"安装成功","Installation timed out.":"安装超时","Installing app...":"安装 app...","Key":"密钥","Keys":"按键","Landscape":"横排","Language":"语言","Launch Activity":"启动活动","Launching activity...":"活动启动中...","Level":"等级","Local Settings":"本地设置","Location":"位置","Lock Rotation":"锁定屏幕旋转","Logs":"日志","Maintenance":"维护","Make sure to copy your access token now. You won't be able to see it again.":"请确保已备份您的身份验证凭证,此凭证后续将不再显示!","Manage Apps":"管理Apps","Manner Mode":"管理模式","Manufacturer":"制造商","Media":"媒体","Memory":"内存","Menu":"菜单","Mobile":"手机","Mobile DUN":"手机网络桥接","Mobile High Priority":"移动网络最高优先级","Mobile MMS":"手机彩信","Mobile SUPL":"平面定位特定移动数据连接","Model":"型号","More about Access Tokens":"关于身份验证凭证","More about ADB Keys":"更多关于安卓调试桥密钥","Mute":"静音","Name":"名称","Native":"本地","Navigation":"导航","Network":"网络","Next":"下一步","No":"否","No access tokens":"没有身份验证凭证","No ADB keys":"没有安卓调试桥的密钥","No clipboard data":"剪贴板没有数据","No cookies to show":"没有本地cookies缓存","No device screen":"没有设备画面","No devices connected":"无设备连接","No photo available":"沒有可用的照片","No Ports Forwarded":"没有端口转发","No screenshots taken":"未拍截图","Normal Mode":"正常模式\"","Not Charging":"未充电","Notes":"标注","Nothing to inspect":"无需检查","Notifications":"通知","Number":"数字","Offline":"离线","Oops!":"出错了!","Open":"打开","Orientation":"屏幕方向","OS":"操作系统","Over Voltage":"电压过高","Overheat":"过热","Package":"程序安装包","Password":"密码","Permissions":"权限","Phone":"手机","Phone ICCID":"手机集成电路卡识别码(手机SIM卡唯一识别码)","Phone IMEI":"手机国际移动设备标识","Physical Device":"物理设备","PID":"进程号","Place":"位置","Platform":"平台","Play/Pause":"播放/暂停","Please enter a valid email":"请输入正确格式的 email","Please enter your email":"请输入您的 email","Please enter your LDAP username":"请输入您的LDAP用户名","Please enter your name":"请输入您的姓名","Please enter your password":"请输入您的密码","Please enter your Store password":"请输入您所保存的密码","Please enter your Store username":"输入您所保存的用户名","Port":"端口","Port Forwarding":"转发端口","Portrait":"竖排","Power":"电源","Power Source":"电源来源","Preparing":"准备中","Press Back button":"按后退键","Press Home button":"按Home键","Press Menu button":"按菜单键","Previous":"先前的","Processing...":"处理中...","Product":"产品","Pushing app...":"正在推送 app...","RAM":"随机存取存储器","Ready":"就绪","Reconnected successfully.":"重新连接成功","Refresh":"刷新","Released":"释放","Reload":"重新加载","Remote debug":"远程调试","Remove":"移除","Reset":"重置","Reset all browser settings":"重置所有浏览器设置","Reset Settings":"重置设置","Restart Device":"重启设备","Retrieving the device screen has timed out.":"获取设备画面超时","Retry":"重试","Rewind":"回滚","Roaming":"漫游状态","ROM":"ROM","Rotate Left":"向左翻转","Rotate Right":"向右翻转","Run":"运行","Run JavaScript":"运行 JavaScript","Run the following on your command line to debug the device from your Browser":"运行下面的命令行从您的浏览器中调试设备","Run the following on your command line to debug the device from your IDE":"运行下面命令行从您的IDE调试设备","Run this command to copy the key to your clipboard":"运行此命令将密钥复制到剪贴板","Sample of log format":"日志格式示例","Save Logs":"保存日志","Save ScreenShot":"保存屏幕截图","Save...":"正在保存","Screen":"屏幕","Screenshot":"屏幕截图","Screenshots":"更多屏幕截图","SD Card Mounted":"SD卡已加载","SDK":"SDK","Search":"搜索","Selects Next IME":"选择下一个输入法","Serial":"串行","Server":"服务器","Server error. Check log output.":"服务器错误,请检查输出的日志纪录。","Set":"设置","Set Cookie":"设置cookies缓存","Settings":"设置","Shell":"Shell脚本","Show Screen":"显示屏幕","Sign In":"登录","Sign Out":"注销","Silent Mode":"静音模式","SIM":"SIM","Size":"大小","Socket connection was lost":"双向的通信连接丢失","Someone stole your device.":"有人占用了你的设备","Special Keys":"特殊要点","Start/Stop Logging":"开始 / 停止 日志纪录","Status":"状态","Stop":"停止","Stop Using":"停止使用","Store Account":"保存账号","Sub Type":"子类型","Switch Charset":"切换字符集","Tag":"标签","Take Pageshot (Needs WebView running)":"使用截图(需要WebView 运行)","Take Screenshot":"屏幕截图","Temperature":"温度","Text":"文字","The current view is marked secure and cannot be viewed remotely.":"当前视图有安全标记,并且不能远程查看","The device will be unavailable for a moment.":"此设备将暂时无法使用","The existing package could not be deleted.":"现有的程序包可能不会被删除","The new package couldn't be installed because the verification did not succeed.":"新包不能安装,因为验证没有成功。","The new package couldn't be installed because the verification timed out.":"新包不能安装,因为验证超时。","The new package couldn't be installed in the specified install location because the media is not available.":"新包不能安装在指定的安装位置,因为媒体是不可用。","The new package couldn't be installed in the specified install location.":"新包不能安装在指定的安装位置。","The new package failed because it contains a content provider with thesame authority as a provider already installed in the system.":"新的包安装失败,因为它包含了相同名字的认证已经安装在系统中提供一个内容提供商。","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.":"新包安装失败,因为它已被指定,它是一个用于测试的包和调用者并没有给它提供INSTALL_ALLOW_TEST标志。","The new package failed because the current SDK version is newer than that required by the package.":"新的包安装失败,因为当前的SDK版本比程序包所依赖的版本高。","The new package failed because the current SDK version is older than that required by the package.":"新的包安装失败,因为当前的SDK比安装包所依赖的版本低。","The new package failed while optimizing and validating its dex files, either because there was not enough storage or the validation failed.":"新的包安装失败,可能没有验证dex文件或者没有足够的存储导致验证失败。","The new package has an older version code than the currently installed package.":"新的软件包比目前安装的软件包代码版本旧。","The new package is assigned a different UID than it previously held.":"新包分配置的UID与它以前持有的不同。","The new package uses a feature that is not available.":"新包使用功能不可用。","The new package uses a shared library that is not available.":"新包使用的共享库不可用","The package archive file is invalid.":"包存档文件无效。","The package being installed contains native code, but none that is compatible with the device's CPU_ABI.":"正在安装软件包中包含源生内核,但都没有与该设备的CPU_ABI兼容。","The package changed from what the calling program expected.":"程序包被预期调用的程序所修改。","The package is already installed.":"程序包已经安装。","The package manager service found that the device didn't have enough storage space to install the app.":"包管理器服务发现该设备没有足够的存储空间来安装应用程序。","The parser did not find any actionable tags (instrumentation or application) in the manifest.":"解析器没有发现清单任何可操作的标签(工具或应用程序) 。","The parser did not find any certificates in the .apk.":"解析器没有发现在.apk文件的任何证书。","The parser encountered a bad or missing package name in the manifest.":"分析器在清单中遇到损坏或丢失的包名。","The parser encountered a bad shared user id name in the manifest.":"分析器在清单中遇到坏共享的用户ID名称。","The parser encountered a CertificateEncodingException in one of the files in the .apk.":"解析器在的apk的一个文件时遇到证书编码异常。","The parser encountered an unexpected exception.":"解析器遇到意外的异常。","The parser encountered some structural problem in the manifest.":"分析器在清单中遇到的一些结构性问题。","The parser found inconsistent certificates on the files in the .apk.":"分析器发现在的apk文件不一致的证书。","The parser was given a path that is not a file, or does not end with the expected '.apk' extension.":"解析器所获得的是一个路径而不是一个文件,或者文件没有以‘.apk’结尾。","The parser was unable to retrieve the AndroidManifest.xml file.":"分析器无法检索AndroidManifest.xml文件。","The requested shared user does not exist.":"所请求的共享用户不存在","The system failed to install the package because its packaged native code did not match any of the ABIs supported by the system.":"该系统无法安装此软件包,因为其包装原生内核没有匹配到应系统所支持的的ABI。","The system failed to install the package because of system issues.":"因为系统问题无法安装程序。","The system failed to install the package because the user is restricted from installing apps.":"程序安装失败,因为用户被限制安装该应用软件","The URI passed in is invalid.":"该URI传递无效","TID":"线程号","Time":"时间","Tip:":"提示:","Title":"标题","Toggle Web/Native":"切换网络/本地","Total Devices":"设备总数","translate":"翻译","Try to reconnect":"尝试重新连接","Type":"类型","Unauthorized":"未授权的","Uninstall":"卸载","Unknown":"未知","Unknown reason.":"未知的原因。","Unlock Rotation":"旋转解锁","Unspecified Failure":"未指定的故障","Upload failed":"上传失败","Upload From Link":"从超链接上传","Upload unknown error":"未知的上传错误","Uploaded file is not valid":"上传的文件是无效的","Uploading...":"上传中...","Usable Devices":"可用的设备","USB":"USB","Usb speed":"USB速度","Use":"使用","User":"用户","Username":"用户名","Using":"使用中","Using Fallback":"使用回退","Version":"版本","Version Update":"版本更新","Vibrate Mode":"振动模式","VNC":"VNC","Voltage":"电压","Volume":"音量","Volume Down":"减小音量","Volume Up":"增加音量","Warning:":"警告:","Web":"Web","Width":"宽度","WiFi":"WiFi","WiMAX":"WiMAX","Wireless":"无线","X DPI":"X DPI","Y DPI":"Y DPI","Yes":"是","You (or someone else) kicked the device.":"你 (或別人) 已移出了该设备。"}} diff --git a/res/test/e2e/control/control-spec.js b/res/test/e2e/control/control-spec.js index 460cbede2e..3af88c1a37 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') - } - this.kickDeviceButton = element.all(by.css('.kick-device')).first() - this.kickDevice = function() { - this.openDevicesDropDown() - this.kickDeviceButton.click() + browser.get(localhost + 'control') } + + this.kickDeviceButton = element.all(by.css('.kick-device')) this.devicesDropDown = element(by.css('.device-name-text')) + this.openDevicesDropDown = function() { - this.devicesDropDown.click() + return this.devicesDropDown.click() + } + + this.getFirstKickDeviceButton = function() { + return this.kickDeviceButton.first() + } + + this.kickDevice = function() { + this.openDevicesDropDown() + 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 30ce796623..aad4097153 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 92529a9574..2884b5e019 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.filterUsingDevices = function() { + return this.searchInput.sendKeys('state: "using"') + } + this.numberOfDevices = function() { - return this.devicesByCss.count() + 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 989e8b60e3..6765a8fae8 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 cb6389cba3..c05c92f0bc 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 1a89644ccc..d1ec56b4a1 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 0000000000..3235b343b1 --- /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 0000000000..24e514d6a2 --- /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 fbe7ea246b..2826f90d5c 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 9c2610cb0b..af51561a2c 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 0ca4ee0547..659a85ae32 100644 Binary files a/vendor/STFService/STFService.apk and b/vendor/STFService/STFService.apk differ diff --git a/yarn.lock b/yarn.lock index 996851b672..a3a16ae0b9 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,10 +274,23 @@ 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" +append-field@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/append-field/-/append-field-1.0.0.tgz#1e3440e915f0b1203d23748e78edd7b9b5b43e56" + integrity sha1-HjRA6RXwsSA9I3SOeO3XubW0PlY= + aproba@^1.0.3: version "1.1.2" resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.1.2.tgz#45c6629094de4e96f693ef7eab74ae079c240fc1" @@ -240,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" @@ -256,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" @@ -282,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" @@ -339,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" @@ -355,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" @@ -373,6 +491,13 @@ async@^2.0.1: dependencies: lodash "^4.14.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== + dependencies: + lodash "^4.17.14" + async@~0.2.6, async@~0.2.7, async@~0.2.9: version "0.2.10" resolved "https://registry.yarnpkg.com/async/-/async-0.2.10.tgz#b6bbe0b0674b9d719708ca38de8c237cb526c3d1" @@ -385,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" @@ -414,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" @@ -486,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" @@ -530,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" @@ -540,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" @@ -564,6 +731,38 @@ bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.1.1, bn.js@^4.4.0: version "4.11.6" resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.6.tgz#53344adb14617a13f6e8dd2ce28905d1c0ba3215" +body-parser@1.18.2: + version "1.18.2" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.18.2.tgz#87678a19d84b47d859b83199bd59bce222b10454" + integrity sha1-h2eKGdhLR9hZuDGZvVm84iKxBFQ= + dependencies: + bytes "3.0.0" + content-type "~1.0.4" + debug "2.6.9" + depd "~1.1.1" + http-errors "~1.6.2" + iconv-lite "0.4.19" + on-finished "~2.3.0" + qs "6.5.1" + 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" @@ -579,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" @@ -589,9 +800,10 @@ boom@2.x.x: dependencies: hoek "2.x.x" -bower@^1.7.2: - version "1.8.0" - resolved "https://registry.yarnpkg.com/bower/-/bower-1.8.0.tgz#55dbebef0ad9155382d9e9d3e497c1372345b44a" +bower@^1.8.8: + version "1.8.8" + resolved "https://registry.yarnpkg.com/bower/-/bower-1.8.8.tgz#82544be34a33aeae7efb8bdf9905247b2cffa985" + integrity sha512-1SrJnXnkP9soITHptSO+ahx3QKp3cVzn8poI6ujqc5SeOkg5iqM1pK9H+DSc2OQ8SnO0jC/NG4Ur/UIwy7574A== brace-expansion@^1.0.0, brace-expansion@^1.1.7: version "1.1.8" @@ -614,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" @@ -686,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" @@ -694,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" @@ -769,6 +1009,31 @@ bytes@2.4.0: version "2.4.0" resolved "https://registry.yarnpkg.com/bytes/-/bytes-2.4.0.tgz#7d97196f9d5baf7f6935e25985549edd2a6c2339" +bytes@3.0.0: + version "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" @@ -906,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" @@ -922,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" @@ -961,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" @@ -987,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" @@ -1043,6 +1359,13 @@ 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: + version "1.0.8" + resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" + integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== + dependencies: + delayed-stream "~1.0.0" + commander@2.8.x: version "2.8.1" resolved "https://registry.yarnpkg.com/commander/-/commander-2.8.1.tgz#06be367febfda0c330aa1e2a072d3dc9762425d4" @@ -1055,6 +1378,16 @@ commander@2.9.x, commander@^2.3.0, commander@^2.5.0, commander@^2.7.1, commander dependencies: graceful-readlink ">= 1.0.0" +commander@~2.11.0: + version "2.11.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.11.0.tgz#157152fd1e7a6c8d98a5b715cf376df928004563" + integrity sha512-b0553uYA5YAEGgyYIGYROzKQ7X5RAqedkfjiZxwi0kL1g3bOaBNNZfYkzt/CL0umgD5wc9Jec2FbB98CjkMRvQ== + +commander@~2.19.0: + version "2.19.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.19.0.tgz#f6198aa84e5b83c46054b94ddedbfed5ee9ff12a" + integrity sha512-6tvAOO+D6OENvRAh524Dh9jcfKTYDQAqvqezbCW82xj5X0pSrcpxtvRKHLG0yBY6SD7PSDrJaj+0AiOcKVd1Xg== + commander@~2.6.0: version "2.6.0" resolved "https://registry.yarnpkg.com/commander/-/commander-2.6.0.tgz#9df7e52fb2a0cb0fb89058ee80c3104225f37e1d" @@ -1089,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" @@ -1181,14 +1519,22 @@ 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" resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.2.tgz#b7d113aee7a8dd27bd21133c4dc2529df1721eed" +content-type@~1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b" + integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA== + convert-to-ecmascript-compatible-varname@^0.1.0: version "0.1.5" resolved "https://registry.yarnpkg.com/convert-to-ecmascript-compatible-varname/-/convert-to-ecmascript-compatible-varname-0.1.5.tgz#f67a4938c5233443564250479c67014bac878499" @@ -1210,10 +1556,20 @@ 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" +cookiejar@^2.1.0: + version "2.1.2" + resolved "https://registry.yarnpkg.com/cookiejar/-/cookiejar-2.1.2.tgz#dd8a235530752f988f9a0844f3fc589e3111125c" + integrity sha512-Mw+adcfzPxcPeI+0WlvRrr/3lGVO0bD75SxX6811cxSh1Wbxx7xZBGK1eVtDf6si8rg2lhnUjsVLMFMfbRIuwA== + cookies@0.7.0: version "0.7.0" resolved "https://registry.yarnpkg.com/cookies/-/cookies-0.7.0.tgz#0bc961d910c35254980fc7c9eff5da12011bbf00" @@ -1221,10 +1577,20 @@ 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" +core-js@^2.5.7: + version "2.6.9" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.9.tgz#6b4b214620c834152e179323727fc19741b084f2" + integrity sha512-HOpZf6eXmnl7la+cUdMnLvUxKNqLUzJvgIziQ0DiF3JwSImNphIqdGqzj6hIKyX04MmV0poclQ7+wjWvxQyR2A== + core-util-is@1.0.2, core-util-is@~1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" @@ -1277,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" @@ -1490,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" @@ -1514,30 +1892,116 @@ debug@2.6.7: dependencies: ms "2.0.0" +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.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" @@ -1569,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" @@ -1581,6 +2057,16 @@ depd@1.1.0, depd@~1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.0.tgz#e1bd82c6aab6ced965b97b88b17ed3e528ca18c3" +depd@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.1.tgz#5783b4e1c459f06fa5ca27f991f3d06e7a310359" + integrity sha1-V4O04cRZ8G+lyif5kfPQbnoxA1k= + +depd@~1.1.1, depd@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" + integrity sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak= + deprecated@^0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/deprecated/-/deprecated-0.0.1.tgz#f9c9af5464afa1e7a971458a8bdef2aa94d5bb19" @@ -1608,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" @@ -1638,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" @@ -1759,6 +2275,18 @@ encodeurl@~1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.1.tgz#79e3d58655346909fe6f0f45a5de68103b294d20" +encodeurl@~1.0.2: + version "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" @@ -1904,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" @@ -1930,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" @@ -2043,6 +2609,11 @@ esprima@^3.1.1, esprima@~3.1.0: version "3.1.3" resolved "https://registry.yarnpkg.com/esprima/-/esprima-3.1.3.tgz#fdca51cee6133895e3c88d535ce49dbff62a4633" +esprima@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" + integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== + "esprima@~ 2.0.0": version "2.0.0" resolved "https://registry.yarnpkg.com/esprima/-/esprima-2.0.0.tgz#609ac5c2667eae5433b41eb9ecece2331b41498f" @@ -2076,6 +2647,11 @@ etag@~1.8.0: version "1.8.0" resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.0.tgz#6f631aef336d6c46362b51764044ce216be3c051" +etag@~1.8.1: + version "1.8.1" + resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" + integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc= + event-emitter@~0.3.5: version "0.3.5" resolved "https://registry.yarnpkg.com/event-emitter/-/event-emitter-0.3.5.tgz#df8c69eef1647923c7157b9ce83840610b02cc39" @@ -2103,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" @@ -2115,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" @@ -2127,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" @@ -2149,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" @@ -2183,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-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@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@^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" @@ -2226,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" @@ -2270,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" @@ -2284,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" @@ -2316,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" @@ -2330,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: @@ -2342,7 +3017,20 @@ finalhandler@1.0.3, finalhandler@~1.0.3: statuses "~1.3.1" unpipe "~1.0.0" -find-index@^0.1.1: +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" @@ -2359,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" @@ -2401,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" @@ -2423,6 +3118,24 @@ form-data@^2.1.1, form-data@~2.1.1: combined-stream "^1.0.5" mime-types "^2.1.12" +form-data@^2.3.1: + version "2.5.1" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.5.1.tgz#f2cbec57b5e59e23716e128fe44d4e5dd23895f4" + integrity sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA== + dependencies: + asynckit "^0.4.0" + 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" @@ -2441,10 +3154,27 @@ 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" +fresh@0.5.2: + version "0.5.2" + resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" + integrity sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac= + from@~0: version "0.1.7" resolved "https://registry.yarnpkg.com/from/-/from-0.1.7.tgz#83c60afc58b9c56997007ed1a768b3ab303a44fe" @@ -2459,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: @@ -2467,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" @@ -2478,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" @@ -2499,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" @@ -2549,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" @@ -2568,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" @@ -2638,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" @@ -2677,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" @@ -2714,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" @@ -2732,6 +3550,13 @@ graphlib@^2.1.1: dependencies: lodash "^4.11.1" +graphlib@^2.1.7: + version "2.1.7" + resolved "https://registry.yarnpkg.com/graphlib/-/graphlib-2.1.7.tgz#b6a69f9f44bd9de3963ce6804a2fc9e73d86aecc" + integrity sha512-TyI9jIy2J4j0qgPmOOrHTCtpPqJGN/aurBwc6ZT+bRii+di1I+Wv3obRhVrmBEXet+qkMaEX67dXrwsd3QQM6w== + dependencies: + lodash "^4.17.5" + gulp-angular-gettext@^2.1.0: version "2.2.0" resolved "https://registry.yarnpkg.com/gulp-angular-gettext/-/gulp-angular-gettext-2.2.0.tgz#90b5f38e89b4be491e8bb5cabaf69b0a71ddf95f" @@ -2856,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" @@ -2876,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" @@ -2912,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" @@ -2993,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" @@ -3030,6 +3936,32 @@ 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" + integrity sha1-CgAsyFcHGSp+eUbO7cERVfYOxzY= + dependencies: + depd "1.1.1" + inherits "2.0.3" + 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" @@ -3047,14 +3979,36 @@ http-errors@~1.6.1: setprototypeof "1.0.3" statuses ">= 1.3.1 < 2" -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-errors@~1.6.2: + version "1.6.3" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.6.3.tgz#8b55680bb4be283a0b5bf4ea2e38580be1d9320d" + integrity sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0= + dependencies: + depd "~1.1.2" + inherits "2.0.3" + setprototypeof "1.1.0" + statuses ">= 1.4.0 < 2" + +http-errors@~1.7.2: + version "1.7.3" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.3.tgz#6c619e4f9c60308c38519498c14fbb10aacebb06" + integrity sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw== + dependencies: + depd "~1.1.2" + inherits "2.0.4" + setprototypeof "1.1.1" + statuses ">= 1.5.0 < 2" + toidentifier "1.0.0" + +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" @@ -3071,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" @@ -3083,10 +4046,30 @@ 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" +iconv-lite@0.4.19: + version "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" @@ -3095,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" @@ -3103,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" @@ -3151,6 +4154,11 @@ inherits@2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.1.tgz#b17d08d326b4423e568eff719f91b0b1cbdf69f1" +inherits@2.0.4, inherits@~2.0.3: + version "2.0.4" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + ini@^1.3.4, ini@~1.3.0: version "1.3.4" resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.4.tgz#0537cb79daf59b59a1a517dff706c86ec039162e" @@ -3173,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" @@ -3185,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" @@ -3200,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" @@ -3220,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" @@ -3244,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" @@ -3284,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" @@ -3329,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" @@ -3355,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" @@ -3377,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" @@ -3395,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" @@ -3421,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" @@ -3444,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" @@ -3463,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" @@ -3491,6 +4668,14 @@ js-tokens@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.1.tgz#08e9f132484a2c45a30907e9dc4d5567b7f114d7" +js-yaml@^3.13.0, js-yaml@^3.3.1: + version "3.13.1" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.13.1.tgz#aff151b30bfdfa8e49e05da22e7415e9dfa37847" + integrity sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw== + dependencies: + argparse "^1.0.7" + esprima "^4.0.0" + js-yaml@^3.3.0, js-yaml@^3.5.1, js-yaml@^3.5.2, js-yaml@^3.8.3: version "3.8.4" resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.8.4.tgz#520b4564f86573ba96662af85a8cafa7b4b5a6f6" @@ -3536,6 +4721,20 @@ json-refs@^2.1.5: slash "^1.0.0" uri-js "^3.0.2" +json-refs@^3.0.2: + version "3.0.13" + resolved "https://registry.yarnpkg.com/json-refs/-/json-refs-3.0.13.tgz#ae8c6afcde2b2576795780699920c34089ae7675" + integrity sha512-/FJ+BJ6BASjmNsBJHE8qMVj46HTS2Pfq5gI5BQRhyUsdrw9HaHRWSOsOh87deTOyWMtGas5Qr8H6ikrcWHdZbw== + dependencies: + commander "~2.19.0" + graphlib "^2.1.7" + js-yaml "^3.13.0" + lodash "^4.17.11" + native-promise-only "^0.8.1" + path-loader "^1.0.10" + slash "^2.0.0" + uri-js "^4.2.2" + json-schema-faker@^0.2.8: version "0.2.16" resolved "https://registry.yarnpkg.com/json-schema-faker/-/json-schema-faker-0.2.16.tgz#51d3ca48955d8fe734f591d747b72453be5a78f2" @@ -3545,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" @@ -3577,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" @@ -3619,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" @@ -3636,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" @@ -3653,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" @@ -3689,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" @@ -3729,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: @@ -3741,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" @@ -3757,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" @@ -3805,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" @@ -3862,10 +5117,76 @@ 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" + integrity sha1-6I7suS4ruEyQZWEv2VigcZzUf5Q= + +lodash._basebind@~2.4.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/lodash._basebind/-/lodash._basebind-2.4.1.tgz#e940b9ebdd27c327e0a8dab1b55916c5341e9575" + integrity sha1-6UC5690nwyfgqNqxtVkWxTQelXU= + dependencies: + lodash._basecreate "~2.4.1" + lodash._setbinddata "~2.4.1" + lodash._slice "~2.4.1" + lodash.isobject "~2.4.1" + +lodash._baseclone@~2.4.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/lodash._baseclone/-/lodash._baseclone-2.4.1.tgz#30f823e57e17e3735d383bd62b60b387543b4186" + integrity sha1-MPgj5X4X43NdODvWK2Czh1Q7QYY= + dependencies: + lodash._getarray "~2.4.1" + lodash._releasearray "~2.4.1" + lodash._slice "~2.4.1" + lodash.assign "~2.4.1" + lodash.foreach "~2.4.1" + lodash.forown "~2.4.1" + lodash.isarray "~2.4.1" + lodash.isobject "~2.4.1" + lodash._basecopy@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/lodash._basecopy/-/lodash._basecopy-3.0.1.tgz#8da0e6a876cf344c0ad8a54882111dd3c5c7ca36" +lodash._basecreate@~2.4.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/lodash._basecreate/-/lodash._basecreate-2.4.1.tgz#f8e6f5b578a9e34e541179b56b8eeebf4a287e08" + integrity sha1-+Ob1tXip405UEXm1a47uv0oofgg= + dependencies: + lodash._isnative "~2.4.1" + lodash.isobject "~2.4.1" + lodash.noop "~2.4.1" + +lodash._basecreatecallback@~2.4.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/lodash._basecreatecallback/-/lodash._basecreatecallback-2.4.1.tgz#7d0b267649cb29e7a139d0103b7c11fae84e4851" + integrity sha1-fQsmdknLKeehOdAQO3wR+uhOSFE= + dependencies: + lodash._setbinddata "~2.4.1" + lodash.bind "~2.4.1" + lodash.identity "~2.4.1" + lodash.support "~2.4.1" + +lodash._basecreatewrapper@~2.4.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/lodash._basecreatewrapper/-/lodash._basecreatewrapper-2.4.1.tgz#4d31f2e7de7e134fbf2803762b8150b32519666f" + integrity sha1-TTHy595+E0+/KAN2K4FQsyUZZm8= + dependencies: + lodash._basecreate "~2.4.1" + lodash._setbinddata "~2.4.1" + lodash._slice "~2.4.1" + lodash.isobject "~2.4.1" + lodash._basetostring@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/lodash._basetostring/-/lodash._basetostring-3.0.1.tgz#d1861d877f824a52f669832dcaf3ee15566a07d5" @@ -3881,6 +5202,23 @@ lodash._createcompounder@^3.0.0: lodash.deburr "^3.0.0" lodash.words "^3.0.0" +lodash._createwrapper@~2.4.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/lodash._createwrapper/-/lodash._createwrapper-2.4.1.tgz#51d6957973da4ed556e37290d8c1a18c53de1607" + integrity sha1-UdaVeXPaTtVW43KQ2MGhjFPeFgc= + dependencies: + lodash._basebind "~2.4.1" + lodash._basecreatewrapper "~2.4.1" + lodash._slice "~2.4.1" + lodash.isfunction "~2.4.1" + +lodash._getarray@~2.4.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/lodash._getarray/-/lodash._getarray-2.4.1.tgz#faf1f7f810fa985a251c2187404481094839e5ee" + integrity sha1-+vH3+BD6mFolHCGHQESBCUg55e4= + dependencies: + lodash._arraypool "~2.4.1" + lodash._getnative@^3.0.0: version "3.9.1" resolved "https://registry.yarnpkg.com/lodash._getnative/-/lodash._getnative-3.9.1.tgz#570bc7dede46d61cdcde687d65d3eecbaa3aaff5" @@ -3889,6 +5227,21 @@ lodash._isiterateecall@^3.0.0: version "3.0.9" resolved "https://registry.yarnpkg.com/lodash._isiterateecall/-/lodash._isiterateecall-3.0.9.tgz#5203ad7ba425fae842460e696db9cf3e6aac057c" +lodash._isnative@~2.4.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/lodash._isnative/-/lodash._isnative-2.4.1.tgz#3ea6404b784a7be836c7b57580e1cdf79b14832c" + integrity sha1-PqZAS3hKe+g2x7V1gOHN95sUgyw= + +lodash._maxpoolsize@~2.4.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/lodash._maxpoolsize/-/lodash._maxpoolsize-2.4.1.tgz#9d482f463b8e66afbe59c2c14edb117060172334" + integrity sha1-nUgvRjuOZq++WcLBTtsRcGAXIzQ= + +lodash._objecttypes@~2.4.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/lodash._objecttypes/-/lodash._objecttypes-2.4.1.tgz#7c0b7f69d98a1f76529f890b0cdb1b4dfec11c11" + integrity sha1-fAt/admKH3ZSn4kLDNsbTf7BHBE= + lodash._reescape@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/lodash._reescape/-/lodash._reescape-3.0.0.tgz#2b1d6f5dfe07c8a355753e5f27fac7f1cde1616a" @@ -3901,24 +5254,77 @@ lodash._reinterpolate@^3.0.0, lodash._reinterpolate@~3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz#0ccf2d89166af03b3663c796538b75ac6e114d9d" -lodash._root@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/lodash._root/-/lodash._root-3.0.1.tgz#fba1c4524c19ee9a5f8136b4609f017cf4ded692" +lodash._releasearray@~2.4.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/lodash._releasearray/-/lodash._releasearray-2.4.1.tgz#a6139630d76d1536b07ddc80962889b082f6a641" + integrity sha1-phOWMNdtFTawfdyAliiJsIL2pkE= + dependencies: + lodash._arraypool "~2.4.1" + lodash._maxpoolsize "~2.4.1" + +lodash._root@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/lodash._root/-/lodash._root-3.0.1.tgz#fba1c4524c19ee9a5f8136b4609f017cf4ded692" + +lodash._setbinddata@~2.4.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/lodash._setbinddata/-/lodash._setbinddata-2.4.1.tgz#f7c200cd1b92ef236b399eecf73c648d17aa94d2" + integrity sha1-98IAzRuS7yNrOZ7s9zxkjReqlNI= + dependencies: + lodash._isnative "~2.4.1" + lodash.noop "~2.4.1" + +lodash._shimkeys@~2.4.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/lodash._shimkeys/-/lodash._shimkeys-2.4.1.tgz#6e9cc9666ff081f0b5a6c978b83e242e6949d203" + integrity sha1-bpzJZm/wgfC1psl4uD4kLmlJ0gM= + dependencies: + lodash._objecttypes "~2.4.1" + +lodash._slice@~2.4.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/lodash._slice/-/lodash._slice-2.4.1.tgz#745cf41a53597b18f688898544405efa2b06d90f" + integrity sha1-dFz0GlNZexj2iImFREBe+isG2Q8= lodash.assign@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/lodash.assign/-/lodash.assign-4.2.0.tgz#0d99f3ccd7a6d261d19bdaeb9245005d285808e7" +lodash.assign@~2.4.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/lodash.assign/-/lodash.assign-2.4.1.tgz#84c39596dd71181a97b0652913a7c9675e49b1aa" + integrity sha1-hMOVlt1xGBqXsGUpE6fJZ15Jsao= + dependencies: + lodash._basecreatecallback "~2.4.1" + lodash._objecttypes "~2.4.1" + lodash.keys "~2.4.1" + lodash.assignwith@^4.0.7: version "4.2.0" resolved "https://registry.yarnpkg.com/lodash.assignwith/-/lodash.assignwith-4.2.0.tgz#127a97f02adc41751a954d24b0de17e100e038eb" +lodash.bind@~2.4.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/lodash.bind/-/lodash.bind-2.4.1.tgz#5d19fa005c8c4d236faf4742c7b7a1fcabe29267" + integrity sha1-XRn6AFyMTSNvr0dCx7eh/Kvikmc= + dependencies: + lodash._createwrapper "~2.4.1" + lodash._slice "~2.4.1" + lodash.camelcase@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-3.0.1.tgz#932c8b87f8a4377897c67197533282f97aeac298" dependencies: lodash._createcompounder "^3.0.0" +lodash.clonedeep@^2.4.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-2.4.1.tgz#f29203b40b12fee0a45d3631648259bebabc7868" + integrity sha1-8pIDtAsS/uCkXTYxZIJZvrq8eGg= + dependencies: + lodash._baseclone "~2.4.1" + lodash._basecreatecallback "~2.4.1" + lodash.clonedeep@^4.3.2: version "4.5.0" resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef" @@ -3939,10 +5345,32 @@ lodash.escape@^3.0.0: dependencies: lodash._root "^3.0.0" -lodash.get@^4.1.2: +lodash.foreach@~2.4.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/lodash.foreach/-/lodash.foreach-2.4.1.tgz#fe3fc3a34c86c94cab6f9522560282741e016309" + integrity sha1-/j/Do0yGyUyrb5UiVgKCdB4BYwk= + dependencies: + lodash._basecreatecallback "~2.4.1" + lodash.forown "~2.4.1" + +lodash.forown@~2.4.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/lodash.forown/-/lodash.forown-2.4.1.tgz#78b41eafe1405fa966459ea4193fd502d084524b" + integrity sha1-eLQer+FAX6lmRZ6kGT/VAtCEUks= + dependencies: + lodash._basecreatecallback "~2.4.1" + lodash._objecttypes "~2.4.1" + lodash.keys "~2.4.1" + +lodash.get@^4.0.0, lodash.get@^4.1.2: version "4.4.2" resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99" +lodash.identity@~2.4.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/lodash.identity/-/lodash.identity-2.4.1.tgz#6694cffa65fef931f7c31ce86c74597cf560f4f1" + integrity sha1-ZpTP+mX++TH3wxzobHRZfPVg9PE= + lodash.isarguments@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz#2f573d85c6a24289ff00663b491c1d338ff3458a" @@ -3951,14 +5379,33 @@ lodash.isarray@^3.0.0: version "3.0.4" resolved "https://registry.yarnpkg.com/lodash.isarray/-/lodash.isarray-3.0.4.tgz#79e4eb88c36a8122af86f844aa9bcd851b5fbb55" +lodash.isarray@~2.4.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/lodash.isarray/-/lodash.isarray-2.4.1.tgz#b52a326c1f62f6d7da73a31d5401df6ef44f0fa1" + integrity sha1-tSoybB9i9tfac6MdVAHfbvRPD6E= + dependencies: + lodash._isnative "~2.4.1" + lodash.isempty@^4.2.1: version "4.4.0" resolved "https://registry.yarnpkg.com/lodash.isempty/-/lodash.isempty-4.4.0.tgz#6f86cbedd8be4ec987be9aaf33c9684db1b31e7e" -lodash.isequal@^4.4.0: +lodash.isequal@^4.0.0, lodash.isequal@^4.4.0: version "4.5.0" resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0" +lodash.isfunction@~2.4.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/lodash.isfunction/-/lodash.isfunction-2.4.1.tgz#2cfd575c73e498ab57e319b77fa02adef13a94d1" + integrity sha1-LP1XXHPkmKtX4xm3f6Aq3vE6lNE= + +lodash.isobject@~2.4.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/lodash.isobject/-/lodash.isobject-2.4.1.tgz#5a2e47fe69953f1ee631a7eba1fe64d2d06558f5" + integrity sha1-Wi5H/mmVPx7mMafrof5k0tBlWPU= + dependencies: + lodash._objecttypes "~2.4.1" + lodash.isplainobject@^4.0.4: version "4.0.6" resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb" @@ -3975,6 +5422,15 @@ lodash.keys@^3.0.0: lodash.isarguments "^3.0.0" lodash.isarray "^3.0.0" +lodash.keys@~2.4.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/lodash.keys/-/lodash.keys-2.4.1.tgz#48dea46df8ff7632b10d706b8acb26591e2b3727" + integrity sha1-SN6kbfj/djKxDXBrissmWR4rNyc= + dependencies: + lodash._isnative "~2.4.1" + lodash._shimkeys "~2.4.1" + lodash.isobject "~2.4.1" + lodash.mapvalues@^4.4.0: version "4.6.0" resolved "https://registry.yarnpkg.com/lodash.mapvalues/-/lodash.mapvalues-4.6.0.tgz#1bafa5005de9dd6f4f26668c30ca37230cc9689c" @@ -3983,6 +5439,11 @@ lodash.memoize@^4.1.2: version "4.1.2" resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe" +lodash.noop@~2.4.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/lodash.noop/-/lodash.noop-2.4.1.tgz#4fb54f816652e5ae10e8f72f717a388c7326538a" + integrity sha1-T7VPgWZS5a4Q6PcvcXo4jHMmU4o= + lodash.pick@^4.2.1: version "4.4.0" resolved "https://registry.yarnpkg.com/lodash.pick/-/lodash.pick-4.4.0.tgz#52f05610fff9ded422611441ed1fc123a03001b3" @@ -3991,6 +5452,13 @@ lodash.restparam@^3.0.0: version "3.6.1" resolved "https://registry.yarnpkg.com/lodash.restparam/-/lodash.restparam-3.6.1.tgz#936a4e309ef330a7645ed4145986c85ae5b20805" +lodash.support@~2.4.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/lodash.support/-/lodash.support-2.4.1.tgz#320e0b67031673c28d7a2bb5d9e0331a45240515" + integrity sha1-Mg4LZwMWc8KNeiu12eAzGkUkBRU= + dependencies: + lodash._isnative "~2.4.1" + lodash.template@^3.0.0: version "3.6.2" resolved "https://registry.yarnpkg.com/lodash.template/-/lodash.template-3.6.2.tgz#f8cdecc6169a255be9098ae8b0c53d378931d14f" @@ -4053,9 +5521,15 @@ 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.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" @@ -4076,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" @@ -4152,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" @@ -4164,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" @@ -4192,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" @@ -4233,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: @@ -4251,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" @@ -4258,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" @@ -4272,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" @@ -4280,13 +5813,29 @@ mime@1.3.x, mime@^1.2.11, mime@^1.3.4: version "1.3.6" resolved "https://registry.yarnpkg.com/mime/-/mime-1.3.6.tgz#591d84d3653a6b0b4a3b9df8de5aa8108e72e5e0" +mime@1.6.0, mime@^1.4.1: + version "1.6.0" + 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" @@ -4330,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" @@ -4346,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" @@ -4366,6 +5935,16 @@ ms@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" +ms@2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a" + integrity sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg== + +ms@^2.1.1: + version "2.1.2" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" + integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== + multer@^1.0.6: version "1.3.0" resolved "https://registry.yarnpkg.com/multer/-/multer-1.3.0.tgz#092b2670f6846fa4914965efc8cf94c20fec6cd2" @@ -4379,6 +5958,33 @@ multer@^1.0.6: type-is "^1.6.4" xtend "^4.0.0" +multer@^1.1.0: + version "1.4.2" + resolved "https://registry.yarnpkg.com/multer/-/multer-1.4.2.tgz#2f1f4d12dbaeeba74cb37e623f234bf4d3d2057a" + integrity sha512-xY8pX7V+ybyUpbYMxtjM9KAiD9ixtg5/JkeKUTD6xilfDv0vzzOFcCp4Ljb1UU3tSOM3VTZtKo63OmzOrGi3Cg== + dependencies: + append-field "^1.0.0" + busboy "^0.2.11" + concat-stream "^1.5.2" + mkdirp "^0.5.1" + object-assign "^4.1.1" + on-finished "^2.3.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" @@ -4405,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" @@ -4413,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" @@ -4442,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" @@ -4456,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" @@ -4534,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" @@ -4598,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" @@ -4617,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" @@ -4654,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" @@ -4666,7 +6364,7 @@ object-assign@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-3.0.0.tgz#9bedd5ca0897949bca47e7ff408062d549f587f2" -object-assign@^4, object-assign@^4.0.1, object-assign@^4.1.0, object-assign@~4.1.1: +object-assign@^4, object-assign@^4.0.1, object-assign@^4.1.0, object-assign@^4.1.1, object-assign@~4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" @@ -4674,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" @@ -4685,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" @@ -4695,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: @@ -4711,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: @@ -4765,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" @@ -4793,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" @@ -4804,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" @@ -4905,6 +6708,16 @@ parseurl@^1.3.0, parseurl@~1.3.1: version "1.3.1" resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.1.tgz#c8ab8c9223ba34888aa64a297b28853bec18da56" +parseurl@~1.3.3: + version "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" @@ -4941,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" @@ -4959,10 +6777,18 @@ 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" +path-loader@^1.0.10: + version "1.0.10" + resolved "https://registry.yarnpkg.com/path-loader/-/path-loader-1.0.10.tgz#dd3d1bd54cb6f2e6423af2ad334a41cc0bce4cf6" + integrity sha512-CMP0v6S6z8PHeJ6NFVyVJm6WyJjIwFvyz2b0n2/4bKdS/0uZa/9sKUlYZzubrn3zuDRU0zIuEDX9DZYQ2ZI8TA== + dependencies: + native-promise-only "^0.8.1" + superagent "^3.8.3" + path-loader@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/path-loader/-/path-loader-1.0.2.tgz#cd5c73e7e39a91011be148d6bfdd8a85bb931ef9" @@ -4994,6 +6820,11 @@ path-to-regexp@^1.2.1: dependencies: isarray "0.0.1" +path-to-regexp@^2.0.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-2.4.0.tgz#35ce7f333d5616f1c1e1bfe266c3aba2e5b2e704" + integrity sha512-G6zHoVqC6GGTQkZwF4lkuEyMbVOjoBKAEybQUypI1WTkqinCOrq2x6U2+phkJ1XsEMTy4LjtwPI7HW+NVrRR2w== + path-type@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/path-type/-/path-type-1.1.0.tgz#59c44f7ee491da704da415da5a4070ba4f8fe441" @@ -5008,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" @@ -5040,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" @@ -5058,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" @@ -5072,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" @@ -5091,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" @@ -5365,7 +7235,12 @@ process-nextick-args@~1.0.6: version "1.0.7" resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-1.0.7.tgz#150e20b756590ad3f91093f25a4f2ad8bff30ba3" -process@^0.11.0, process@~0.11.0: +process-nextick-args@~2.0.0: + version "2.0.1" + 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.1, process@~0.11.0: version "0.11.10" resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182" @@ -5404,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: @@ -5431,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" @@ -5446,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" @@ -5549,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" @@ -5577,10 +7499,30 @@ qs@6.4.0, qs@^6.1.0, qs@^6.4.0, qs@~6.4.0: version "6.4.0" resolved "https://registry.yarnpkg.com/qs/-/qs-6.4.0.tgz#13e26d28ad6b0ffaa91312cd3bf708ed351e7233" +qs@6.5.1: + version "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" + integrity sha512-tPSkj8y92PfZVbinY1n84i1Qdx75lZjMQYx9WZhnkofyxzw2r7Ho39G3/aEvSUdebxpnnM4LZJCtvE/Aq3+s9w== + 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" @@ -5596,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" @@ -5632,6 +7571,31 @@ range-parser@^1.0.3, range-parser@^1.2.0, range-parser@~1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.0.tgz#f49be6b487894ddc40dcc94a322f611092e00d5e" +range-parser@~1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" + integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== + +raw-body@2.3.2: + version "2.3.2" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.3.2.tgz#bcd60c77d3eb93cde0050295c3f379389bc88f89" + integrity sha1-vNYMd9Prk83gBQKVw/N5OJvIj4k= + dependencies: + bytes "3.0.0" + http-errors "1.6.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" @@ -5653,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" @@ -5713,6 +7687,28 @@ readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.5, readable string_decoder "~1.0.0" util-deprecate "~1.0.1" +readable-stream@^2.3.5: + version "2.3.6" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.6.tgz#b11c27d88b8ff1fbe070643cf94b0c79ae1b0aaf" + integrity sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw== + 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" + +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" @@ -5724,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" @@ -5733,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" @@ -5813,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" @@ -5847,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" @@ -5894,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" @@ -5934,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" @@ -5949,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" @@ -5992,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" @@ -6035,18 +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.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" @@ -6068,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" @@ -6082,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" @@ -6095,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" @@ -6105,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" @@ -6117,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" @@ -6135,6 +8290,25 @@ send@0.15.3: range-parser "~1.2.0" statuses "~1.3.1" +send@0.17.1: + version "0.17.1" + resolved "https://registry.yarnpkg.com/send/-/send-0.17.1.tgz#c1d8b059f7900f7466dd4938bdc44e11ddb376c8" + integrity sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg== + dependencies: + debug "2.6.9" + depd "~1.1.2" + destroy "~1.0.4" + encodeurl "~1.0.2" + escape-html "~1.0.3" + etag "~1.8.1" + fresh "0.5.2" + http-errors "~1.7.2" + mime "1.6.0" + ms "2.1.1" + on-finished "~2.3.0" + range-parser "~1.2.1" + statuses "~1.5.0" + sequencify@~0.0.7: version "0.0.7" resolved "https://registry.yarnpkg.com/sequencify/-/sequencify-0.0.7.tgz#90cff19d02e07027fd767f5ead3e7b95d1e7380c" @@ -6161,7 +8335,17 @@ serve-index@^1.7.2: mime-types "~2.1.15" parseurl "~1.3.1" -serve-static@1.12.3, serve-static@^1.9.2: +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== + dependencies: + encodeurl "~1.0.2" + escape-html "~1.0.3" + 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: @@ -6174,10 +8358,20 @@ 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" @@ -6190,6 +8384,16 @@ setprototypeof@1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.0.3.tgz#66567e37043eeb4f04d91bd658c0cbefb55b8e04" +setprototypeof@1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.0.tgz#d0bd85536887b6fe7c0d818cb962d9d91c54e656" + integrity sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ== + +setprototypeof@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.1.tgz#7e95acb24aa92f5885e0abef5ba131330d4ae683" + integrity sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw== + sha.js@2.2.6: version "2.2.6" resolved "https://registry.yarnpkg.com/sha.js/-/sha.js-2.2.6.tgz#17ddeddc5f722fb66501658895461977867315ba" @@ -6200,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" @@ -6241,10 +8457,45 @@ slash@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/slash/-/slash-1.0.0.tgz#c41f2f6c39fc16d1cd17ad4b5d896114ae470d55" +slash@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/slash/-/slash-2.0.0.tgz#de552851a1759df3a8f206535442f5ec4ddeab44" + integrity sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A== + 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" @@ -6365,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" @@ -6393,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" @@ -6415,6 +8684,11 @@ source-map@0.5.x, source-map@^0.5.3, source-map@^0.5.6, source-map@~0.5.0, sourc version "0.5.6" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.6.tgz#75ce38f52bf0733c5a7f0c118d81334a2bb5f412" +spark-md5@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/spark-md5/-/spark-md5-3.0.0.tgz#3722227c54e2faf24b1dc6d933cc144e6f71bfef" + integrity sha1-NyIifFTi+vJLHcbZM8wUTm9xv+8= + sparkles@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/sparkles/-/sparkles-1.0.0.tgz#1acbbfb592436d10bbe8f785b7cc6f82815012c3" @@ -6433,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" @@ -6471,10 +8775,23 @@ 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" +"statuses@>= 1.4.0 < 2", "statuses@>= 1.5.0 < 2", statuses@~1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" + integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow= + stf-appstore-db@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/stf-appstore-db/-/stf-appstore-db-1.0.0.tgz#3ddaf62d509d6c3125db17813cba34b54435b7e4" @@ -6505,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" @@ -6552,16 +8865,54 @@ 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" dependencies: safe-buffer "~5.0.1" +string_decoder@~1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" + integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== + dependencies: + safe-buffer "~5.1.0" + stringmap@~0.2.2: version "0.2.2" resolved "https://registry.yarnpkg.com/stringmap/-/stringmap-0.2.2.tgz#556c137b258f942b8776f5b2ef582aa069d7d1b1" @@ -6580,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" @@ -6636,16 +8994,39 @@ superagent@^3.5.2: qs "^6.1.0" readable-stream "^2.0.5" +superagent@^3.8.3: + version "3.8.3" + resolved "https://registry.yarnpkg.com/superagent/-/superagent-3.8.3.tgz#460ea0dbdb7d5b11bc4f78deba565f86a178e128" + integrity sha512-GLQtLMCoEIK4eDv6OGtkOoSMt3D+oq0y3dsxMuYuDvaNUvuT8eFBuLmfR0iYYzHC1e8hpzC6ZsxbuP6DIalMFA== + dependencies: + component-emitter "^1.2.0" + cookiejar "^2.1.0" + debug "^3.1.0" + extend "^3.0.0" + form-data "^2.3.1" + formidable "^1.2.0" + methods "^1.1.1" + mime "^1.4.1" + qs "^6.5.1" + readable-stream "^2.3.5" + 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" @@ -6658,6 +9039,13 @@ svgo@^0.7.0: sax "~1.2.1" whet.extend "~0.9.9" +swagger-converter@^0.1.7: + version "0.1.7" + resolved "https://registry.yarnpkg.com/swagger-converter/-/swagger-converter-0.1.7.tgz#a097519c6f1ee4dd67e308d9b53ddc9c2b257f97" + integrity sha1-oJdRnG8e5N1n4wjZtT3cnCslf5c= + dependencies: + lodash.clonedeep "^2.4.1" + swagger-express-mw@^0.7.0: version "0.7.0" resolved "https://registry.yarnpkg.com/swagger-express-mw/-/swagger-express-mw-0.7.0.tgz#49f5db72d1d4b3827336ee6cc7b369caed5bf4c8" @@ -6690,6 +9078,29 @@ swagger-schema-official@2.0.0-bab6bed: version "2.0.0-bab6bed" resolved "https://registry.yarnpkg.com/swagger-schema-official/-/swagger-schema-official-2.0.0-bab6bed.tgz#70070468d6d2977ca5237b2e519ca7d06a2ea3fd" +swagger-tools@^0.10.3: + version "0.10.4" + resolved "https://registry.yarnpkg.com/swagger-tools/-/swagger-tools-0.10.4.tgz#2949b00ca17da0d4f91ad74c44027de250c4d849" + integrity sha512-VQpijIi8cpB/frUZOZlVpS7U3CrdSAZBfiHu448R1njiNXUnE7heF3Svz3qFBr5SYtaPvaqWpHMbvboirCXVzA== + dependencies: + async "^2.5.0" + body-parser "1.18.2" + commander "~2.11.0" + debug "^3.1.0" + js-yaml "^3.3.1" + json-refs "^3.0.2" + lodash "^4.17.4" + multer "^1.1.0" + parseurl "^1.3.0" + path-to-regexp "^2.0.0" + qs "^6.0.3" + serve-static "^1.10.0" + spark-md5 "^3.0.0" + superagent "^3.5.2" + swagger-converter "^0.1.7" + traverse "^0.6.6" + z-schema "^3.15.4" + sway@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/sway/-/sway-1.0.0.tgz#368ffc0e96bd84226ed1b9b33d66be57da04f09a" @@ -6754,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" @@ -6810,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" @@ -6836,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" @@ -6850,6 +9286,36 @@ 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" + integrity sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw== + token-stream@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/token-stream/-/token-stream-0.0.1.tgz#ceeefc717a76c4316f126d0b9dbaa55d7e7df01a" @@ -6860,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" @@ -6874,6 +9348,11 @@ transliteration@^1.1.6: dependencies: yargs "^8.0.1" +traverse@^0.6.6: + version "0.6.6" + resolved "https://registry.yarnpkg.com/traverse/-/traverse-0.6.6.tgz#cbdf560fd7b9af632502fed40f918c157ea97137" + integrity sha1-y99WD9e5r2MlAv7UD5GMFX6pcTc= + trim-newlines@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-1.0.0.tgz#5887966bb582a4503a41eb524f7d35011815a613" @@ -6929,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" @@ -7010,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" @@ -7028,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" @@ -7042,6 +9557,18 @@ uri-js@^3.0.2: dependencies: punycode "^2.1.0" +uri-js@^4.2.2: + version "4.2.2" + resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.2.2.tgz#94c540e1ff772956e2299507c010aea6c8838eb0" + integrity sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ== + 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" @@ -7057,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" - 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" +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 "~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" @@ -7085,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" @@ -7113,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" @@ -7127,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" @@ -7156,14 +9688,24 @@ validator@5.7.x: version "5.7.0" resolved "https://registry.yarnpkg.com/validator/-/validator-5.7.0.tgz#7a87a58146b695ac486071141c0c49d67da05e5c" +validator@^10.0.0: + version "10.11.0" + resolved "https://registry.yarnpkg.com/validator/-/validator-10.11.0.tgz#003108ea6e9a9874d31ccc9e5006856ccd76b228" + integrity sha512-X/p3UZerAIsbBfN/IwahhYaBbY68EN/UQBWHtsbXGT5bfrH/p4NQzUCG1kF/rtKaNpnJ7jAu6NGTdSNtyNIXMw== + 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" @@ -7240,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" @@ -7255,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" @@ -7262,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: @@ -7271,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" @@ -7434,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: @@ -7486,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" @@ -7502,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" @@ -7526,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" @@ -7534,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" @@ -7556,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" @@ -7647,6 +10319,18 @@ yeast@0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/yeast/-/yeast-0.1.2.tgz#008e06d8094320c372dbc2f8ed76a0ca6c8ac419" +z-schema@^3.15.4: + version "3.25.1" + resolved "https://registry.yarnpkg.com/z-schema/-/z-schema-3.25.1.tgz#7e14663be2b96003d938a56f644fb8561643fb7e" + integrity sha512-7tDlwhrBG+oYFdXNOjILSurpfQyuVgkRe3hB2q8TEssamDHB7BbLWYkYO98nTn0FibfdFroFKDjndbgufAgS/Q== + dependencies: + core-js "^2.5.7" + lodash.get "^4.0.0" + lodash.isequal "^4.0.0" + validator "^10.0.0" + optionalDependencies: + commander "^2.7.1" + z-schema@^3.16.1: version "3.18.2" resolved "https://registry.yarnpkg.com/z-schema/-/z-schema-3.18.2.tgz#e422196b5efe60b46adef3c3f2aef2deaa911161"